Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ae5c4460d | ||
|
|
135957831b | ||
|
|
c0c204ba50 | ||
|
|
fbae82e024 | ||
|
|
6c6cc37e1d | ||
|
|
1e3e04bbac | ||
|
|
4e64b75044 | ||
|
|
aee1487410 | ||
|
|
e35a77f676 | ||
|
|
9458217d34 | ||
|
|
20f6c98ec5 | ||
|
|
408c14baff | ||
|
|
c42c785e2c | ||
|
|
75550256ef | ||
|
|
ca85e876ad | ||
|
|
b9ccff4ad6 | ||
|
|
6110f118e3 | ||
|
|
3073d1b2fd | ||
|
|
c091e4434f | ||
|
|
e016f6a20f | ||
|
|
f13b145f23 | ||
|
|
036bd530fc | ||
|
|
422dd7761a | ||
|
|
497e974178 | ||
|
|
2566f53734 | ||
|
|
1abee91721 | ||
|
|
efa9e89468 | ||
|
|
753cc9c259 | ||
|
|
88c54eab44 | ||
|
|
cf57c87daf | ||
|
|
945225d942 | ||
|
|
7f2d84d2c7 | ||
|
|
3e443bf81c | ||
|
|
8be1c4d9ef |
No files matched your search
@@ -2138,6 +2138,160 @@ turn. Passage, capsule, support, open-air, bridge, binding, strict layout, captu
|
||||
and cost evidence are recorded in
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
||||
|
||||
### 8.49 Walk-floor washboard and FI mouth naturalness — World Assembler M3 L36
|
||||
|
||||
L35's exported owner-SW mid-tunnel floor measured a 7.250 m repeat at 0.02252 m amplitude; its
|
||||
upper-SW fixture arrival measured a 1.438 m repeat at 0.02482 m. Both came from segment-built,
|
||||
one-sided support treads meeting the native terrain zero crossing at per-segment joins. The floor
|
||||
probe samples the exported walk mesh along the resolved route, separately from the wall-radius scan.
|
||||
Eligible tread sections overlap by up to 2 m and all sparse route segments now use a bounded
|
||||
signed-distance slab, including SurfaceWorld-to-TunnelNetwork routes. This keeps the tread top stable
|
||||
across segment joins while capsule-clearance and native-solid gates retain walkability and open air.
|
||||
Regular tread tops remain seated 5 cm lower and ease back to 0.20 m within 20 m of a lower landing;
|
||||
seeded shoulder grain is removed, and gravity-facing relief is reduced while side-wall and ceiling
|
||||
relief remain irregular.
|
||||
|
||||
On v23, the same mesh probes measure 7.188 m / 0.00067 m (0.067 cm) on owner-SW and 6.500 m /
|
||||
0.00057 m (0.057 cm) on the upper fixture. The density audit checks five 32 m walk-floor windows at
|
||||
0.125 m spacing, rejects amplitudes of 0.015 m or more, and detects its beaded control. The FI mouth
|
||||
uses a local 2 m symmetric signed-field filter with the route protected and joins route-mouth support
|
||||
to native terrain with a 1.5 m smooth signed-distance union. Its mesh audit reduces the L35
|
||||
42.27° fade-ring jump to 27.13° and finds no detached component above the 0.0001 m² area floor; one
|
||||
8-triangle subthreshold component at 0.0000266 m² is recorded. The evaluator is version 23. Full
|
||||
recipe/style audits, route support, open-air, strict layout, legacy validators, determinism, capture and
|
||||
paired-cost results are in
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
||||
|
||||
### 8.50 Immutable field binding — World Assembler M3 L40
|
||||
|
||||
`FStandaloneShapeWorld` now keeps a sorted `FBoundStratumField` table built from the resolved layout
|
||||
at world construction. Each entry owns a concrete SurfaceWorld, FloatingIslands, or legacy adapter
|
||||
in `FGeneratorFieldAdapterV1`, plus the resolved bounds and native plan pointer. The adapters share
|
||||
the immutable `densityAt(x,y,z,out)` shape, while `std::variant` keeps dispatch concrete instead of
|
||||
adding a virtual call to every density sample. FloatingIslands keeps its original fixed-point entry
|
||||
point for sample evaluation and its boundary parameter table is captured when the adapter is bound.
|
||||
|
||||
`DensityAt`, native SurfaceWorld/FloatingIslands evaluation, legacy SurfaceWorld bridging and
|
||||
`ClassifyBoxDetailed` select from the bound table. The native adapters no longer ask the manager
|
||||
which generator or plan to use per sample; box proof reuses the bound field objects. The existing
|
||||
float-to-fixed conversion order, density arithmetic and shared boundary composition remain intact.
|
||||
The manager's layout-width value is captured once for the legacy-compatible chunk-index projection.
|
||||
|
||||
The legacy adapter is still a compatibility bridge: `UVoxelGenerator::GetDensityAt` retains its
|
||||
own manager routing when invoked for legacy SurfaceWorld fields or out-of-plan fallback queries.
|
||||
Other shared-composition policy branches and passage/boundary planning remain outside this slice.
|
||||
No Unreal adapter or `VoxelWorld.cpp` change was needed, and the evaluator version is unchanged.
|
||||
L37/L39 owner hashes are the parity baseline; the L40 report records fast, medium, full and paired
|
||||
cost evidence.
|
||||
|
||||
### 8.51 Resolved metre layout authority — World Assembler M3 L41
|
||||
|
||||
`UVoxelStrateManager::Initialize` now resolves the layout in integer micrometres before passages or
|
||||
Core work are scheduled. The configured layout width is rounded once to micrometres; legacy height
|
||||
and inter-strate gap counts are multiplied by that width. Each slot stores its half-open physical
|
||||
interval `[bottom, top)`, height, transition mode/width, copied passage settings and their physical
|
||||
dimensions. The effective world seed is captured in the manager before passage generation.
|
||||
|
||||
The historical 8 m layout keeps its old placement: the first upper edge is +8 m, and each authored
|
||||
chunk of height or gap is exactly 8 m. `TopChunkZ` and `BottomChunkZ` are floor-based projections of
|
||||
the resolved interval, including mathematical floor for negative Z. Season schema v2 still validates
|
||||
its aligned top/bottom chunk values and stacked order as before, then maps those accepted projections
|
||||
to the same physical intervals. L29's facing ceiling/bedrock bands are derived from these physical
|
||||
intervals and the copied transition width, so their metre bounds retain the prior chunk-based values.
|
||||
|
||||
The Core adapter builds SurfaceWorld and FloatingIslands bounds, shared-layout bands, and resolved
|
||||
passage dimensions from the micrometre fields. FI and SW both receive the same
|
||||
`LayoutChunkWidthSubvoxels = round(layoutWidthMicrometres * 256 / coreVoxelMicrometres)`; layout
|
||||
conversion does not use the owner/Core scale. Owner/Core ratios remain in the legacy FloatingIslands
|
||||
parameter adapter for generator dimensions that are still authored in owner voxels. The manager's
|
||||
legacy passage builder converts resolved passage micrometres back to owner voxels only at its legacy
|
||||
geometry API boundary.
|
||||
|
||||
Chunk coordinates remain in `GetStrateForChunk`, `GetGeneratorTypeForChunk`, chunk-parameter queries,
|
||||
season-v2 validation, and legacy generator dispatch because those interfaces accept layout-chunk
|
||||
indices. Their values are cached projections from the physical slot bounds. World-Z queries and the
|
||||
Core snapshot use the half-open metre intervals. L40's bound Core field table remains the native
|
||||
per-sample dispatch path; this migration adds no manager lookup to it. The Unreal manager and adapter
|
||||
cannot be compiled in this Linux workspace, so their small source changes still need the Windows
|
||||
Unreal build before integration.
|
||||
|
||||
### 8.52 Resolved boundary and landing capabilities — World Assembler M3 L42
|
||||
|
||||
`ResolvedLayoutSnapshotV3::finalize` materializes runtime capability records from the immutable
|
||||
strate/interface fields before sampling begins. The upper interface records whether its Gradient FI
|
||||
bedrock air gap remains open and whether a SurfaceWorld-to-TunnelNetwork passage gets the special
|
||||
landing support profile. Per-strate passage records carry the existing SurfaceWorld landing profile,
|
||||
FloatingIslands route-support behavior, and passage-anchor column center policy. Shared boundary and
|
||||
passage composition reads these records; it no longer compares generator-kind strings.
|
||||
|
||||
The records are deterministic derivatives of `generator_kind`, `transition_mode`, and the resolved
|
||||
adjacent strate, and are omitted from canonical JSON. Those source fields remain in the hashed layout,
|
||||
so owner layout IDs and density hashes retain their L41 identity. The upper strate still controls
|
||||
Hard/Gradient/Interleaved evaluation; each strate still composes ceiling, content, then breakable
|
||||
bedrock. L29 transition samples and the passage-mouth fade arithmetic are unchanged.
|
||||
|
||||
`BindResolvedStratumFields` still tests the resolved generator kind once to bind the concrete
|
||||
SurfaceWorld, FloatingIslands, or legacy evaluator. Passage planning in `ResolvedLayoutInternal.h`
|
||||
also retains generator-specific anchor and default-mouth placement rules; those are outside the
|
||||
shared composition boundary policy. No Unreal source changed. See the L42 report for hashes, tier
|
||||
results and paired cost measurements.
|
||||
|
||||
### 8.53 Shared Core landmark SDF stamps — World Assembler M3 L43
|
||||
|
||||
`ResolvedLayoutStampV1` adds a small, version-1 SDF vocabulary to the shared resolved layout:
|
||||
carve ellipsoid, carve arch, fill pillar and fill dais. Requests and resolved records use integer
|
||||
micrometres and carry a stable ID, strate, position, conservative bounds, half extents, smooth-CSG
|
||||
width, bounded irregularity amplitude/wavelength and quarter-turn orientation. A stamp's seeded
|
||||
noise and bounded placement offsets derive from `(world seed, strate, stable ID)`; records are
|
||||
sorted by stable ID before the layout identity is finalized. If no request is made, the stamp fields
|
||||
are omitted from canonical JSON, preserving the no-stamp layout identity and density hashes.
|
||||
|
||||
Resolution retries a finite, deterministic set of candidates and rejects a record if its complete
|
||||
bound intersects a passage mouth or walk corridor, reaches a strate content face, or overlaps the
|
||||
ceiling or bedrock band. The director reports failure if no safe candidate exists. The strict-layout
|
||||
negative control forces a candidate onto a route and into a ceiling band and expects both rejections.
|
||||
|
||||
`FStandaloneShapeWorld` applies stamps after the shared resolved-strate boundary and passage
|
||||
composition. Carves use smooth maximum with the negated stamp SDF; fills use smooth minimum. Seeded
|
||||
trilinear value noise adds low-frequency relief without changing the conservative bound. The tile
|
||||
query scope builds a BVH candidate list from each tile's sample bounds. Samples in a tile outside
|
||||
every stamp box do no stamp SDF evaluation. Box classification marks stamp-intersecting boxes mixed,
|
||||
and native-only readiness refuses to skip them.
|
||||
|
||||
The `--landmark-chamber-demo` recipe flag adds an irregular ellipsoid chamber, a separate arched
|
||||
entrance and a low fill dais in the owner SurfaceWorld strate, about 31 m from the passage arrival
|
||||
mouth and off the walk route. It stays disabled by default. Active records carry vocabulary version
|
||||
1 in the layout identity, making the demo an explicit world change; no Unreal adapter was added.
|
||||
L43's report records the off/on hashes, walk and interface audits, outside-stamp timing, tier
|
||||
results, captures and any failed runs.
|
||||
|
||||
### 8.54 Opt-in native lattice field — World Assembler M3 R26 slice 1 (Linux L58)
|
||||
|
||||
`Core.Lattice.PrimitiveField` is a version-1 recipe selected only for one Maze or VerticalShafts
|
||||
strate by `--recipe=lattice --lattice-strate=N`. The legacy generator assets and their default
|
||||
dispatch stay unchanged. Planning first binds the actual incoming lower mouth and outgoing upper
|
||||
mouth from the resolved passage records, then creates arrival/exit overlap corridors, a mandatory
|
||||
four-turn helix and the maze spine; two deterministic dead ends are added after that route. The
|
||||
resolved room and edge seeds use `(world seed, strate, recipe version, stable ID)`. An audit control
|
||||
translates the helix 20 m away from both route junctions to make the shaft network orphaned.
|
||||
|
||||
The prototype helix uses a 6 m centreline radius, 3 m clear width, 5 m pitch, four turns and 2.6 m
|
||||
finished floor-to-soffit height. Its centreline descends 20 m over roughly 151 m; the inner-edge
|
||||
grade is about 10.0 degrees. The 2.4 m floor-wall smooth-max gives an isolated ideal fillet radius
|
||||
of 0.8485 m. A separate 0.4 m feature-union blend prevents adjoining ramps from erasing the tread.
|
||||
The swept-clear diameter is 15 m; the conservative padded feature envelope is about 20.6 m (the
|
||||
approved approximately 21 m site envelope).
|
||||
|
||||
The layout reuses the resolved cavern graph vocabulary. A per-strate opt-in selects the native
|
||||
negative-solid/positive-air field; canonical feature order is stable-ID order. Room/corridor bounds
|
||||
are indexed by the graph BVH and polyline segments have a nested deterministic BVH for density and
|
||||
interval queries. Box classification stays conservative, and `isTileShapeFree` refuses any tile
|
||||
that intersects the graph. The L58 pipeline audits the composed field from actual mouth to mouth
|
||||
with a 0.34 m by 1.76 m capsule, per-station support, 2.2 m clearance and a 15 degree grade limit;
|
||||
it also checks the orphan control, brute-force interval samples and the composed wall-foot fillet.
|
||||
The Linux58 report contains the measured reachability, surface, peer-determinism, timing and capture
|
||||
results; the code path is not promoted to legacy Maze/VerticalShafts generation.
|
||||
|
||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||
|
||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||
|
||||
@@ -30,7 +30,7 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
||||
| Term | Meaning |
|
||||
|------|---------|
|
||||
| Legacy chunk | 32×32×32 voxels (`CHUNK_SIZE`). At the legacy 12.5 cm voxel size it spans 4 m; it is not the strate-layout unit or the Core tile. |
|
||||
| Strate-layout chunk | World-level `StrateLayoutChunkSizeMeters`, default 8 m, shared by all generators and independent of voxel size. The director resolves it once and Core reads the resolved snapshot. At 25 cm it is 32 Core voxels; at 20 cm it is 40 Core voxels. |
|
||||
| Strate-layout chunk | World-level `StrateLayoutChunkSizeMeters`, default 8 m, shared by all generators and independent of voxel size. `UVoxelStrateManager::Initialize` resolves it to integer micrometres; slot bounds, heights, gap and transition widths use those physical values. Core derives the common `LayoutChunkWidthSubvoxels` from that same width. At 25 cm it is 32 Core voxels; at 20 cm it is 40 Core voxels. |
|
||||
| Core tile | Internal 32×32×32 Core voxel tile. Its metre size follows the Core voxel size and stays separate from the configurable layout chunk. |
|
||||
| Density | Scalar field. `< 0` solid, `> 0` air, `0` = surface (`IsoLevel`). |
|
||||
| Strate | Vertical layer of the world, stacked downward. Has its own generator + params. |
|
||||
@@ -70,8 +70,10 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
||||
(RealtimeMeshComponent)
|
||||
```
|
||||
|
||||
`UVoxelStrateManager` is the side oracle: "what strate is at this Z, what params,
|
||||
what generator type, and is there a passage/elevator SDF near here?"
|
||||
`UVoxelStrateManager` owns resolved half-open metre intervals (`[bottom, top)`) and is the side
|
||||
oracle: "what strate is at this Z, what params, what generator type, and is there a passage/elevator
|
||||
SDF near here?" Integer chunk bounds on each slot are compatibility projections for existing chunk
|
||||
dispatch APIs.
|
||||
|
||||
---
|
||||
|
||||
@@ -168,13 +170,15 @@ remains `0`, and lateral regions remain gated off.
|
||||
| `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. |
|
||||
| `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, honored transition settings, deterministic per-column transition bands, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.42, §8.46. |
|
||||
| `ResolvedLayoutStratePassageCapabilitiesV1::{hasNativeCaveField,hasNativeLatticeField}`, `IsNativeLatticeFieldStratum`, `HasNativeGraphField` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit per-strate native graph capability; `Core.Lattice.PrimitiveField` is restricted to Maze/VerticalShafts recipe version 1. Lattice seeds include world seed, strate, recipe version and stable feature ID; explicit polyline records keep their endpoints and receive conservative bounds. §8.54. |
|
||||
| `FCavernRuntimeFeature::FSegmentNode`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | A deterministic nested segment BVH accelerates native graph distance and conservative interval evaluation; the outer graph BVH still supplies tile candidates and protects `isTileShapeFree`. The 0.4 m feature-union width is separate from the floor fillet width. §8.54. |
|
||||
| `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. |
|
||||
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval.
|
||||
| Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. |
|
||||
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
|
||||
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; schema-v1 owner-asset export check and BuildHost Core actor tests. `-coreworldassembly=PATH` supplies a reviewed manifest transiently to `-coreworldtest=1`; it does not save a data asset. `FStandaloneCoreAdapterScheduler` runs Core work at 1/8/16 workers; `VerifyStandaloneCoreAdapter` checks hashes and walkability. Existing bounded render/`VF_` walk/OBJ orchestration and research options remain editor/commandlet-only and are not linked by Game/Shipping. |
|
||||
| `Source/VoxelForge/Public/VoxelSettings.h` / `Private/VoxelSettings.cpp` | `UVoxelSettings` owns optional canonical world-assembly JSON and its Core digest. The editor file picker plus `ImportWorldAssemblyManifestJson` imports the reviewed manifest into the cooked settings asset and validates the recipe seed against the effective settings seed. |
|
||||
| `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Validates the optional manifest against each resolved `SurfaceWorld` slot (unique strate index and exact top/bottom chunks), then exports canonical `resolved_layout` separately from `world_assembly` and its hash. The layout digest does not alter recipe hashes. Invalid plans fail Core initialization. |
|
||||
| `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Resolves Core's common `LayoutChunkWidthSubvoxels` from the manager's physical layout width; builds SurfaceWorld/FloatingIslands bounds, L29 bands and passage settings from resolved micrometre slot fields; keeps owner/Core ratios in the legacy FloatingIslands parameter adapter. Chunk fields validate imported plans as compatibility projections. Exports canonical `resolved_layout` separately from `world_assembly` and its hash. Invalid plans fail Core initialization. |
|
||||
| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c through step 2f. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches dynamically so surface-heavy work balances across workers; steps 2e and 2f use 4³ proof bricks and stage each evaluated brick before copying contiguous X rows, while step 2d retains 8³. Each skip requires a macro Lipschitz bound plus the complete final-detail supremum; uncertain bricks retain every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. |
|
||||
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. |
|
||||
| `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2f parameterized field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. |
|
||||
@@ -598,13 +602,16 @@ content lists, audio, `GameplayTags`(393). EditConditions show/hide param groups
|
||||
type.
|
||||
|
||||
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:152).
|
||||
Maps depth→strate at runtime; owns passages.
|
||||
Resolves each slot's physical metre interval, layout gap, transition width, world seed and passage
|
||||
settings during `Initialize`, then owns passages. World-Z APIs query the physical intervals; legacy
|
||||
chunk APIs use the cached integer projection.
|
||||
- `FVoxelPassage` (h:46): standing endpoints, radius, type, wandering control points, and
|
||||
upper/lower local landing descriptors (room, floor, and door; no root connector).
|
||||
- `FStrateSlot` (h:128): definition + chunk-Z range + index.
|
||||
- `FStrateSlot` (h:128): content definition, resolved micrometre bounds/height, resolved transition
|
||||
and passage inputs, and compatibility chunk-Z projection + strate index.
|
||||
| Method | .cpp line | Role |
|
||||
|--------|-----------|------|
|
||||
| `Initialize` | 1164 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
|
||||
| `Initialize` | 1186 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
|
||||
| `GeneratePassages` | 1659 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized local room/floor. The origin landing is reserved for the future shaft; no landing-to-root road is generated. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. |
|
||||
| `EvaluateModifierSDF` | 2643 | SDF of passage tubes plus local landing rooms at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. `FPassageEvaluationCache::FloorProjections` reuses native/generic floor projections and walkable-air results once per `(sample, passage)` using exact double XYZ equality and unchanged float outputs; `voxel.FloorRound1PassageProjectionCache=0` disables only this A/B cache. §8.10. |
|
||||
| `ApplyPassageModifier` | 2772 | Shared legacy/op-stack passage post: carves tube and local landing air through the idempotent landing primitive, then restores the local landing support slab in internal positive-solid density. |
|
||||
@@ -612,18 +619,18 @@ Maps depth→strate at runtime; owns passages.
|
||||
| `ApplyPassageLandingFloorMC` | 2983 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. |
|
||||
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
||||
| `AnyPassageLandingFloorNearBox` | — | Conservative local-landing-floor AABB guard. It kills the `AllAir` hypothesis wherever a proved landing floor can occur; the op-stack returns `Both` for the same box. |
|
||||
| `FindSlotIndexForChunkZ` | 6111 | Z → layout index. |
|
||||
| `GetStrateAt` / `GetStrateIndex` | 6127 / 6139 | World-Z queries. |
|
||||
| `FindSlotIndexForChunkZ` / `FindSlotIndexForWorldZCentimetres` | 6239 / 6251 | Chunk callers use the cached compatibility projection; world-Z callers search physical half-open micrometre intervals. |
|
||||
| `GetStrateAt` / `GetStrateIndex` | 6276 / 6287 | World-Z queries against half-open micrometre intervals. |
|
||||
| `GetLayoutVersion` | h:250 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
|
||||
| `GetStrateForChunk` | 6150 | Chunk → definition. |
|
||||
| `GetGeneratorTypeForChunk` | 6176 | Chunk → generator type. |
|
||||
| `UsesOperatorStackForChunk` | 6203 | Chunk → does its slot have an operator stack? True for any in-layout slot with a definition whose archetype is one of the 8 (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater); false outside the layout. **That list is written down here and nowhere else.** `ClassifyTile` guards on it; `GetDensityAt` uses it to share TunnelNetwork stacks by chunk Z. |
|
||||
| `GetStrateForChunk` | 6298 | Chunk → definition via the compatibility projection. |
|
||||
| `GetGeneratorTypeForChunk` | 6324 | Chunk → generator type via the compatibility projection. |
|
||||
| `UsesOperatorStackForChunk` | 6351 | Chunk → does its slot have an operator stack? True for any in-layout slot with a definition whose archetype is one of the 8 (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater); false outside the layout. **That list is written down here and nowhere else.** `ClassifyTile` guards on it; `GetDensityAt` uses it to share TunnelNetwork stacks by chunk Z. |
|
||||
| `GetRecipeForChunk` | — | Runtime immutable recipe/vector/strate-seed copy for cooked seasons; editor slot overrides use the same worker hand-off. |
|
||||
| `GetSlabParamsForChunk` | 6263 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
||||
| `GetSlabParamsForChunk` | 6415 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
||||
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
|
||||
| `GetGenerationParams` | 6509 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
||||
| `GetGenerationParams` | 6661 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
||||
| `SetComposerOverrideForStrate` / `GetComposerOverrideForChunk` | editor-only | Temporary density-only candidate overlay for one existing slot. Copies immutable params/recipe into the worker refetch path, leaves layout/content/passages intact, and bumps `PassagesVersion` to invalidate generator memos. |
|
||||
| `BuildParamsFromDefinition` (static) | 6815 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
|
||||
| `BuildParamsFromDefinition` (static) | 6985 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
|
||||
|
||||
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
|
||||
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
|
||||
@@ -1250,11 +1257,72 @@ ARCHITECTURE §8.47 and
|
||||
| `VF_FieldPassageWallRelief` | `Standalone/Core/src/VoxelForgeFieldShared.h` | Supplies passage-only irregular wall relief at rotated 43/29/19 m scales. Other `VF_FieldSurfaceRelief` users stay unchanged. |
|
||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Catmull–Rom smooths eligible interior sweep joins, applies passage-only wall relief, and tapers/grains the entry support shoulder below the capsule/support probe footprint. |
|
||||
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Gives each Cascade turn a seeded shallow radial profile instead of identical repeated full-depth cosine lobes. |
|
||||
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples field wall radius at 0.125 m spacing, scans periods 0.5–8 m in 32 m windows, and rejects amplitudes ≥0.035 m. The negative control uses 2 m radius spheres at 1.5 m spacing. |
|
||||
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples wall radii and walk-floor zero crossings at 0.125 m spacing, scans periods 0.5–8 m in 32 m windows, and rejects wall/floor amplitudes ≥0.035/0.015 m. Sphere and beaded-floor controls must be detected. |
|
||||
|
||||
`WorldAssemblyEvaluatorVersion=21`. See ARCHITECTURE §8.48 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
||||
|
||||
### 3.32 Walk-floor washboard and FI mouth blend — World Assembler M3 L36
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Extends eligible tread sections by up to 2 m and uses bounded signed-distance support on every sparse route, including SurfaceWorld-to-TunnelNetwork, removes seeded shoulder grain, and seats regular tread tops 5 cm lower with a smooth return at the lower landing. Capsule/open-air gates remain. Gravity-facing wall relief is reduced; side and ceiling relief remain. |
|
||||
| `ApplyResolvedStratePassageCarves` mouth support | `Standalone/Core/src/ShapeWorldInternal.h` | Builds exact signed-distance route-mouth tread boxes and smooth-unions them with native terrain over 1.5 m. |
|
||||
| `NativeFloatingIslandDensityAt` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies a local 2 m symmetric signed-field filter around FI passage mouths, exempts resolved route support, and removes isolated negative samples thinner than 2 m on all axes. |
|
||||
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Keeps the wall-radius audit and adds walk-floor zero-crossing profiles at 0.125 m, with five 32 m windows and a 0.015 m limit plus a beaded-floor negative control. |
|
||||
| `RunMouthFragmentAuditL36.py`, `ProbeWalkFloorProfileL36.py` | `Standalone/Research/WorldAssemblerM3/Linux36/` | Compare L35 floor and FI-mouth controls with L36; the mouth check fails on detached components at or above 0.0001 m² and checks normal jumps across the fade ring. |
|
||||
|
||||
`WorldAssemblyEvaluatorVersion=23`. See ARCHITECTURE §8.49 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
||||
|
||||
### 3.33 Immutable field binding — World Assembler M3 L40
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `FSurfaceWorldFieldAdapter`, `FFloatingIslandFieldAdapter`, `FLegacyGeneratorFieldAdapter`, `FGeneratorFieldAdapterV1` | `Standalone/Core/src/ShapeWorldInternal.h` | Concrete, immutable density adapters with one shared `densityAt(x,y,z,out)` call shape; the variant avoids per-sample virtual dispatch. FI retains its fixed-point entry point and prebound legacy boundary parameters. |
|
||||
| `FBoundStratumField`, `BindResolvedStratumFields`, `FindBoundStratumForChunk` | `Standalone/Core/src/ShapeWorldInternal.h` | Bind a resolved strate's plan, evaluator and chunk interval once during world initialization; native field and classification queries select this table. |
|
||||
| `NativeSurfaceDensityAt`, `NativeFloatingIslandDensityAt`, `ClassifyBoxDetailed` | `Standalone/Core/src/ShapeWorldInternal.h` | Consume the prebound adapters and plan pointers instead of looking up strate type/parameters through the manager per query. |
|
||||
|
||||
Hash parity is unchanged from L37/L39; see ARCHITECTURE §8.50 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux40/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux40/REPORT.md).
|
||||
|
||||
### 3.34 Resolved boundary and landing capabilities — World Assembler M3 L42
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `ResolvedLayoutStratePassageCapabilitiesV1`, `ResolvedLayoutInterfaceCapabilitiesV1` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Runtime-only resolved records for surface/island landing, passage-column anchoring, the FI Gradient air gap, and SW-to-TunnelNetwork support. They are derived during layout finalization from fields already included in the canonical layout identity. |
|
||||
| `ResolvedLayoutSnapshotV3::finalize` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Resolves those capabilities once before sampling; canonical JSON and stable layout IDs stay unchanged. |
|
||||
| `FStandaloneShapeWorld::ApplyResolvedStrateBoundaryBand`, `ApplyResolvedStratePassageCarves`, `BuildPassageVoidCrossingPlan`, `PassageBridgeSignedDistance` | `Standalone/Core/src/ShapeWorldInternal.h` | Consume resolved capability records. Generator-kind selection remains only in `BindResolvedStratumFields`, where it binds the concrete evaluator once. |
|
||||
|
||||
Owner hashes match L41; see ARCHITECTURE §8.52 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux42/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux42/REPORT.md).
|
||||
|
||||
### 3.35 Core landmark SDF stamps — World Assembler M3 L43
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `ResolvedLayoutStampRequestV1`, `ResolvedLayoutStampV1`, `ResolvedSdfStampKindV1` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Version-1 bounded stamp vocabulary and integer-micrometre request/resolved records shared by all Core generators. |
|
||||
| `ResolveResolvedLayoutStampsV1`, `AuditResolvedLayoutStampPlacementV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Seed/strate/stable-ID placement with bounded retries; rejects passage-mouth, walk-corridor, content-face, ceiling and bedrock conflicts. Empty requests preserve the old canonical layout payload. |
|
||||
| `BuildStampSpatialIndex`, `ApplyResolvedSdfStamps`, `StampSdf` | `Standalone/Core/src/ShapeWorldInternal.h` | BVH-built conservative bounds select stamps once per tile; shared density composition applies smooth carve/fill SDFs with seeded value-noise relief. Box classification and native-only readiness decline boxes touched by a stamp. |
|
||||
| `--landmark-chamber-demo` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Opt-in owner-world recipe: one SW ellipsoid chamber, arched entrance and low fill dais, placed beside the passage arrival. Off by default. |
|
||||
|
||||
See ARCHITECTURE §8.53 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md)
|
||||
for layout identities, owner hashes, outside-box timing, audits and captures.
|
||||
|
||||
### 3.36 Native lattice field — World Assembler M3 R26 slice 1 (Linux L58)
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed`, `ResolvedCavernRoomV1::unionWidthMicrometres`, `ResolvedCavernEdgeV1::unionWidthMicrometres` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h` | Versioned Maze/VerticalShafts opt-in and deterministic seed from `(seed, strate, recipe version, stable ID)`; optional union width preserves prior cave JSON when unset. |
|
||||
| `MakeNativeLatticeSpine`, `--recipe=lattice`, `--lattice-strate=N` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Resolves actual adjacent mouths, builds a mandatory four-turn helix and maze spine, then adds two seeded dead ends; `--lattice-orphan-control` moves the helix away from both junctions. |
|
||||
| `AuditNativeLatticeField` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the final field along the mouth-to-mouth capsule route, checking support, headroom, grade, orphan rejection, composed fillet, interval signs and feature-tile exclusion. |
|
||||
| `BuildLatticeFixtureL58.py`, `CaptureNativeLatticeL58.py`, `AuditLatticeSurfaceL58.py` | `Standalone/Research/WorldAssemblerM3/Linux58/` | Isolated Maze strate fixture, four reviewed captures and the L54 whole-surface triangle audit adapted to explicit native polylines. |
|
||||
|
||||
See ARCHITECTURE §8.54 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md)
|
||||
for resolved records, final-field reachability, controls, timing, hashes and captures.
|
||||
|
||||
## 4. The density pipeline (most-edited hot path)
|
||||
|
||||
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||
|
||||
@@ -4,6 +4,27 @@
|
||||
|
||||
namespace VoxelClipmapDesiredTiles
|
||||
{
|
||||
FORCEINLINE int64 FloorDivWide(int64 Value, int64 Divisor)
|
||||
{
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int64 Quotient = Value / Divisor;
|
||||
const int64 Remainder = Value % Divisor;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE int64 CeilDivWide(int64 Value, int64 Divisor)
|
||||
{
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int64 Quotient = Value / Divisor;
|
||||
const int64 Remainder = Value % Divisor;
|
||||
return Remainder > 0 ? Quotient + 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE int32 ClampInt32(int64 Value)
|
||||
{
|
||||
return static_cast<int32>(FMath::Clamp<int64>(Value, MIN_int32, MAX_int32));
|
||||
}
|
||||
|
||||
struct FParameters
|
||||
{
|
||||
int32 ClipRadius = 3;
|
||||
@@ -13,7 +34,10 @@ namespace VoxelClipmapDesiredTiles
|
||||
|
||||
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
|
||||
{
|
||||
return Value >= 0 ? (Value / Divisor) : -(((-Value) + Divisor - 1) / Divisor);
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int32 Quotient = Value / Divisor;
|
||||
const int32 Remainder = Value % Divisor;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
|
||||
@@ -24,7 +48,10 @@ namespace VoxelClipmapDesiredTiles
|
||||
|
||||
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
|
||||
{
|
||||
return -FloorDiv(-Value, Divisor);
|
||||
if (Divisor <= 0) { return 0; }
|
||||
const int32 Quotient = Value / Divisor;
|
||||
const int32 Remainder = Value % Divisor;
|
||||
return Remainder > 0 ? Quotient + 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
|
||||
@@ -33,10 +60,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
||||
int32& OutAxisMin, int32& OutAxisMax)
|
||||
{
|
||||
const int32 BaseMin = CenterAxis - Radius;
|
||||
const int32 BaseEnd = CenterAxis + Radius + 1;
|
||||
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment;
|
||||
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1;
|
||||
const int64 SafeRadius = FMath::Max<int64>(0, Radius);
|
||||
const int64 SafeAlignment = FMath::Max<int64>(1, Alignment);
|
||||
const int64 BaseMin = static_cast<int64>(CenterAxis) - SafeRadius;
|
||||
const int64 BaseEnd = static_cast<int64>(CenterAxis) + SafeRadius + 1;
|
||||
OutAxisMin = ClampInt32(FloorDivWide(BaseMin, SafeAlignment) * SafeAlignment);
|
||||
OutAxisMax = ClampInt32(CeilDivWide(BaseEnd, SafeAlignment) * SafeAlignment - 1);
|
||||
};
|
||||
AlignAxis(Center.X, OutMin.X, OutMax.X);
|
||||
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
||||
@@ -49,8 +78,13 @@ namespace VoxelClipmapDesiredTiles
|
||||
{
|
||||
return Radius;
|
||||
}
|
||||
return FMath::Max(Radius,
|
||||
(Parameters.RenderDistanceChunks + (1 << MaxLevel) - 1) >> MaxLevel);
|
||||
const int32 SafeLevel = FMath::Clamp(MaxLevel, 0, 8);
|
||||
const int64 Scale = static_cast<int64>(uint32{1} << static_cast<uint32>(SafeLevel));
|
||||
const int64 Numerator = static_cast<int64>(Parameters.RenderDistanceChunks)
|
||||
+ Scale - 1;
|
||||
const int64 DesiredRadius = Numerator / Scale;
|
||||
return static_cast<int32>(FMath::Clamp<int64>(
|
||||
FMath::Max<int64>(Radius, DesiredRadius), 0, MAX_int32));
|
||||
}
|
||||
|
||||
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
|
||||
@@ -73,12 +107,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
return;
|
||||
}
|
||||
|
||||
OutMinChunkZ = CenterChunkZ - ViewDistanceDown;
|
||||
OutMaxChunkZ = CenterChunkZ + ViewDistanceUp;
|
||||
OutMinChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) - ViewDistanceDown);
|
||||
OutMaxChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) + ViewDistanceUp);
|
||||
if (bHasStrate)
|
||||
{
|
||||
OutMinChunkZ = StrateBottomChunkZ - StrateViewMarginChunks;
|
||||
OutMaxChunkZ = StrateTopChunkZ + StrateViewMarginChunks;
|
||||
OutMinChunkZ = ClampInt32(static_cast<int64>(StrateBottomChunkZ) - StrateViewMarginChunks);
|
||||
OutMaxChunkZ = ClampInt32(static_cast<int64>(StrateTopChunkZ) + StrateViewMarginChunks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +128,7 @@ namespace VoxelClipmapDesiredTiles
|
||||
|
||||
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
||||
{
|
||||
const int32 Pow = 1 << Level;
|
||||
const int32 Pow = static_cast<int32>(uint32{1} << static_cast<uint32>(Level));
|
||||
const FIntVector CenterLevel = FloorDiv(Center, Pow);
|
||||
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
||||
|
||||
@@ -105,24 +139,23 @@ namespace VoxelClipmapDesiredTiles
|
||||
FIntVector RingMin, RingMax;
|
||||
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
||||
|
||||
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
|
||||
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
|
||||
int64 TileMinZ = RingMin.Z;
|
||||
int64 TileMaxZ = RingMax.Z;
|
||||
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
||||
{
|
||||
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
|
||||
DeltaZMax = FMath::Min(DeltaZMax, FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z);
|
||||
TileMinZ = FMath::Max<int64>(TileMinZ, FloorDiv(MinChunkZ, Pow));
|
||||
TileMaxZ = FMath::Min<int64>(TileMaxZ, FloorDiv(MaxChunkZ, Pow));
|
||||
}
|
||||
|
||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
||||
for (int32 DeltaY = RingMin.Y - CenterLevel.Y;
|
||||
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY)
|
||||
for (int32 DeltaX = RingMin.X - CenterLevel.X;
|
||||
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
|
||||
for (int64 Z = TileMinZ; Z <= TileMaxZ; ++Z)
|
||||
for (int64 Y = RingMin.Y; Y <= RingMax.Y; ++Y)
|
||||
for (int64 X = RingMin.X; X <= RingMax.X; ++X)
|
||||
{
|
||||
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
|
||||
const int32 TileMinZ = Tile.Z << Level;
|
||||
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1;
|
||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
||||
const FIntVector Tile(static_cast<int32>(X), static_cast<int32>(Y),
|
||||
static_cast<int32>(Z));
|
||||
const int64 TileBottomChunkZ = static_cast<int64>(Tile.Z) * Pow;
|
||||
const int64 TileTopChunkZ = (static_cast<int64>(Tile.Z) + 1) * Pow - 1;
|
||||
if (TileTopChunkZ < MinChunkZ || TileBottomChunkZ > MaxChunkZ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -130,12 +163,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
if (Level > 0)
|
||||
{
|
||||
const bool bCovered =
|
||||
(2 * Tile.X >= PreviousRingMin.X)
|
||||
&& (2 * Tile.X + 1 <= PreviousRingMax.X)
|
||||
&& (2 * Tile.Y >= PreviousRingMin.Y)
|
||||
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y)
|
||||
&& (2 * Tile.Z >= PreviousRingMin.Z)
|
||||
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z);
|
||||
(2 * static_cast<int64>(Tile.X) >= PreviousRingMin.X)
|
||||
&& (2 * static_cast<int64>(Tile.X) + 1 <= PreviousRingMax.X)
|
||||
&& (2 * static_cast<int64>(Tile.Y) >= PreviousRingMin.Y)
|
||||
&& (2 * static_cast<int64>(Tile.Y) + 1 <= PreviousRingMax.Y)
|
||||
&& (2 * static_cast<int64>(Tile.Z) >= PreviousRingMin.Z)
|
||||
&& (2 * static_cast<int64>(Tile.Z) + 1 <= PreviousRingMax.Z);
|
||||
if (bCovered)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -559,9 +559,13 @@ int32 UVoxelContentManager::RegionSize() const
|
||||
// truncation, a negative-coord cell is queued under one region but routed back to another → its march
|
||||
// result is dropped → blank chunk. Truncation == floor for A>=0 and for R==1, which is exactly why only
|
||||
// NEGATIVE coordinates at R>=2 were affected. R (region size) is always > 0 here.
|
||||
static FORCEINLINE int32 FloorDivPos(int32 A, int32 R)
|
||||
static FORCEINLINE int32 FloorDivPos(int64 A, int32 R)
|
||||
{
|
||||
return (A >= 0) ? (A / R) : -(((-A) + R - 1) / R);
|
||||
if (R <= 0) { return 0; }
|
||||
const int64 Quotient = A / R;
|
||||
const int64 Remainder = A % R;
|
||||
return static_cast<int32>(FMath::Clamp<int64>(
|
||||
Remainder < 0 ? Quotient - 1 : Quotient, MIN_int32, MAX_int32));
|
||||
}
|
||||
|
||||
static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
|
||||
@@ -573,10 +577,10 @@ static FORCEINLINE FIntPoint CellToRegion(const FIntPoint& Cell, int32 R)
|
||||
static FORCEINLINE bool IsRegionDesired(const FIntPoint& Region, const FIntPoint& PlayerCell,
|
||||
int32 FarR, int32 R)
|
||||
{
|
||||
const int32 MinX = FloorDivPos(PlayerCell.X - FarR, R);
|
||||
const int32 MaxX = FloorDivPos(PlayerCell.X + FarR, R);
|
||||
const int32 MinY = FloorDivPos(PlayerCell.Y - FarR, R);
|
||||
const int32 MaxY = FloorDivPos(PlayerCell.Y + FarR, R);
|
||||
const int32 MinX = FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R);
|
||||
const int32 MaxX = FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R);
|
||||
const int32 MinY = FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R);
|
||||
const int32 MaxY = FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R);
|
||||
return Region.X >= MinX && Region.X <= MaxX && Region.Y >= MinY && Region.Y <= MaxY;
|
||||
}
|
||||
|
||||
@@ -1102,17 +1106,22 @@ void UVoxelContentManager::RebuildDesiredCells(FDecoGrid& G, const FIntPoint& Pl
|
||||
const int32 R = RegionSize();
|
||||
|
||||
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
|
||||
const FIntPoint RMin(FloorDivPos(PlayerCell.X - FarR, R),
|
||||
FloorDivPos(PlayerCell.Y - FarR, R));
|
||||
const FIntPoint RMax(FloorDivPos(PlayerCell.X + FarR, R),
|
||||
FloorDivPos(PlayerCell.Y + FarR, R));
|
||||
const FIntPoint RMin(FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R),
|
||||
FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R));
|
||||
const FIntPoint RMax(FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R),
|
||||
FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R));
|
||||
|
||||
TSet<FIntPoint> DesiredRegions;
|
||||
DesiredRegions.Reserve((RMax.X - RMin.X + 1) * (RMax.Y - RMin.Y + 1));
|
||||
for (int32 ry = RMin.Y; ry <= RMax.Y; ++ry)
|
||||
for (int32 rx = RMin.X; rx <= RMax.X; ++rx)
|
||||
constexpr int64 MaxRegionReserve = 4096;
|
||||
const int64 WidthX = static_cast<int64>(RMax.X) - RMin.X + 1;
|
||||
const int64 WidthY = static_cast<int64>(RMax.Y) - RMin.Y + 1;
|
||||
const int64 ReserveCount = WidthY > 0 && WidthX > MaxRegionReserve / WidthY
|
||||
? MaxRegionReserve : FMath::Max<int64>(0, WidthX * WidthY);
|
||||
DesiredRegions.Reserve(static_cast<int32>(ReserveCount));
|
||||
for (int64 ry = RMin.Y; ry <= RMax.Y; ++ry)
|
||||
for (int64 rx = RMin.X; rx <= RMax.X; ++rx)
|
||||
{
|
||||
DesiredRegions.Add(FIntPoint(rx, ry));
|
||||
DesiredRegions.Add(FIntPoint(static_cast<int32>(rx), static_cast<int32>(ry)));
|
||||
}
|
||||
|
||||
// Unload loaded regions no longer desired (plain DestroyComponent — no per-instance removal).
|
||||
@@ -1464,8 +1473,15 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u));
|
||||
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
|
||||
|
||||
const int32 Span = FMath::Max(0, Comp.CountMax - Comp.CountMin);
|
||||
const int32 Count = Comp.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(CH ^ 0x1234u)) * (float)(Span + 1));
|
||||
const int64 Span = FMath::Max<int64>(0,
|
||||
static_cast<int64>(Comp.CountMax) - Comp.CountMin);
|
||||
const uint64 CountRange = static_cast<uint64>(Span) + 1u;
|
||||
const float CountOffset = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(CH ^ 0x1234u)) * static_cast<float>(CountRange);
|
||||
const int64 CountWide = static_cast<int64>(Comp.CountMin)
|
||||
+ static_cast<int64>(CountOffset);
|
||||
const int32 Count = static_cast<int32>(FMath::Clamp<int64>(
|
||||
CountWide, MIN_int32, MAX_int32));
|
||||
for (int32 ii = 0; ii < Count; ++ii)
|
||||
{
|
||||
if (CompBudget <= 0) break;
|
||||
@@ -1514,8 +1530,15 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu));
|
||||
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
|
||||
|
||||
const int32 SubSpan = FMath::Max(0, Sub.CountMax - Sub.CountMin);
|
||||
const int32 SubCount = Sub.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(SH ^ 0x1234u)) * (float)(SubSpan + 1));
|
||||
const int64 SubSpan = FMath::Max<int64>(0,
|
||||
static_cast<int64>(Sub.CountMax) - Sub.CountMin);
|
||||
const uint64 SubCountRange = static_cast<uint64>(SubSpan) + 1u;
|
||||
const float SubCountOffset = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(SH ^ 0x1234u)) * static_cast<float>(SubCountRange);
|
||||
const int64 SubCountWide = static_cast<int64>(Sub.CountMin)
|
||||
+ static_cast<int64>(SubCountOffset);
|
||||
const int32 SubCount = static_cast<int32>(FMath::Clamp<int64>(
|
||||
SubCountWide, MIN_int32, MAX_int32));
|
||||
for (int32 sj = 0; sj < SubCount; ++sj)
|
||||
{
|
||||
if (CompBudget <= 0) break;
|
||||
|
||||
@@ -155,9 +155,11 @@ int32 UVoxelDensityVolume::NumLevels() const
|
||||
|
||||
FORCEINLINE int32 UVoxelDensityVolume::FloorDiv(int32 A, int32 B)
|
||||
{
|
||||
// True floor division (B > 0). FMath::DivideAndRoundDown truncates toward zero for negatives —
|
||||
// a footgun the content manager hit too (see FloorDivPos there). Cells span the origin, so floor.
|
||||
return (A >= 0) ? (A / B) : -(((-A) + B - 1) / B);
|
||||
// True floor division (B > 0). Remainder adjustment handles INT_MIN without negating it.
|
||||
if (B <= 0) { return 0; }
|
||||
const int32 Quotient = A / B;
|
||||
const int32 Remainder = A % B;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
}
|
||||
|
||||
FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity)
|
||||
@@ -180,7 +182,8 @@ void UVoxelDensityVolume::EnsureAllocated()
|
||||
for (int32 L = 0; L < N; ++L)
|
||||
{
|
||||
FClipLevel& Lv = Levels[L];
|
||||
Lv.Step = 1 << L;
|
||||
Lv.Step = static_cast<int32>(uint32{1}
|
||||
<< static_cast<uint32>(FMath::Clamp(L, 0, 8)));
|
||||
Lv.OriginCells = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||
Lv.bHasData = false;
|
||||
Lv.Density.SetNumZeroed(Count); // start all-air (0)
|
||||
@@ -632,7 +635,8 @@ void UVoxelDensityVolume::ProcessOneFill(const FPendingFill& F)
|
||||
if (!Gen) return;
|
||||
if (bShuttingDown.load(std::memory_order_relaxed) || bFillThreadStop.load(std::memory_order_relaxed)) return;
|
||||
|
||||
const int32 Step = 1 << F.Level;
|
||||
const int32 Step = static_cast<int32>(uint32{1}
|
||||
<< static_cast<uint32>(FMath::Clamp(F.Level, 0, 8)));
|
||||
FFillResult R;
|
||||
R.Level = F.Level;
|
||||
R.Epoch = F.Epoch;
|
||||
|
||||
@@ -107,6 +107,14 @@ std::int64_t WorldAssemblyRoundRatio(std::int64_t Numerator, std::int64_t Denomi
|
||||
return -((-Numerator + Denominator / 2) / Denominator);
|
||||
}
|
||||
|
||||
std::int64_t WorldAssemblyMicrometresToSubvoxels(
|
||||
std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres)
|
||||
{
|
||||
if (CoreVoxelMicrometres == 0) return 0;
|
||||
return WorldAssemblyRoundRatio(Micrometres * WorldAssemblySubvoxelsPerVoxel,
|
||||
static_cast<std::int64_t>(CoreVoxelMicrometres));
|
||||
}
|
||||
|
||||
std::uint64_t WorldAssemblyRoundedSqrt(std::uint64_t Value)
|
||||
{
|
||||
std::uint64_t Root = 0;
|
||||
@@ -312,17 +320,18 @@ VoxelForgeCore::WorldAssemblyBiomeProfileV1 BuildSurfaceProfile(
|
||||
bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
const FStrateSlot& Slot,
|
||||
VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum, FString& OutError)
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum,
|
||||
std::uint32_t CoreVoxelMicrometres, FString& OutError)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
|
||||
const FSurfaceGenerationParams Surface = StrateManager->GetSurfaceParamsForChunk(Chunk);
|
||||
const FBiomeContext BiomeContext = StrateManager->GetBiomeContextForChunk(Chunk);
|
||||
const UVoxelStrateDefinition* Definition = Slot.Definition;
|
||||
const double LayoutChunkVoxels = static_cast<double>(Recipe.layoutChunkWidthSubvoxels)
|
||||
/ WorldAssemblySubvoxelsPerVoxel;
|
||||
const double BottomVoxel = static_cast<double>(Slot.BottomChunkZ) * LayoutChunkVoxels;
|
||||
const double HeightVoxels = static_cast<double>(Slot.TopChunkZ - Slot.BottomChunkZ + 1) * LayoutChunkVoxels;
|
||||
const double BottomVoxel = static_cast<double>(Slot.BottomWorldMicrometres)
|
||||
/ std::max<std::uint32_t>(1, CoreVoxelMicrometres);
|
||||
const double HeightVoxels = static_cast<double>(Slot.HeightMicrometres)
|
||||
/ std::max<std::uint32_t>(1, CoreVoxelMicrometres);
|
||||
const FBiomeMapParams& Map = BiomeContext.Map;
|
||||
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||
Recipe.biomeCellSizeSubvoxels = WorldAssemblyFixed(Map.CellSize);
|
||||
@@ -454,9 +463,12 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
return false;
|
||||
}
|
||||
Shelf.minimumSubvoxels = {WorldAssemblyFixed(MinX), WorldAssemblyFixed(MinY),
|
||||
std::max(WorldAssemblyFixed(BottomVoxel), LowestGround - WorldAssemblyFixed(4.0))};
|
||||
std::max(WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.BottomWorldMicrometres, CoreVoxelMicrometres),
|
||||
LowestGround - WorldAssemblyFixed(4.0))};
|
||||
Shelf.maximumSubvoxels = {WorldAssemblyFixed(MaxX), WorldAssemblyFixed(MaxY),
|
||||
std::min(WorldAssemblyFixed((Slot.TopChunkZ + 1) * LayoutChunkVoxels),
|
||||
std::min(WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.TopWorldMicrometres, CoreVoxelMicrometres),
|
||||
HighestGround + WorldAssemblyFixed(ShelfHeight + 4.0))};
|
||||
Shelf.parametersSubvoxels = {WorldAssemblyFixed(DirX), WorldAssemblyFixed(DirY),
|
||||
WorldAssemblyFixed(ShelfHeight), WorldAssemblyFixed(Reach)};
|
||||
@@ -479,13 +491,14 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
using namespace VoxelForgeCore;
|
||||
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
|
||||
const FFloatingIslandParams Params = StrateManager->GetFloatingIslandParamsForChunk(Chunk);
|
||||
const FStratePassageConfig& Passage = Slot.Definition->PassageConfig;
|
||||
const FStratePassageConfig& Passage = Slot.ResolvedPassageConfig;
|
||||
WorldAssemblyFloatingIslandPlanV1 IslandPlan;
|
||||
IslandPlan.strateIndex = Slot.StrateIndex;
|
||||
IslandPlan.topChunkZ = Slot.TopChunkZ;
|
||||
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
|
||||
auto& Recipe = IslandPlan.manifest.recipe;
|
||||
Recipe.geometryVersion = 8;
|
||||
Recipe.terrainFollowingRoute = static_cast<std::uint32_t>(Passage.Style) == 0;
|
||||
Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels;
|
||||
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||
Recipe.ownerVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
||||
@@ -499,10 +512,10 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
}
|
||||
Recipe.ownerToCoreScaleNumerator = Recipe.ownerVoxelSizeMicrons / ScaleGcd;
|
||||
Recipe.ownerToCoreScaleDenominator = Recipe.coreVoxelSizeMicrons / ScaleGcd;
|
||||
Recipe.layoutMinimumZSubvoxels = static_cast<std::int64_t>(Slot.BottomChunkZ)
|
||||
* LayoutChunkWidthSubvoxels;
|
||||
Recipe.layoutMaximumZSubvoxels = static_cast<std::int64_t>(Slot.TopChunkZ + 1)
|
||||
* LayoutChunkWidthSubvoxels;
|
||||
Recipe.layoutMinimumZSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.BottomWorldMicrometres, Recipe.coreVoxelSizeMicrons);
|
||||
Recipe.layoutMaximumZSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.TopWorldMicrometres, Recipe.coreVoxelSizeMicrons);
|
||||
Recipe.layoutCenterTopZSubvoxels = (Recipe.layoutMinimumZSubvoxels
|
||||
+ Recipe.layoutMaximumZSubvoxels) / 2;
|
||||
Recipe.islandDensityQ16 = WorldAssemblyQ16(FMath::Clamp(Params.IslandDensity, 0.0f, 1.0f));
|
||||
@@ -523,20 +536,35 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||
*WorldAssemblySubvoxelsPerVoxel/Recipe.coreVoxelSizeMicrons));
|
||||
};
|
||||
Recipe.anchorIslandOrdinal=Recipe.primaryIslandCount-1;
|
||||
Recipe.anchorPlacementRadiusSubvoxels=OwnerVoxelsToCoreQ(Passage.DistanceMax);
|
||||
const std::int64_t spiralRadius=OwnerVoxelsToCoreQ(Passage.SpiralRadius);
|
||||
Recipe.anchorPlacementRadiusSubvoxels=WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageDistanceMaximumMicrometres, Recipe.coreVoxelSizeMicrons);
|
||||
const std::int64_t spiralRadius=WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageSpiralRadiusMicrometres, Recipe.coreVoxelSizeMicrons);
|
||||
// Reserve at least a 1.5 m mid-bore through the anchor root.
|
||||
constexpr double AnchorPassageMinimumBoreOwnerVoxels=12.0;
|
||||
const std::int64_t mouthBore=std::max(OwnerVoxelsToCoreQ(Passage.MouthRadius),
|
||||
OwnerVoxelsToCoreQ(12.0));
|
||||
const std::int64_t bore=std::max(OwnerVoxelsToCoreQ(Passage.MidRadius),
|
||||
OwnerVoxelsToCoreQ(AnchorPassageMinimumBoreOwnerVoxels));
|
||||
const std::int64_t wallReserve=OwnerVoxelsToCoreQ(12.0);
|
||||
Recipe.anchorMouthOffsetSubvoxels=spiralRadius+mouthBore+OwnerVoxelsToCoreQ(4.0);
|
||||
const std::int64_t excursion=std::max(OwnerVoxelsToCoreQ(Passage.Wander),spiralRadius);
|
||||
const std::int64_t OwnerVoxelMicrometres = Recipe.ownerVoxelSizeMicrons;
|
||||
const std::int64_t mouthBore=std::max(
|
||||
WorldAssemblyMicrometresToSubvoxels(Slot.PassageMouthRadiusMicrometres,
|
||||
Recipe.coreVoxelSizeMicrons),
|
||||
WorldAssemblyMicrometresToSubvoxels(12 * OwnerVoxelMicrometres,
|
||||
Recipe.coreVoxelSizeMicrons));
|
||||
const std::int64_t bore=std::max(
|
||||
WorldAssemblyMicrometresToSubvoxels(Slot.PassageMidRadiusMicrometres,
|
||||
Recipe.coreVoxelSizeMicrons),
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
static_cast<std::int64_t>(AnchorPassageMinimumBoreOwnerVoxels * OwnerVoxelMicrometres),
|
||||
Recipe.coreVoxelSizeMicrons));
|
||||
const std::int64_t wallReserve=WorldAssemblyMicrometresToSubvoxels(
|
||||
12 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons);
|
||||
const std::int64_t FourOwnerVoxelsSubvoxels=WorldAssemblyMicrometresToSubvoxels(
|
||||
4 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons);
|
||||
Recipe.anchorMouthOffsetSubvoxels=spiralRadius+mouthBore+FourOwnerVoxelsSubvoxels;
|
||||
const std::int64_t excursion=std::max(
|
||||
WorldAssemblyMicrometresToSubvoxels(Slot.PassageWanderMicrometres,
|
||||
Recipe.coreVoxelSizeMicrons), spiralRadius);
|
||||
Recipe.anchorRootRadiusSubvoxels=excursion+bore+wallReserve;
|
||||
Recipe.anchorIslandMinimumRadiusSubvoxels=std::max(Recipe.minimumRadiusSubvoxels,
|
||||
Recipe.anchorMouthOffsetSubvoxels+mouthBore+OwnerVoxelsToCoreQ(4.0));
|
||||
Recipe.anchorMouthOffsetSubvoxels+mouthBore+FourOwnerVoxelsSubvoxels);
|
||||
Recipe.anchorRootBottomZSubvoxels=Recipe.layoutMinimumZSubvoxels
|
||||
+Recipe.boundarySealSubvoxels;
|
||||
std::string PlanError;
|
||||
@@ -881,7 +909,7 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
SlotRecipe.layoutChunkWidthSubvoxels=LayoutChunkWidthSubvoxels;
|
||||
VoxelForgeCore::WorldAssemblyStratumPlanV1 Stratum;
|
||||
if (!BuildSurfaceStratumPlan(StrateManager, Slot,
|
||||
SlotRecipe, Stratum, OutError))
|
||||
SlotRecipe, Stratum, CoreVoxelMicrometres, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1047,41 +1075,47 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
return "Unknown";
|
||||
};
|
||||
const std::int64_t OwnerVoxelMicrons = Layout.ownerVoxelSizeMicrometres;
|
||||
const auto FixedToMicrons = [ExpectedCoreVoxelMicrons](std::int64_t Fixed)
|
||||
{
|
||||
return static_cast<std::int64_t>(std::llround(static_cast<double>(Fixed)
|
||||
* ExpectedCoreVoxelMicrons / VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
||||
};
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutPassageConfigV1> PassageConfigs;
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const FStratePassageConfig& AuthoredPassage = Slot.Definition->PassageConfig;
|
||||
const auto OwnerVoxelsToCoreQ = [OwnerVoxelMicrons, ExpectedCoreVoxelMicrons](double Value)
|
||||
{
|
||||
return static_cast<std::int64_t>(std::llround(Value * OwnerVoxelMicrons
|
||||
* VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel / ExpectedCoreVoxelMicrons));
|
||||
};
|
||||
const FStratePassageConfig& ResolvedPassage = Slot.ResolvedPassageConfig;
|
||||
VoxelForgeCore::ResolvedLayoutPassageConfigV1 PassageConfig;
|
||||
PassageConfig.upperStrateIndex = Slot.StrateIndex;
|
||||
PassageConfig.connections = AuthoredPassage.Connections;
|
||||
PassageConfig.style = static_cast<std::uint32_t>(AuthoredPassage.Style);
|
||||
PassageConfig.mouthRadiusSubvoxels = std::max(OwnerVoxelsToCoreQ(AuthoredPassage.MouthRadius),
|
||||
OwnerVoxelsToCoreQ(12.0));
|
||||
PassageConfig.midRadiusSubvoxels = std::max(OwnerVoxelsToCoreQ(AuthoredPassage.MidRadius),
|
||||
OwnerVoxelsToCoreQ(12.0));
|
||||
PassageConfig.reachMinimumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.ReachMin);
|
||||
PassageConfig.reachMaximumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.ReachMax);
|
||||
PassageConfig.distanceMinimumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.DistanceMin);
|
||||
PassageConfig.distanceMaximumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.DistanceMax);
|
||||
PassageConfig.wanderSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.Wander);
|
||||
PassageConfig.segments = static_cast<std::uint32_t>(FMath::Clamp(AuthoredPassage.Segments,1,48));
|
||||
PassageConfig.verticalWobbleSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.VerticalWobble);
|
||||
PassageConfig.spiralRadiusSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.SpiralRadius);
|
||||
PassageConfig.connections = ResolvedPassage.Connections;
|
||||
PassageConfig.style = static_cast<std::uint32_t>(ResolvedPassage.Style);
|
||||
PassageConfig.mouthRadiusSubvoxels = std::max(
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMouthRadiusMicrometres, ExpectedCoreVoxelMicrons),
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||
PassageConfig.midRadiusSubvoxels = std::max(
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMidRadiusMicrometres, ExpectedCoreVoxelMicrons),
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||
PassageConfig.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageReachMinimumMicrometres, ExpectedCoreVoxelMicrons);
|
||||
PassageConfig.reachMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageReachMaximumMicrometres, ExpectedCoreVoxelMicrons);
|
||||
PassageConfig.distanceMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageDistanceMinimumMicrometres, ExpectedCoreVoxelMicrons);
|
||||
PassageConfig.distanceMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageDistanceMaximumMicrometres, ExpectedCoreVoxelMicrons);
|
||||
PassageConfig.wanderSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageWanderMicrometres, ExpectedCoreVoxelMicrons);
|
||||
PassageConfig.segments = static_cast<std::uint32_t>(
|
||||
FMath::Clamp(ResolvedPassage.Segments, 1, 48));
|
||||
PassageConfig.verticalWobbleSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageVerticalWobbleMicrometres, ExpectedCoreVoxelMicrons);
|
||||
PassageConfig.spiralRadiusSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageSpiralRadiusMicrometres, ExpectedCoreVoxelMicrons);
|
||||
PassageConfig.spiralTurnsQ16 = static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(AuthoredPassage.SpiralTurns)*65536.0));
|
||||
PassageConfig.cascadeSteps = static_cast<std::uint32_t>(FMath::Clamp(AuthoredPassage.CascadeSteps,1,16));
|
||||
PassageConfig.cascadeLedgeSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.CascadeLedge);
|
||||
static_cast<double>(ResolvedPassage.SpiralTurns) * 65536.0));
|
||||
PassageConfig.cascadeSteps = static_cast<std::uint32_t>(
|
||||
FMath::Clamp(ResolvedPassage.CascadeSteps, 1, 16));
|
||||
PassageConfig.cascadeLedgeSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageCascadeLedgeMicrometres, ExpectedCoreVoxelMicrons);
|
||||
PassageConfigs.push_back(PassageConfig);
|
||||
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(
|
||||
FIntVector(0, 0, Slot.TopChunkZ));
|
||||
@@ -1095,16 +1129,13 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
: std::string("legacy.") + generatorName(Type);
|
||||
Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0;
|
||||
Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons;
|
||||
Item.interiorBottomMicrometres = static_cast<std::int64_t>(Slot.BottomChunkZ)
|
||||
* LayoutChunkSizeMicrometres;
|
||||
Item.interiorTopMicrometres = static_cast<std::int64_t>(Slot.TopChunkZ + 1)
|
||||
* LayoutChunkSizeMicrometres;
|
||||
Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres;
|
||||
Item.interiorTopMicrometres = Slot.TopWorldMicrometres;
|
||||
Item.managerChunkWidthMicrometres = LayoutChunkSizeMicrometres;
|
||||
Item.dispatchChunkWidthMicrometres = LayoutChunkSizeMicrometres;
|
||||
Item.transitionMode = transitionName(Slot.Definition->TransitionType);
|
||||
Item.transitionBlendChunks = Slot.Definition->TransitionBlendChunks;
|
||||
Item.transitionWidthMicrometres = static_cast<std::int64_t>(Item.transitionBlendChunks)
|
||||
* Item.managerChunkWidthMicrometres;
|
||||
Item.transitionMode = transitionName(Slot.ResolvedTransitionType);
|
||||
Item.transitionBlendChunks = Slot.ResolvedTransitionBlendChunks;
|
||||
Item.transitionWidthMicrometres = Slot.TransitionWidthMicrometres;
|
||||
Item.transitionWarpAmplitudeMicrometres = Item.transitionMode == "Interleaved"
|
||||
? std::min(Item.transitionWidthMicrometres / 2, Item.managerChunkWidthMicrometres) : 0;
|
||||
Item.transitionInterpenetrationMicrometres = Item.transitionMode == "Interleaved"
|
||||
@@ -1160,11 +1191,8 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
Item.queryDispatchPath = "NativeFloatingIslandDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3";
|
||||
const auto Plan = std::find_if(Manifest->floatingIslands.begin(), Manifest->floatingIslands.end(),
|
||||
[&Slot](const auto& Entry) { return Entry.strateIndex == Slot.StrateIndex; });
|
||||
std::int64_t SealMicrometres = 0;
|
||||
if (Plan != Manifest->floatingIslands.end())
|
||||
{
|
||||
Item.interiorBottomMicrometres = FixedToMicrons(Plan->manifest.recipe.layoutMinimumZSubvoxels);
|
||||
Item.interiorTopMicrometres = FixedToMicrons(Plan->manifest.recipe.layoutMaximumZSubvoxels);
|
||||
Item.recipeVersion = Plan->manifest.recipe.geometryVersion;
|
||||
for (std::size_t IslandIndex = 0; IslandIndex < Plan->manifest.plan.islands.size(); ++IslandIndex)
|
||||
{
|
||||
@@ -1314,6 +1342,8 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
}
|
||||
|
||||
const int32 Seed = StrateManager->GetWorldSeed();
|
||||
const std::uint32_t LayoutChunkSizeMicrometres =
|
||||
StrateManager->GetStrateLayoutChunkSizeMicrometres();
|
||||
const float OriginSpineRadius = Settings->GetEffectiveOriginSpineRadius();
|
||||
const float WorldRadiusVoxels = Settings->GetEffectiveWorldRadiusVoxels();
|
||||
const int32 CoreClipRadius = FMath::Clamp(ClipRadius, 1, 64);
|
||||
@@ -1331,7 +1361,7 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
SettingsValue->AsObject()->SetNumberField(TEXT("ClipRadius"), CoreClipRadius);
|
||||
SettingsValue->AsObject()->SetNumberField(TEXT("MaxClipLevel"), CoreMaxClipLevel);
|
||||
SettingsValue->AsObject()->SetNumberField(TEXT("StrateLayoutChunkSizeMeters"),
|
||||
Settings->StrateLayoutChunkSizeMeters);
|
||||
static_cast<double>(StrateManager->GetStrateLayoutChunkSizeMicrometres()) / 1000000.0);
|
||||
|
||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
Root->SetNumberField(TEXT("schema_version"), 1);
|
||||
@@ -1342,17 +1372,20 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0)));
|
||||
Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("core_voxel_size_microns"), CoreVoxelSizeMicrons);
|
||||
Root->SetNumberField(TEXT("owner_to_core_length_scale"),
|
||||
OwnerVoxelSizeCm / ResolvedCoreVoxelSizeCm);
|
||||
Root->SetNumberField(TEXT("seed"), Seed);
|
||||
Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry);
|
||||
Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||
Root->SetNumberField(TEXT("world_radius_voxels"), WorldRadiusVoxels);
|
||||
Root->SetNumberField(TEXT("edge_seal_thickness"), Settings->EdgeSealThickness);
|
||||
Root->SetNumberField(TEXT("total_strates"), StrateManager->GetNumStrates());
|
||||
Root->SetNumberField(TEXT("inter_strate_gap_chunks"), Settings->InterStrateGapChunks);
|
||||
const std::int64_t ResolvedGapMicrometres =
|
||||
StrateManager->GetResolvedInterStrateGapMicrometres();
|
||||
Root->SetNumberField(TEXT("inter_strate_gap_chunks"),
|
||||
LayoutChunkSizeMicrometres > 0
|
||||
? ResolvedGapMicrometres / LayoutChunkSizeMicrometres : 0);
|
||||
Root->SetNumberField(TEXT("inter_strate_gap_um"), ResolvedGapMicrometres);
|
||||
Root->SetNumberField(TEXT("strate_layout_chunk_size_um"),
|
||||
std::llround(Settings->StrateLayoutChunkSizeMeters*1000000.0));
|
||||
LayoutChunkSizeMicrometres);
|
||||
Root->SetNumberField(TEXT("clip_radius"), CoreClipRadius);
|
||||
Root->SetNumberField(TEXT("max_clip_level"), CoreMaxClipLevel);
|
||||
Root->SetNumberField(TEXT("full_res_clip_levels"), Settings->FullResClipLevels);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include <cmath> // std::llround
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/Crc.h"
|
||||
#include "Misc/Parse.h"
|
||||
@@ -24,6 +25,27 @@ namespace
|
||||
{
|
||||
std::atomic<uint64> GNextStrateManagerLifetimeId { 0 };
|
||||
|
||||
int64 VF_OwnerVoxelsToMicrometres(double OwnerVoxels)
|
||||
{
|
||||
return static_cast<int64>(std::llround(
|
||||
OwnerVoxels * static_cast<double>(VOXEL_SIZE) * 10000.0));
|
||||
}
|
||||
|
||||
float VF_ResolvedWorldMicrometresToOwnerVoxels(int64 WorldMicrometres)
|
||||
{
|
||||
return static_cast<float>(static_cast<double>(WorldMicrometres)
|
||||
/ (static_cast<double>(VOXEL_SIZE) * 10000.0));
|
||||
}
|
||||
|
||||
int64 VF_FloorDivide64(int64 Numerator, int64 Denominator)
|
||||
{
|
||||
if (Denominator <= 0) return 0;
|
||||
int64 Quotient = Numerator / Denominator;
|
||||
const int64 Remainder = Numerator % Denominator;
|
||||
if (Remainder < 0) --Quotient;
|
||||
return Quotient;
|
||||
}
|
||||
|
||||
// Exact A/B switch for the repeated passage-floor projection item. The other field-
|
||||
// preserving rewrites in this round are deliberately unconditional; this switch isolates
|
||||
// the projection reuse without changing any density arithmetic.
|
||||
@@ -1300,8 +1322,11 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
}
|
||||
}
|
||||
|
||||
// Current Z position (in chunks). Starts at 0 and goes downward (negative).
|
||||
int32 CurrentTopZ = 0;
|
||||
// Resolve the stack in physical integer micrometres first. The historical chunk indices are
|
||||
// projected from these half-open bounds only after each slot is complete.
|
||||
int64 CurrentTopWorldMicrometres = StrateLayoutChunkSizeMicrometres;
|
||||
const int64 ResolvedGapMicrometres = static_cast<int64>(LayoutGapChunks)
|
||||
* StrateLayoutChunkSizeMicrometres;
|
||||
|
||||
for (int32 i = 0; i < TotalStrates; i++)
|
||||
{
|
||||
@@ -1356,16 +1381,28 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
const int32 LayoutChunkVoxels=FMath::RoundToInt(StrateLayoutChunkWidthVoxels);
|
||||
Slot.TopChunkZ = SeasonStrate.TopWorldZ / LayoutChunkVoxels - 1;
|
||||
Slot.BottomChunkZ = SeasonStrate.BottomWorldZ / LayoutChunkVoxels;
|
||||
Slot.TopWorldMicrometres = (static_cast<int64>(Slot.TopChunkZ) + 1)
|
||||
* StrateLayoutChunkSizeMicrometres;
|
||||
Slot.BottomWorldMicrometres = static_cast<int64>(Slot.BottomChunkZ)
|
||||
* StrateLayoutChunkSizeMicrometres;
|
||||
Slot.HeightMicrometres = Slot.TopWorldMicrometres - Slot.BottomWorldMicrometres;
|
||||
const int64 ExpectedTopChunk = VF_FloorDivide64(
|
||||
CurrentTopWorldMicrometres, StrateLayoutChunkSizeMicrometres) - 1;
|
||||
const int64 ExpectedBottomChunk = ExpectedTopChunk - (Slot.HeightInChunks - 1);
|
||||
if (SeasonStrate.TopWorldZ % LayoutChunkVoxels != 0
|
||||
|| SeasonStrate.BottomWorldZ % LayoutChunkVoxels != 0
|
||||
|| Slot.TopChunkZ != CurrentTopZ
|
||||
|| Slot.BottomChunkZ != CurrentTopZ - (Slot.HeightInChunks - 1))
|
||||
|| Slot.TopChunkZ != ExpectedTopChunk
|
||||
|| Slot.BottomChunkZ != ExpectedBottomChunk
|
||||
|| Slot.TopWorldMicrometres != CurrentTopWorldMicrometres
|
||||
|| Slot.HeightMicrometres != static_cast<int64>(Slot.HeightInChunks)
|
||||
* StrateLayoutChunkSizeMicrometres)
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[StrateManager] Season slot %d bounds do not describe the declared stacked layout."), i);
|
||||
RestorePreviousLayout();
|
||||
return false;
|
||||
}
|
||||
CurrentTopWorldMicrometres = Slot.BottomWorldMicrometres - ResolvedGapMicrometres;
|
||||
SeasonStrates.Add(SeasonStrate);
|
||||
}
|
||||
else
|
||||
@@ -1388,15 +1425,50 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute Z range from definition's height
|
||||
// Resolve the authored chunk height into an exact physical interval.
|
||||
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
|
||||
Slot.TopChunkZ = CurrentTopZ;
|
||||
Slot.BottomChunkZ = CurrentTopZ - (Slot.HeightInChunks - 1);
|
||||
Slot.TopWorldMicrometres = CurrentTopWorldMicrometres;
|
||||
Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks)
|
||||
* StrateLayoutChunkSizeMicrometres;
|
||||
Slot.BottomWorldMicrometres = Slot.TopWorldMicrometres - Slot.HeightMicrometres;
|
||||
Slot.TopChunkZ = static_cast<int32>(VF_FloorDivide64(
|
||||
Slot.TopWorldMicrometres, StrateLayoutChunkSizeMicrometres) - 1);
|
||||
Slot.BottomChunkZ = static_cast<int32>(VF_FloorDivide64(
|
||||
Slot.BottomWorldMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||
CurrentTopWorldMicrometres = Slot.BottomWorldMicrometres - ResolvedGapMicrometres;
|
||||
}
|
||||
|
||||
// Move the cursor down for the next strate, leaving a solid-bedrock gap of
|
||||
// InterStrateGapChunks chunks between this strate and the next.
|
||||
CurrentTopZ = Slot.BottomChunkZ - 1 - LayoutGapChunks;
|
||||
Slot.ResolvedTransitionType = Slot.Definition->TransitionType;
|
||||
Slot.ResolvedTransitionBlendChunks = Slot.Definition->TransitionBlendChunks;
|
||||
Slot.TransitionWidthMicrometres = static_cast<int64>(
|
||||
Slot.ResolvedTransitionBlendChunks) * StrateLayoutChunkSizeMicrometres;
|
||||
Slot.ResolvedPassageConfig = Slot.Definition->PassageConfig;
|
||||
Slot.PassageMouthRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.MouthRadius);
|
||||
Slot.PassageMidRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.MidRadius);
|
||||
Slot.PassageReachMinimumMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.ReachMin);
|
||||
Slot.PassageReachMaximumMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.ReachMax);
|
||||
Slot.PassageDistanceMinimumMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.DistanceMin);
|
||||
Slot.PassageDistanceMaximumMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.DistanceMax);
|
||||
Slot.PassageWanderMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.Wander);
|
||||
Slot.PassageVerticalWobbleMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.VerticalWobble);
|
||||
Slot.PassageSpiralRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.SpiralRadius);
|
||||
Slot.PassageCascadeLedgeMicrometres = VF_OwnerVoxelsToMicrometres(
|
||||
Slot.ResolvedPassageConfig.CascadeLedge);
|
||||
Slot.LegacyTopWorldZVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Slot.TopWorldMicrometres);
|
||||
Slot.LegacyBottomWorldZVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Slot.BottomWorldMicrometres);
|
||||
Slot.LegacyHeightVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Slot.HeightMicrometres);
|
||||
|
||||
StrateLayout.Add(Slot);
|
||||
|
||||
@@ -1429,6 +1501,7 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
OriginSpineRadius = bUseSeason
|
||||
? SeasonManifest.OriginSpineRadius : Settings->OriginSpineRadius;
|
||||
InterStrateGapChunks = LayoutGapChunks;
|
||||
ResolvedInterStrateGapMicrometres = ResolvedGapMicrometres;
|
||||
if (bUseSeason)
|
||||
{
|
||||
ActiveSeasonContentHash = SeasonManifest.ContentHash;
|
||||
@@ -1537,8 +1610,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
|
||||
}
|
||||
VF_SetComposerRuntimeBounds(
|
||||
Override->Params,
|
||||
(float)(TargetSlot->TopChunkZ + 1) * StrateLayoutChunkWidthVoxels,
|
||||
(float)TargetSlot->BottomChunkZ * StrateLayoutChunkWidthVoxels);
|
||||
TargetSlot->LegacyTopWorldZVoxels,
|
||||
TargetSlot->LegacyBottomWorldZVoxels);
|
||||
Override->bUseRecipe = bUseRecipe;
|
||||
if (Recipe != nullptr)
|
||||
{
|
||||
@@ -1557,8 +1630,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
|
||||
Override->bUseRegions = true;
|
||||
Override->Regions = *InRegions;
|
||||
VF_RekeyStrateRegionManifest(Override->Regions, CandidateSeed, StrateIndex);
|
||||
Override->Regions.StrateTopWorldZ = (float)(TargetSlot->TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
Override->Regions.StrateBottomWorldZ = (float)TargetSlot->BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
||||
Override->Regions.StrateTopWorldZ = TargetSlot->LegacyTopWorldZVoxels;
|
||||
Override->Regions.StrateBottomWorldZ = TargetSlot->LegacyBottomWorldZVoxels;
|
||||
VF_SetStrateArchetypeRuntimeBounds(Override->Params,
|
||||
Override->Regions.StrateTopWorldZ,
|
||||
Override->Regions.StrateBottomWorldZ);
|
||||
@@ -1805,14 +1878,36 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
// that opens the next room rather than a second vertical spine.
|
||||
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
|
||||
if (!UpperDef) continue;
|
||||
const FStratePassageConfig& Cfg = UpperDef->PassageConfig;
|
||||
// GeneratePassages is the legacy owner-voxel route adapter. Its immutable source is the
|
||||
// physical passage record resolved during Initialize, converted back to owner voxels here.
|
||||
FStratePassageConfig Cfg = Upper.ResolvedPassageConfig;
|
||||
Cfg.MouthRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Upper.PassageMouthRadiusMicrometres);
|
||||
Cfg.MidRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Upper.PassageMidRadiusMicrometres);
|
||||
Cfg.ReachMin = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Upper.PassageReachMinimumMicrometres);
|
||||
Cfg.ReachMax = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Upper.PassageReachMaximumMicrometres);
|
||||
Cfg.DistanceMin = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Upper.PassageDistanceMinimumMicrometres);
|
||||
Cfg.DistanceMax = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Upper.PassageDistanceMaximumMicrometres);
|
||||
Cfg.Wander = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Upper.PassageWanderMicrometres);
|
||||
Cfg.VerticalWobble = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Upper.PassageVerticalWobbleMicrometres);
|
||||
Cfg.SpiralRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Upper.PassageSpiralRadiusMicrometres);
|
||||
Cfg.CascadeLedge = VF_ResolvedWorldMicrometresToOwnerVoxels(
|
||||
Upper.PassageCascadeLedgeMicrometres);
|
||||
|
||||
// Upper strate floor and lower strate ceiling (differ when there's a bedrock gap).
|
||||
const float UpperTopZ = (float)(Upper.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
const float UpperBottomZ = (float)(Upper.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
const float LowerTopZ = (float)(Lower.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
const float UpperMax = (float)Upper.HeightInChunks * StrateLayoutChunkWidthVoxels * 0.9f;
|
||||
const float LowerMax = (float)Lower.HeightInChunks * StrateLayoutChunkWidthVoxels * 0.9f;
|
||||
const float UpperTopZ = Upper.LegacyTopWorldZVoxels;
|
||||
const float UpperBottomZ = Upper.LegacyBottomWorldZVoxels;
|
||||
const float LowerTopZ = Lower.LegacyTopWorldZVoxels;
|
||||
const float UpperMax = Upper.LegacyHeightVoxels * 0.9f;
|
||||
const float LowerMax = Lower.LegacyHeightVoxels * 0.9f;
|
||||
|
||||
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||
@@ -1942,7 +2037,7 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
LowerDef->FloatingIslandParams,
|
||||
LowerQuerySeed,
|
||||
LowerTopZ,
|
||||
(float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels,
|
||||
Lower.LegacyBottomWorldZVoxels,
|
||||
PX,
|
||||
PY,
|
||||
MaxLateralSnap,
|
||||
@@ -2002,7 +2097,7 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
if (bAimedLowerAtPlayerFitPoint)
|
||||
{
|
||||
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
|
||||
const float LowerBottomZ = (float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
const float LowerBottomZ = Lower.LegacyBottomWorldZVoxels;
|
||||
const float InnerBottomZ = LowerBottomZ + LowerSealThickness;
|
||||
const float InnerTopZ = LowerTopZ - LowerSealThickness;
|
||||
if (VoxelMath::IsFinite(SuggestedLowerPoint.X)
|
||||
@@ -2170,7 +2265,7 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
Cfg.MouthRadius,
|
||||
LowerDoorDirection,
|
||||
LowerTopZ,
|
||||
(float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels,
|
||||
Lower.LegacyBottomWorldZVoxels,
|
||||
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
|
||||
bAimedLowerAtPlayerFitPoint);
|
||||
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
|
||||
@@ -2595,8 +2690,8 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
if (bOpenSurfaceEntry && OriginSpineRadius > 0.0f && StrateLayout.Num() > 0)
|
||||
{
|
||||
const FStrateSlot& Top = StrateLayout[0];
|
||||
const float TopZ = (float)(Top.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
const float BottomZ = (float)Top.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
||||
const float TopZ = Top.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Top.LegacyBottomWorldZVoxels;
|
||||
const float TopSeal = Top.Definition
|
||||
? BoundarySealThicknessFor(*Top.Definition) : 0.0f;
|
||||
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
|
||||
@@ -5921,8 +6016,8 @@ bool UVoxelStrateManager::AnyLandingFloorAtLattice(
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||
@@ -5964,8 +6059,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearBox(
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
if (VoxelPassageGeometry::OriginLandingRoomTouchesBox(
|
||||
VoxelBox, TopZ, BottomZ,
|
||||
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
||||
@@ -5985,8 +6080,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearBox(
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
|
||||
VoxelBox, TopZ, BottomZ,
|
||||
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
||||
@@ -6010,8 +6105,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearLattice(
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||
if (VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
|
||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius,
|
||||
@@ -6048,8 +6143,8 @@ bool UVoxelStrateManager::AnyOriginLandingAirNearLattice(
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||
@@ -6125,8 +6220,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
|
||||
for (const FStrateSlot& Slot : StrateLayout)
|
||||
{
|
||||
if (Slot.Definition == nullptr) continue;
|
||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||
if (VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
|
||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
|
||||
@@ -6143,15 +6238,36 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
|
||||
|
||||
int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
||||
{
|
||||
// Linear search through strate layout.
|
||||
// With ~10-20 strates this is fine. If we ever have hundreds,
|
||||
// switch to binary search (layout is sorted by Z).
|
||||
for (int32 i = 0; i < StrateLayout.Num(); i++)
|
||||
// Legacy chunk callers use the projected interval cached during Initialize. The source bounds
|
||||
// are the slot's physical micrometre interval; these indices do not define or stack strates.
|
||||
for (int32 i = 0; i < StrateLayout.Num(); ++i)
|
||||
{
|
||||
const FStrateSlot& Slot = StrateLayout[i];
|
||||
if (ChunkZ <= Slot.TopChunkZ && ChunkZ >= Slot.BottomChunkZ)
|
||||
if (ChunkZ <= Slot.TopChunkZ && ChunkZ >= Slot.BottomChunkZ) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32 UVoxelStrateManager::FindSlotIndexForWorldZCentimetres(double WorldZCentimetres) const
|
||||
{
|
||||
return i;
|
||||
const double WorldZMicrometres = WorldZCentimetres * 10000.0;
|
||||
int32 Low = 0;
|
||||
int32 High = StrateLayout.Num() - 1;
|
||||
while (Low <= High)
|
||||
{
|
||||
const int32 Mid = Low + (High - Low) / 2;
|
||||
const FStrateSlot& Slot = StrateLayout[Mid];
|
||||
if (WorldZMicrometres >= static_cast<double>(Slot.TopWorldMicrometres))
|
||||
{
|
||||
High = Mid - 1;
|
||||
}
|
||||
else if (WorldZMicrometres < static_cast<double>(Slot.BottomWorldMicrometres))
|
||||
{
|
||||
Low = Mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Mid;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
@@ -6159,9 +6275,8 @@ int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
||||
|
||||
UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
||||
{
|
||||
// Convert world Z to chunk Z coordinate
|
||||
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
|
||||
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||
if (!FMath::IsFinite(WorldZ)) return nullptr;
|
||||
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||
if (SlotIdx >= 0)
|
||||
{
|
||||
return StrateLayout[SlotIdx].Definition;
|
||||
@@ -6171,8 +6286,8 @@ UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
||||
|
||||
int32 UVoxelStrateManager::GetStrateIndex(float WorldZ) const
|
||||
{
|
||||
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
|
||||
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||
if (!FMath::IsFinite(WorldZ)) return -1;
|
||||
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||
if (SlotIdx >= 0)
|
||||
{
|
||||
return StrateLayout[SlotIdx].StrateIndex;
|
||||
@@ -6285,9 +6400,13 @@ bool UVoxelStrateManager::IsGapChunk(const FIntVector& ChunkCoord) const
|
||||
if (StrateLayout.Num() == 0) return false;
|
||||
|
||||
// Above the top strate or below the bottom strate = open air, NOT a gap.
|
||||
const int32 StackTop = StrateLayout[0].TopChunkZ;
|
||||
const int32 StackBottom = StrateLayout.Last().BottomChunkZ;
|
||||
if (ChunkCoord.Z > StackTop || ChunkCoord.Z < StackBottom) return false;
|
||||
const int64 ChunkCenterMicrometres = static_cast<int64>(ChunkCoord.Z)
|
||||
* StrateLayoutChunkSizeMicrometres + StrateLayoutChunkSizeMicrometres / 2;
|
||||
if (ChunkCenterMicrometres >= StrateLayout[0].TopWorldMicrometres
|
||||
|| ChunkCenterMicrometres < StrateLayout.Last().BottomWorldMicrometres)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Inside the stack's Z span but not in any strate slot → it's a bedrock gap.
|
||||
return FindSlotIndexForChunkZ(ChunkCoord.Z) < 0;
|
||||
@@ -6331,8 +6450,8 @@ FSlabGenerationParams UVoxelStrateManager::GetSlabParamsForChunk(const FIntVecto
|
||||
// Fill in the runtime Z bounds (voxel coordinates, same convention as
|
||||
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
||||
// TopChunkZ+1 because the top chunk's CEILING is at (TopChunkZ+1)*StrateLayoutChunkWidthVoxels.
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
|
||||
return Result;
|
||||
}
|
||||
@@ -6365,8 +6484,8 @@ FMazeGenerationParams UVoxelStrateManager::GetMazeParamsForChunk(const FIntVecto
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -6392,8 +6511,8 @@ FSurfaceGenerationParams UVoxelStrateManager::GetSurfaceParamsForChunk(const FIn
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -6419,8 +6538,8 @@ FVerticalShaftParams UVoxelStrateManager::GetVerticalShaftParamsForChunk(const F
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -6446,8 +6565,8 @@ FFloatingIslandParams UVoxelStrateManager::GetFloatingIslandParamsForChunk(const
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -6481,14 +6600,14 @@ FBiomeContext UVoxelStrateManager::GetBiomeContextForChunk(const FIntVector& Chu
|
||||
|
||||
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
|
||||
{
|
||||
const int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
|
||||
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
||||
if (!FMath::IsFinite(WorldZ)) return false;
|
||||
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||
if (SlotIdx < 0) return false;
|
||||
|
||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||
// Voxel-space Z bounds → Unreal units. Ceiling = top chunk's upper edge.
|
||||
OutTopZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels * VOXEL_SIZE;
|
||||
OutBottomZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels * VOXEL_SIZE;
|
||||
// Return the resolved physical bounds in Unreal centimeters.
|
||||
OutTopZ = static_cast<float>(static_cast<double>(Slot.TopWorldMicrometres) / 10000.0);
|
||||
OutBottomZ = static_cast<float>(static_cast<double>(Slot.BottomWorldMicrometres) / 10000.0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6501,8 +6620,8 @@ FStrateDisturbanceParams UVoxelStrateManager::GetDisturbanceParamsForChunk(const
|
||||
}
|
||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||
FStrateDisturbanceParams Result = Slot.Definition->Disturbances;
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
return Result;
|
||||
}
|
||||
|
||||
@@ -6534,8 +6653,8 @@ float UVoxelStrateManager::GetWaterLevelWorldZForChunk(const FIntVector& ChunkCo
|
||||
}
|
||||
if (Rel <= 0.0f) return -FLT_MAX;
|
||||
|
||||
const float BottomZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
const float TopZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||
return FMath::Lerp(BottomZ, TopZ, FMath::Clamp(Rel, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
@@ -6579,8 +6698,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
FindComposerOverride(Slot.StrateIndex))
|
||||
{
|
||||
FStrateGenerationParams Result = Override->Params.TunnelNetworkParams;
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
// A candidate is a hard replacement of this slot. Do not blend its rolled vector with an
|
||||
// authored neighbour at a boundary; this is also how the offline fixture measures it.
|
||||
return ResolveFinalParams(Result);
|
||||
@@ -6590,8 +6709,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
if (SeasonStrates.IsValidIndex(SlotIdx))
|
||||
{
|
||||
FStrateGenerationParams Result = SeasonStrates[SlotIdx].Params.TunnelNetworkParams;
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
||||
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
// Season vectors are already final, measured slot records. Vertical structural posts own
|
||||
// their boundary; blending them with another selected recipe would describe neither one.
|
||||
return ResolveFinalParams(Result);
|
||||
@@ -6608,8 +6727,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
//
|
||||
// TopChunkZ=0, CHUNK_SIZE=32: top of the strate = chunk 0's top edge = voxel Z=32
|
||||
// BottomChunkZ=-3: bottom of the strate = chunk -3's bottom edge = voxel Z=-3*32 = -96
|
||||
BaseParams.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
BaseParams.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
BaseParams.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||
BaseParams.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||
|
||||
// Core currently dispatches one generator field per resolved strate. Gradient and
|
||||
// interleaved metadata therefore use the explicitly declared Hard fallback for every
|
||||
@@ -6656,18 +6775,23 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
//---------------------------------------------------------------------
|
||||
// CHECK BOTTOM BOUNDARY (transitioning to strate below)
|
||||
//---------------------------------------------------------------------
|
||||
// DistFromBottom = how many chunks above the bottom edge of this strate.
|
||||
// Legacy chunk callers project their sample to a layout interval; physical distance and
|
||||
// transition width are both resolved in micrometres before projecting the blend fraction.
|
||||
// When 0, we're right at the boundary. When == TransitionBlendChunks, we're at
|
||||
// the outer edge of the transition zone.
|
||||
int32 DistFromBottom = ChunkCoord.Z - Slot.BottomChunkZ;
|
||||
const int64 DistFromBottomMicrometres = static_cast<int64>(ChunkCoord.Z)
|
||||
* StrateLayoutChunkSizeMicrometres - Slot.BottomWorldMicrometres;
|
||||
const int32 DistFromBottom = static_cast<int32>(VF_FloorDivide64(
|
||||
DistFromBottomMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||
|
||||
if (SlotIdx + 1 < StrateLayout.Num())
|
||||
{
|
||||
// The upper strate (this one) controls the transition type at its lower edge
|
||||
const EVoxelStrateTransition TransType = Slot.Definition->TransitionType;
|
||||
const EVoxelStrateTransition TransType = Slot.ResolvedTransitionType;
|
||||
|
||||
// Per-definition blend distance.
|
||||
const int32 EffectiveBlend = Slot.Definition->TransitionBlendChunks;
|
||||
const int32 EffectiveBlend = static_cast<int32>(VF_FloorDivide64(
|
||||
Slot.TransitionWidthMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||
|
||||
// Prepare the neighbor's params (only used for Gradient and Interleaved)
|
||||
const FStrateSlot& BelowSlot = StrateLayout[SlotIdx + 1];
|
||||
@@ -6691,8 +6815,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
if (EffectiveBlend > 0 && DistFromBottom < EffectiveBlend)
|
||||
{
|
||||
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
|
||||
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
|
||||
|
||||
// Linear alpha: 0 at EffectiveBlend chunks away, 1 at the boundary
|
||||
float Alpha = 1.0f - ((float)DistFromBottom / (float)EffectiveBlend);
|
||||
@@ -6729,8 +6853,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
if (DistFromBottom < CheckRange)
|
||||
{
|
||||
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
|
||||
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
|
||||
|
||||
// Sample 3D Perlin noise to warp the boundary position.
|
||||
// XY frequency 0.15 gives medium-scale variation (~6-7 chunks per cycle).
|
||||
@@ -6768,14 +6892,18 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
//---------------------------------------------------------------------
|
||||
// Mirror logic: the ABOVE strate's TransitionType controls its lower edge,
|
||||
// which is this strate's upper edge. So we read from StrateLayout[SlotIdx-1].
|
||||
int32 DistFromTop = Slot.TopChunkZ - ChunkCoord.Z;
|
||||
const int64 DistFromTopMicrometres = Slot.TopWorldMicrometres
|
||||
- (static_cast<int64>(ChunkCoord.Z) + 1) * StrateLayoutChunkSizeMicrometres;
|
||||
const int32 DistFromTop = static_cast<int32>(VF_FloorDivide64(
|
||||
DistFromTopMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||
|
||||
if (SlotIdx > 0)
|
||||
{
|
||||
// The strate ABOVE controls the transition at its lower edge (= our upper edge)
|
||||
const FStrateSlot& AboveSlot = StrateLayout[SlotIdx - 1];
|
||||
const EVoxelStrateTransition TransType = AboveSlot.Definition->TransitionType;
|
||||
const int32 EffectiveBlend = AboveSlot.Definition->TransitionBlendChunks;
|
||||
const EVoxelStrateTransition TransType = AboveSlot.ResolvedTransitionType;
|
||||
const int32 EffectiveBlend = static_cast<int32>(VF_FloorDivide64(
|
||||
AboveSlot.TransitionWidthMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||
|
||||
switch (TransType)
|
||||
{
|
||||
@@ -6790,8 +6918,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
if (EffectiveBlend > 0 && DistFromTop < EffectiveBlend)
|
||||
{
|
||||
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
|
||||
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
|
||||
|
||||
float Alpha = 1.0f - ((float)DistFromTop / (float)EffectiveBlend);
|
||||
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
||||
@@ -6812,8 +6940,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
if (DistFromTop < CheckRange)
|
||||
{
|
||||
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
||||
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
||||
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
|
||||
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
|
||||
|
||||
// Same noise function but with a different seed offset to avoid
|
||||
// symmetry between top and bottom boundaries of adjacent strates
|
||||
|
||||
@@ -82,6 +82,17 @@
|
||||
#include <atomic>
|
||||
#include <unordered_set>
|
||||
|
||||
static FORCEINLINE int32 VF_ClipLevelScale(int32 Level)
|
||||
{
|
||||
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
|
||||
return static_cast<int32>(uint32{1} << SafeLevel);
|
||||
}
|
||||
static FORCEINLINE int32 VF_ClipLevelExtentVoxels(int32 Level)
|
||||
{
|
||||
return static_cast<int32>(static_cast<uint32>(CHUNK_SIZE)
|
||||
* static_cast<uint32>(VF_ClipLevelScale(Level)));
|
||||
}
|
||||
|
||||
static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile,
|
||||
const FVector& BoundsMin, const FVector& BoundsMax);
|
||||
static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B);
|
||||
@@ -6240,17 +6251,19 @@ static void VF_AppendTouchingAncestors(
|
||||
const FVoxelTileKey& Tile, int32 AncestorLevel, CandidateArray& OutCandidates)
|
||||
{
|
||||
if (AncestorLevel <= Tile.Level) { return; }
|
||||
const int32 Divisor = 1 << (AncestorLevel - Tile.Level);
|
||||
const auto FloorDiv = [Divisor](int32 Value)
|
||||
const int32 Divisor = VF_ClipLevelScale(AncestorLevel - Tile.Level);
|
||||
const auto FloorDiv = [Divisor](int64 Value)
|
||||
{
|
||||
return Value >= 0 ? Value / Divisor : -(((-Value) + Divisor - 1) / Divisor);
|
||||
const int64 Quotient = Value / Divisor;
|
||||
const int64 Remainder = Value % Divisor;
|
||||
return static_cast<int32>(Remainder < 0 ? Quotient - 1 : Quotient);
|
||||
};
|
||||
const auto AxisCandidates = [&FloorDiv, Divisor](int32 Value)
|
||||
{
|
||||
TArray<int32, TInlineAllocator<2>> Candidates;
|
||||
int32 First = FloorDiv(Value);
|
||||
if ((Value % Divisor) == 0) { --First; }
|
||||
const int32 Last = FloorDiv(Value + 1);
|
||||
const int32 Last = FloorDiv(static_cast<int64>(Value) + 1);
|
||||
for (int32 Coord = First; Coord <= Last; ++Coord) { Candidates.Add(Coord); }
|
||||
return Candidates;
|
||||
};
|
||||
@@ -6338,7 +6351,7 @@ void AVoxelWorld::UpdateVisualStalenessAudit()
|
||||
const FVector& WorldPosition, int32 Level, auto& OutCandidates)
|
||||
{
|
||||
const FVector LocalVoxel = WorldToLocalVoxel(WorldPosition);
|
||||
const float TileExtentVoxels = static_cast<float>(CHUNK_SIZE * (1 << Level));
|
||||
const float TileExtentVoxels = static_cast<float>(VF_ClipLevelExtentVoxels(Level));
|
||||
TArray<int32, TInlineAllocator<2>> XCandidates;
|
||||
TArray<int32, TInlineAllocator<2>> YCandidates;
|
||||
TArray<int32, TInlineAllocator<2>> ZCandidates;
|
||||
@@ -7182,7 +7195,7 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
{
|
||||
const FIntVector Min = SteadyLevelMins.FindChecked(Level);
|
||||
const FIntVector Max = SteadyLevelMaxs.FindChecked(Level);
|
||||
const int32 ExtentVoxels = CHUNK_SIZE << Level;
|
||||
const int32 ExtentVoxels = VF_ClipLevelExtentVoxels(Level);
|
||||
const double TileExtentMetres = static_cast<double>(ExtentVoxels) * VOXEL_SIZE / 100.0;
|
||||
const double SpanX = static_cast<double>(Max.X - Min.X + 1) * TileExtentMetres;
|
||||
const double SpanY = static_cast<double>(Max.Y - Min.Y + 1) * TileExtentMetres;
|
||||
@@ -7663,10 +7676,20 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
};
|
||||
for (const VoxelForgeCore::TileKey& Key : InitialDesired)
|
||||
{
|
||||
const int32 Extent = 32 << Key.lod;
|
||||
const int32 OriginX = Key.coordinate.x * Extent;
|
||||
const int32 OriginY = Key.coordinate.y * Extent;
|
||||
const int32 OriginZ = Key.coordinate.z * Extent;
|
||||
const int32 Extent = VF_ClipLevelExtentVoxels(Key.lod);
|
||||
const int64 OriginXWide = static_cast<int64>(Key.coordinate.x) * Extent;
|
||||
const int64 OriginYWide = static_cast<int64>(Key.coordinate.y) * Extent;
|
||||
const int64 OriginZWide = static_cast<int64>(Key.coordinate.z) * Extent;
|
||||
if (OriginXWide < MIN_int32 || OriginYWide < MIN_int32 || OriginZWide < MIN_int32
|
||||
|| OriginXWide + Extent > MAX_int32 || OriginYWide + Extent > MAX_int32
|
||||
|| OriginZWide + Extent > MAX_int32)
|
||||
{
|
||||
OutFailureReason = TEXT("Core initial clipmap exceeds the supported voxel-coordinate range.");
|
||||
return false;
|
||||
}
|
||||
const int32 OriginX = static_cast<int32>(OriginXWide);
|
||||
const int32 OriginY = static_cast<int32>(OriginYWide);
|
||||
const int32 OriginZ = static_cast<int32>(OriginZWide);
|
||||
IncludeExtent(ClipMin.x, ClipMax.x, OriginX, Extent);
|
||||
IncludeExtent(ClipMin.y, ClipMax.y, OriginY, Extent);
|
||||
IncludeExtent(ClipMin.z, ClipMax.z, OriginZ, Extent);
|
||||
@@ -7771,6 +7794,12 @@ void AVoxelWorld::FallBackToLegacyAfterCoreInitializationFailure(const FString&
|
||||
VoxelForgeCoreWorldAssemblyJson.Reset();
|
||||
VoxelForgeCoreWorldAssemblyHash.Reset();
|
||||
bUseVoxelForgeCore = false;
|
||||
// The Core adapter sets this before validation; legacy generation must not inherit it.
|
||||
// L'adaptateur Core l'active avant validation ; le générateur historique ne doit pas en hériter.
|
||||
if (StrateManager != nullptr)
|
||||
{
|
||||
StrateManager->SetHardTransitionFallbackForAssembly(false);
|
||||
}
|
||||
CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
||||
if (Mesher != nullptr)
|
||||
{
|
||||
@@ -10068,8 +10097,11 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
||||
{
|
||||
BandChunkLo = MeshBandChunkLo;
|
||||
BandChunkHi = MeshBandChunkHi;
|
||||
BandVoxLo = MeshBandChunkLo * CHUNK_SIZE;
|
||||
BandVoxHi = (MeshBandChunkHi + 1) * CHUNK_SIZE - 1;
|
||||
BandVoxLo = static_cast<int32>(FMath::Clamp<int64>(
|
||||
static_cast<int64>(MeshBandChunkLo) * CHUNK_SIZE, MIN_int32, MAX_int32));
|
||||
BandVoxHi = static_cast<int32>(FMath::Clamp<int64>(
|
||||
(static_cast<int64>(MeshBandChunkHi) + 1) * CHUNK_SIZE - 1,
|
||||
MIN_int32, MAX_int32));
|
||||
}
|
||||
|
||||
FChunkResult Result;
|
||||
@@ -10232,7 +10264,7 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
ReadyToHide.Reserve(TileComponents.Num());
|
||||
auto GetAncestorAtLevel = [](const FVoxelTileKey& Tile, int32 AncestorLevel)
|
||||
{
|
||||
const int32 Divisor = 1 << (AncestorLevel - Tile.Level);
|
||||
const int32 Divisor = VF_ClipLevelScale(AncestorLevel - Tile.Level);
|
||||
return FVoxelTileKey(VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord, Divisor), AncestorLevel);
|
||||
};
|
||||
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& Pair : TileComponents)
|
||||
@@ -10355,7 +10387,10 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
||||
// Integer floor-division (correct for negatives), scalar + vector.
|
||||
static FORCEINLINE int32 VF_FloorDiv(int32 V, int32 D)
|
||||
{
|
||||
return V >= 0 ? (V / D) : -(((-V) + D - 1) / D);
|
||||
if (D <= 0) { return 0; }
|
||||
const int32 Quotient = V / D;
|
||||
const int32 Remainder = V % D;
|
||||
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||
}
|
||||
static FORCEINLINE FIntVector VF_FloorDiv(const FIntVector& V, int32 D)
|
||||
{
|
||||
@@ -11435,13 +11470,13 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
|
||||
{
|
||||
VF_OuterShell(Settings, R, MaxLevelBase, MaxLevel, ROuter);
|
||||
}
|
||||
const int32 PowMax = 1 << MaxLevel;
|
||||
const int32 PowMax = VF_ClipLevelScale(MaxLevel);
|
||||
const FIntVector CMax = VF_FloorDiv(Center, PowMax);
|
||||
|
||||
FVector CV;
|
||||
if (bUseVoxelForgeCore)
|
||||
{
|
||||
const double TileExtentVoxels = static_cast<double>(CHUNK_SIZE << Tile.Level);
|
||||
const double TileExtentVoxels = static_cast<double>(VF_ClipLevelExtentVoxels(Tile.Level));
|
||||
CV = FVector(
|
||||
(static_cast<double>(Tile.Coord.X) + 0.5) * TileExtentVoxels,
|
||||
(static_cast<double>(Tile.Coord.Y) + 0.5) * TileExtentVoxels,
|
||||
@@ -11457,9 +11492,9 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
|
||||
VF_FloorDiv(FMath::FloorToInt(CV.Y), SizeMax),
|
||||
VF_FloorDiv(FMath::FloorToInt(CV.Z), SizeMax));
|
||||
|
||||
return FMath::Abs(TMax.X - CMax.X) <= ROuter
|
||||
&& FMath::Abs(TMax.Y - CMax.Y) <= ROuter
|
||||
&& FMath::Abs(TMax.Z - CMax.Z) <= ROuter;
|
||||
return FMath::Abs(static_cast<int64>(TMax.X) - CMax.X) <= ROuter
|
||||
&& FMath::Abs(static_cast<int64>(TMax.Y) - CMax.Y) <= ROuter
|
||||
&& FMath::Abs(static_cast<int64>(TMax.Z) - CMax.Z) <= ROuter;
|
||||
}
|
||||
|
||||
int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
||||
@@ -12052,7 +12087,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
LevelSummaries.Num(), Leavers.Num(), CollisionOnlyTiles.Num()));
|
||||
for (const FDesiredLevelSummary& Summary : LevelSummaries)
|
||||
{
|
||||
const int32 ExtentVoxels = CHUNK_SIZE << Summary.Level;
|
||||
const int32 ExtentVoxels = VF_ClipLevelExtentVoxels(Summary.Level);
|
||||
const double TileExtentMetres = static_cast<double>(ExtentVoxels) * VOXEL_SIZE / 100.0;
|
||||
const double SpanX = static_cast<double>(Summary.Max.X - Summary.Min.X + 1) * TileExtentMetres;
|
||||
const double SpanY = static_cast<double>(Summary.Max.Y - Summary.Min.Y + 1) * TileExtentMetres;
|
||||
@@ -12103,8 +12138,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
for (const FVoxelTileKey& T : LoadedTiles)
|
||||
{
|
||||
if (T.Level < CutMin) continue;
|
||||
const int32 CLo = T.Coord.Z << T.Level;
|
||||
const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1;
|
||||
const int64 LevelScale = VF_ClipLevelScale(T.Level);
|
||||
const int64 CLo = static_cast<int64>(T.Coord.Z) * LevelScale;
|
||||
const int64 CHi = (static_cast<int64>(T.Coord.Z) + 1) * LevelScale - 1;
|
||||
const bool bInOld = CLo >= MeshBandChunkLo && CHi <= MeshBandChunkHi;
|
||||
const bool bInNew = CLo >= NewLo && CHi <= NewHi;
|
||||
const bool bOutOld = CHi < MeshBandChunkLo || CLo > MeshBandChunkHi;
|
||||
@@ -12324,7 +12360,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
}
|
||||
Heading = Heading.GetSafeNormal();
|
||||
const FVector PlayerLocalCm = WorldToLocalCm(CenterPosition);
|
||||
const double TileSizeCm = static_cast<double>(CHUNK_SIZE << Level)
|
||||
const double TileSizeCm = static_cast<double>(VF_ClipLevelExtentVoxels(Level))
|
||||
* GetActiveVoxelSizeCm();
|
||||
LevelTiles.Sort([&](const FVoxelTileKey& A, const FVoxelTileKey& B)
|
||||
{
|
||||
@@ -12782,7 +12818,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
const int32 FullRes = GVoxelForgeTestFullResClipLevels >= 0
|
||||
? FMath::Clamp(GVoxelForgeTestFullResClipLevels, 1, 9)
|
||||
: (Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2);
|
||||
const int32 Extent = CHUNK_SIZE << Tile.Level;
|
||||
const int32 Extent = VF_ClipLevelExtentVoxels(Tile.Level);
|
||||
|
||||
const int32 Cells = (Tile.Level < FullRes)
|
||||
? CHUNK_SIZE
|
||||
@@ -12847,7 +12883,9 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
// et mélanges de matériaux qu'avant la coupe) — la tuile ne peut rendre que des artefacts
|
||||
// ⇒ on n'émet RIEN. Résultat vide via ProcessQueue (bookkeeping normal : PendingTiles,
|
||||
// LoadedTiles, epoch) ; jamais figé — le changement de bande re-queue via BandRemeshQueue.
|
||||
if (Step > (BandChunkHi - BandChunkLo + 1) * CHUNK_SIZE)
|
||||
const int64 BandHeightVoxels =
|
||||
(static_cast<int64>(BandChunkHi) - BandChunkLo + 1) * CHUNK_SIZE;
|
||||
if (static_cast<int64>(Step) > BandHeightVoxels)
|
||||
{
|
||||
FChunkResult Empty;
|
||||
Empty.Tile = Tile;
|
||||
@@ -13982,7 +14020,8 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
// Convertir les bornes physiques de tuile vers les strates owner, y compris les rapports fractionnaires.
|
||||
const double OwnerChunksPerCoreTile = static_cast<double>(GetActiveVoxelSizeCm())
|
||||
/ VOXEL_SIZE;
|
||||
const double TileExtentCoreVoxels = static_cast<double>(CHUNK_SIZE << Tile.Level);
|
||||
const double TileExtentCoreVoxels =
|
||||
static_cast<double>(VF_ClipLevelExtentVoxels(Tile.Level));
|
||||
const int32 MinOwnerChunkX = FMath::FloorToInt(
|
||||
Tile.Coord.X * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE);
|
||||
const int32 MinOwnerChunkY = FMath::FloorToInt(
|
||||
@@ -15174,7 +15213,7 @@ void AVoxelWorld::RemeshDirtyChunks(const TArray<FIntVector>& DirtyCoords, const
|
||||
// BackgroundHigh; the old component remains visible until its post-edit replacement is applied.
|
||||
auto TileCoversDirtyCoord = [&DirtyCoords](const FVoxelTileKey& Tile) -> bool
|
||||
{
|
||||
const int32 Span = 1 << Tile.Level;
|
||||
const int32 Span = VF_ClipLevelScale(Tile.Level);
|
||||
const FIntVector MinChunk = Tile.Coord * Span;
|
||||
const FIntVector MaxChunk = MinChunk + FIntVector(Span, Span, Span);
|
||||
for (const FIntVector& Coord : DirtyCoords)
|
||||
|
||||
@@ -261,8 +261,12 @@ namespace VoxelHash
|
||||
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
|
||||
{
|
||||
uint32 H = Seed;
|
||||
H ^= Mix((uint32)(CellX + 0x7FFFFFFF));
|
||||
H ^= Mix((uint32)(CellY + 0x7FFFFFFF) * 2654435761u);
|
||||
// Bias in unsigned arithmetic. Adding the signed cell coordinate first overflows for
|
||||
// positive cells and makes the world hash compiler-dependent.
|
||||
const uint32 BiasedX = static_cast<uint32>(CellX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedY = static_cast<uint32>(CellY) + 0x7FFFFFFFu;
|
||||
H ^= Mix(BiasedX);
|
||||
H ^= Mix(BiasedY * 2654435761u);
|
||||
return Mix(H);
|
||||
}
|
||||
|
||||
@@ -274,10 +278,14 @@ namespace VoxelHash
|
||||
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
|
||||
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
|
||||
uint32 H = Seed ^ 0xDEADBEEF;
|
||||
H ^= Mix((uint32)(MinX + 0x7FFFFFFF));
|
||||
H ^= Mix((uint32)(MinY + 0x7FFFFFFF) * 2654435761u);
|
||||
H ^= Mix((uint32)(MaxX + 0x7FFFFFFF) * 374761393u);
|
||||
H ^= Mix((uint32)(MaxY + 0x7FFFFFFF) * 668265263u);
|
||||
const uint32 BiasedMinX = static_cast<uint32>(MinX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedMinY = static_cast<uint32>(MinY) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedMaxX = static_cast<uint32>(MaxX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedMaxY = static_cast<uint32>(MaxY) + 0x7FFFFFFFu;
|
||||
H ^= Mix(BiasedMinX);
|
||||
H ^= Mix(BiasedMinY * 2654435761u);
|
||||
H ^= Mix(BiasedMaxX * 374761393u);
|
||||
H ^= Mix(BiasedMaxY * 668265263u);
|
||||
return Mix(H);
|
||||
}
|
||||
|
||||
@@ -477,10 +485,13 @@ namespace VoxelNoise
|
||||
|
||||
// Hash the neighbor cell to get a feature point position [0,1)
|
||||
// Using three different hash mixes for X, Y, Z offsets
|
||||
const uint32 BiasedX = static_cast<uint32>(NX) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedY = static_cast<uint32>(NY) + 0x7FFFFFFFu;
|
||||
const uint32 BiasedZ = static_cast<uint32>(NZ) + 0x7FFFFFFFu;
|
||||
uint32 H = VoxelHash::Mix(
|
||||
(uint32)(NX + 0x7FFFFFFF)
|
||||
^ VoxelHash::Mix((uint32)(NY + 0x7FFFFFFF) * 2654435761u)
|
||||
^ VoxelHash::Mix((uint32)(NZ + 0x7FFFFFFF) * 374761393u)
|
||||
BiasedX
|
||||
^ VoxelHash::Mix(BiasedY * 2654435761u)
|
||||
^ VoxelHash::Mix(BiasedZ * 374761393u)
|
||||
);
|
||||
|
||||
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
|
||||
|
||||
@@ -121,8 +121,8 @@ struct FVoxelPassage
|
||||
* FStrateSlot — Runtime info for one strate in the layout.
|
||||
*
|
||||
* Created during Initialize() and stored in the StrateLayout array.
|
||||
* Each slot knows its definition, its Z-range in chunk coordinates,
|
||||
* and its index in the sequence.
|
||||
* Each slot owns its resolved half-open physical Z interval. Chunk coordinates and legacy
|
||||
* authoring values below are projections kept for existing callers and assets.
|
||||
*/
|
||||
USTRUCT()
|
||||
struct FStrateSlot
|
||||
@@ -136,6 +136,33 @@ struct FStrateSlot
|
||||
// Strate index (0 = topmost, increases downward)
|
||||
int32 StrateIndex = 0;
|
||||
|
||||
// Resolved physical bounds, in micrometres, with interval [bottom, top). These are computed
|
||||
// once during Initialize from the configured metre chunk width and legacy asset dimensions.
|
||||
int64 TopWorldMicrometres = 0;
|
||||
int64 BottomWorldMicrometres = 0;
|
||||
int64 HeightMicrometres = 0;
|
||||
float LegacyTopWorldZVoxels = 0.0f;
|
||||
float LegacyBottomWorldZVoxels = 0.0f;
|
||||
float LegacyHeightVoxels = 0.0f;
|
||||
|
||||
// Immutable transition and passage inputs copied when this slot is resolved. The width is
|
||||
// physical; chunk counts and raw owner-voxel values remain compatibility projections for
|
||||
// legacy parameter and route adapters.
|
||||
EVoxelStrateTransition ResolvedTransitionType = EVoxelStrateTransition::Gradient;
|
||||
int32 ResolvedTransitionBlendChunks = 2;
|
||||
int64 TransitionWidthMicrometres = 0;
|
||||
FStratePassageConfig ResolvedPassageConfig;
|
||||
int64 PassageMouthRadiusMicrometres = 0;
|
||||
int64 PassageMidRadiusMicrometres = 0;
|
||||
int64 PassageReachMinimumMicrometres = 0;
|
||||
int64 PassageReachMaximumMicrometres = 0;
|
||||
int64 PassageDistanceMinimumMicrometres = 0;
|
||||
int64 PassageDistanceMaximumMicrometres = 0;
|
||||
int64 PassageWanderMicrometres = 0;
|
||||
int64 PassageVerticalWobbleMicrometres = 0;
|
||||
int64 PassageSpiralRadiusMicrometres = 0;
|
||||
int64 PassageCascadeLedgeMicrometres = 0;
|
||||
|
||||
// Z chunk range (inclusive). TopZ > BottomZ since Z decreases downward.
|
||||
// Example: TopZ = 0, BottomZ = -3 means this strate spans chunks Z=0 to Z=-3
|
||||
int32 TopChunkZ = 0;
|
||||
@@ -248,7 +275,11 @@ public:
|
||||
// This is independent of Core's 32^3 tile width.
|
||||
float GetStrateLayoutChunkWidthVoxels() const { return StrateLayoutChunkWidthVoxels; }
|
||||
uint32 GetStrateLayoutChunkSizeMicrometres() const { return StrateLayoutChunkSizeMicrometres; }
|
||||
void SetHardTransitionFallbackForAssembly(bool bEnabled)
|
||||
int64 GetResolvedInterStrateGapMicrometres() const
|
||||
{ return ResolvedInterStrateGapMicrometres; }
|
||||
// Called by the Core adapter through a const manager pointer; the flag is runtime routing
|
||||
// state, not layout data, hence const + mutable.
|
||||
void SetHardTransitionFallbackForAssembly(bool bEnabled) const
|
||||
{ bHardTransitionFallbackForAssembly = bEnabled; }
|
||||
|
||||
/** Layout/passage generation counter (bumped by every Initialize → GeneratePassages).
|
||||
@@ -528,14 +559,15 @@ protected:
|
||||
// Copied from VoxelSettings::OriginSpineRadius.
|
||||
float OriginSpineRadius = 28.0f;
|
||||
|
||||
// Solid-bedrock gap between consecutive strates, in chunks. Copied from
|
||||
// VoxelSettings::InterStrateGapChunks during Initialize().
|
||||
// Compatibility gap projection, in layout chunks. Physical authority is the micrometre value
|
||||
// resolved during Initialize below.
|
||||
int32 InterStrateGapChunks = 0;
|
||||
int64 ResolvedInterStrateGapMicrometres = 0;
|
||||
|
||||
// Resolved once from VoxelSettings::StrateLayoutChunkSizeMeters during Initialize().
|
||||
float StrateLayoutChunkWidthVoxels = 32.0f;
|
||||
uint32 StrateLayoutChunkSizeMicrometres = 8000000;
|
||||
bool bHardTransitionFallbackForAssembly = false;
|
||||
mutable bool bHardTransitionFallbackForAssembly = false;
|
||||
|
||||
// Production keeps the legacy writer available only as a fail-closed fallback for a
|
||||
// non-native/malformed passage; native D-floors bypass it. This switch never changes the
|
||||
@@ -552,6 +584,7 @@ protected:
|
||||
// Find which slot a chunk Z coordinate falls into.
|
||||
// Returns INDEX into StrateLayout, or -1 if not found.
|
||||
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
|
||||
int32 FindSlotIndexForWorldZCentimetres(double WorldZCentimetres) const;
|
||||
|
||||
#if WITH_EDITOR
|
||||
const FVoxelStrateComposerSlotOverride* FindComposerOverride(int32 StrateIndex) const;
|
||||
|
||||
@@ -332,16 +332,33 @@ struct FVoxelTileKey
|
||||
FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {}
|
||||
|
||||
// Cell size (sampling step) in voxels, and tile extent in voxels per axis.
|
||||
int32 StepVoxels() const { return 1 << Level; }
|
||||
int32 ExtentVoxels() const { return CHUNK_SIZE << Level; }
|
||||
int32 StepVoxels() const
|
||||
{
|
||||
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
|
||||
return static_cast<int32>(uint32{1} << SafeLevel);
|
||||
}
|
||||
int32 ExtentVoxels() const
|
||||
{
|
||||
const uint32 SafeLevel = static_cast<uint32>(FMath::Clamp(Level, 0, 8));
|
||||
return static_cast<int32>(static_cast<uint32>(CHUNK_SIZE) * (uint32{1} << SafeLevel));
|
||||
}
|
||||
|
||||
// Min-corner of the tile in VOXEL coords (what the mesher takes).
|
||||
FIntVector OriginVoxels() const { return Coord * (CHUNK_SIZE << Level); }
|
||||
FIntVector OriginVoxels() const
|
||||
{
|
||||
const int64 Extent = ExtentVoxels();
|
||||
const auto ScaleAxis = [Extent](int32 Axis)
|
||||
{
|
||||
return static_cast<int32>(FMath::Clamp<int64>(
|
||||
static_cast<int64>(Axis) * Extent, MIN_int32, MAX_int32));
|
||||
};
|
||||
return FIntVector(ScaleAxis(Coord.X), ScaleAxis(Coord.Y), ScaleAxis(Coord.Z));
|
||||
}
|
||||
|
||||
// Tile centre in world cm (for distance sorting / LOD selection).
|
||||
FVector CenterCm() const
|
||||
{
|
||||
const double Ext = (double)(CHUNK_SIZE << Level) * (double)VOXEL_SIZE;
|
||||
const double Ext = static_cast<double>(ExtentVoxels()) * static_cast<double>(VOXEL_SIZE);
|
||||
return FVector((Coord.X + 0.5) * Ext, (Coord.Y + 0.5) * Ext, (Coord.Z + 0.5) * Ext);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,14 @@ fi
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$SCRIPT_DIR"
|
||||
PROJECT_ROOT="$(cd -- "$ROOT/.." && pwd)"
|
||||
VERIFY_DIR="$ROOT/Tools/verify"
|
||||
COMPILER="clang++-19"
|
||||
VARIANT="clang19"
|
||||
FP_CONTRACT="off"
|
||||
JOBS=12
|
||||
SANITIZERS=""
|
||||
EXCLUDE_UNSIGNED_OVERFLOW=0
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
@@ -18,6 +22,8 @@ while (($#)); do
|
||||
--variant=*) VARIANT="${1#*=}" ;;
|
||||
--fp-contract=*) FP_CONTRACT="${1#*=}" ;;
|
||||
--jobs=*) JOBS="${1#*=}" ;;
|
||||
--sanitize=*) SANITIZERS="${1#*=}" ;;
|
||||
--exclude-unsigned-integer-overflow) EXCLUDE_UNSIGNED_OVERFLOW=1 ;;
|
||||
*) printf 'Unknown build option: %s\n' "$1" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
@@ -41,6 +47,15 @@ BIN_DIR="${VOXELFORGE_BIN_DIR:-$ROOT/bin/linux}"
|
||||
BUILD_DIR="$BUILD_ROOT/linux-$VARIANT"
|
||||
mkdir -p "$BUILD_DIR/wrappers" "$BUILD_DIR/objects" "$BIN_DIR"
|
||||
|
||||
STAMP_HELPER="$VERIFY_DIR/source_stamp.py"
|
||||
STAMP_ARGS=()
|
||||
if [[ "${VOXELFORGE_INCLUDE_UNREAL_ADAPTER:-0}" == "1" ]]; then
|
||||
STAMP_ARGS+=(--include-unreal-adapter)
|
||||
fi
|
||||
SOURCE_STAMP="$(python3 -B "$STAMP_HELPER" stamp --root "$PROJECT_ROOT" "${STAMP_ARGS[@]}")"
|
||||
python3 -B "$STAMP_HELPER" write-manifest --root "$PROJECT_ROOT" --output "$BUILD_DIR/source-manifest.txt" "${STAMP_ARGS[@]}" >/dev/null
|
||||
printf 'Source stamp: %s\n' "$SOURCE_STAMP"
|
||||
|
||||
INCLUDES=(
|
||||
"-I$ROOT/Core/include"
|
||||
"-I$ROOT/Core/src"
|
||||
@@ -49,10 +64,22 @@ INCLUDES=(
|
||||
"-I$ROOT/Source/VoxelForge/Public"
|
||||
"-I$ROOT/Source/VoxelForge/Private"
|
||||
)
|
||||
OPT_LEVEL=-O2
|
||||
LINK_FLAGS=()
|
||||
if [[ -n "$SANITIZERS" ]]; then
|
||||
OPT_LEVEL=-O1
|
||||
LINK_FLAGS+=("-fsanitize=$SANITIZERS" -fno-sanitize-recover=undefined)
|
||||
if ((EXCLUDE_UNSIGNED_OVERFLOW)); then
|
||||
LINK_FLAGS+=(-fno-sanitize=unsigned-integer-overflow)
|
||||
fi
|
||||
fi
|
||||
|
||||
FLAGS=(
|
||||
-std=c++20 -O2 -mavx2 -mfma -fno-fast-math
|
||||
-std=c++20 "$OPT_LEVEL" -mavx2 -mfma -fno-fast-math
|
||||
-fexceptions -pthread "${INCLUDES[@]}"
|
||||
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\""
|
||||
)
|
||||
FLAGS+=("${LINK_FLAGS[@]}")
|
||||
if [[ "$FP_CONTRACT" != "default" ]]; then
|
||||
FLAGS+=("-ffp-contract=$FP_CONTRACT")
|
||||
fi
|
||||
@@ -119,11 +146,19 @@ PIDS=()
|
||||
CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
|
||||
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
|
||||
OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT"
|
||||
"$COMPILER" -pthread "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT"
|
||||
"$COMPILER" -pthread "${LINK_FLAGS[@]}" "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT"
|
||||
CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT"
|
||||
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
|
||||
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\"" -I"$ROOT/Source" \
|
||||
"$ROOT/Source/LinuxOffscreenCapture.cpp" -o "$CAPTURE_OUTPUT" \
|
||||
$(pkg-config --cflags --libs egl glesv2 libpng)
|
||||
|
||||
POST_BUILD_STAMP="$(python3 -B "$STAMP_HELPER" stamp --root "$PROJECT_ROOT" "${STAMP_ARGS[@]}")"
|
||||
if [[ "$POST_BUILD_STAMP" != "$SOURCE_STAMP" ]]; then
|
||||
printf 'Source changed during build: before=%s after=%s\n' "$SOURCE_STAMP" "$POST_BUILD_STAMP" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'Built Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT"
|
||||
printf 'Built EGL offscreen capture: %s\n' "$CAPTURE_OUTPUT"
|
||||
printf 'Compiler: %s | variant: %s | fp-contract: %s | jobs: %s\n' "$COMPILER" "$VARIANT" "$FP_CONTRACT" "$JOBS"
|
||||
printf 'Compiler: %s | variant: %s | fp-contract: %s | sanitizers: %s | jobs: %s\n' \
|
||||
"$COMPILER" "$VARIANT" "$FP_CONTRACT" "${SANITIZERS:-off}" "$JOBS"
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))
|
||||
#include <intrin.h>
|
||||
#elif defined(__i386__) || defined(__x86_64__)
|
||||
#include <immintrin.h>
|
||||
#endif
|
||||
|
||||
namespace VoxelForgeCoreShared
|
||||
{
|
||||
inline std::uint64_t ReadCpuCycles() noexcept
|
||||
{
|
||||
#if defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))
|
||||
_mm_lfence();
|
||||
const std::uint64_t value=__rdtsc();
|
||||
_mm_lfence();
|
||||
return value;
|
||||
#elif defined(__i386__) || defined(__x86_64__)
|
||||
_mm_lfence();
|
||||
const std::uint64_t value=__rdtsc();
|
||||
_mm_lfence();
|
||||
return value;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline std::uint64_t CpuCycleReadPairOverhead() noexcept
|
||||
{
|
||||
static const std::uint64_t overhead=[]()
|
||||
{
|
||||
std::uint64_t minimum=std::numeric_limits<std::uint64_t>::max();
|
||||
for(unsigned i=0;i<64;++i)
|
||||
{
|
||||
const std::uint64_t start=ReadCpuCycles();
|
||||
const std::uint64_t end=ReadCpuCycles();
|
||||
minimum=std::min(minimum,end-start);
|
||||
}
|
||||
return minimum==std::numeric_limits<std::uint64_t>::max()?0:minimum;
|
||||
}();
|
||||
return overhead;
|
||||
}
|
||||
|
||||
inline std::uint64_t AdjustedCpuCycleDelta(std::uint64_t start,
|
||||
std::uint64_t end) noexcept
|
||||
{
|
||||
const std::uint64_t delta=end-start;
|
||||
const std::uint64_t overhead=CpuCycleReadPairOverhead();
|
||||
return delta>overhead?delta-overhead:0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
#pragma once
|
||||
|
||||
#include "VoxelForgeCore/CpuCycleCounter.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace VoxelForgeCore::floating_island_detail
|
||||
{
|
||||
enum class EIslandFieldCaller : std::uint8_t
|
||||
{
|
||||
GridFill=0,Skirt=1,Boundary=2,PassageFilter=3,Other=4,Count=5
|
||||
};
|
||||
|
||||
enum class EIslandAttributionBucket : std::uint8_t
|
||||
{
|
||||
Field=0,Island=1,Root=2,Connector=3,Helix=4,Column=5,
|
||||
LocalProbe=6,SharedCache=7,MouthFilter=8,Boundary=9,CallerGrid=10,
|
||||
CallerSkirt=11,CallerBoundary=12,CallerPassageFilter=13,CallerOther=14,
|
||||
Count=15
|
||||
};
|
||||
|
||||
struct FIslandFieldAttributionInstrumentation
|
||||
{
|
||||
static constexpr std::uint64_t SampleMask=255;
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> sampledFieldCalls{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> fieldCycles{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> sampledNativeCalls{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandFieldCaller::Count)> nativeCycles{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> exclusiveCycles{};
|
||||
std::array<std::uint64_t,static_cast<std::size_t>(EIslandAttributionBucket::Count)> scopeSamples{};
|
||||
std::uint64_t traceSampledMisses=0,traceSeenBefore=0,traceEvictions=0,
|
||||
traceReusedEvictions=0;
|
||||
std::array<std::uint64_t,8> traceSet{};
|
||||
std::array<std::int64_t,8> traceX{},traceY{},traceEvictedX{},traceEvictedY{};
|
||||
std::array<std::uint64_t,8> traceIsland{},traceEvictedIsland{};
|
||||
std::array<std::uint8_t,8> traceFlags{};
|
||||
std::size_t traceCount=0;
|
||||
};
|
||||
|
||||
inline FIslandFieldAttributionInstrumentation*& ActiveIslandFieldAttribution() noexcept
|
||||
{
|
||||
static thread_local FIslandFieldAttributionInstrumentation* active=nullptr;
|
||||
return active;
|
||||
}
|
||||
|
||||
inline FIslandFieldAttributionInstrumentation* SetActiveIslandFieldAttribution(
|
||||
FIslandFieldAttributionInstrumentation* value) noexcept
|
||||
{
|
||||
auto* previous=ActiveIslandFieldAttribution();
|
||||
ActiveIslandFieldAttribution()=value;
|
||||
return previous;
|
||||
}
|
||||
|
||||
inline EIslandFieldCaller& ActiveIslandFieldCaller() noexcept
|
||||
{
|
||||
static thread_local EIslandFieldCaller caller=EIslandFieldCaller::Other;
|
||||
return caller;
|
||||
}
|
||||
|
||||
struct FScopedIslandFieldCaller
|
||||
{
|
||||
explicit FScopedIslandFieldCaller(EIslandFieldCaller value) noexcept
|
||||
:previous(ActiveIslandFieldCaller()),enabled(ActiveIslandFieldAttribution()!=nullptr)
|
||||
{if(enabled)ActiveIslandFieldCaller()=value;}
|
||||
~FScopedIslandFieldCaller(){if(enabled)ActiveIslandFieldCaller()=previous;}
|
||||
EIslandFieldCaller previous;
|
||||
bool enabled=false;
|
||||
};
|
||||
|
||||
inline std::array<std::uint64_t,
|
||||
static_cast<std::size_t>(EIslandAttributionBucket::Count)>&
|
||||
IslandComponentSampleSequence() noexcept
|
||||
{
|
||||
static thread_local std::array<std::uint64_t,
|
||||
static_cast<std::size_t>(EIslandAttributionBucket::Count)> sequence{};
|
||||
return sequence;
|
||||
}
|
||||
|
||||
inline std::array<std::uint64_t,
|
||||
static_cast<std::size_t>(EIslandFieldCaller::Count)>&
|
||||
IslandFieldSampleSequence() noexcept
|
||||
{
|
||||
static thread_local std::array<std::uint64_t,
|
||||
static_cast<std::size_t>(EIslandFieldCaller::Count)> sequence{};
|
||||
return sequence;
|
||||
}
|
||||
|
||||
inline std::array<std::uint64_t,
|
||||
static_cast<std::size_t>(EIslandFieldCaller::Count)>&
|
||||
IslandNativeSampleSequence() noexcept
|
||||
{
|
||||
static thread_local std::array<std::uint64_t,
|
||||
static_cast<std::size_t>(EIslandFieldCaller::Count)> sequence{};
|
||||
return sequence;
|
||||
}
|
||||
|
||||
struct FScopedIslandAttributionTimer
|
||||
{
|
||||
explicit FScopedIslandAttributionTimer(EIslandAttributionBucket bucket) noexcept
|
||||
:metrics(ActiveIslandFieldAttribution()),bucketIndex(static_cast<std::size_t>(bucket))
|
||||
{
|
||||
if(!metrics)return;
|
||||
if(bucketIndex==static_cast<std::size_t>(EIslandAttributionBucket::Field))
|
||||
{
|
||||
const auto callerIndex=static_cast<std::size_t>(ActiveIslandFieldCaller());
|
||||
const auto calls=++IslandFieldSampleSequence()[callerIndex];
|
||||
const std::uint64_t phase=(callerIndex*43u+17u)
|
||||
&FIslandFieldAttributionInstrumentation::SampleMask;
|
||||
sampled=(((calls+phase)&FIslandFieldAttributionInstrumentation::SampleMask)==0);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto& calls=IslandComponentSampleSequence()[bucketIndex];
|
||||
++calls;
|
||||
const std::uint64_t phase=(static_cast<std::uint64_t>(bucketIndex)*29u+17u)
|
||||
&FIslandFieldAttributionInstrumentation::SampleMask;
|
||||
sampled=(((calls+phase)&FIslandFieldAttributionInstrumentation::SampleMask)==0);
|
||||
}
|
||||
if(sampled)start=VoxelForgeCoreShared::ReadCpuCycles();
|
||||
}
|
||||
~FScopedIslandAttributionTimer(){Stop();}
|
||||
std::uint64_t Stop() noexcept
|
||||
{
|
||||
if(!sampled)return 0;
|
||||
sampled=false;
|
||||
const std::uint64_t raw=VoxelForgeCoreShared::ReadCpuCycles()-start;
|
||||
const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead();
|
||||
const std::uint64_t cycles=raw>overhead?raw-overhead:0;
|
||||
metrics->exclusiveCycles[bucketIndex]+=cycles;
|
||||
++metrics->scopeSamples[bucketIndex];
|
||||
if(bucketIndex==static_cast<std::size_t>(EIslandAttributionBucket::Field))
|
||||
{
|
||||
const std::size_t callerIndex=static_cast<std::size_t>(ActiveIslandFieldCaller());
|
||||
metrics->fieldCycles[callerIndex]+=cycles;
|
||||
++metrics->sampledFieldCalls[callerIndex];
|
||||
}
|
||||
return cycles;
|
||||
}
|
||||
FIslandFieldAttributionInstrumentation* metrics=nullptr;
|
||||
std::size_t bucketIndex=0;
|
||||
std::uint64_t start=0;
|
||||
bool sampled=false;
|
||||
};
|
||||
|
||||
struct FScopedIslandAttributionSample
|
||||
{
|
||||
explicit FScopedIslandAttributionSample() noexcept
|
||||
{
|
||||
auto* attribution=ActiveIslandFieldAttribution();
|
||||
if(!attribution)return;
|
||||
const auto caller=ActiveIslandFieldCaller();
|
||||
const std::size_t callerIndex=static_cast<std::size_t>(caller);
|
||||
const std::uint64_t sequence=++IslandNativeSampleSequence()[callerIndex];
|
||||
const std::uint64_t phase=(callerIndex*43u+93u)
|
||||
&FIslandFieldAttributionInstrumentation::SampleMask;
|
||||
if(((sequence+phase)&FIslandFieldAttributionInstrumentation::SampleMask)!=0)return;
|
||||
sampledAttribution=attribution;
|
||||
sampledCaller=caller;
|
||||
sampled=true;
|
||||
start=VoxelForgeCoreShared::ReadCpuCycles();
|
||||
++attribution->sampledNativeCalls[callerIndex];
|
||||
}
|
||||
~FScopedIslandAttributionSample()
|
||||
{
|
||||
if(!sampled)return;
|
||||
const std::uint64_t raw=VoxelForgeCoreShared::ReadCpuCycles()-start;
|
||||
const std::uint64_t overhead=VoxelForgeCoreShared::CpuCycleReadPairOverhead();
|
||||
const std::uint64_t duration=raw>overhead?raw-overhead:0;
|
||||
const std::size_t callerIndex=static_cast<std::size_t>(sampledCaller);
|
||||
sampledAttribution->nativeCycles[callerIndex]+=duration;
|
||||
}
|
||||
FIslandFieldAttributionInstrumentation* sampledAttribution=nullptr;
|
||||
EIslandFieldCaller sampledCaller=EIslandFieldCaller::Other;
|
||||
std::uint64_t start=0;
|
||||
bool sampled=false;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "VoxelForgeCore/CpuCycleCounter.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
@@ -12,6 +15,23 @@
|
||||
|
||||
namespace VoxelForgeCoreShared
|
||||
{
|
||||
struct FColumnCacheMetrics
|
||||
{
|
||||
std::uint64_t hits = 0;
|
||||
std::uint64_t misses = 0;
|
||||
std::uint64_t computes = 0;
|
||||
std::uint64_t conflictReplacements = 0;
|
||||
std::uint64_t lockWaitNanoseconds = 0;
|
||||
std::uint64_t sampledInitialReadCycles = 0;
|
||||
std::uint64_t sampledInitialReads = 0;
|
||||
std::uint64_t sampledLockedReadCycles = 0;
|
||||
std::uint64_t sampledLockedReads = 0;
|
||||
std::uint64_t sampledLockCycles = 0;
|
||||
std::uint64_t sampledLocks = 0;
|
||||
std::uint64_t sampledBuildCycles = 0;
|
||||
std::uint64_t sampledBuilds = 0;
|
||||
};
|
||||
|
||||
// Exact input identity for a pure XY column value. LOD is deliberately not part of this key:
|
||||
// the same recipe/plan column has the same bits at every sampling step.
|
||||
struct FColumnCacheKey
|
||||
@@ -34,12 +54,36 @@ namespace VoxelForgeCoreShared
|
||||
}
|
||||
};
|
||||
|
||||
// Exact identity for one world-lattice density sample. Unlike a column key this includes
|
||||
// Z, sampling settings, and the edit generation; all coordinates remain explicit words.
|
||||
struct FDensitySampleCacheKey
|
||||
{
|
||||
std::uint64_t fieldIdentity = 0;
|
||||
std::uint64_t fieldGeneration = 0;
|
||||
std::uint64_t editSequence = 0;
|
||||
std::uint32_t lod = 0;
|
||||
std::uint32_t step = 1;
|
||||
std::uint32_t octaveDrop = 0;
|
||||
std::int64_t interpolationStride = 0;
|
||||
std::int64_t x = 0, y = 0, z = 0;
|
||||
std::uint64_t queryContextLow = 0, queryContextHigh = 0;
|
||||
|
||||
std::array<std::uint64_t, 12> words() const noexcept
|
||||
{
|
||||
return {fieldIdentity, fieldGeneration, editSequence, lod, step, octaveDrop,
|
||||
static_cast<std::uint64_t>(interpolationStride), static_cast<std::uint64_t>(x),
|
||||
static_cast<std::uint64_t>(y), static_cast<std::uint64_t>(z),
|
||||
queryContextLow, queryContextHigh};
|
||||
}
|
||||
};
|
||||
|
||||
// A bounded, world-owned second-level cache. Existing thread-local caches stay first, so
|
||||
// their ordinary hit path remains private and lock-free. Shared hits read atomic words; only
|
||||
// a shared miss takes the set spin lock. Entries are four-way set associative, evict on
|
||||
// collision, and carry pointer identities plus content hashes to reject stale reuse.
|
||||
// Les caches TLS restent le premier niveau; seul un defaut partage prend le verrou du groupe.
|
||||
template <typename TValue, std::size_t SetCount, std::size_t Ways = 4>
|
||||
template <typename TValue, std::size_t SetCount, std::size_t Ways = 4,
|
||||
typename TKey = FColumnCacheKey>
|
||||
class TSharedColumnValueCache
|
||||
{
|
||||
static_assert(SetCount > 0 && (SetCount & (SetCount - 1)) == 0,
|
||||
@@ -49,14 +93,14 @@ namespace VoxelForgeCoreShared
|
||||
"shared column values must be trivially copyable");
|
||||
|
||||
static constexpr std::size_t ValueWordCount = (sizeof(TValue) + 7) / 8;
|
||||
using FKeyWords = std::array<std::uint64_t, 7>;
|
||||
using FKeyWords = decltype(std::declval<TKey>().words());
|
||||
using FValueWords = std::array<std::uint64_t, ValueWordCount>;
|
||||
|
||||
struct FEntry
|
||||
{
|
||||
std::atomic<std::uint64_t> Version{0};
|
||||
std::atomic<std::uint64_t> Tag{0};
|
||||
std::array<std::atomic<std::uint64_t>, 7> Key{};
|
||||
std::array<std::atomic<std::uint64_t>, FKeyWords{}.size()> Key{};
|
||||
std::array<std::atomic<std::uint64_t>, ValueWordCount> Value{};
|
||||
};
|
||||
|
||||
@@ -69,13 +113,28 @@ namespace VoxelForgeCoreShared
|
||||
|
||||
struct FSpinGuard
|
||||
{
|
||||
explicit FSpinGuard(std::atomic_flag& value) : Flag(value)
|
||||
explicit FSpinGuard(std::atomic_flag& value, FColumnCacheMetrics* metrics,
|
||||
bool sampleCycles)
|
||||
: Flag(value), Metrics(metrics)
|
||||
{
|
||||
const auto started = Metrics ? std::chrono::steady_clock::now()
|
||||
: std::chrono::steady_clock::time_point{};
|
||||
const std::uint64_t cycleStart=sampleCycles?ReadCpuCycles():0;
|
||||
while (Flag.test_and_set(std::memory_order_acquire))
|
||||
std::this_thread::yield();
|
||||
if (Metrics)
|
||||
Metrics->lockWaitNanoseconds += static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now() - started).count());
|
||||
if(sampleCycles)
|
||||
{
|
||||
Metrics->sampledLockCycles+=AdjustedCpuCycleDelta(cycleStart,ReadCpuCycles());
|
||||
++Metrics->sampledLocks;
|
||||
}
|
||||
}
|
||||
~FSpinGuard() { Flag.clear(std::memory_order_release); }
|
||||
std::atomic_flag& Flag;
|
||||
FColumnCacheMetrics* Metrics;
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -93,8 +152,8 @@ namespace VoxelForgeCoreShared
|
||||
static TSharedColumnValueCache* threadActive() noexcept { return Active; }
|
||||
|
||||
template <typename TFactory>
|
||||
TValue getOrCompute(const FColumnCacheKey& key, TFactory&& factory,
|
||||
bool& computed)
|
||||
TValue getOrCompute(const TKey& key, TFactory&& factory,
|
||||
bool& computed, FColumnCacheMetrics* metrics = nullptr)
|
||||
{
|
||||
computed = false;
|
||||
const FKeyWords keyWords = key.words();
|
||||
@@ -102,13 +161,47 @@ namespace VoxelForgeCoreShared
|
||||
const std::size_t setIndex = static_cast<std::size_t>(tag) & (SetCount - 1);
|
||||
FSet& set = Sets[setIndex];
|
||||
TValue value{};
|
||||
if (TryReadSet(set, keyWords, tag, value)) return value;
|
||||
static thread_local std::uint64_t timingSequence=0;
|
||||
const bool sampleCycles=metrics&&((++timingSequence&127u)==0);
|
||||
const std::uint64_t initialReadStart=sampleCycles?ReadCpuCycles():0;
|
||||
const bool initialHit=TryReadSet(set, keyWords, tag, value);
|
||||
if(sampleCycles)
|
||||
{
|
||||
metrics->sampledInitialReadCycles+=AdjustedCpuCycleDelta(
|
||||
initialReadStart,ReadCpuCycles());
|
||||
++metrics->sampledInitialReads;
|
||||
}
|
||||
if (initialHit)
|
||||
{
|
||||
if (metrics) ++metrics->hits;
|
||||
return value;
|
||||
}
|
||||
if (metrics) ++metrics->misses;
|
||||
|
||||
FSpinGuard lock(set.Writer);
|
||||
if (TryReadSet(set, keyWords, tag, value)) return value;
|
||||
FSpinGuard lock(set.Writer, metrics,sampleCycles);
|
||||
const std::uint64_t lockedReadStart=sampleCycles?ReadCpuCycles():0;
|
||||
const bool lockedHit=TryReadSet(set, keyWords, tag, value);
|
||||
if(sampleCycles)
|
||||
{
|
||||
metrics->sampledLockedReadCycles+=AdjustedCpuCycleDelta(
|
||||
lockedReadStart,ReadCpuCycles());
|
||||
++metrics->sampledLockedReads;
|
||||
}
|
||||
if (lockedHit)
|
||||
{
|
||||
if (metrics) ++metrics->hits;
|
||||
return value;
|
||||
}
|
||||
|
||||
const std::uint64_t buildStart=sampleCycles?ReadCpuCycles():0;
|
||||
value = std::forward<TFactory>(factory)();
|
||||
if(sampleCycles)
|
||||
{
|
||||
metrics->sampledBuildCycles+=AdjustedCpuCycleDelta(buildStart,ReadCpuCycles());
|
||||
++metrics->sampledBuilds;
|
||||
}
|
||||
computed = true;
|
||||
if (metrics) ++metrics->computes;
|
||||
FValueWords valueWords{};
|
||||
std::memcpy(valueWords.data(), &value, sizeof(TValue));
|
||||
|
||||
@@ -118,6 +211,7 @@ namespace VoxelForgeCoreShared
|
||||
{ replacement = way; break; }
|
||||
if (replacement == Ways)
|
||||
{
|
||||
if (metrics) ++metrics->conflictReplacements;
|
||||
replacement = set.NextWay;
|
||||
set.NextWay = static_cast<std::uint8_t>((set.NextWay + 1) % Ways);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,9 @@ namespace VoxelForgeCore
|
||||
Vec3d normal;
|
||||
Vec2f uv;
|
||||
ColorRgba8 color;
|
||||
std::uint16_t hostMaterialId = 1;
|
||||
std::array<std::uint16_t, 2> resourceMaterialIds{};
|
||||
std::array<std::uint16_t, 2> resourceWeightsQ16{};
|
||||
};
|
||||
|
||||
struct MeshGroups
|
||||
@@ -136,6 +139,18 @@ namespace VoxelForgeCore
|
||||
std::uint64_t columnCacheConflictMisses = 0;
|
||||
std::uint64_t sharedColumnCacheHits = 0;
|
||||
std::uint64_t columnEvaluations = 0;
|
||||
std::uint64_t densitySampleCacheHits = 0;
|
||||
std::uint64_t densitySampleCacheComputes = 0;
|
||||
std::uint64_t marchingCubesSkirtTrianglesEmitted = 0;
|
||||
std::uint64_t marchingCubesSkirtTriangleSeedsSkipped = 0;
|
||||
std::uint64_t islandLocalCacheHits = 0;
|
||||
std::uint64_t islandLocalCacheMisses = 0;
|
||||
std::uint64_t islandLocalCacheConflictReplacements = 0;
|
||||
std::uint64_t islandSharedCacheHits = 0;
|
||||
std::uint64_t islandSharedCacheMisses = 0;
|
||||
std::uint64_t islandSharedCacheComputes = 0;
|
||||
std::uint64_t islandSharedCacheConflictReplacements = 0;
|
||||
std::uint64_t islandSharedCacheLockWaitNanoseconds = 0;
|
||||
std::uint64_t uniformBlockProofsChecked = 0;
|
||||
std::uint64_t uniformBlockProofSamplesChecked = 0;
|
||||
std::uint64_t uniformBlockProofFallbackBlocks = 0;
|
||||
@@ -157,12 +172,30 @@ namespace VoxelForgeCore
|
||||
double marchingCubesSkirtMilliseconds = 0.0;
|
||||
double marchingCubesOtherMilliseconds = 0.0;
|
||||
std::uint64_t marchingCubesGradientFallbackDensityCalls = 0;
|
||||
std::uint64_t marchingCubesGradientCalls = 0;
|
||||
std::uint64_t marchingCubesEdgeMidpointDensityCalls = 0;
|
||||
std::uint64_t marchingCubesEdgeMidpointDensityMemoHits = 0;
|
||||
std::uint64_t marchingCubesGradientMemoHits = 0;
|
||||
std::uint64_t marchingCubesX8DensityCalls = 0;
|
||||
std::uint64_t marchingCubesX8ActiveLanes = 0;
|
||||
std::uint64_t marchingCubesCandidateCells = 0;
|
||||
std::uint64_t marchingCubesZBandRejectedCells = 0;
|
||||
std::uint64_t marchingCubesCellClassificationCalls = 0;
|
||||
std::uint64_t marchingCubesSurfaceCellCalls = 0;
|
||||
std::uint64_t marchingCubesProofSkippedSolidCells = 0;
|
||||
std::uint64_t marchingCubesProofSkippedAirCells = 0;
|
||||
std::uint64_t marchingCubesProofSkippedDensitySamples = 0;
|
||||
std::uint64_t marchingCubesSkirtEdgeCandidates = 0;
|
||||
std::uint64_t marchingCubesSkirtBoundaryCandidates = 0;
|
||||
std::uint64_t marchingCubesSkirtDensityProbes = 0;
|
||||
double groundRefinementExclusiveMilliseconds = 0.0;
|
||||
double capRefinementExclusiveMilliseconds = 0.0;
|
||||
double meshAssemblyMilliseconds = 0.0;
|
||||
double meshingMilliseconds = 0.0;
|
||||
double materialQueryMilliseconds = 0.0;
|
||||
std::uint64_t materialQueries = 0;
|
||||
std::uint64_t materialCandidateChecks = 0;
|
||||
bool materialTileOutsideEveryVein = true;
|
||||
double featureListMilliseconds = 0.0;
|
||||
double farShellMilliseconds = 0.0;
|
||||
double farStratumShellMilliseconds = 0.0;
|
||||
@@ -216,7 +249,9 @@ namespace VoxelForgeCore
|
||||
std::uint64_t densitySampleCount = 0;
|
||||
std::uint64_t meshPayloadAllocatedBytes = 0;
|
||||
std::uint64_t densityHash = 0, meshHash = 0, payloadHash = 0, topologyHash = 0;
|
||||
bool hasDensityHash = false, hasMeshHash = false, hasPayloadHash = false, hasTopologyHash = false;
|
||||
std::uint64_t materialHash = 0;
|
||||
bool hasDensityHash = false, hasMeshHash = false, hasPayloadHash = false,
|
||||
hasTopologyHash = false, hasMaterialHash = false;
|
||||
};
|
||||
|
||||
struct WorldConfiguration
|
||||
@@ -250,8 +285,128 @@ namespace VoxelForgeCore
|
||||
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
||||
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces;
|
||||
// version 20 restores full Gradient amplitude and uses a continuous signed-distance band union.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 21;
|
||||
// version 20 restores full Gradient amplitude and uses a continuous signed-distance band union;
|
||||
// version 22 overlaps tread sections and smooths FloatingIslands passage-mouth features;
|
||||
// version 23 uses signed-distance support for sparse SurfaceWorld-to-TunnelNetwork treads;
|
||||
// version 24 narrows passage-mouth support and filters native ground detail;
|
||||
// version 25 joins open-mouth treads to native terrain relief before flattening inward;
|
||||
// version 26 tapers mouth width and bounds the support to the native terrain surface;
|
||||
// version 27 keeps the native blend through the entrance ramp and narrows its filter;
|
||||
// version 28 tapers the open end to a worn path and blends it into walkable support;
|
||||
// version 29 curves the supported and carved path through both mouth shoulders;
|
||||
// version 30 narrows the flat core and blends support through a noisy natural shoulder;
|
||||
// version 31 adds shared, capsule-safe field relief to both open-mouth walking surfaces;
|
||||
// version 32 lowers the mouth core into the bore and removes thin filtered-field remnants;
|
||||
// version 33 fades relief before small detached field remnants;
|
||||
// version 34 keeps the open-mouth tread within native terrain curvature and noise;
|
||||
// version 35 follows the native heightfield farther into the mouth ramp;
|
||||
// version 36 protects the walking line with a gradual filter fade;
|
||||
// version 37 lengthens that fade and restores the last capsule margin;
|
||||
// version 38 keeps native ground at the lip and blends the tread in below it;
|
||||
// version 39 bends the mouth tread across the native field and restores the open bore;
|
||||
// version 40 uses broad route-only filtering and smooth, low-frequency tread relief;
|
||||
// version 41 rounds the mouth tread and removes filter route masks;
|
||||
// version 42 clips the mouth bore at the native walking surface;
|
||||
// version 43 localizes the mouth field filter without masking the surface route;
|
||||
// version 44 widens the natural shoulder blend and follows lower-frequency terrain;
|
||||
// version 45 tests the passage tread independently of route-anchor support;
|
||||
// version 46 fades the native filter broadly along the authored surface path;
|
||||
// version 47 carries the full winding route with a narrow native-height tread;
|
||||
// version 48 fades route support out before the surface mouth;
|
||||
// version 49 keeps the mouth filtered and restores detail on the outer route;
|
||||
// version 50 limits route-field restoration to 65% of the convolution weight;
|
||||
// version 51 narrows the route fade to 8 m horizontal and 6 m vertical;
|
||||
// version 52 evaluates mouth support in positive-density route tiles;
|
||||
// version 53 fades the supported passage tread in from native ground at both mouths;
|
||||
// version 54 tapers mouth-support width and depth;
|
||||
// version 55 blends that tread from native ground and applies route support after carving;
|
||||
// version 56 seats route support at the audited floor;
|
||||
// version 57's raised lip overfilled the mouth;
|
||||
// version 58 restores the native-height lip and explicitly covers route-support queries;
|
||||
// version 59 fixes route-support distance units and lengthens the worn-mouth taper;
|
||||
// version 60 brings the narrow terrain-following route tread in before the outer mouth lip;
|
||||
// version 61 finishes that tread by five metres and preserves its exact walking boundary;
|
||||
// version 62 fades the route tread along the authored path and seats it below the capsule;
|
||||
// version 63 only adds that tread through native-air gaps and fades it over two path metres;
|
||||
// version 64 rounds the path-to-ground join while keeping its top below the capsule;
|
||||
// version 65 tests rounded tread cross-sections and softer native-height following;
|
||||
// version 66 fades the mouth carve and support together so native crater ground forms the lip;
|
||||
// version 67 follows the native route height more closely and completes the entrance fade sooner;
|
||||
// version 68 narrows the flat walk core and eases a shallow worn ramp into the cave floor;
|
||||
// version 69 lets capsule-cleared mouth treads bridge positive-density floor gaps;
|
||||
// version 70 lowers the mouth tread enough to clear the capsule's toe margin;
|
||||
// version 71 keeps its rounded top inside the route's shallow support-probe band;
|
||||
// version 72 adds mouth support only below its signed floor, avoiding capsule-space fill;
|
||||
// version 73 carries the terrain-following route tread to the actual passage mouth;
|
||||
// version 74 seats that short connection in the audited shallow support band;
|
||||
// version 75 keeps the rounded path edge outside the capsule's shallow floor probes;
|
||||
// version 76 carries the endpoint tread through the passage's first support-probe depth;
|
||||
// version 77 joins the mouth floor to native ground through a noisy walk ramp;
|
||||
// version 78 rounds the ramp shoulder and clears its initial capsule envelope;
|
||||
// version 79 tested the unbuilt native-ground mouth;
|
||||
// version 80 narrows the walk tread;
|
||||
// version 81 adds bounded path relief and a wavering, worn shoulder;
|
||||
// version 82 lowers the relief into the tread to keep its capsule clearance;
|
||||
// version 83 increases the natural edge warp and recessed surface variation;
|
||||
// version 84 moves that variation to the actual mouth and restores support width;
|
||||
// version 85 keeps the established lower-mouth capsule profile while shaping entry mouths;
|
||||
// version 86 restricts route reinforcement to native solid and recesses the worn top;
|
||||
// version 87 restores its pre-carve route placement.
|
||||
// version 88 removes the independent mouth cap; the native crater surface forms the entry ramp.
|
||||
// version 89 removes the one-sided entry tread band as well.
|
||||
// version 90 lengthens the entrance floor-carve fade to retain native ground beneath the route.
|
||||
// version 91 lets a narrow mouth support follow the full native surface relief.
|
||||
// version 92 derives mouth floor alignment from the entrance path for first-segment samples too.
|
||||
// version 93 blends the carve against native density so a zero fade really preserves ground.
|
||||
// version 94 applies the mouth floor fade only beneath the walking surface.
|
||||
// version 95 seats retained native ground just below the capsule's foot clearance.
|
||||
// version 96 limits that preservation to the shallow support band, away from inner coils.
|
||||
// version 97 extends the native support band down to the sampled rock envelope.
|
||||
// version 98 raises the support band back to the route floor while keeping it below the capsule.
|
||||
// version 99 restores native-solid route support after the passage carve;
|
||||
// version 100 restores a shallow, noise-shaped entrance tread under its capsule gate;
|
||||
// version 101 preserves the native FI field around the resolved walking line;
|
||||
// version 102 tapers the mouth filter from its 1.5 m core to the same 12.5 m edge;
|
||||
// version 103 blends route protection over a metre-scale shoulder;
|
||||
// version 104 composes overlapping route masks once, by maximum influence;
|
||||
// version 105 widens the route blend to soften the native/filter seam;
|
||||
// version 106 preserves raw negative rock under the route without unfiltering its crown;
|
||||
// version 107 softens that restore and broadens the narrow upper-coil walk core;
|
||||
// version 108 deepens relief and lowers route restoration to soften the remaining crease;
|
||||
// version 109 blended 60% of the two-metre mouth field convolution;
|
||||
// version 110 restores the full filtered contour and widens the upper walk core;
|
||||
// version 111 permits the capsule-cleared signed tread to bridge the upper mouth gap;
|
||||
// version 112 adds a tenth-metre margin to the rounded upper tread edge.
|
||||
// version 139 keeps native ground only in a capsule-scale mouth lane and carries
|
||||
// the seeded terrain contour through the full upper-mouth support;
|
||||
// versions 140-145 tested short-span decks, mouth fade length, route phase and tread SDF;
|
||||
// versions 146-148 tested a sunk chord, near-side phase and all-style FI spiral.
|
||||
// version 149 routes non-spiral FI mouths into a tight near-side ring.
|
||||
// version 150 widened that ring but left a central pillar.
|
||||
// version 151 keeps the ring half a metre outside the passage bore.
|
||||
// version 152 fills capsule-clear positive-density gaps under the narrow mouth tread.
|
||||
// version 153 tested a one-metre entry-turn sink; it left the raised route visible and rippled the floor.
|
||||
// version 154 tightens the FI ring and uses a steeper, still walkable descent to separate hidden coils.
|
||||
// version 155's tighter coil removed the fin but made sharp fragments and route collisions.
|
||||
// version 156 keeps the 3 m ring, overlaps the bore by 0.25 m, and seats the native tread below the capsule.
|
||||
// version 157 joins the near-side FI ring at a tangent so its entrance does not kink across the crater.
|
||||
// version 158 tested a descending entry coil; its exposed canopy is rejected.
|
||||
// version 159 keeps the tangent and uses a steeper sub-15-degree FI descent to separate its coils.
|
||||
// version 160 removed the mouth walk lane; it left the bridge and lost one support sample.
|
||||
// version 161 tapered the anchored entry radius to 3 m; a fin remained at the crater lip.
|
||||
// version 162 disabled native-tread restoration alone; the carve fade still preserved the ridge.
|
||||
// version 163 disabled both passage restorations; it removed the path support.
|
||||
// version 164 narrowed the first two inner FI route points; it enlarged the lip fin.
|
||||
// version 165 disabled native-rock blending around the FI stratum route; the ridge remained.
|
||||
// version 166 disabled the FI mouth support handoff; route support failed on the island approach.
|
||||
// version 167 tested centerline footing on the FI approach; its bridge remained and floor ripple rose.
|
||||
// version 168 removed the connector's 1 m lift; the crater crossing remained exposed.
|
||||
// version 169 sank every connector 2 m; it left four FI route support probes low.
|
||||
// version 170 sinks only connectors touching the anchored root by 1 m.
|
||||
// Version 208 keeps the crater tread scoped to style-zero FI mouths.
|
||||
// Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges.
|
||||
// Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 211;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
@@ -298,6 +453,59 @@ namespace VoxelForgeCore
|
||||
std::int64_t capRidgeWavelengthSubvoxels = 256 * 64;
|
||||
};
|
||||
|
||||
enum class WorldAssemblySurfaceProfileKindV1 : std::uint32_t
|
||||
{
|
||||
natural = 0,
|
||||
periodicHills = 1,
|
||||
mirroredRelief = 2,
|
||||
flatField = 3
|
||||
};
|
||||
|
||||
// Version zero is the historical noise terrain and is intentionally omitted from
|
||||
// canonical JSON and hashes. Version one uses metre-derived fixed-scale dimensions;
|
||||
// phases are Q0.32 turns generated from the recipe seed during plan construction.
|
||||
struct WorldAssemblySurfaceProfileV1
|
||||
{
|
||||
std::uint32_t version = 0;
|
||||
WorldAssemblySurfaceProfileKindV1 kind = WorldAssemblySurfaceProfileKindV1::natural;
|
||||
std::int64_t h0Subvoxels = 0;
|
||||
std::int64_t amplitudeXSubvoxels = 0;
|
||||
std::int64_t amplitudeYSubvoxels = 0;
|
||||
std::int64_t wavelengthXSubvoxels = 0;
|
||||
std::int64_t wavelengthYSubvoxels = 0;
|
||||
std::uint32_t phaseXTurnsQ32 = 0;
|
||||
std::uint32_t phaseYTurnsQ32 = 0;
|
||||
std::int64_t mirrorLambdaQ16 = 0;
|
||||
std::int64_t minimumArenaClearanceSubvoxels = 0;
|
||||
|
||||
friend bool operator==(const WorldAssemblySurfaceProfileV1&,
|
||||
const WorldAssemblySurfaceProfileV1&) = default;
|
||||
};
|
||||
|
||||
inline std::uint32_t WorldAssemblySurfaceProfileSeededPhaseQ32(
|
||||
std::uint32_t seed, std::uint32_t axis) noexcept
|
||||
{
|
||||
std::uint64_t value = static_cast<std::uint64_t>(seed)
|
||||
^ (axis == 0 ? 0x706572696f645831ull : 0x706572696f645931ull);
|
||||
value += 0x9E3779B97F4A7C15ull;
|
||||
value = (value ^ (value >> 30)) * 0xBF58476D1CE4E5B9ull;
|
||||
value = (value ^ (value >> 27)) * 0x94D049BB133111EBull;
|
||||
value ^= value >> 31;
|
||||
return static_cast<std::uint32_t>(value >> 32);
|
||||
}
|
||||
|
||||
inline void ResolveWorldAssemblySurfaceProfilePhasesV1(
|
||||
WorldAssemblySurfaceProfileV1& profile, std::uint32_t seed) noexcept
|
||||
{
|
||||
if (profile.version == 1
|
||||
&& (profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills
|
||||
|| profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief))
|
||||
{
|
||||
profile.phaseXTurnsQ32 = WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 0);
|
||||
profile.phaseYTurnsQ32 = WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 1);
|
||||
}
|
||||
}
|
||||
|
||||
struct WorldAssemblyRecipeV1
|
||||
{
|
||||
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
|
||||
@@ -315,6 +523,7 @@ namespace VoxelForgeCore
|
||||
std::int64_t biomeClimateReliefWavelengthSubvoxels = 256 * 3333;
|
||||
std::int64_t biomeClimateMoistureWavelengthSubvoxels = 256 * 2500;
|
||||
std::int64_t biomeClimateReliefContrastQ16 = 2ll << 16;
|
||||
WorldAssemblySurfaceProfileV1 surfaceProfile;
|
||||
std::vector<WorldAssemblyBiomeProfileV1> biomes;
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
@@ -374,6 +583,7 @@ namespace VoxelForgeCore
|
||||
std::int64_t capRidgeWavelengthSubvoxels = 256 * 64;
|
||||
std::int64_t waterSurfaceSubvoxels = 0;
|
||||
bool waterEnabled = false;
|
||||
WorldAssemblySurfaceProfileV1 surfaceProfile;
|
||||
std::vector<WorldAssemblyFeatureV1> features;
|
||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||
std::vector<WorldAssemblyRoutePointV1> route;
|
||||
@@ -445,6 +655,8 @@ namespace VoxelForgeCore
|
||||
std::int64_t anchorIslandMinimumRadiusSubvoxels = 0;
|
||||
std::int64_t anchorRootBottomZSubvoxels = 0;
|
||||
std::int64_t anchorMouthOffsetSubvoxels = 0;
|
||||
// Style-zero mouths follow the sampled island surface into the passage lip.
|
||||
bool terrainFollowingRoute = false;
|
||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
@@ -511,8 +723,42 @@ namespace VoxelForgeCore
|
||||
// Hors du domaine prepare, revenir au parcours complet canonique.
|
||||
std::array<std::int64_t, 3> minimumSubvoxels{};
|
||||
std::array<std::int64_t, 3> maximumSubvoxels{};
|
||||
// Stable semantic identity used by the exact shared density-sample cache.
|
||||
std::uint64_t densityCacheContextLow = 0;
|
||||
std::uint64_t densityCacheContextHigh = 0;
|
||||
};
|
||||
|
||||
inline void FinalizeWorldAssemblyFeatureTileListDensityContextV1(
|
||||
WorldAssemblyFeatureTileListV1& list) noexcept
|
||||
{
|
||||
std::uint64_t low = 0x243f6a8885a308d3ull;
|
||||
std::uint64_t high = 0x13198a2e03707344ull;
|
||||
const auto add = [&](std::uint64_t value)
|
||||
{
|
||||
low ^= value + 0x9e3779b97f4a7c15ull + (low << 6) + (low >> 2);
|
||||
low *= 0xbf58476d1ce4e5b9ull;
|
||||
high ^= value + 0x94d049bb133111ebull + (high << 7) + (high >> 3);
|
||||
high *= 0x9e3779b185ebca87ull;
|
||||
};
|
||||
add(list.featureIndices.size());
|
||||
for (const auto value : list.featureIndices) add(value);
|
||||
add(list.groupBounds.size());
|
||||
for (const auto& group : list.groupBounds)
|
||||
{
|
||||
for (auto value : group.minimumSubvoxels) add(static_cast<std::uint64_t>(value));
|
||||
for (auto value : group.maximumSubvoxels) add(static_cast<std::uint64_t>(value));
|
||||
}
|
||||
add(list.groupSearchOrder.size());
|
||||
for (const auto value : list.groupSearchOrder) add(value);
|
||||
add(list.groupDistanceLowerBoundsSubvoxels.size());
|
||||
for (const auto value : list.groupDistanceLowerBoundsSubvoxels)
|
||||
add(static_cast<std::uint64_t>(value));
|
||||
add(list.groupSearchComplete ? 1u : 0u);
|
||||
add(list.candidateEvaluationUseful ? 1u : 0u);
|
||||
list.densityCacheContextLow = low;
|
||||
list.densityCacheContextHigh = high;
|
||||
}
|
||||
|
||||
using FloatingIslandTileFeatureListV1 = WorldAssemblyFeatureTileListV1;
|
||||
using FloatingIslandFeatureGroupBoundsV1 = WorldAssemblyFeatureGroupBoundsV1;
|
||||
|
||||
@@ -535,10 +781,13 @@ namespace VoxelForgeCore
|
||||
ScopedWorldAssemblyFeatureQueriesV1& operator=(const ScopedWorldAssemblyFeatureQueriesV1&) = delete;
|
||||
static const WorldAssemblyFeatureTileListV1* find(const void* recipe, const void* plan,
|
||||
std::int64_t x, std::int64_t y, std::int64_t z, bool columnOnly = false) noexcept;
|
||||
static std::array<std::uint64_t, 2> densityCacheContext(
|
||||
std::int64_t x, std::int64_t y, std::int64_t z) noexcept;
|
||||
private:
|
||||
std::span<const WorldAssemblyFeatureQueryV1> queries_;
|
||||
bool forceCandidates_ = false;
|
||||
const ScopedWorldAssemblyFeatureQueriesV1* previous_ = nullptr;
|
||||
std::uint64_t densityCacheContextLow_ = 0, densityCacheContextHigh_ = 0;
|
||||
};
|
||||
|
||||
struct VFCORE_API FloatingIslandManifestV1
|
||||
@@ -585,8 +834,139 @@ namespace VoxelForgeCore
|
||||
// Core adapter. Coordinates are half-open micrometre intervals; recipe anchors retain their
|
||||
// source fixed-point units so this record never rounds the recipe itself.
|
||||
inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV3 = 3;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3;
|
||||
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
|
||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||
|
||||
// Stable, bounded geometry edits composed into every Core field after the shared
|
||||
// strate boundaries and passages. Coordinates and extents use integer micrometres.
|
||||
enum class ResolvedSdfStampKindV1 : std::uint8_t
|
||||
{
|
||||
carveEllipsoid = 1,
|
||||
carveArch = 2,
|
||||
fillPillar = 3,
|
||||
fillDais = 4,
|
||||
fillMonolith = 5,
|
||||
fillPlatform = 6
|
||||
};
|
||||
|
||||
struct ResolvedLayoutStampRequestV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid;
|
||||
// A preferred center and a bounded deterministic search radius.
|
||||
std::array<std::int64_t, 3> anchorPositionMicrometres{};
|
||||
std::int64_t placementRadiusMicrometres = 0;
|
||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||
std::int64_t smoothWidthMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::int64_t roundingRadiusMicrometres = 0;
|
||||
std::int64_t minimumSpacingMicrometres = 0;
|
||||
// 0 = free placement, 1 = seated on the authored flat profile floor.
|
||||
std::uint8_t attachmentPolicy = 0;
|
||||
std::int64_t floorMicrometres = 0;
|
||||
std::uint8_t yawQuarterTurns = 0;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutStampV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid;
|
||||
std::array<std::int64_t, 3> positionMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||
std::int64_t smoothWidthMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::int64_t roundingRadiusMicrometres = 0;
|
||||
std::int64_t minimumSpacingMicrometres = 0;
|
||||
std::uint8_t attachmentPolicy = 0;
|
||||
std::int64_t floorMicrometres = 0;
|
||||
std::uint64_t irregularitySeed = 0;
|
||||
std::uint8_t yawQuarterTurns = 0;
|
||||
};
|
||||
|
||||
// Optional room and swept-edge records for a resolved cavern graph. Geometry is stored
|
||||
// in integer micrometres; bounds include smooth blending, relief, and floor support.
|
||||
struct ResolvedCavernRoomV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
std::array<std::int64_t, 3> centerMicrometres{};
|
||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||
std::int64_t floorMicrometres = 0;
|
||||
std::int64_t smoothWidthMicrometres = 0;
|
||||
// Optional feature-to-feature union width. Zero preserves the historical
|
||||
// behavior where smoothWidthMicrometres also controls the graph union.
|
||||
std::int64_t unionWidthMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::uint64_t irregularitySeed = 0;
|
||||
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
};
|
||||
|
||||
struct ResolvedCavernEdgeV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::string startAnchorStableId;
|
||||
std::string endRoomStableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
// A single cubic Bezier in XY. Its four Z values are the cubic floor profile.
|
||||
std::array<std::array<std::int64_t, 3>, 4> controlPointsMicrometres{};
|
||||
// Resolve-time, error-bounded approximation of the cubic centerline. Each
|
||||
// consecutive pair is a physical segment used by the native cave field.
|
||||
std::vector<std::array<std::int64_t, 3>> polylineMicrometres;
|
||||
std::int64_t boreRadiusMicrometres = 0;
|
||||
std::int64_t boreHeightMicrometres = 0;
|
||||
std::int64_t floorSupportDepthMicrometres = 0;
|
||||
std::int64_t smoothWidthMicrometres = 0;
|
||||
// Optional feature-to-feature union width; floor fillets still use smoothWidth.
|
||||
std::int64_t unionWidthMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::uint64_t irregularitySeed = 0;
|
||||
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
};
|
||||
|
||||
// Immutable material-only deposits. The capsule and its conservative bounds are in
|
||||
// micrometres; bounds include irregularity and the blend envelope.
|
||||
struct ResolvedLayoutOreVeinRequestV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
std::uint16_t resourceMaterialId = 0;
|
||||
std::array<std::array<std::int64_t, 3>, 2> anchorEndpointsMicrometres{};
|
||||
std::int64_t placementRadiusMicrometres = 0;
|
||||
std::int64_t radiusMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::int64_t blendWidthMicrometres = 0;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutOreVeinV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
std::uint16_t resourceMaterialId = 0;
|
||||
std::array<std::array<std::int64_t, 3>, 2> endpointsMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
std::int64_t radiusMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::int64_t blendWidthMicrometres = 0;
|
||||
std::uint64_t irregularitySeed = 0;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutAnchorV2
|
||||
{
|
||||
std::string kind;
|
||||
@@ -609,6 +989,24 @@ namespace VoxelForgeCore
|
||||
std::int64_t solidDensityQ16 = 1ll << 16;
|
||||
};
|
||||
|
||||
// Runtime policy resolved once from the director's immutable strate/interface
|
||||
// description. These derived capabilities are deliberately omitted from canonical
|
||||
// JSON: their source fields are already hashed and the records preserve that identity.
|
||||
struct ResolvedLayoutStratePassageCapabilitiesV1
|
||||
{
|
||||
bool hasSurfaceLanding = false;
|
||||
bool hasFloatingIslandLanding = false;
|
||||
bool usePassageAnchorForColumns = false;
|
||||
bool hasNativeCaveField = false;
|
||||
bool hasNativeLatticeField = false;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutInterfaceCapabilitiesV1
|
||||
{
|
||||
bool preserveUpperBedrockAirGap = false;
|
||||
bool surfaceToTunnelLandingSupport = false;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutStratumV2
|
||||
{
|
||||
std::string stableId;
|
||||
@@ -638,8 +1036,29 @@ namespace VoxelForgeCore
|
||||
std::int64_t managerChunkWidthMicrometres = 0;
|
||||
std::int64_t dispatchChunkWidthMicrometres = 0;
|
||||
std::vector<ResolvedLayoutAnchorV2> anchors;
|
||||
ResolvedLayoutStratePassageCapabilitiesV1 passageCapabilities;
|
||||
ResolvedLayoutInterfaceCapabilitiesV1 lowerInterfaceCapabilities;
|
||||
};
|
||||
|
||||
inline bool IsNativeCaveFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
|
||||
{
|
||||
return stratum.generatorKind=="Caves"
|
||||
&&stratum.recipeId=="Core.Caves.PrimitiveField"
|
||||
&&stratum.recipeVersion==1;
|
||||
}
|
||||
|
||||
inline bool IsNativeLatticeFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept
|
||||
{
|
||||
return (stratum.generatorKind=="Maze"||stratum.generatorKind=="VerticalShafts")
|
||||
&&stratum.recipeId=="Core.Lattice.PrimitiveField"
|
||||
&&stratum.recipeVersion==1;
|
||||
}
|
||||
|
||||
inline bool HasNativeGraphField(const ResolvedLayoutStratumV2& stratum) noexcept
|
||||
{
|
||||
return IsNativeCaveFieldStratum(stratum)||IsNativeLatticeFieldStratum(stratum);
|
||||
}
|
||||
|
||||
/** Per-column resolved bands for one adjacent bedrock/ceiling interface.
|
||||
* crossingDepthMicrometres is the actual overlap depth between the two facing bands.
|
||||
*/
|
||||
@@ -669,6 +1088,8 @@ namespace VoxelForgeCore
|
||||
std::string stableId;
|
||||
std::int32_t upperStrateIndex = -1;
|
||||
std::int32_t lowerStrateIndex = -1;
|
||||
// EVoxelPassageStyle numeric value used by style-specific passage support.
|
||||
std::uint32_t style = 1;
|
||||
std::int64_t placementRadiusSubvoxels = 0;
|
||||
std::array<std::int64_t, 3> upperMouthSubvoxels{};
|
||||
std::array<std::int64_t, 3> lowerMouthSubvoxels{};
|
||||
@@ -732,6 +1153,16 @@ namespace VoxelForgeCore
|
||||
std::int64_t strateLayoutChunkWidthSubvoxels = 8192;
|
||||
std::vector<ResolvedLayoutStratumV2> strates;
|
||||
std::vector<ResolvedLayoutPassageV1> passages;
|
||||
// Kept absent from canonical layout identity when no feature requests stamps.
|
||||
std::uint32_t stampVocabularyVersion = 0;
|
||||
std::vector<ResolvedLayoutStampV1> stamps;
|
||||
// Kept absent from canonical identity when there are no graph records.
|
||||
std::uint32_t cavernGraphVocabularyVersion = 0;
|
||||
std::vector<ResolvedCavernRoomV1> cavernRooms;
|
||||
std::vector<ResolvedCavernEdgeV1> cavernEdges;
|
||||
// Omitted from canonical identity when no records are present.
|
||||
std::uint32_t oreVeinVocabularyVersion = 0;
|
||||
std::vector<ResolvedLayoutOreVeinV1> oreVeins;
|
||||
std::string stableIdentity;
|
||||
|
||||
bool finalize(std::string& error);
|
||||
@@ -767,6 +1198,68 @@ namespace VoxelForgeCore
|
||||
const std::vector<ResolvedLayoutPassageConfigV1>& configs,
|
||||
std::string& error);
|
||||
|
||||
struct ResolvedLayoutStampPlacementAuditV1
|
||||
{
|
||||
std::uint32_t negativeControlsRun = 0;
|
||||
std::uint32_t negativeControlsRejected = 0;
|
||||
bool passed = false;
|
||||
};
|
||||
VFCORE_API bool ResolveResolvedLayoutStampsV1(
|
||||
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||
std::span<const ResolvedLayoutStampRequestV1> requests,
|
||||
std::string& error);
|
||||
VFCORE_API bool ResolveResolvedLayoutStampsV2(
|
||||
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||
std::span<const ResolvedLayoutStampRequestV1> requests,
|
||||
std::string& error);
|
||||
// Vocabulary 3 adds explicit rounded capped-cylinder monoliths and rounded-box platforms.
|
||||
VFCORE_API bool ResolveResolvedLayoutStampsV3(
|
||||
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||
std::span<const ResolvedLayoutStampRequestV1> requests,
|
||||
std::string& error);
|
||||
VFCORE_API ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1(
|
||||
const ResolvedLayoutSnapshotV3& layout);
|
||||
|
||||
VFCORE_API bool ResolveResolvedCavernGraphV1(
|
||||
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||
std::span<const ResolvedCavernRoomV1> rooms,
|
||||
std::span<const ResolvedCavernEdgeV1> edges,
|
||||
std::string& error);
|
||||
VFCORE_API bool ResolveResolvedLayoutOreVeinsV1(
|
||||
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||
std::span<const ResolvedLayoutOreVeinRequestV1> requests,
|
||||
std::string& error);
|
||||
|
||||
struct ResolvedLayoutOreVeinPlacementAuditV1
|
||||
{
|
||||
std::uint32_t negativeControlsRun = 0;
|
||||
std::uint32_t negativeControlsRejected = 0;
|
||||
bool passed = false;
|
||||
};
|
||||
VFCORE_API ResolvedLayoutOreVeinPlacementAuditV1 AuditResolvedLayoutOreVeinPlacementV1(
|
||||
const ResolvedLayoutSnapshotV3& layout);
|
||||
|
||||
struct MaterialSampleV1
|
||||
{
|
||||
std::uint16_t hostMaterialId = 1;
|
||||
std::array<std::uint16_t, 2> resourceMaterialIds{};
|
||||
std::array<std::uint16_t, 2> resourceWeightsQ16{};
|
||||
};
|
||||
|
||||
struct ResourceVolumeV1
|
||||
{
|
||||
std::uint16_t resourceMaterialId = 0;
|
||||
std::uint64_t weightedSamplesQ16 = 0;
|
||||
double volumeCubicMetres = 0.0;
|
||||
};
|
||||
|
||||
struct OreMiningYieldV1
|
||||
{
|
||||
std::uint64_t solidSamplesRemoved = 0;
|
||||
double sampleSpacingMetres = 0.0;
|
||||
std::vector<ResourceVolumeV1> resources;
|
||||
};
|
||||
|
||||
struct FloatingIslandBoundsAuditResult
|
||||
{
|
||||
std::uint64_t boxesTested = 0;
|
||||
@@ -891,6 +1384,8 @@ namespace VoxelForgeCore
|
||||
std::uint64_t columnConflictMisses = 0;
|
||||
std::uint64_t sharedColumnHits = 0;
|
||||
std::uint64_t columnEvaluations = 0;
|
||||
std::uint64_t densitySampleCacheHits = 0;
|
||||
std::uint64_t densitySampleCacheComputes = 0;
|
||||
double columnComputeMilliseconds = 0.0;
|
||||
bool captureCoordinates = false;
|
||||
bool coordinateCaptureComplete = true;
|
||||
@@ -1045,6 +1540,7 @@ namespace VoxelForgeCore
|
||||
const WorldConfiguration& configuration() const;
|
||||
const WorldAssemblyManifest* worldAssembly() const noexcept;
|
||||
const ResolvedLayoutSnapshotV3* resolvedLayout() const noexcept;
|
||||
ResolvedWorldSnapshot withResolvedLayout(const ResolvedLayoutSnapshotV3& layout) const;
|
||||
ResolvedWorldSnapshot withoutResolvedLayoutPassages() const;
|
||||
|
||||
private:
|
||||
@@ -1088,6 +1584,7 @@ namespace VoxelForgeCore
|
||||
bool roundTunnels = false;
|
||||
bool enhancedTunnelWalls = true;
|
||||
bool profileDetailBand = false;
|
||||
bool parallelSignEdgeRepair = false;
|
||||
// World-wide walkability is a validation pass, not an input to the compiled field.
|
||||
// Keep it enabled for benchmarks/audits; the interactive viewer can defer it.
|
||||
bool validatePassageRoutes = true;
|
||||
@@ -1287,9 +1784,18 @@ namespace VoxelForgeCore
|
||||
// Diagnostic seulement; enregistre les colonnes XY evaluees par cette tuile LOD8.
|
||||
bool captureColumnCoordinates = false;
|
||||
bool useSharedColumnCache = true;
|
||||
// Fixed visibility mask for the four horizontal skirts and the two vertical cap skirts.
|
||||
// The mask is part of the standalone mesh-cache identity and defaults to all faces.
|
||||
std::uint8_t visibleSkirtFaceMask = 0x3f;
|
||||
// Separate cold pass only; samples the MC inner stages without changing output.
|
||||
// Passe froide separee seulement; mesure les etapes internes MC sans changer la sortie.
|
||||
bool profileMesherStages = false;
|
||||
// Research-only: collect tile-drain kernel counters and floating-island cache metrics.
|
||||
// Inactive on ordinary generation so the hot path only pays the option check.
|
||||
bool instrumentTileDrain = false;
|
||||
bool exactMidpointDensityMemo = false;
|
||||
bool activeLaneBatching = false;
|
||||
std::uint32_t proofBlockCells = 8;
|
||||
// Diagnostic only; fills and checks every lattice sample in blocks certified uniform.
|
||||
// Diagnostic seulement; evalue et verifie tous les echantillons des blocs uniformes.
|
||||
bool auditUniformBlockProofs = false;
|
||||
@@ -1306,7 +1812,7 @@ namespace VoxelForgeCore
|
||||
// LOD4-8 mesh hashes are salted; density and LOD0-3 keep their direct identities.
|
||||
// Incremente quand les plafonds partages ou l'horizon lointain changent.
|
||||
// Les hashes de maillage LOD4-8 incluent cette version; LOD0-3 gardent leur identite directe.
|
||||
std::uint32_t farHorizonVersion = 4;
|
||||
std::uint32_t farHorizonVersion = 5;
|
||||
// Linux research controls for reproducing L5 negative controls. Defaults preserve output.
|
||||
std::uint32_t diagnosticGlobalOctaveDrop = 0;
|
||||
std::uint32_t diagnosticCellStride = 1;
|
||||
@@ -1360,6 +1866,12 @@ namespace VoxelForgeCore
|
||||
ShapeBuildStats buildStats() const;
|
||||
|
||||
float densityAt(Vec3d voxelPosition) const;
|
||||
// Companion material query for the bound strate evaluator; coordinates use Core voxels.
|
||||
MaterialSampleV1 materialAt(Vec3d voxelPosition) const;
|
||||
// Measure-only carve estimate. This does not apply an edit or grant inventory.
|
||||
OreMiningYieldV1 measureOreMiningYieldForSphere(
|
||||
Vec3d centerVoxel, double radiusMetres,
|
||||
double sampleSpacingMetres = 0.25) const;
|
||||
// Audit-only view of density-field columns and short spans supporting passage voids.
|
||||
std::vector<PassageVoidCrossingAuditV1> passageVoidCrossings() const;
|
||||
bool isPassageColumnSample(std::size_t passageIndex, std::size_t segmentIndex,
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
||||
#include "VoxelForgeCore/SharedColumnCache.h"
|
||||
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
|
||||
|
||||
// Keep Core's expression-level FMA behavior fixed regardless of the toolchain setting.
|
||||
// Clang's "fast" mode ignores the pragma, so Core's runtime self-test rejects that build.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
||||
#include "VoxelForgeCore/CpuCycleCounter.h"
|
||||
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
|
||||
#include "VoxelForgeCore/SharedColumnCache.h"
|
||||
#include "Json.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
@@ -203,6 +206,7 @@ namespace floating_island_detail
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.boundarySealSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeHalfWidthSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeThicknessSubvoxels), 8);
|
||||
if(recipe.terrainFollowingRoute)HashWord(hash,1,1);
|
||||
HashWord(hash, HashRouteConstraints(recipe.routeConstraints), 8);
|
||||
return hash;
|
||||
}
|
||||
@@ -470,13 +474,69 @@ namespace floating_island_detail
|
||||
double DeltaX=0.0,DeltaY=0.0,Radial=0.0,Angle=0.0,LobePhase=0.0,TopSurface=0.0;
|
||||
};
|
||||
|
||||
struct FIslandColumnCacheInstrumentation
|
||||
{
|
||||
std::uint64_t localHits=0,localMisses=0,localConflictReplacements=0;
|
||||
std::uint64_t localProbeCycles=0,localProbeSamples=0;
|
||||
std::uint64_t tileStartCycles=0,tileCycles=0;
|
||||
VoxelForgeCoreShared::FColumnCacheMetrics shared;
|
||||
FIslandFieldAttributionInstrumentation attribution;
|
||||
};
|
||||
|
||||
inline FIslandColumnCacheInstrumentation*& ActiveIslandColumnCacheInstrumentation() noexcept
|
||||
{
|
||||
static thread_local FIslandColumnCacheInstrumentation* active=nullptr;
|
||||
return active;
|
||||
}
|
||||
|
||||
inline FIslandColumnCacheInstrumentation* SetIslandColumnCacheInstrumentation(
|
||||
FIslandColumnCacheInstrumentation* value) noexcept
|
||||
{
|
||||
FIslandColumnCacheInstrumentation* previous=ActiveIslandColumnCacheInstrumentation();
|
||||
ActiveIslandColumnCacheInstrumentation()=value;
|
||||
SetActiveIslandFieldAttribution(value?&value->attribution:nullptr);
|
||||
return previous;
|
||||
}
|
||||
|
||||
using FSharedIslandColumnCache =
|
||||
VoxelForgeCoreShared::TSharedColumnValueCache<FIslandColumnCacheEntry,16384,4>;
|
||||
|
||||
struct FIslandColumnTraceKey
|
||||
{
|
||||
bool valid=false;
|
||||
std::uint64_t recipeHash=0,planHash=0,island=0;
|
||||
std::int64_t x=0,y=0;
|
||||
};
|
||||
|
||||
struct FIslandColumnReuseTrace
|
||||
{
|
||||
std::array<FIslandColumnTraceKey,128> recentMisses{};
|
||||
std::array<FIslandColumnTraceKey,128> recentEvictions{};
|
||||
std::uint64_t missSequence=0,printedEvents=0;
|
||||
};
|
||||
|
||||
inline bool SameTraceKey(const FIslandColumnTraceKey& left,
|
||||
const FIslandColumnTraceKey& right) noexcept
|
||||
{
|
||||
return left.valid&&right.valid&&left.recipeHash==right.recipeHash
|
||||
&&left.planHash==right.planHash&&left.island==right.island
|
||||
&&left.x==right.x&&left.y==right.y;
|
||||
}
|
||||
|
||||
inline std::size_t TraceKeyIndex(const FIslandColumnTraceKey& key) noexcept
|
||||
{
|
||||
std::uint64_t hash=key.island^static_cast<std::uint64_t>(key.x)*0x9e3779b97f4a7c15ull
|
||||
^static_cast<std::uint64_t>(key.y)*0xc2b2ae3d27d4eb4full
|
||||
^key.recipeHash^std::rotl(key.planHash,17);
|
||||
hash^=hash>>33;hash*=0xff51afd7ed558ccdull;hash^=hash>>33;
|
||||
return static_cast<std::size_t>(hash&127u);
|
||||
}
|
||||
|
||||
inline const FIslandColumnCacheEntry& IslandColumnCache(
|
||||
const FloatingIslandRecipeV1& recipe,const FloatingIslandPlanV1& plan,
|
||||
const FloatingIslandAnchorV1& island,double x,double y)
|
||||
{
|
||||
FScopedIslandAttributionTimer columnTimer(EIslandAttributionBucket::Column);
|
||||
// Density samples arrive on integer Core lattice columns; reuse the exact column metrics
|
||||
// across Z samples without changing the field value or touching shared world state.
|
||||
// Les echantillons suivent les colonnes Core entieres; reutiliser les memes metriques XY.
|
||||
@@ -486,6 +546,7 @@ namespace floating_island_detail
|
||||
static thread_local VoxelForgeCoreThreadLocal::FLazy<
|
||||
std::array<FIslandColumnCacheEntry,SetCount*Ways>> Cache;
|
||||
auto& cache=Cache.Get();
|
||||
FScopedIslandAttributionTimer localProbeTimer(EIslandAttributionBucket::LocalProbe);
|
||||
const std::int64_t SampleX=static_cast<std::int64_t>(x);
|
||||
const std::int64_t SampleY=static_cast<std::int64_t>(y);
|
||||
std::uint64_t IndexKey=island.stableId
|
||||
@@ -500,6 +561,7 @@ namespace floating_island_detail
|
||||
IndexKey^=IndexKey>>33;
|
||||
const std::size_t Base=static_cast<std::size_t>(IndexKey&(SetCount-1))*Ways;
|
||||
FIslandColumnCacheEntry* Replacement=&cache[Base+((IndexKey>>11)&(Ways-1))];
|
||||
bool bFoundInvalid=false;
|
||||
for(std::size_t Way=0;Way<Ways;++Way)
|
||||
{
|
||||
FIslandColumnCacheEntry& Entry=cache[Base+Way];
|
||||
@@ -515,8 +577,64 @@ namespace floating_island_detail
|
||||
&&Entry.TopHalf==island.topHalfSubvoxels
|
||||
&&Entry.TopFlattenQ16==recipe.topFlattenQ16
|
||||
&&Entry.ContourWarp==recipe.contourWarpSubvoxels;
|
||||
if(bKeyMatches)return Entry;
|
||||
if(!Entry.bValid)Replacement=&Entry;
|
||||
if(bKeyMatches)
|
||||
{
|
||||
if(auto* stats=ActiveIslandColumnCacheInstrumentation())++stats->localHits;
|
||||
return Entry;
|
||||
}
|
||||
if(!Entry.bValid&&!bFoundInvalid)
|
||||
{Replacement=&Entry;bFoundInvalid=true;}
|
||||
}
|
||||
localProbeTimer.Stop();
|
||||
if(auto* stats=ActiveIslandColumnCacheInstrumentation())
|
||||
{
|
||||
++stats->localMisses;
|
||||
if(!bFoundInvalid)++stats->localConflictReplacements;
|
||||
static thread_local FIslandColumnReuseTrace trace;
|
||||
if((++trace.missSequence&1023u)==0)
|
||||
{
|
||||
auto& a=stats->attribution;
|
||||
++a.traceSampledMisses;
|
||||
const FIslandColumnTraceKey requested{true,recipe.contentHash,
|
||||
plan.contentHash,island.stableId,SampleX,SampleY};
|
||||
const std::size_t requestSlot=TraceKeyIndex(requested);
|
||||
const bool seenBefore=SameTraceKey(trace.recentMisses[requestSlot],requested);
|
||||
const bool reusedEviction=SameTraceKey(
|
||||
trace.recentEvictions[requestSlot],requested);
|
||||
if(seenBefore)++a.traceSeenBefore;
|
||||
if(reusedEviction)
|
||||
{
|
||||
++a.traceReusedEvictions;
|
||||
trace.recentEvictions[requestSlot].valid=false;
|
||||
}
|
||||
FIslandColumnTraceKey evicted{};
|
||||
std::size_t replacementWay=0;
|
||||
if(!bFoundInvalid&&Replacement)
|
||||
{
|
||||
evicted={Replacement->bValid,Replacement->RecipeContentHash,
|
||||
Replacement->PlanContentHash,Replacement->StableId,
|
||||
Replacement->SampleX,Replacement->SampleY};
|
||||
replacementWay=static_cast<std::size_t>(Replacement-&cache[Base]);
|
||||
++a.traceEvictions;
|
||||
if(evicted.valid)
|
||||
trace.recentEvictions[TraceKeyIndex(evicted)]=evicted;
|
||||
}
|
||||
trace.recentMisses[requestSlot]=requested;
|
||||
if(trace.printedEvents<16&&a.traceCount<a.traceSet.size())
|
||||
{
|
||||
const std::size_t event=a.traceCount++;
|
||||
++trace.printedEvents;
|
||||
a.traceSet[event]=Base/Ways;
|
||||
a.traceX[event]=SampleX;a.traceY[event]=SampleY;
|
||||
a.traceIsland[event]=island.stableId;
|
||||
a.traceEvictedX[event]=evicted.valid?evicted.x:0;
|
||||
a.traceEvictedY[event]=evicted.valid?evicted.y:0;
|
||||
a.traceEvictedIsland[event]=evicted.valid?evicted.island:0;
|
||||
a.traceFlags[event]=static_cast<std::uint8_t>(
|
||||
(!bFoundInvalid?1u:0u)|(seenBefore?2u:0u)|(reusedEviction?4u:0u)
|
||||
|(replacementWay<<3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto build=[&]()
|
||||
@@ -543,9 +661,14 @@ namespace floating_island_detail
|
||||
Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY);
|
||||
Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX);
|
||||
Entry.LobePhase=LobePhase(recipe,island);
|
||||
double contourOffset=0.0;
|
||||
{
|
||||
FScopedIslandAttributionTimer helixTimer(EIslandAttributionBucket::Helix);
|
||||
contourOffset=LobeOffsetWithPhase(recipe,Entry.Angle,
|
||||
static_cast<double>(island.topZSubvoxels),Entry.LobePhase);
|
||||
}
|
||||
const double EffectiveRadius=std::max(1.0,static_cast<double>(island.radiusSubvoxels)
|
||||
+LobeOffsetWithPhase(recipe,Entry.Angle,static_cast<double>(island.topZSubvoxels),
|
||||
Entry.LobePhase));
|
||||
+contourOffset);
|
||||
const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0);
|
||||
const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
||||
*static_cast<double>(island.topHalfSubvoxels)*2.0*Edge*Edge;
|
||||
@@ -561,7 +684,12 @@ namespace floating_island_detail
|
||||
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
|
||||
plan.contentHash,SampleX,SampleY,island.stableId};
|
||||
bool computed=false;
|
||||
Entry=shared->getOrCompute(key,build,computed);
|
||||
auto* stats=ActiveIslandColumnCacheInstrumentation();
|
||||
{
|
||||
FScopedIslandAttributionTimer sharedCacheTimer(EIslandAttributionBucket::SharedCache);
|
||||
Entry=shared->getOrCompute(key,build,computed,
|
||||
stats?&stats->shared:nullptr);
|
||||
}
|
||||
}
|
||||
else Entry=build();
|
||||
*Replacement=Entry;
|
||||
@@ -624,6 +752,7 @@ namespace floating_island_detail
|
||||
const FloatingIslandAnchorV1& island,
|
||||
std::int32_t rootIndex, double x, double y, double z)
|
||||
{
|
||||
FScopedIslandAttributionTimer attributionTimer(EIslandAttributionBucket::Root);
|
||||
// Cache derived root constants per worker. The complete immutable input key keeps this
|
||||
// value-only cache safe across worlds, epochs and worker schedules.
|
||||
// Mettre en cache les constantes par worker; la cle complete evite tout partage d'etat.
|
||||
@@ -694,6 +823,38 @@ namespace floating_island_detail
|
||||
return (std::sqrt(std::max(q, 0.0)) - 1.0) * Parameters.MinimumRadius;
|
||||
}
|
||||
|
||||
inline FRootLobeParameters RootLobeParametersForBounds(
|
||||
const FloatingIslandRecipeV1& recipe,const FloatingIslandAnchorV1& island,
|
||||
std::int32_t rootIndex)
|
||||
{
|
||||
constexpr std::array<std::array<double,2>,4> pattern{{
|
||||
{{-0.92,-0.12}},{{0.28,0.98}},{{0.83,-0.42}},{{-0.16,-0.94}}}};
|
||||
const std::uint64_t salt=0x524f4f540000ull+static_cast<std::uint64_t>(rootIndex);
|
||||
const std::uint64_t roll=Roll(recipe.seed,island.stableId,salt);
|
||||
const double radius=static_cast<double>(island.radiusSubvoxels);
|
||||
const double rootDepth=radius*Q16ToDouble(recipe.rootDepthRatioQ16);
|
||||
const double crustBottom=static_cast<double>(IslandCrustBottom(recipe,island));
|
||||
const bool centralButtress=rootIndex==4;
|
||||
const double orbit=centralButtress?0.0:radius*(0.17+0.07*UnitHash(roll));
|
||||
const double rootX=static_cast<double>(island.centerSubvoxels[0])
|
||||
+(centralButtress?0.0:pattern[static_cast<std::size_t>(rootIndex)][0]*orbit);
|
||||
const double rootY=static_cast<double>(island.centerSubvoxels[1])
|
||||
+(centralButtress?0.0:pattern[static_cast<std::size_t>(rootIndex)][1]*orbit);
|
||||
const double shortRootVariation=UnitHash(Roll(recipe.seed,island.stableId,salt^0x4c454eull));
|
||||
const double lengthRatio=centralButtress?1.0:(rootIndex==0?1.0
|
||||
:Q16ToDouble(island.taperEndQ16)*(0.82+0.18*shortRootVariation));
|
||||
const double lowerDepth=rootDepth*lengthRatio;
|
||||
const double embedDepth=rootDepth*(centralButtress?0.22:(rootIndex==0?0.30:0.22));
|
||||
const double verticalRadius=0.5*(lowerDepth+embedDepth);
|
||||
const double centerZ=crustBottom+0.5*(embedDepth-lowerDepth);
|
||||
const double rx=radius*(centralButtress?0.25
|
||||
:0.15+0.055*UnitHash(Roll(recipe.seed,island.stableId,salt^0x5258ull)));
|
||||
const double ry=radius*(centralButtress?0.25
|
||||
:0.15+0.065*UnitHash(Roll(recipe.seed,island.stableId,salt^0x5259ull)));
|
||||
return {rootX,rootY,centerZ,rx*rx,ry*ry,verticalRadius*verticalRadius,
|
||||
std::min({rx,ry,verticalRadius})};
|
||||
}
|
||||
|
||||
inline double ApplySurfaceDetail(const FloatingIslandRecipeV1& recipe,
|
||||
std::uint64_t featureId, double structural,
|
||||
double x, double y, double z, double detailScale = 1.0)
|
||||
@@ -712,6 +873,7 @@ namespace floating_island_detail
|
||||
const FloatingIslandAnchorV1& island,
|
||||
double x, double y, double z)
|
||||
{
|
||||
FScopedIslandAttributionTimer islandTimer(EIslandAttributionBucket::Island);
|
||||
if (recipe.geometryVersion < 2)
|
||||
{
|
||||
const double dx = x - static_cast<double>(island.centerSubvoxels[0]);
|
||||
@@ -739,8 +901,13 @@ namespace floating_island_detail
|
||||
const double radial=Column.Radial;
|
||||
const double topZ = static_cast<double>(island.topZSubvoxels);
|
||||
const double radius = static_cast<double>(island.radiusSubvoxels);
|
||||
double contourOffset=0.0;
|
||||
{
|
||||
FScopedIslandAttributionTimer helixTimer(EIslandAttributionBucket::Helix);
|
||||
contourOffset=LobeOffsetWithPhase(recipe,Column.Angle,z,Column.LobePhase);
|
||||
}
|
||||
const double effectiveRadius = std::max(0.0,
|
||||
radius + LobeOffsetWithPhase(recipe,Column.Angle,z,Column.LobePhase));
|
||||
radius + contourOffset);
|
||||
const double edge = std::clamp(radial / std::max(effectiveRadius, 1.0), 0.0, 1.0);
|
||||
const double lowerSurface = static_cast<double>(IslandCrustBottom(recipe, island))
|
||||
+ radius * 0.16 * edge * edge;
|
||||
@@ -787,6 +954,7 @@ namespace floating_island_detail
|
||||
bool* topSurface = nullptr,
|
||||
double* alongFraction = nullptr)
|
||||
{
|
||||
FScopedIslandAttributionTimer connectorTimer(EIslandAttributionBucket::Connector);
|
||||
const double ax = static_cast<double>(edge.startSubvoxels[0]);
|
||||
const double ay = static_cast<double>(edge.startSubvoxels[1]);
|
||||
const double az = static_cast<double>(edge.startSubvoxels[2]);
|
||||
@@ -805,7 +973,37 @@ namespace floating_island_detail
|
||||
const double t = std::clamp(along / length, 0.0, 1.0);
|
||||
if (alongFraction) *alongFraction = t;
|
||||
const double deck = az + (static_cast<double>(edge.endSubvoxels[2]) - az) * t;
|
||||
const std::uint64_t westernEdgeId = StableId(recipe.seed, 0x200000000ull,
|
||||
0x425249444745ull);
|
||||
const bool westernSupportConnector=edge.stableId==westernEdgeId;
|
||||
if(recipe.geometryVersion>=2&&WorldAssemblyEvaluatorVersion>=183
|
||||
&&westernSupportConnector)
|
||||
{
|
||||
const double phase=static_cast<double>(Roll(recipe.seed,edge.stableId,0x5749445448ull)
|
||||
&0xffffffffull)*(2.0*Pi/4294967296.0);
|
||||
const double localHalfWidth=std::max(
|
||||
static_cast<double>(recipe.routeConstraints.capsuleRadiusSubvoxels+Q),
|
||||
static_cast<double>(edge.halfWidthSubvoxels)
|
||||
+0.48*static_cast<double>(Q)*std::sin(6.0*Pi*t+phase));
|
||||
const double localThickness=static_cast<double>(edge.thicknessSubvoxels);
|
||||
const double rounding=std::min({1.5*static_cast<double>(Q),
|
||||
localHalfWidth*0.6,localThickness*0.45});
|
||||
const double roundedX=side-(localHalfWidth-rounding);
|
||||
const double roundedY=std::abs(along-0.5*length)-(0.5*length-rounding);
|
||||
const double roundedZ=std::abs(z-(deck-0.5*localThickness))
|
||||
-(0.5*localThickness-rounding);
|
||||
const double roundedOutside=std::sqrt(
|
||||
std::pow(std::max(roundedX,0.0),2)
|
||||
+std::pow(std::max(roundedY,0.0),2)
|
||||
+std::pow(std::max(roundedZ,0.0),2));
|
||||
const double roundedInside=std::min(std::max(roundedX,
|
||||
std::max(roundedY,roundedZ)),0.0);
|
||||
if(topSurface)*topSurface=z-deck>=std::max(horizontal,
|
||||
deck-localThickness-z);
|
||||
return roundedOutside+roundedInside-rounding;
|
||||
}
|
||||
const double arch = recipe.geometryVersion >= 2
|
||||
&& WorldAssemblyEvaluatorVersion < 172
|
||||
? 2.0 * static_cast<double>(edge.thicknessSubvoxels) * (4.0 * t * (1.0 - t))
|
||||
: 0.0;
|
||||
const double upper = z - deck;
|
||||
@@ -814,6 +1012,227 @@ namespace floating_island_detail
|
||||
return std::max({horizontal, upper, lower});
|
||||
}
|
||||
|
||||
struct FScalarRange
|
||||
{
|
||||
double minimum=0.0,maximum=0.0;
|
||||
};
|
||||
|
||||
struct FDensityRange
|
||||
{
|
||||
double minimum=0.0,maximum=0.0;
|
||||
};
|
||||
|
||||
inline double DistanceToRange(double minimum,double maximum,double value)
|
||||
{
|
||||
if(value<minimum)return minimum-value;
|
||||
if(value>maximum)return value-maximum;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
inline double MaximumDistanceToRange(double minimum,double maximum,double value)
|
||||
{
|
||||
return std::max(std::abs(minimum-value),std::abs(maximum-value));
|
||||
}
|
||||
|
||||
inline FScalarRange AffineRange(double xMinimum,double xMaximum,double xOffset,
|
||||
double xScale,double yMinimum,double yMaximum,double yOffset,double yScale)
|
||||
{
|
||||
const double xa=(xMinimum-xOffset)*xScale,xb=(xMaximum-xOffset)*xScale;
|
||||
const double ya=(yMinimum-yOffset)*yScale,yb=(yMaximum-yOffset)*yScale;
|
||||
return {std::min(xa,xb)+std::min(ya,yb),
|
||||
std::max(xa,xb)+std::max(ya,yb)};
|
||||
}
|
||||
|
||||
inline FScalarRange AbsoluteRange(double minimum,double maximum)
|
||||
{
|
||||
const double minimumAbsolute=minimum<=0.0&&maximum>=0.0
|
||||
?0.0:std::min(std::abs(minimum),std::abs(maximum));
|
||||
return {minimumAbsolute,std::max(std::abs(minimum),std::abs(maximum))};
|
||||
}
|
||||
|
||||
inline double RectangleSdf(double x,double y)
|
||||
{
|
||||
return std::hypot(std::max(x,0.0),std::max(y,0.0))
|
||||
+std::min(std::max(x,y),0.0);
|
||||
}
|
||||
|
||||
inline double RoundedBoxSdf(double x,double y,double z)
|
||||
{
|
||||
return std::sqrt(std::max(x,0.0)*std::max(x,0.0)
|
||||
+std::max(y,0.0)*std::max(y,0.0)
|
||||
+std::max(z,0.0)*std::max(z,0.0))
|
||||
+std::min(std::max(x,std::max(y,z)),0.0);
|
||||
}
|
||||
|
||||
inline FDensityRange RootLobeRange(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandAnchorV1& island,std::int32_t rootIndex,
|
||||
const std::array<double,3>& minimum,const std::array<double,3>& maximum)
|
||||
{
|
||||
const FRootLobeParameters root=RootLobeParametersForBounds(recipe,island,rootIndex);
|
||||
const double dxMinimum=DistanceToRange(minimum[0],maximum[0],root.RootX);
|
||||
const double dyMinimum=DistanceToRange(minimum[1],maximum[1],root.RootY);
|
||||
const double dzMinimum=DistanceToRange(minimum[2],maximum[2],root.CenterZ);
|
||||
const double dxMaximum=MaximumDistanceToRange(minimum[0],maximum[0],root.RootX);
|
||||
const double dyMaximum=MaximumDistanceToRange(minimum[1],maximum[1],root.RootY);
|
||||
const double dzMaximum=MaximumDistanceToRange(minimum[2],maximum[2],root.CenterZ);
|
||||
const double qMinimum=dxMinimum*dxMinimum/root.RadiusXSquared
|
||||
+dyMinimum*dyMinimum/root.RadiusYSquared
|
||||
+dzMinimum*dzMinimum/root.VerticalRadiusSquared;
|
||||
const double qMaximum=dxMaximum*dxMaximum/root.RadiusXSquared
|
||||
+dyMaximum*dyMaximum/root.RadiusYSquared
|
||||
+dzMaximum*dzMaximum/root.VerticalRadiusSquared;
|
||||
return {(std::sqrt(std::max(qMinimum,0.0))-1.0)*root.MinimumRadius,
|
||||
(std::sqrt(std::max(qMaximum,0.0))-1.0)*root.MinimumRadius};
|
||||
}
|
||||
|
||||
inline FDensityRange IslandDensityRange(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandAnchorV1& island,const std::array<double,3>& minimum,
|
||||
const std::array<double,3>& maximum)
|
||||
{
|
||||
const double radius=static_cast<double>(island.radiusSubvoxels);
|
||||
const double contour=static_cast<double>(recipe.contourWarpSubvoxels);
|
||||
const double radialMinimum=std::hypot(
|
||||
DistanceToRange(minimum[0],maximum[0],
|
||||
static_cast<double>(island.centerSubvoxels[0])),
|
||||
DistanceToRange(minimum[1],maximum[1],
|
||||
static_cast<double>(island.centerSubvoxels[1])));
|
||||
const double radialMaximum=std::max({
|
||||
std::hypot(minimum[0]-island.centerSubvoxels[0],minimum[1]-island.centerSubvoxels[1]),
|
||||
std::hypot(minimum[0]-island.centerSubvoxels[0],maximum[1]-island.centerSubvoxels[1]),
|
||||
std::hypot(maximum[0]-island.centerSubvoxels[0],minimum[1]-island.centerSubvoxels[1]),
|
||||
std::hypot(maximum[0]-island.centerSubvoxels[0],maximum[1]-island.centerSubvoxels[1])});
|
||||
|
||||
const double top=static_cast<double>(island.topZSubvoxels);
|
||||
const double topDrop=std::max(0.0,(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
||||
*static_cast<double>(island.topHalfSubvoxels)*2.0);
|
||||
const double topDistanceMinimum=minimum[2]-top;
|
||||
const double topDistanceMaximum=maximum[2]-(top-topDrop);
|
||||
const double sideDistanceMinimum=radialMinimum-std::max(0.0,radius+contour);
|
||||
const double sideDistanceMaximum=recipe.geometryVersion<2
|
||||
?radialMaximum:radialMaximum-std::max(0.0,radius-contour);
|
||||
double lowerMinimum=0.0,lowerMaximum=0.0;
|
||||
if(recipe.geometryVersion<2)
|
||||
{
|
||||
const double bottom=static_cast<double>(island.bottomZSubvoxels);
|
||||
lowerMinimum=bottom-maximum[2];
|
||||
lowerMaximum=bottom-minimum[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
const double crust=static_cast<double>(IslandCrustBottom(recipe,island));
|
||||
lowerMinimum=crust-maximum[2];
|
||||
lowerMaximum=crust+radius*0.16-minimum[2];
|
||||
}
|
||||
double structuralMinimum=std::max({topDistanceMinimum,
|
||||
sideDistanceMinimum,lowerMinimum});
|
||||
double structuralMaximum=std::max({topDistanceMaximum,
|
||||
sideDistanceMaximum,lowerMaximum});
|
||||
|
||||
if(recipe.geometryVersion>=2)
|
||||
{
|
||||
const double rootBlend=std::clamp(radius*0.12,2.0*Q,5.0*Q);
|
||||
for(std::int32_t root=0;root<RootCount(recipe,island);++root)
|
||||
{
|
||||
const FDensityRange rootRange=RootLobeRange(recipe,island,root,minimum,maximum);
|
||||
structuralMinimum=std::min(structuralMinimum,rootRange.minimum)-rootBlend*0.25;
|
||||
structuralMaximum=std::min(structuralMaximum,rootRange.maximum);
|
||||
}
|
||||
if(recipe.geometryVersion>=5&&recipe.geometryVersion<8
|
||||
&&island.ordinal==recipe.anchorIslandOrdinal
|
||||
&&recipe.anchorRootBottomZSubvoxels<island.bottomZSubvoxels)
|
||||
{
|
||||
const double rootTop=static_cast<double>(IslandCrustBottom(recipe,island));
|
||||
const double rootBottom=static_cast<double>(recipe.anchorRootBottomZSubvoxels);
|
||||
const double rootRadius=static_cast<double>(recipe.anchorRootRadiusSubvoxels);
|
||||
const double anchorMinimum=std::max({radialMinimum-rootRadius,
|
||||
rootBottom-maximum[2],minimum[2]-rootTop});
|
||||
const double anchorMaximum=std::max({radialMaximum-0.92*rootRadius,
|
||||
rootBottom-minimum[2],maximum[2]-rootTop});
|
||||
const double anchorDetail=0.75*static_cast<double>(recipe.roughnessSubvoxels);
|
||||
const double anchorBlend=std::clamp(rootRadius*0.12,3.0*Q,6.0*Q);
|
||||
structuralMinimum=std::min(structuralMinimum,anchorMinimum-anchorDetail)
|
||||
-anchorBlend*0.25;
|
||||
structuralMaximum=std::min(structuralMaximum,anchorMaximum+anchorDetail);
|
||||
}
|
||||
}
|
||||
const double finalDetail=static_cast<double>(recipe.roughnessSubvoxels);
|
||||
return {structuralMinimum-finalDetail,structuralMaximum+finalDetail};
|
||||
}
|
||||
|
||||
inline FDensityRange ConnectorDensityRange(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandConnectorV1& edge,const std::array<double,3>& minimum,
|
||||
const std::array<double,3>& maximum)
|
||||
{
|
||||
const double ax=static_cast<double>(edge.startSubvoxels[0]);
|
||||
const double ay=static_cast<double>(edge.startSubvoxels[1]);
|
||||
const double az=static_cast<double>(edge.startSubvoxels[2]);
|
||||
const double dx=static_cast<double>(edge.endSubvoxels[0])-ax;
|
||||
const double dy=static_cast<double>(edge.endSubvoxels[1])-ay;
|
||||
const double dz=static_cast<double>(edge.endSubvoxels[2])-az;
|
||||
const double length=std::hypot(dx,dy);
|
||||
if(!(length>0.0))return {static_cast<double>(MaxDensity),static_cast<double>(MaxDensity)};
|
||||
const double ux=dx/length,uy=dy/length;
|
||||
const FScalarRange along=AffineRange(minimum[0],maximum[0],ax,ux,
|
||||
minimum[1],maximum[1],ay,uy);
|
||||
const FScalarRange signedSide=AffineRange(minimum[0],maximum[0],ax,-uy,
|
||||
minimum[1],maximum[1],ay,ux);
|
||||
const FScalarRange side=AbsoluteRange(signedSide.minimum,signedSide.maximum);
|
||||
const double qxMinimum=side.minimum-static_cast<double>(edge.halfWidthSubvoxels);
|
||||
const double qxMaximum=side.maximum-static_cast<double>(edge.halfWidthSubvoxels);
|
||||
const FScalarRange alongCenter=AbsoluteRange(along.minimum-0.5*length,
|
||||
along.maximum-0.5*length);
|
||||
const double qyMinimum=alongCenter.minimum-0.5*length;
|
||||
const double qyMaximum=alongCenter.maximum-0.5*length;
|
||||
const double horizontalMinimum=RectangleSdf(qxMinimum,qyMinimum);
|
||||
const double horizontalMaximum=RectangleSdf(qxMaximum,qyMaximum);
|
||||
|
||||
const double tMinimum=std::clamp(along.minimum/length,0.0,1.0);
|
||||
const double tMaximum=std::clamp(along.maximum/length,0.0,1.0);
|
||||
const double deckAtMinimum=az+dz*tMinimum;
|
||||
const double deckAtMaximum=az+dz*tMaximum;
|
||||
const double deckMinimum=std::min(deckAtMinimum,deckAtMaximum);
|
||||
const double deckMaximum=std::max(deckAtMinimum,deckAtMaximum);
|
||||
const std::uint64_t westernEdgeId=StableId(recipe.seed,0x200000000ull,
|
||||
0x425249444745ull);
|
||||
const bool westernSupportConnector=edge.stableId==westernEdgeId;
|
||||
if(recipe.geometryVersion>=2&&WorldAssemblyEvaluatorVersion>=183
|
||||
&&westernSupportConnector)
|
||||
{
|
||||
const double minimumHalfWidth=static_cast<double>(recipe.routeConstraints.capsuleRadiusSubvoxels+Q);
|
||||
const double baseHalfWidth=static_cast<double>(edge.halfWidthSubvoxels);
|
||||
const double halfWidthMinimum=std::max(minimumHalfWidth,baseHalfWidth-0.48*Q);
|
||||
const double halfWidthMaximum=std::max(minimumHalfWidth,baseHalfWidth+0.48*Q);
|
||||
const double roundingMinimum=std::min({1.5*Q,halfWidthMinimum*0.6,
|
||||
static_cast<double>(edge.thicknessSubvoxels)*0.45});
|
||||
const double roundingMaximum=std::min({1.5*Q,halfWidthMaximum*0.6,
|
||||
static_cast<double>(edge.thicknessSubvoxels)*0.45});
|
||||
const double roundedXMinimum=side.minimum-halfWidthMaximum+roundingMinimum;
|
||||
const double roundedXMaximum=side.maximum-halfWidthMinimum+roundingMaximum;
|
||||
const double roundedYMinimum=alongCenter.minimum-0.5*length+roundingMinimum;
|
||||
const double roundedYMaximum=alongCenter.maximum-0.5*length+roundingMaximum;
|
||||
const double halfThickness=0.5*static_cast<double>(edge.thicknessSubvoxels);
|
||||
const double centerMinimum=deckMinimum-halfThickness;
|
||||
const double centerMaximum=deckMaximum-halfThickness;
|
||||
const double differenceMinimum=minimum[2]-centerMaximum;
|
||||
const double differenceMaximum=maximum[2]-centerMinimum;
|
||||
const FScalarRange absoluteDifference=AbsoluteRange(differenceMinimum,differenceMaximum);
|
||||
const double roundedZMinimum=absoluteDifference.minimum-halfThickness+roundingMinimum;
|
||||
const double roundedZMaximum=absoluteDifference.maximum-halfThickness+roundingMaximum;
|
||||
return {RoundedBoxSdf(roundedXMinimum,roundedYMinimum,roundedZMinimum),
|
||||
RoundedBoxSdf(roundedXMaximum,roundedYMaximum,roundedZMaximum)};
|
||||
}
|
||||
|
||||
const double thickness=static_cast<double>(edge.thicknessSubvoxels);
|
||||
const double maximumArch=recipe.geometryVersion>=2&&WorldAssemblyEvaluatorVersion<172
|
||||
?2.0*thickness:0.0;
|
||||
const double upperMinimum=minimum[2]-deckMaximum;
|
||||
const double upperMaximum=maximum[2]-deckMinimum;
|
||||
const double lowerMinimum=deckMinimum-thickness-maximumArch-maximum[2];
|
||||
const double lowerMaximum=deckMaximum-thickness-minimum[2];
|
||||
return {std::max({horizontalMinimum,upperMinimum,lowerMinimum}),
|
||||
std::max({horizontalMaximum,upperMaximum,lowerMaximum})};
|
||||
}
|
||||
|
||||
inline std::int64_t FieldDensity(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandPlanV1& plan,
|
||||
std::int64_t x, std::int64_t y, std::int64_t z)
|
||||
@@ -1174,8 +1593,8 @@ namespace floating_island_detail
|
||||
// Land each connector well inside the broad top so the full capsule footprint remains
|
||||
// supported across the bridge-to-island transition, including bounded surface roughness.
|
||||
// Les connecteurs recouvrent le plateau pour soutenir toute la capsule au raccord.
|
||||
// Keep the narrow span exposed to open sky; a smaller fraction buries most of it in the island tops.
|
||||
// Garder la passerelle etroite visible dans le vide ; une fraction plus faible l'enfouit dans les plateaux.
|
||||
// Keep the capsule footprint supported while seating L37 connectors just below the native island tops.
|
||||
// Garder l'appui de la capsule tout en enfouissant les connecteurs L37 juste sous les plateaux.
|
||||
const double landingFraction = recipe.geometryVersion >= 2 ? 0.52 : 0.62;
|
||||
const double fromReach = static_cast<double>(from.radiusSubvoxels) * landingFraction;
|
||||
const double toReach = static_cast<double>(to.radiusSubvoxels) * landingFraction;
|
||||
@@ -1183,7 +1602,19 @@ namespace floating_island_detail
|
||||
const double sy = from.centerSubvoxels[1] + uy * fromReach;
|
||||
const double ex = to.centerSubvoxels[0] - ux * toReach;
|
||||
const double ey = to.centerSubvoxels[1] - uy * toReach;
|
||||
const double routeDeckLift = recipe.geometryVersion >= 2 ? 1.0 * Q : 0.0;
|
||||
const bool craterMouthConnector=WorldAssemblyEvaluatorVersion>=174
|
||||
&&(from.ordinal==recipe.anchorIslandOrdinal
|
||||
||to.ordinal==recipe.anchorIslandOrdinal);
|
||||
const bool westernSupportConnector=WorldAssemblyEvaluatorVersion>=178
|
||||
&&((from.ordinal==0&&to.ordinal==1)||(from.ordinal==1&&to.ordinal==0));
|
||||
const double routeDeckLift=recipe.geometryVersion>=2
|
||||
?(WorldAssemblyEvaluatorVersion>=178
|
||||
?(westernSupportConnector
|
||||
?(WorldAssemblyEvaluatorVersion>=181?-1.0
|
||||
:(WorldAssemblyEvaluatorVersion>=180?-1.25
|
||||
:(WorldAssemblyEvaluatorVersion>=179?-1.5:-1.0))):-2.0)
|
||||
:(craterMouthConnector?-2.0
|
||||
:(WorldAssemblyEvaluatorVersion>=172?-1.0:1.0)))*Q:0.0;
|
||||
const double sz = IslandTopSurface(recipe, from, sx, sy) + routeDeckLift;
|
||||
const double ez = IslandTopSurface(recipe, to, ex, ey) + routeDeckLift;
|
||||
edge.startSubvoxels = {RoundQ(sx), RoundQ(sy), RoundQ(sz)};
|
||||
@@ -1336,6 +1767,7 @@ std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
||||
manifest.recipe.anchorMouthOffsetSubvoxels))
|
||||
{error="floating-island geometry version 7 is missing a passage-rooted anchor field";return std::nullopt;}
|
||||
}
|
||||
manifest.recipe.terrainFollowingRoute=recipeValue.Get("terrain_following_route").Boolean(false);
|
||||
if (!floating_island_detail::ReadU32(root.Get("schema_version"), schema)
|
||||
|| !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash)
|
||||
|| !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion)
|
||||
@@ -1495,6 +1927,7 @@ std::string FloatingIslandManifestV1::ToJson() const
|
||||
<< ",\"anchor_island_minimum_radius_q\":" << recipe.anchorIslandMinimumRadiusSubvoxels
|
||||
<< ",\"anchor_root_bottom_z_q\":" << recipe.anchorRootBottomZSubvoxels
|
||||
<< ",\"anchor_mouth_offset_q\":" << recipe.anchorMouthOffsetSubvoxels;
|
||||
if(recipe.terrainFollowingRoute)out << ",\"terrain_following_route\":true";
|
||||
out << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels
|
||||
<< ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels
|
||||
<< ",\"roughness_q\":" << recipe.roughnessSubvoxels
|
||||
@@ -1746,6 +2179,26 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
||||
const std::int64_t y = floating_island_detail::RoundQ(
|
||||
static_cast<double>(a.centerSubvoxels[1]) + dy * t);
|
||||
std::int64_t routeX=x,routeY=y;
|
||||
if(WorldAssemblyEvaluatorVersion>=192&&recipe.terrainFollowingRoute
|
||||
&&recipe.geometryVersion>=5
|
||||
&&segment+2==count&&pieces>1)
|
||||
{
|
||||
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
||||
const std::uint32_t side=static_cast<std::uint32_t>(
|
||||
floating_island_detail::UnitQ16(recipe.seed,anchor.stableId,
|
||||
0x414e43484f524d50ull)&1u);
|
||||
const double length=std::hypot(dx,dy);
|
||||
const double bend=10.0*floating_island_detail::Q
|
||||
*std::pow(std::sin(floating_island_detail::Pi*t),2.0);
|
||||
const double sign=side?-1.0:1.0;
|
||||
if(length>0.0)
|
||||
{
|
||||
routeX=floating_island_detail::RoundQ(
|
||||
static_cast<double>(routeX)+sign*(-dy/length)*bend);
|
||||
routeY=floating_island_detail::RoundQ(
|
||||
static_cast<double>(routeY)+sign*(dx/length)*bend);
|
||||
}
|
||||
}
|
||||
if(recipe.geometryVersion>=5&&segment+2==count&&piece==pieces)
|
||||
{
|
||||
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
||||
@@ -1762,7 +2215,10 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
||||
routeX=floating_island_detail::RoundQ(mouthX);
|
||||
routeY=floating_island_detail::RoundQ(mouthY);
|
||||
}
|
||||
const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
||||
const auto surface = WorldAssemblyEvaluatorVersion>=199
|
||||
&&recipe.terrainFollowingRoute
|
||||
?floating_island_detail::FindSurfaceZ(recipe,manifest.plan,routeX,routeY)
|
||||
:floating_island_detail::FindCapsuleFootingTopZ(
|
||||
recipe,manifest.plan,routeX,routeY);
|
||||
if (!surface)
|
||||
{ error = "island planner could not find a full capsule-width support surface along its connector chain"; return std::nullopt; }
|
||||
@@ -1830,14 +2286,19 @@ bool FloatingIslandFieldV1::isValid() const noexcept
|
||||
std::int64_t FloatingIslandFieldV1::densitySubvoxels(std::int64_t x,
|
||||
std::int64_t y, std::int64_t z) const noexcept
|
||||
{
|
||||
floating_island_detail::FScopedIslandAttributionTimer fieldTimer(
|
||||
floating_island_detail::EIslandAttributionBucket::Field);
|
||||
// The selected evaluator validates once; do not scan the plan twice per scoped sample.
|
||||
// L'evaluateur choisi valide une seule fois; ne pas reparcourir le plan a chaque echantillon.
|
||||
std::int64_t density=floating_island_detail::MaxDensity;
|
||||
if (const auto* list = ScopedWorldAssemblyFeatureQueriesV1::find(recipe_, plan_, x, y, z))
|
||||
return densitySubvoxelsForTile(x, y, z, *list);
|
||||
if (!isValid() || !floating_island_detail::ValidScalar(x)
|
||||
density=densitySubvoxelsForTile(x, y, z, *list);
|
||||
else if (!isValid() || !floating_island_detail::ValidScalar(x)
|
||||
|| !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z))
|
||||
return floating_island_detail::MaxDensity;
|
||||
return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
||||
density=floating_island_detail::MaxDensity;
|
||||
else density=floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
||||
fieldTimer.Stop();
|
||||
return density;
|
||||
}
|
||||
|
||||
std::vector<WorldAssemblyFeatureBoundsV1> FloatingIslandFieldV1::solidSupportBounds() const
|
||||
@@ -1864,7 +2325,8 @@ FloatingIslandTileFeatureListV1 FloatingIslandFieldV1::buildFeatureTileList(
|
||||
result.candidateEvaluationUseful=plan_->islands.size()>4;
|
||||
result.minimumSubvoxels = tileMinimumSubvoxels;
|
||||
result.maximumSubvoxels = tileMaximumSubvoxels;
|
||||
if (!isValid()) return result;
|
||||
if (!isValid())
|
||||
{ FinalizeWorldAssemblyFeatureTileListDensityContextV1(result); return result; }
|
||||
const auto features = solidSupportBounds();
|
||||
|
||||
result.featureIndices = BuildWorldAssemblyFeatureTileListV1(features,
|
||||
@@ -1886,7 +2348,8 @@ FloatingIslandTileFeatureListV1 FloatingIslandFieldV1::buildFeatureTileList(
|
||||
{
|
||||
const auto source = findSourceIsland(edge);
|
||||
if (source == plan_->islands.end())
|
||||
{ result.groupBounds.clear(); result.featureIndices.clear(); return result; }
|
||||
{ result.groupBounds.clear(); result.featureIndices.clear();
|
||||
FinalizeWorldAssemblyFeatureTileListDensityContextV1(result); return result; }
|
||||
auto& group = result.groupBounds[static_cast<std::size_t>(source - plan_->islands.begin())];
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
@@ -1977,10 +2440,12 @@ FloatingIslandTileFeatureListV1 FloatingIslandFieldV1::buildFeatureTileList(
|
||||
{
|
||||
const std::size_t edgeIndex = static_cast<std::size_t>(featureIndex) - islandCount;
|
||||
if (edgeIndex >= plan_->clusterEdges.size())
|
||||
{ result.groupBounds.clear(); result.featureIndices.clear(); return result; }
|
||||
{ result.groupBounds.clear(); result.featureIndices.clear();
|
||||
FinalizeWorldAssemblyFeatureTileListDensityContextV1(result); return result; }
|
||||
const auto source = findSourceIsland(plan_->clusterEdges[edgeIndex]);
|
||||
if (source == plan_->islands.end())
|
||||
{ result.groupBounds.clear(); result.featureIndices.clear(); return result; }
|
||||
{ result.groupBounds.clear(); result.featureIndices.clear();
|
||||
FinalizeWorldAssemblyFeatureTileListDensityContextV1(result); return result; }
|
||||
selectedGroups[static_cast<std::size_t>(source - plan_->islands.begin())] = 1;
|
||||
}
|
||||
}
|
||||
@@ -2003,6 +2468,7 @@ FloatingIslandTileFeatureListV1 FloatingIslandFieldV1::buildFeatureTileList(
|
||||
if (plan_->clusterEdges[edgeIndex].fromIslandId == plan_->islands[islandIndex].stableId)
|
||||
insertSorted(static_cast<std::uint32_t>(islandCount + edgeIndex));
|
||||
}
|
||||
FinalizeWorldAssemblyFeatureTileListDensityContextV1(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2036,19 +2502,46 @@ FieldInterval FloatingIslandFieldV1::interval(std::int64_t minX, std::int64_t mi
|
||||
|| !floating_island_detail::ValidScalar(minX) || !floating_island_detail::ValidScalar(minY)
|
||||
|| !floating_island_detail::ValidScalar(minZ) || !floating_island_detail::ValidScalar(maxX)
|
||||
|| !floating_island_detail::ValidScalar(maxY) || !floating_island_detail::ValidScalar(maxZ)) return result;
|
||||
const std::array<std::int64_t, 3> minimum{minX, minY, minZ};
|
||||
const std::array<std::int64_t, 3> maximum{maxX, maxY, maxZ};
|
||||
using namespace floating_island_detail;
|
||||
if (plan_->islands.empty())
|
||||
return {true,MaxDensity,MaxDensity};
|
||||
|
||||
const std::array<double,3> minimum{static_cast<double>(minX),
|
||||
static_cast<double>(minY),static_cast<double>(minZ)};
|
||||
const std::array<double,3> maximum{static_cast<double>(maxX),
|
||||
static_cast<double>(maxY),static_cast<double>(maxZ)};
|
||||
double fieldMinimum=static_cast<double>(MaxDensity);
|
||||
double fieldMaximum=static_cast<double>(MaxDensity);
|
||||
for(const auto& island:plan_->islands)
|
||||
if (floating_island_detail::Intersects(minimum, maximum,
|
||||
island.influenceMinimumSubvoxels, island.influenceMaximumSubvoxels)) return result;
|
||||
{
|
||||
FDensityRange group=IslandDensityRange(*recipe_,island,minimum,maximum);
|
||||
for(const auto& edge:plan_->clusterEdges)
|
||||
if (floating_island_detail::Intersects(minimum, maximum,
|
||||
edge.influenceMinimumSubvoxels, edge.influenceMaximumSubvoxels)) return result;
|
||||
// Root is void. A box outside every lobe/taper/blend/connector influence bound proves air.
|
||||
// La racine est le vide : une boite hors des influences bornees prouve l'air.
|
||||
{
|
||||
if(edge.fromIslandId!=island.stableId)continue;
|
||||
const FDensityRange connector=ConnectorDensityRange(*recipe_,edge,minimum,maximum);
|
||||
const double detail=recipe_->geometryVersion>=2
|
||||
?static_cast<double>(recipe_->roughnessSubvoxels):0.0;
|
||||
group.minimum=std::min(group.minimum,connector.minimum-detail)
|
||||
-static_cast<double>(recipe_->blendRadiusSubvoxels)*0.25;
|
||||
group.maximum=std::min(group.maximum,connector.maximum+detail);
|
||||
}
|
||||
fieldMinimum=std::min(fieldMinimum,group.minimum);
|
||||
fieldMaximum=std::min(fieldMaximum,group.maximum);
|
||||
}
|
||||
if(!std::isfinite(fieldMinimum)||!std::isfinite(fieldMaximum))return result;
|
||||
|
||||
// These interval operations are shared by native and future generator classifiers: SDF
|
||||
// intersections take max, unions take min, and smooth unions subtract at most k/4.
|
||||
// Keep two subvoxels of slack for floating bounds followed by the field's integer rounding.
|
||||
// Ces bornes servent les generateurs actuels et futurs : intersections=max, unions=min.
|
||||
const double minimumRounded=std::clamp(std::floor(fieldMinimum)-2.0,
|
||||
-static_cast<double>(MaxDensity),static_cast<double>(MaxDensity));
|
||||
const double maximumRounded=std::clamp(std::ceil(fieldMaximum)+2.0,
|
||||
-static_cast<double>(MaxDensity),static_cast<double>(MaxDensity));
|
||||
if(minimumRounded>maximumRounded)return result;
|
||||
result.known=true;
|
||||
result.minimumDensitySubvoxels = 0;
|
||||
result.maximumDensitySubvoxels = floating_island_detail::MaxDensity;
|
||||
result.minimumDensitySubvoxels=static_cast<std::int64_t>(minimumRounded);
|
||||
result.maximumDensitySubvoxels=static_cast<std::int64_t>(maximumRounded);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
#include <sstream>
|
||||
|
||||
namespace vfcore::detail
|
||||
{
|
||||
@@ -216,6 +218,19 @@ namespace VoxelForgeCore
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::uint64_t HashMaterialSamples(std::span<const MeshVertex> vertices)
|
||||
{
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
AddWord(hash,vertices.size(),8);
|
||||
for(const MeshVertex& vertex:vertices)
|
||||
{
|
||||
AddWord(hash,vertex.hostMaterialId,2);
|
||||
for(const std::uint16_t id:vertex.resourceMaterialIds)AddWord(hash,id,2);
|
||||
for(const std::uint16_t weight:vertex.resourceWeightsQ16)AddWord(hash,weight,2);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::uint64_t HashBrushTransaction(const EditTransaction& transaction)
|
||||
{
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
@@ -989,6 +1004,15 @@ namespace VoxelForgeCore
|
||||
const ResolvedLayoutSnapshotV3* ResolvedWorldSnapshot::resolvedLayout() const noexcept
|
||||
{ return data_?data_->resolvedLayout.get():nullptr; }
|
||||
|
||||
ResolvedWorldSnapshot ResolvedWorldSnapshot::withResolvedLayout(
|
||||
const ResolvedLayoutSnapshotV3& layout) const
|
||||
{
|
||||
if(!data_)return {};
|
||||
auto copy=std::make_shared<Data>(*data_);
|
||||
copy->resolvedLayout=std::make_shared<ResolvedLayoutSnapshotV3>(layout);
|
||||
return ResolvedWorldSnapshot(std::move(copy));
|
||||
}
|
||||
|
||||
ResolvedWorldSnapshot ResolvedWorldSnapshot::withoutResolvedLayoutPassages() const
|
||||
{
|
||||
if(!data_||!data_->resolvedLayout)return {};
|
||||
@@ -1017,6 +1041,8 @@ namespace VoxelForgeCore
|
||||
std::atomic<bool> shapeReady{false};
|
||||
std::unique_ptr<world_assembly_detail::FSharedHeightfieldColumnCache>
|
||||
sharedHeightfieldColumns;
|
||||
std::unique_ptr<world_assembly_detail::FSharedDensitySampleCache>
|
||||
sharedDensitySamples;
|
||||
std::unique_ptr<floating_island_detail::FSharedIslandColumnCache>
|
||||
sharedIslandColumns;
|
||||
ShapeBuildRequest buildRequest;
|
||||
@@ -1046,6 +1072,18 @@ namespace VoxelForgeCore
|
||||
}
|
||||
};
|
||||
|
||||
struct FScopedIslandColumnCacheInstrumentation
|
||||
{
|
||||
floating_island_detail::FIslandColumnCacheInstrumentation* Previous=nullptr;
|
||||
explicit FScopedIslandColumnCacheInstrumentation(
|
||||
floating_island_detail::FIslandColumnCacheInstrumentation* Current,bool Enabled)
|
||||
:Previous(Enabled?floating_island_detail::SetIslandColumnCacheInstrumentation(Current):nullptr),
|
||||
bEnabled(Enabled){}
|
||||
~FScopedIslandColumnCacheInstrumentation()
|
||||
{if(bEnabled)floating_island_detail::SetIslandColumnCacheInstrumentation(Previous);}
|
||||
bool bEnabled=false;
|
||||
};
|
||||
|
||||
World::World(ResolvedWorldSnapshot snapshot):impl_(std::make_unique<Impl>())
|
||||
{
|
||||
if(snapshot.data_)
|
||||
@@ -1054,6 +1092,8 @@ namespace VoxelForgeCore
|
||||
impl_->worldAssembly=snapshot.data_->worldAssembly;
|
||||
impl_->resolvedLayout=snapshot.data_->resolvedLayout;
|
||||
impl_->configuration=snapshot.data_->configuration;
|
||||
impl_->sharedDensitySamples=
|
||||
std::make_unique<world_assembly_detail::FSharedDensitySampleCache>();
|
||||
if(impl_->worldAssembly)
|
||||
{
|
||||
if(!impl_->worldAssembly->plan.strates.empty())
|
||||
@@ -1207,6 +1247,7 @@ namespace VoxelForgeCore
|
||||
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
||||
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
||||
for(const auto& passage:layout.passages)
|
||||
{
|
||||
for(std::size_t segment=1;segment<passage.controlPoints.size();++segment)
|
||||
{
|
||||
const auto& a=passage.controlPoints[segment-1];
|
||||
@@ -1228,6 +1269,128 @@ namespace VoxelForgeCore
|
||||
overlap&=high[axis]>=tileMinimum[axis]&&low[axis]<=tileMaximum[axis];
|
||||
if(overlap)return true;
|
||||
}
|
||||
|
||||
// L27 can add a long support column around an unsupported passage span. Its
|
||||
// radius can exceed the route and bridge bounds above by the column wall and
|
||||
// flare allowance. The precompile shape-free proof runs before that crossing
|
||||
// plan exists, so conservatively bound every possible column from the resolved
|
||||
// route itself. The center is either the passage anchor or an average of route
|
||||
// samples; in the latter case it lies inside the route's XY bounds.
|
||||
if(passage.controlPoints.size()>=2)
|
||||
{
|
||||
double routeMinimum[3]={std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::infinity()};
|
||||
double routeMaximum[3]={-std::numeric_limits<double>::infinity(),
|
||||
-std::numeric_limits<double>::infinity(),
|
||||
-std::numeric_limits<double>::infinity()};
|
||||
double maximumBoreRadius=0.0;
|
||||
for(const auto& point:passage.controlPoints)
|
||||
{
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
const double value=static_cast<double>(point.positionSubvoxels[axis])/Q;
|
||||
routeMinimum[axis]=std::min(routeMinimum[axis],value);
|
||||
routeMaximum[axis]=std::max(routeMaximum[axis],value);
|
||||
}
|
||||
maximumBoreRadius=std::max(maximumBoreRadius,
|
||||
static_cast<double>(point.radiusSubvoxels)/Q);
|
||||
}
|
||||
|
||||
double centerMinimumX=routeMinimum[0],centerMaximumX=routeMaximum[0];
|
||||
double centerMinimumY=routeMinimum[1],centerMaximumY=routeMaximum[1];
|
||||
double maximumRouteOffset=std::hypot(
|
||||
routeMaximum[0]-routeMinimum[0],routeMaximum[1]-routeMinimum[1]);
|
||||
const auto upper=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[&passage](const auto& stratum)
|
||||
{return stratum.strateIndex==passage.upperStrateIndex;});
|
||||
if(upper!=layout.strates.end()
|
||||
&&upper->passageCapabilities.usePassageAnchorForColumns)
|
||||
{
|
||||
const auto anchor=std::find_if(upper->anchors.begin(),upper->anchors.end(),
|
||||
[](const auto& value)
|
||||
{return value.kind=="passage_anchor"&&value.values.size()>=2;});
|
||||
if(anchor!=upper->anchors.end())
|
||||
{
|
||||
const double centerX=static_cast<double>(anchor->values[0])/Q;
|
||||
const double centerY=static_cast<double>(anchor->values[1])/Q;
|
||||
centerMinimumX=centerMaximumX=centerX;
|
||||
centerMinimumY=centerMaximumY=centerY;
|
||||
maximumRouteOffset=0.0;
|
||||
for(const auto& point:passage.controlPoints)
|
||||
maximumRouteOffset=std::max(maximumRouteOffset,std::hypot(
|
||||
static_cast<double>(point.positionSubvoxels[0])/Q-centerX,
|
||||
static_cast<double>(point.positionSubvoxels[1])/Q-centerY));
|
||||
}
|
||||
}
|
||||
// The generated maximum column radius is the route offset, bore radius,
|
||||
// and 13.25 m of wall/irregularity/flare allowance.
|
||||
const double columnRadius=maximumRouteOffset+maximumBoreRadius
|
||||
+13.25*voxelsPerMetre;
|
||||
const double columnMinimum[3]={centerMinimumX-columnRadius,
|
||||
centerMinimumY-columnRadius,routeMinimum[2]-6.0*voxelsPerMetre};
|
||||
const double columnMaximum[3]={centerMaximumX+columnRadius,
|
||||
centerMaximumY+columnRadius,routeMaximum[2]+5.0*voxelsPerMetre};
|
||||
bool overlap=true;
|
||||
for(int axis=0;axis<3;++axis)
|
||||
overlap=overlap&&columnMaximum[axis]>=tileMinimum[axis]
|
||||
&&columnMinimum[axis]<=tileMaximum[axis];
|
||||
if(overlap)return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ResolvedStampTouchesBox(const ResolvedLayoutSnapshotV3& layout,
|
||||
const FIntVector& minimum,const FIntVector& maximum,
|
||||
std::uint32_t coreVoxelSizeMicrons)
|
||||
{
|
||||
if(layout.stamps.empty()||coreVoxelSizeMicrons==0)return false;
|
||||
const double tileMinimum[3]={static_cast<double>(minimum.X),
|
||||
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
|
||||
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
||||
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
||||
for(const auto& stamp:layout.stamps)
|
||||
{
|
||||
bool overlap=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double low=static_cast<double>(stamp.boundsMinimumMicrometres[axis])
|
||||
/coreVoxelSizeMicrons;
|
||||
const double high=static_cast<double>(stamp.boundsMaximumMicrometres[axis])
|
||||
/coreVoxelSizeMicrons;
|
||||
overlap=overlap&&high>=tileMinimum[axis]&&low<=tileMaximum[axis];
|
||||
}
|
||||
if(overlap)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ResolvedCavernGraphTouchesBox(const ResolvedLayoutSnapshotV3& layout,
|
||||
const FIntVector& minimum,const FIntVector& maximum,
|
||||
std::uint32_t coreVoxelSizeMicrons)
|
||||
{
|
||||
if((layout.cavernRooms.empty()&&layout.cavernEdges.empty())
|
||||
||coreVoxelSizeMicrons==0)return false;
|
||||
const double tileMinimum[3]={static_cast<double>(minimum.X),
|
||||
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
|
||||
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
||||
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
||||
const auto overlaps=[&](const auto& feature)
|
||||
{
|
||||
bool overlap=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double low=static_cast<double>(feature.boundsMinimumMicrometres[axis])
|
||||
/coreVoxelSizeMicrons;
|
||||
const double high=static_cast<double>(feature.boundsMaximumMicrometres[axis])
|
||||
/coreVoxelSizeMicrons;
|
||||
overlap=overlap&&high>=tileMinimum[axis]&&low<=tileMaximum[axis];
|
||||
}
|
||||
return overlap;
|
||||
};
|
||||
for(const auto& room:layout.cavernRooms)if(overlaps(room))return true;
|
||||
for(const auto& edge:layout.cavernEdges)if(overlaps(edge))return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1248,7 +1411,8 @@ namespace VoxelForgeCore
|
||||
static_cast<int32>(std::clamp<std::uint32_t>(request.workerLimit,1,16)),ToNative(request.detail),
|
||||
request.includeFarWorld,error,&scheduler,request.roundTunnels,request.profileDetailBand,
|
||||
request.validatePassageRoutes,request.gpuVisualDetailTiles,
|
||||
request.cpuVisualDetailTiles,impl_->worldAssembly.get(),impl_->resolvedLayout.get());
|
||||
request.cpuVisualDetailTiles,impl_->worldAssembly.get(),impl_->resolvedLayout.get(),
|
||||
request.parallelSignEdgeRepair);
|
||||
if(ok)
|
||||
{
|
||||
impl_->shape=std::move(shape);
|
||||
@@ -1275,13 +1439,31 @@ namespace VoxelForgeCore
|
||||
const int32 firstSurfaceChunk=FloorDiv(boundsMin.Z,LayoutChunkWidth);
|
||||
const int32 lastSurfaceChunk=FloorDiv(boundsMax.Z,LayoutChunkWidth);
|
||||
const WorldAssemblyStratumPlanV1* selectedPlan=nullptr;
|
||||
const ResolvedLayoutStratumV2* selectedGraphStratum=nullptr;
|
||||
for(int32 chunkZ=firstSurfaceChunk;chunkZ<=lastSurfaceChunk;++chunkZ)
|
||||
{
|
||||
if(impl_->owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,chunkZ))
|
||||
!=ECaveGeneratorType::SurfaceWorld)return false;
|
||||
const int32 strateIndex=impl_->owner->Manager->GetStrateIndex(
|
||||
static_cast<float>((static_cast<double>(chunkZ)+0.5)
|
||||
*impl_->resolvedLayout->strateLayoutChunkSizeMicrometres/10000.0));
|
||||
const auto resolved=std::find_if(impl_->resolvedLayout->strates.begin(),
|
||||
impl_->resolvedLayout->strates.end(),[strateIndex](const auto& stratum)
|
||||
{return stratum.strateIndex==strateIndex;});
|
||||
const bool nativeGraph=resolved!=impl_->resolvedLayout->strates.end()
|
||||
&&HasNativeGraphField(*resolved)
|
||||
&&(IsNativeCaveFieldStratum(*resolved)
|
||||
?resolved->passageCapabilities.hasNativeCaveField
|
||||
:resolved->passageCapabilities.hasNativeLatticeField);
|
||||
const auto managerType=impl_->owner->Manager->GetGeneratorTypeForChunk(
|
||||
FIntVector(0,0,chunkZ));
|
||||
if(!nativeGraph&&managerType!=ECaveGeneratorType::SurfaceWorld)return false;
|
||||
if(nativeGraph)
|
||||
{
|
||||
if(selectedGraphStratum&&selectedGraphStratum!=&*resolved)return false;
|
||||
selectedGraphStratum=&*resolved;
|
||||
if(selectedPlan)return false;
|
||||
continue;
|
||||
}
|
||||
if(selectedGraphStratum)return false;
|
||||
const auto found=std::find_if(impl_->worldAssembly->plan.strates.begin(),
|
||||
impl_->worldAssembly->plan.strates.end(),[strateIndex,chunkZ](const auto& plan)
|
||||
{return plan.strateIndex==strateIndex&&chunkZ<=plan.topChunkZ
|
||||
@@ -1290,7 +1472,9 @@ namespace VoxelForgeCore
|
||||
||(selectedPlan&&selectedPlan!=&*found))return false;
|
||||
selectedPlan=&*found;
|
||||
}
|
||||
if(!selectedPlan||!Heightfield2D(impl_->worldAssembly->recipe,*selectedPlan).isValid())
|
||||
if((!selectedGraphStratum&&(!selectedPlan
|
||||
||!Heightfield2D(impl_->worldAssembly->recipe,*selectedPlan).isValid()))
|
||||
||(selectedGraphStratum&&selectedPlan))
|
||||
return false;
|
||||
|
||||
const double coreVoxelsPerOwnerChunk=static_cast<double>(
|
||||
@@ -1308,10 +1492,17 @@ namespace VoxelForgeCore
|
||||
tileMinimum[2]*scale};
|
||||
const std::int64_t subMaximum[3]={tileMaximum[0]*scale,tileMaximum[1]*scale,
|
||||
tileMaximum[2]*scale};
|
||||
if(selectedPlan)
|
||||
{
|
||||
for(const WorldAssemblyFeatureV1& feature:selectedPlan->features)
|
||||
if(IntersectsBox(feature.minimumSubvoxels,feature.maximumSubvoxels,
|
||||
subMinimum,subMaximum))return false;
|
||||
if(RouteTouchesBox(*selectedPlan,subMinimum,subMaximum))return false;
|
||||
}
|
||||
if(ResolvedStampTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||
if(ResolvedCavernGraphTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||
if(ResolvedPassageTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||
if(OwnerPassageTouchesBox(*impl_->owner,boundsMin,boundsMax))return false;
|
||||
@@ -1570,6 +1761,62 @@ namespace VoxelForgeCore
|
||||
|
||||
float World::densityAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
|
||||
MaterialSampleV1 World::materialAt(Vec3d position) const
|
||||
{
|
||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape)
|
||||
return {};
|
||||
return impl_->shape->MaterialAt(static_cast<float>(position.x),
|
||||
static_cast<float>(position.y),static_cast<float>(position.z));
|
||||
}
|
||||
OreMiningYieldV1 World::measureOreMiningYieldForSphere(Vec3d centerVoxel,
|
||||
double radiusMetres,double sampleSpacingMetres) const
|
||||
{
|
||||
OreMiningYieldV1 result;result.sampleSpacingMetres=sampleSpacingMetres;
|
||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape
|
||||
||!std::isfinite(radiusMetres)||!std::isfinite(sampleSpacingMetres)
|
||||
||radiusMetres<=0.0||sampleSpacingMetres<0.05||sampleSpacingMetres>2.0)
|
||||
return result;
|
||||
const std::uint32_t voxelMicrometres=impl_->configuration.voxelSizeMicrons;
|
||||
if(voxelMicrometres==0)return result;
|
||||
const double voxelMetres=static_cast<double>(voxelMicrometres)/1000000.0;
|
||||
const double radiusVoxels=radiusMetres/voxelMetres;
|
||||
const double spacingVoxels=sampleSpacingMetres/voxelMetres;
|
||||
const int steps=static_cast<int>(std::ceil(radiusMetres/sampleSpacingMetres));
|
||||
const double radiusSquared=radiusMetres*radiusMetres;
|
||||
std::map<std::uint16_t,std::uint64_t> weightedSamples;
|
||||
for(int z=-steps;z<=steps;++z)
|
||||
for(int y=-steps;y<=steps;++y)
|
||||
for(int x=-steps;x<=steps;++x)
|
||||
{
|
||||
const double dx=x*sampleSpacingMetres;
|
||||
const double dy=y*sampleSpacingMetres;
|
||||
const double dz=z*sampleSpacingMetres;
|
||||
const double distanceSquared=dx*dx+dy*dy+dz*dz;
|
||||
if(distanceSquared>radiusSquared)continue;
|
||||
const Vec3d point={centerVoxel.x+x*spacingVoxels,
|
||||
centerVoxel.y+y*spacingVoxels,centerVoxel.z+z*spacingVoxels};
|
||||
const float before=impl_->shape->DensityAt(static_cast<float>(point.x),
|
||||
static_cast<float>(point.y),static_cast<float>(point.z));
|
||||
const double carveDensity=radiusVoxels
|
||||
-std::sqrt(distanceSquared)/voxelMetres;
|
||||
const double after=std::max(static_cast<double>(before),carveDensity);
|
||||
if(!(before<0.0f&&after>=0.0))continue;
|
||||
++result.solidSamplesRemoved;
|
||||
const MaterialSampleV1 material=materialAt(point);
|
||||
for(std::size_t slot=0;slot<2;++slot)
|
||||
if(material.resourceMaterialIds[slot]
|
||||
&&material.resourceWeightsQ16[slot])
|
||||
weightedSamples[material.resourceMaterialIds[slot]]
|
||||
+=material.resourceWeightsQ16[slot];
|
||||
}
|
||||
const double sampleVolume=sampleSpacingMetres*sampleSpacingMetres
|
||||
*sampleSpacingMetres;
|
||||
result.resources.reserve(weightedSamples.size());
|
||||
for(const auto& [id,weighted]:weightedSamples)
|
||||
result.resources.push_back({id,weighted,
|
||||
static_cast<double>(weighted)/65535.0*sampleVolume});
|
||||
return result;
|
||||
}
|
||||
std::vector<PassageVoidCrossingAuditV1> World::passageVoidCrossings() const
|
||||
{
|
||||
std::vector<PassageVoidCrossingAuditV1> result;
|
||||
@@ -1770,6 +2017,10 @@ namespace VoxelForgeCore
|
||||
FScopedSharedColumnCaches sharedColumnCaches(options.useSharedColumnCache
|
||||
?impl_->sharedHeightfieldColumns.get():nullptr,
|
||||
options.useSharedColumnCache?impl_->sharedIslandColumns.get():nullptr);
|
||||
floating_island_detail::FIslandColumnCacheInstrumentation islandCacheInstrumentation;
|
||||
islandCacheInstrumentation.tileStartCycles=VoxelForgeCoreShared::ReadCpuCycles();
|
||||
FScopedIslandColumnCacheInstrumentation islandCacheScope(
|
||||
&islandCacheInstrumentation,options.instrumentTileDrain);
|
||||
const auto& world=*impl_->owner;
|
||||
FStandaloneShapeWorld nativeOnlyShape;
|
||||
FIntVector nativeOnlyMinimum,nativeOnlyMaximum;
|
||||
@@ -1799,7 +2050,12 @@ namespace VoxelForgeCore
|
||||
mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors;
|
||||
mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups;
|
||||
mesher.bStandaloneEnableGradientMemoization=options.memoizeMcGradients;
|
||||
mesher.bStandaloneEnableExactMidpointDensityMemo=options.exactMidpointDensityMemo;
|
||||
mesher.bStandaloneEnableActiveLaneBatching=options.activeLaneBatching;
|
||||
mesher.bStandaloneProofBlockCells=static_cast<int32>(std::clamp<std::uint32_t>(
|
||||
options.proofBlockCells,1u,static_cast<std::uint32_t>(std::max(1,CHUNK_SIZE))));
|
||||
mesher.bGenerateSkirts=world.bGenerateSkirts;
|
||||
mesher.SetVisibleSkirtFaceMask(options.visibleSkirtFaceMask);
|
||||
mesher.SkirtCells=static_cast<float>(world.SkirtCells)
|
||||
*static_cast<float>(world.OwnerVoxelSizeMicrons)
|
||||
/static_cast<float>(world.CoreVoxelSizeMicrons);
|
||||
@@ -1906,9 +2162,129 @@ namespace VoxelForgeCore
|
||||
MeshCells=FMath::Max(1,cells/Stride);
|
||||
MeshStep=FMath::Max(1,extent/MeshCells);
|
||||
}
|
||||
if(!bNativeOnly&&options.useSharedColumnCache&&impl_->sharedDensitySamples)
|
||||
{
|
||||
mesher.SetSharedDensitySampleCache(impl_->sharedDensitySamples.get(),
|
||||
[](void* Cache,const FStandaloneShapeWorld* ShapeWorld,uint64 FieldGeneration,
|
||||
uint64 EditSequence,int32 Lod,int32 Step,int32 X,int32 Y,int32 Z,
|
||||
bool& OutComputed)->float
|
||||
{
|
||||
VoxelForgeCoreShared::FDensitySampleCacheKey Key;
|
||||
Key.fieldIdentity=static_cast<uint64>(reinterpret_cast<std::uintptr_t>(ShapeWorld));
|
||||
Key.fieldGeneration=FieldGeneration;Key.editSequence=EditSequence;
|
||||
Key.lod=static_cast<uint32>(Lod);Key.step=static_cast<uint32>(Step);
|
||||
Key.octaveDrop=Heightfield2D::threadColumnOctaveDrop();
|
||||
Key.interpolationStride=Heightfield2D::threadColumnInterpolationStrideSubvoxels();
|
||||
Key.x=X;Key.y=Y;Key.z=Z;
|
||||
const auto queryContext = ScopedWorldAssemblyFeatureQueriesV1::densityCacheContext(X,Y,Z);
|
||||
const auto passageContext = FStandaloneShapeWorld::FScopedPassageTileQueries
|
||||
::densityCacheContext(*ShapeWorld);
|
||||
const auto mixContext=[](uint64 Value)
|
||||
{
|
||||
Value^=Value>>30;Value*=0xbf58476d1ce4e5b9ull;
|
||||
Value^=Value>>27;Value*=0x94d049bb133111ebull;
|
||||
return Value^(Value>>31);
|
||||
};
|
||||
Key.queryContextLow=mixContext(queryContext[0]^passageContext[0]
|
||||
^0x9e3779b97f4a7c15ull);
|
||||
Key.queryContextHigh=mixContext(queryContext[1]^passageContext[1]
|
||||
^0xd1b54a32d192ed03ull);
|
||||
const float Value=static_cast<world_assembly_detail::FSharedDensitySampleCache*>(Cache)
|
||||
->getOrCompute(Key,[&](){return ShapeWorld->DensityAtLattice(X,Y,Z);},OutComputed);
|
||||
auto& Stats=world_assembly_detail::HeightfieldColumnStats;
|
||||
if(OutComputed)++Stats.densitySampleCacheComputes;
|
||||
else ++Stats.densitySampleCacheHits;
|
||||
return Value;
|
||||
},static_cast<uint64>(reinterpret_cast<std::uintptr_t>(impl_->worldAssembly.get())),
|
||||
impl_->lastSequence,key.lod,MeshStep);
|
||||
}
|
||||
int64 sampleCount=0;std::vector<float> densityValues;
|
||||
FVoxelMarchingCubesStageTimings McStageTimings;
|
||||
TileMesh out;out.key=key;
|
||||
const auto CopyIslandCacheMetrics=[&]()
|
||||
{
|
||||
if(!options.instrumentTileDrain)return;
|
||||
islandCacheInstrumentation.tileCycles=VoxelForgeCoreShared::ReadCpuCycles()
|
||||
-islandCacheInstrumentation.tileStartCycles;
|
||||
out.diagnostics.islandLocalCacheHits=islandCacheInstrumentation.localHits;
|
||||
out.diagnostics.islandLocalCacheMisses=islandCacheInstrumentation.localMisses;
|
||||
out.diagnostics.islandLocalCacheConflictReplacements=
|
||||
islandCacheInstrumentation.localConflictReplacements;
|
||||
out.diagnostics.islandSharedCacheHits=islandCacheInstrumentation.shared.hits;
|
||||
out.diagnostics.islandSharedCacheMisses=islandCacheInstrumentation.shared.misses;
|
||||
out.diagnostics.islandSharedCacheComputes=islandCacheInstrumentation.shared.computes;
|
||||
out.diagnostics.islandSharedCacheConflictReplacements=
|
||||
islandCacheInstrumentation.shared.conflictReplacements;
|
||||
out.diagnostics.islandSharedCacheLockWaitNanoseconds=
|
||||
islandCacheInstrumentation.shared.lockWaitNanoseconds;
|
||||
const auto& a=islandCacheInstrumentation.attribution;
|
||||
const auto Estimate=[](std::uint64_t cycles,std::uint64_t calls,
|
||||
std::uint64_t samples)->std::uint64_t
|
||||
{
|
||||
return samples?static_cast<std::uint64_t>(
|
||||
static_cast<long double>(cycles)*calls/samples):0;
|
||||
};
|
||||
constexpr std::array<const char*,5> CallerNames={
|
||||
"grid","skirt","boundary","filter","other"};
|
||||
constexpr std::array<const char*,10> BucketNames={
|
||||
"field","island","roots","connectors","helix","column",
|
||||
"local_probe","shared_cache","mouth_filter","boundary"};
|
||||
std::uint64_t sampledNativeCalls=0;
|
||||
for(const auto samples:a.sampledNativeCalls)sampledNativeCalls+=samples;
|
||||
std::ostringstream line;
|
||||
line<<"[FiFieldAttribution] key=("<<key.coordinate.x<<","<<key.coordinate.y<<","<<key.coordinate.z
|
||||
<<",lod"<<key.lod<<") tile_cycles="<<islandCacheInstrumentation.tileCycles
|
||||
<<" sample_period=256 native_calls_est="<<sampledNativeCalls*256
|
||||
<<" native_samples="<<sampledNativeCalls;
|
||||
for(std::size_t i=0;i<a.sampledNativeCalls.size();++i)
|
||||
{
|
||||
line<<" native_"<<CallerNames[i]<<"="<<a.sampledNativeCalls[i]*256<<"/"
|
||||
<<a.sampledNativeCalls[i]<<"/"
|
||||
<<Estimate(a.nativeCycles[i],a.sampledNativeCalls[i]*256,
|
||||
a.sampledNativeCalls[i]);
|
||||
line<<" field_"<<CallerNames[i]<<"="<<a.sampledFieldCalls[i]*256<<"/"
|
||||
<<a.sampledFieldCalls[i]<<"/"
|
||||
<<Estimate(a.fieldCycles[i],a.sampledFieldCalls[i]*256,
|
||||
a.sampledFieldCalls[i]);
|
||||
}
|
||||
for(std::size_t i=0;i<BucketNames.size();++i)
|
||||
line<<" comp_"<<BucketNames[i]<<"="
|
||||
<<Estimate(a.exclusiveCycles[i],a.scopeSamples[i]*256,
|
||||
a.scopeSamples[i])
|
||||
<<"/"<<a.scopeSamples[i]<<"/"<<a.scopeSamples[i]*256;
|
||||
const auto& shared=islandCacheInstrumentation.shared;
|
||||
line<<" local_probe_calls="<<(islandCacheInstrumentation.localHits
|
||||
+islandCacheInstrumentation.localMisses)
|
||||
<<" shared_calls_est="<<shared.sampledInitialReads*128
|
||||
<<" shared_initial_read="<<shared.sampledInitialReads<<"/"
|
||||
<<Estimate(shared.sampledInitialReadCycles,shared.sampledInitialReads*128,
|
||||
shared.sampledInitialReads)
|
||||
<<" shared_locked_read="<<shared.sampledLockedReads<<"/"
|
||||
<<Estimate(shared.sampledLockedReadCycles,shared.sampledLockedReads*128,
|
||||
shared.sampledLockedReads)
|
||||
<<" shared_lock="<<shared.sampledLocks<<"/"
|
||||
<<Estimate(shared.sampledLockCycles,shared.sampledLocks*128,
|
||||
shared.sampledLocks)
|
||||
<<" shared_build="<<shared.sampledBuilds<<"/"
|
||||
<<Estimate(shared.sampledBuildCycles,shared.sampledBuilds*128,
|
||||
shared.sampledBuilds)
|
||||
<<" trace_sampled="<<a.traceSampledMisses
|
||||
<<" trace_seen_before="<<a.traceSeenBefore
|
||||
<<" trace_evictions="<<a.traceEvictions
|
||||
<<" trace_reused_evictions="<<a.traceReusedEvictions
|
||||
<<" trace_events="<<a.traceCount;
|
||||
std::printf("%s\n",line.str().c_str());
|
||||
for(std::size_t i=0;i<a.traceCount;++i)
|
||||
std::printf("[FiReuseTrace] tile=(%d,%d,%d,lod%d) set=%llu island=%llu xy=(%lld,%lld) evicted_island=%llu evicted_xy=(%lld,%lld) flags=%u\n",
|
||||
key.coordinate.x,key.coordinate.y,key.coordinate.z,key.lod,
|
||||
static_cast<unsigned long long>(a.traceSet[i]),
|
||||
static_cast<unsigned long long>(a.traceIsland[i]),
|
||||
static_cast<long long>(a.traceX[i]),static_cast<long long>(a.traceY[i]),
|
||||
static_cast<unsigned long long>(a.traceEvictedIsland[i]),
|
||||
static_cast<long long>(a.traceEvictedX[i]),
|
||||
static_cast<long long>(a.traceEvictedY[i]),
|
||||
static_cast<unsigned>(a.traceFlags[i]));
|
||||
};
|
||||
out.diagnostics.featureRefinedResolution=bFeatureRefinedResolution;
|
||||
if(options.profileMesherStages)
|
||||
{
|
||||
@@ -1945,14 +2321,43 @@ namespace VoxelForgeCore
|
||||
bHasNativeSurfaceHorizonPlan=bHasNativeSurfaceHorizonPlan
|
||||
||(TileMinimumZ<=PlanMaximumZ&&TileMaximumZ>=PlanMinimumZ);
|
||||
}
|
||||
constexpr int64 IslandQueryPad=2*WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::array<int64,3> TileMinimum={
|
||||
(static_cast<int64>(origin.X)*WorldAssemblySubvoxelsPerVoxel)-IslandQueryPad,
|
||||
(static_cast<int64>(origin.Y)*WorldAssemblySubvoxelsPerVoxel)-IslandQueryPad,
|
||||
(static_cast<int64>(origin.Z)*WorldAssemblySubvoxelsPerVoxel)-IslandQueryPad};
|
||||
const std::array<int64,3> TileMaximum={
|
||||
((static_cast<int64>(origin.X)+extent)*WorldAssemblySubvoxelsPerVoxel)+IslandQueryPad,
|
||||
((static_cast<int64>(origin.Y)+extent)*WorldAssemblySubvoxelsPerVoxel)+IslandQueryPad,
|
||||
((static_cast<int64>(origin.Z)+extent)*WorldAssemblySubvoxelsPerVoxel)+IslandQueryPad};
|
||||
for(const auto& Entry:impl_->worldAssembly->floatingIslands)
|
||||
{
|
||||
const int64 PlanMinimumZ=static_cast<int64>(std::min(Entry.bottomChunkZ,Entry.topChunkZ))
|
||||
*impl_->resolvedLayout->strateLayoutChunkWidthSubvoxels/WorldAssemblySubvoxelsPerVoxel;
|
||||
const int64 PlanMaximumZ=(static_cast<int64>(std::max(Entry.bottomChunkZ,Entry.topChunkZ)+1))
|
||||
*impl_->resolvedLayout->strateLayoutChunkWidthSubvoxels/WorldAssemblySubvoxelsPerVoxel;
|
||||
if(TileMinimumZ>PlanMaximumZ||TileMaximumZ<PlanMinimumZ)continue;
|
||||
|
||||
const auto IntersectsTile=[&](const auto& Feature)
|
||||
{
|
||||
for(std::size_t Axis=0;Axis<3;++Axis)
|
||||
if(Feature.influenceMinimumSubvoxels[Axis]>TileMaximum[Axis]
|
||||
||Feature.influenceMaximumSubvoxels[Axis]<TileMinimum[Axis])return false;
|
||||
return true;
|
||||
};
|
||||
bool bIslandFeatureOverlap=std::any_of(Entry.manifest.plan.islands.begin(),
|
||||
Entry.manifest.plan.islands.end(),IntersectsTile)
|
||||
||std::any_of(Entry.manifest.plan.clusterEdges.begin(),
|
||||
Entry.manifest.plan.clusterEdges.end(),IntersectsTile);
|
||||
const auto& Recipe=Entry.manifest.recipe;
|
||||
const int64 Bottom=Recipe.layoutMinimumZSubvoxels;
|
||||
const int64 Top=Recipe.layoutMaximumZSubvoxels;
|
||||
const int64 Seal=Recipe.boundarySealSubvoxels;
|
||||
const bool bBoundarySealOverlap=(TileMinimum[2]<=Top&&TileMaximum[2]>=Top-Seal)
|
||||
||(Recipe.geometryVersion<3&&TileMinimum[2]<Bottom+Seal
|
||||
&&TileMaximum[2]>=Bottom);
|
||||
bHasIslandPlanInTile=bHasIslandPlanInTile
|
||||
||(TileMinimumZ<=PlanMaximumZ&&TileMaximumZ>=PlanMinimumZ);
|
||||
||bIslandFeatureOverlap||bBoundarySealOverlap;
|
||||
}
|
||||
}
|
||||
const bool bUseNativeSurfaceHorizon=key.lod>=5&&!bOverlapsPlayerEdit
|
||||
@@ -2033,6 +2438,7 @@ namespace VoxelForgeCore
|
||||
out.diagnostics.columnCoordinateCaptureComplete=Columns.coordinateCaptureComplete;
|
||||
out.diagnostics.evaluatedColumnCoordinates=Columns.evaluatedCoordinates;
|
||||
}
|
||||
CopyIslandCacheMetrics();
|
||||
out.densitySampleCount=0;
|
||||
return out;
|
||||
}
|
||||
@@ -2125,6 +2531,88 @@ namespace VoxelForgeCore
|
||||
RelevantMinimum=std::min(RelevantMinimum,FeatureMinimum);
|
||||
RelevantMaximum=std::max(RelevantMaximum,FeatureMaximum);
|
||||
}
|
||||
// A resolved passage void column can add solid support above or
|
||||
// below the native heightfield's local Z range. Its finite radial
|
||||
// and vertical bounds must participate before the mesh band is clipped.
|
||||
for(const auto& Crossing:tileShape.GetPassageVoidCrossings())
|
||||
{
|
||||
if(!Crossing.needsColumn||Crossing.maximumRadiusSubvoxels<=0
|
||||
||Crossing.topZSubvoxels<=Crossing.bottomZSubvoxels)continue;
|
||||
const double Radius=static_cast<double>(Crossing.maximumRadiusSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale);
|
||||
const double CenterX=static_cast<double>(Crossing.centerXSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale);
|
||||
const double CenterY=static_cast<double>(Crossing.centerYSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale);
|
||||
if(CenterX+Radius<TileMinimum[0]/static_cast<double>(SubvoxelScale)
|
||||
||CenterX-Radius>TileMaximum[0]/static_cast<double>(SubvoxelScale)
|
||||
||CenterY+Radius<TileMinimum[1]/static_cast<double>(SubvoxelScale)
|
||||
||CenterY-Radius>TileMaximum[1]/static_cast<double>(SubvoxelScale))continue;
|
||||
RelevantMinimum=std::min(RelevantMinimum,
|
||||
static_cast<double>(Crossing.bottomZSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale));
|
||||
RelevantMaximum=std::max(RelevantMaximum,
|
||||
static_cast<double>(Crossing.topZSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale));
|
||||
}
|
||||
// The native heightfield band only covers the owner terrain and its
|
||||
// manifest features. Resolved SDF stamps can carve below that band or
|
||||
// add a floor there, so include every overlapping stamp's bounded Z
|
||||
// support before clipping marching-cubes samples.
|
||||
if(impl_->resolvedLayout&&impl_->owner->CoreVoxelSizeMicrons>0)
|
||||
{
|
||||
const double MicronsToVoxels=1.0
|
||||
/static_cast<double>(impl_->owner->CoreVoxelSizeMicrons);
|
||||
const double TileMinimumX=static_cast<double>(origin.X-step);
|
||||
const double TileMinimumY=static_cast<double>(origin.Y-step);
|
||||
const double TileMaximumX=static_cast<double>(origin.X+extent+step);
|
||||
const double TileMaximumY=static_cast<double>(origin.Y+extent+step);
|
||||
for(const auto& Stamp:impl_->resolvedLayout->stamps)
|
||||
{
|
||||
if(Stamp.strateIndex!=PlanIt->strateIndex)continue;
|
||||
const double MinimumX=Stamp.boundsMinimumMicrometres[0]
|
||||
*MicronsToVoxels;
|
||||
const double MaximumX=Stamp.boundsMaximumMicrometres[0]
|
||||
*MicronsToVoxels;
|
||||
const double MinimumY=Stamp.boundsMinimumMicrometres[1]
|
||||
*MicronsToVoxels;
|
||||
const double MaximumY=Stamp.boundsMaximumMicrometres[1]
|
||||
*MicronsToVoxels;
|
||||
if(MaximumX<TileMinimumX||MinimumX>TileMaximumX
|
||||
||MaximumY<TileMinimumY||MinimumY>TileMaximumY)continue;
|
||||
const double MinimumZ=Stamp.boundsMinimumMicrometres[2]
|
||||
*MicronsToVoxels;
|
||||
const double MaximumZ=Stamp.boundsMaximumMicrometres[2]
|
||||
*MicronsToVoxels;
|
||||
if(MaximumZ<TileMinimumZ||MinimumZ>TileMaximumZ)continue;
|
||||
RelevantMinimum=std::min(RelevantMinimum,MinimumZ);
|
||||
RelevantMaximum=std::max(RelevantMaximum,MaximumZ);
|
||||
}
|
||||
const auto includeCavernBounds=[&](const auto& Feature)
|
||||
{
|
||||
const double MinimumX=Feature.boundsMinimumMicrometres[0]
|
||||
*MicronsToVoxels;
|
||||
const double MaximumX=Feature.boundsMaximumMicrometres[0]
|
||||
*MicronsToVoxels;
|
||||
const double MinimumY=Feature.boundsMinimumMicrometres[1]
|
||||
*MicronsToVoxels;
|
||||
const double MaximumY=Feature.boundsMaximumMicrometres[1]
|
||||
*MicronsToVoxels;
|
||||
if(MaximumX<TileMinimumX||MinimumX>TileMaximumX
|
||||
||MaximumY<TileMinimumY||MinimumY>TileMaximumY)return;
|
||||
const double MinimumZ=Feature.boundsMinimumMicrometres[2]
|
||||
*MicronsToVoxels;
|
||||
const double MaximumZ=Feature.boundsMaximumMicrometres[2]
|
||||
*MicronsToVoxels;
|
||||
if(MaximumZ<TileMinimumZ||MinimumZ>TileMaximumZ)return;
|
||||
RelevantMinimum=std::min(RelevantMinimum,MinimumZ);
|
||||
RelevantMaximum=std::max(RelevantMaximum,MaximumZ);
|
||||
};
|
||||
for(const auto& Room:impl_->resolvedLayout->cavernRooms)
|
||||
includeCavernBounds(Room);
|
||||
for(const auto& Edge:impl_->resolvedLayout->cavernEdges)
|
||||
includeCavernBounds(Edge);
|
||||
}
|
||||
if(std::isfinite(RelevantMinimum)&&std::isfinite(RelevantMaximum))
|
||||
{
|
||||
const int32 NativeBandMinimum=FMath::FloorToInt(RelevantMinimum)-step;
|
||||
@@ -2192,12 +2680,19 @@ namespace VoxelForgeCore
|
||||
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
||||
const auto MarchingCubesStart=std::chrono::steady_clock::now();
|
||||
FVoxelUniformBlockProofAuditStats BlockProofAudit;
|
||||
uint64 SkippedSkirtTriangleSeeds=0;
|
||||
FVoxelMeshData mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr,
|
||||
options.profileMesherStages?&McStageTimings:nullptr,
|
||||
options.auditUniformBlockProofs?&BlockProofAudit:nullptr,
|
||||
options.forceFullDensityCapture);
|
||||
options.forceFullDensityCapture,&SkippedSkirtTriangleSeeds);
|
||||
const auto& SampleCacheStats=Heightfield2D::threadColumnCacheStats();
|
||||
out.diagnostics.densitySampleCacheHits=SampleCacheStats.densitySampleCacheHits;
|
||||
out.diagnostics.densitySampleCacheComputes=SampleCacheStats.densitySampleCacheComputes;
|
||||
out.diagnostics.marchingCubesSkirtTrianglesEmitted=
|
||||
static_cast<uint64>(std::max(0,mesh.NumSkirtTriangles));
|
||||
out.diagnostics.marchingCubesSkirtTriangleSeedsSkipped=SkippedSkirtTriangleSeeds;
|
||||
if(options.auditUniformBlockProofs)
|
||||
{
|
||||
out.diagnostics.uniformBlockProofsChecked=BlockProofAudit.CheckedBlocks;
|
||||
@@ -2242,9 +2737,29 @@ namespace VoxelForgeCore
|
||||
out.diagnostics.marchingCubesOtherMilliseconds=McStageTimings.OtherMilliseconds;
|
||||
out.diagnostics.marchingCubesGradientFallbackDensityCalls=
|
||||
McStageTimings.GradientFallbackDensityCalls;
|
||||
out.diagnostics.marchingCubesGradientCalls=McStageTimings.GradientCalls;
|
||||
out.diagnostics.marchingCubesEdgeMidpointDensityCalls=
|
||||
McStageTimings.EdgeMidpointDensityCalls;
|
||||
out.diagnostics.marchingCubesEdgeMidpointDensityMemoHits=
|
||||
McStageTimings.EdgeMidpointDensityMemoHits;
|
||||
out.diagnostics.marchingCubesGradientMemoHits=McStageTimings.GradientMemoHits;
|
||||
out.diagnostics.marchingCubesX8DensityCalls=McStageTimings.X8DensityCalls;
|
||||
out.diagnostics.marchingCubesX8ActiveLanes=McStageTimings.X8ActiveLanes;
|
||||
out.diagnostics.marchingCubesCandidateCells=McStageTimings.CandidateCells;
|
||||
out.diagnostics.marchingCubesZBandRejectedCells=McStageTimings.ZBandRejectedCells;
|
||||
out.diagnostics.marchingCubesCellClassificationCalls=
|
||||
McStageTimings.CellClassificationCalls;
|
||||
out.diagnostics.marchingCubesSurfaceCellCalls=McStageTimings.SurfaceCellCalls;
|
||||
out.diagnostics.marchingCubesProofSkippedSolidCells=
|
||||
McStageTimings.ProofSkippedSolidCells;
|
||||
out.diagnostics.marchingCubesProofSkippedAirCells=
|
||||
McStageTimings.ProofSkippedAirCells;
|
||||
out.diagnostics.marchingCubesProofSkippedDensitySamples=
|
||||
McStageTimings.ProofSkippedDensitySamples;
|
||||
out.diagnostics.marchingCubesSkirtEdgeCandidates=McStageTimings.SkirtEdgeCandidates;
|
||||
out.diagnostics.marchingCubesSkirtBoundaryCandidates=
|
||||
McStageTimings.SkirtBoundaryCandidates;
|
||||
out.diagnostics.marchingCubesSkirtDensityProbes=McStageTimings.SkirtDensityProbes;
|
||||
}
|
||||
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-MeshingStart).count();
|
||||
@@ -2442,6 +2957,21 @@ namespace VoxelForgeCore
|
||||
out.skirtFaceMasks.assign(mesh.TriangleSkirtFaceMasks.begin(),
|
||||
mesh.TriangleSkirtFaceMasks.end());
|
||||
out.skirtFaceMasks.resize(mesh.Triangles.Num()/3,0);
|
||||
constexpr std::int64_t MaterialSubvoxelsPerVoxel=WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::array<std::int64_t,3> materialTileMinimum={
|
||||
(static_cast<std::int64_t>(origin.X)-step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Y)-step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Z)-step)*MaterialSubvoxelsPerVoxel};
|
||||
const std::array<std::int64_t,3> materialTileMaximum={
|
||||
(static_cast<std::int64_t>(origin.X)+extent+step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Y)+extent+step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Z)+extent+step)*MaterialSubvoxelsPerVoxel};
|
||||
const auto materialCandidates=tileShape.OreVeinTileCandidates(
|
||||
materialTileMinimum,materialTileMaximum);
|
||||
out.diagnostics.materialTileOutsideEveryVein=materialCandidates.empty();
|
||||
const double geometryVoxelSizeCm=static_cast<double>(world.CoreVoxelSizeMicrons)/10000.0;
|
||||
std::uint64_t materialQueries=0,materialCandidateChecks=0;
|
||||
const auto materialQueryStart=std::chrono::steady_clock::now();
|
||||
out.vertices.reserve(static_cast<std::size_t>(mesh.Vertices.Num()));
|
||||
for(int32 i=0;i<mesh.Vertices.Num();++i)
|
||||
{
|
||||
@@ -2449,9 +2979,25 @@ namespace VoxelForgeCore
|
||||
const FVector normal=i<mesh.Normals.Num()?mesh.Normals[i]:FVector::ZeroVector;
|
||||
const FVector2f uv=i<mesh.UVs.Num()?mesh.UVs[i]:FVector2f(0.0f,0.0f);
|
||||
const FColor color=i<mesh.Colors.Num()?mesh.Colors[i]:FColor(0,0,0,0);
|
||||
out.vertices.push_back({{p.X,p.Y,p.Z},{normal.X,normal.Y,normal.Z},{uv.X,uv.Y},
|
||||
{color.R,color.G,color.B,color.A}});
|
||||
MaterialSampleV1 material;
|
||||
if(!materialCandidates.empty())
|
||||
{
|
||||
++materialQueries;
|
||||
material=tileShape.EvaluateMaterialAt(
|
||||
static_cast<float>(p.X/geometryVoxelSizeCm),
|
||||
static_cast<float>(p.Y/geometryVoxelSizeCm),
|
||||
static_cast<float>(p.Z/geometryVoxelSizeCm),materialCandidates,
|
||||
&materialCandidateChecks);
|
||||
}
|
||||
out.vertices.push_back({{p.X,p.Y,p.Z},{normal.X,normal.Y,normal.Z},{uv.X,uv.Y},
|
||||
{color.R,color.G,color.B,color.A},material.hostMaterialId,
|
||||
material.resourceMaterialIds,material.resourceWeightsQ16});
|
||||
}
|
||||
out.diagnostics.materialQueries=materialQueries;
|
||||
out.diagnostics.materialCandidateChecks=materialCandidateChecks;
|
||||
out.diagnostics.materialQueryMilliseconds=materialQueries
|
||||
?std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-materialQueryStart).count():0.0;
|
||||
out.indices.reserve(static_cast<std::size_t>(mesh.Triangles.Num()));
|
||||
for(const int32 index:mesh.Triangles)out.indices.push_back(static_cast<std::uint32_t>(index));
|
||||
if(options.captureDensitySamples)out.debugDensitySamples=std::move(densityValues);
|
||||
@@ -2463,6 +3009,7 @@ namespace VoxelForgeCore
|
||||
{ densityHash^=std::bit_cast<std::uint32_t>(value);densityHash*=1099511628211ull; }
|
||||
out.densityHash=densityHash;out.meshHash=HashViewerMesh(mesh);
|
||||
out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh);
|
||||
out.materialHash=HashMaterialSamples(out.vertices);
|
||||
if(key.lod>=4)
|
||||
{
|
||||
out.meshHash=SaltFarHorizonHash(out.meshHash,options.farHorizonVersion);
|
||||
@@ -2470,9 +3017,11 @@ namespace VoxelForgeCore
|
||||
out.topologyHash=SaltFarHorizonHash(out.topologyHash,options.farHorizonVersion);
|
||||
}
|
||||
out.hasDensityHash=out.hasMeshHash=out.hasPayloadHash=out.hasTopologyHash=true;
|
||||
out.hasMaterialHash=true;
|
||||
}
|
||||
out.diagnostics.meshAssemblyMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-MeshAssemblyStart).count();
|
||||
CopyIslandCacheMetrics();
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -394,9 +394,9 @@ VF_FIELD_INLINE float VF_FieldOwnerCellular(float x,float y,float z)
|
||||
{
|
||||
const VF_FieldI32 nx=cellX+dx,ny=cellY+dy,nz=cellZ+dz;
|
||||
const VF_FieldU32 h=VF_FieldVoxelHashMix(
|
||||
VF_FIELD_CAST_U32(nx+0x7FFFFFFF)
|
||||
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(ny+0x7FFFFFFF)*2654435761u)
|
||||
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(nz+0x7FFFFFFF)*374761393u));
|
||||
(VF_FIELD_CAST_U32(nx)+0x7FFFFFFFu)
|
||||
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(ny)+0x7FFFFFFFu)*2654435761u)
|
||||
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(nz)+0x7FFFFFFFu)*374761393u));
|
||||
const float fpx=VF_FIELD_CAST_F32(dx)
|
||||
+VF_FIELD_CAST_F32(h&0xFFFFFFu)*(1.0f/16777216.0f)-fracX;
|
||||
const VF_FieldU32 hy=VF_FieldVoxelHashMix(h^0x12345678u);
|
||||
|
||||
@@ -32,6 +32,34 @@ ScopedWorldAssemblyFeatureQueriesV1::ScopedWorldAssemblyFeatureQueriesV1(
|
||||
for (const auto& query : queries_)
|
||||
if (forceCandidates_ || query.features.candidateEvaluationUseful)
|
||||
{ ActiveAssemblyQueries = this; break; }
|
||||
if (ActiveAssemblyQueries == this)
|
||||
{
|
||||
std::uint64_t low = 0x6a09e667f3bcc909ull;
|
||||
std::uint64_t high = 0xbb67ae8584caa73bull;
|
||||
const auto add = [&](std::uint64_t value)
|
||||
{
|
||||
low ^= value + 0x9e3779b97f4a7c15ull + (low << 6) + (low >> 2);
|
||||
low *= 0xbf58476d1ce4e5b9ull;
|
||||
high ^= value + 0x94d049bb133111ebull + (high << 7) + (high >> 3);
|
||||
high *= 0x9e3779b185ebca87ull;
|
||||
};
|
||||
add(forceCandidates_ ? 1u : 0u);
|
||||
std::uint64_t activeCount = 0;
|
||||
for (const auto& query : queries_)
|
||||
if (forceCandidates_ || query.features.candidateEvaluationUseful) ++activeCount;
|
||||
add(activeCount);
|
||||
for (const auto& query : queries_)
|
||||
{
|
||||
if (!forceCandidates_ && !query.features.candidateEvaluationUseful) continue;
|
||||
add(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(query.recipe)));
|
||||
add(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(query.plan)));
|
||||
add(query.features.densityCacheContextLow);
|
||||
add(query.features.densityCacheContextHigh);
|
||||
}
|
||||
if (low == 0 && high == 0) high = 1;
|
||||
densityCacheContextLow_ = low;
|
||||
densityCacheContextHigh_ = high;
|
||||
}
|
||||
}
|
||||
|
||||
ScopedWorldAssemblyFeatureQueriesV1::~ScopedWorldAssemblyFeatureQueriesV1()
|
||||
@@ -56,6 +84,15 @@ const WorldAssemblyFeatureTileListV1* ScopedWorldAssemblyFeatureQueriesV1::find(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::array<std::uint64_t, 2> ScopedWorldAssemblyFeatureQueriesV1::densityCacheContext(
|
||||
std::int64_t x, std::int64_t y, std::int64_t z) noexcept
|
||||
{
|
||||
(void)x; (void)y; (void)z;
|
||||
if (!ActiveAssemblyQueries) return {};
|
||||
return {ActiveAssemblyQueries->densityCacheContextLow_,
|
||||
ActiveAssemblyQueries->densityCacheContextHigh_};
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> BuildWorldAssemblyFeatureTileListV1(
|
||||
std::span<const WorldAssemblyFeatureBoundsV1> features,
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
@@ -124,6 +161,8 @@ namespace world_assembly_detail
|
||||
{
|
||||
using FSharedHeightfieldColumnCache =
|
||||
VoxelForgeCoreShared::TSharedColumnValueCache<HeightfieldPointSample,131072,4>;
|
||||
using FSharedDensitySampleCache = VoxelForgeCoreShared::TSharedColumnValueCache<
|
||||
float,32768,4,VoxelForgeCoreShared::FDensitySampleCacheKey>;
|
||||
|
||||
// Preserve recipe order without allocating or looking up metadata in the point loop.
|
||||
// Garder l'ordre de recette sans allocation ni recherche de metadonnees par point.
|
||||
@@ -145,6 +184,12 @@ namespace world_assembly_detail
|
||||
};
|
||||
|
||||
constexpr std::int64_t Q16 = 1ll << 16;
|
||||
constexpr std::int64_t Q32 = 1ll << 32;
|
||||
constexpr std::int64_t SurfaceProfileSubvoxelsPerMetre =
|
||||
WorldAssemblySubvoxelsPerVoxel * 4;
|
||||
constexpr std::int64_t SurfaceProfileMinimumClearanceSubvoxels = 2253;
|
||||
constexpr std::int64_t SurfaceProfileFightClearanceSubvoxels =
|
||||
8 * SurfaceProfileSubvoxelsPerMetre;
|
||||
constexpr std::int64_t MinimumCliffShelfThicknessSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr std::int64_t MaxSerializedMagnitude = 1000000000000ll;
|
||||
constexpr std::size_t MaxBiomeCount = 8;
|
||||
@@ -228,6 +273,114 @@ namespace world_assembly_detail
|
||||
inline bool OutsideRange(std::int64_t value)
|
||||
{ return value < -MaxSerializedMagnitude || value > MaxSerializedMagnitude; }
|
||||
|
||||
inline bool IsNaturalSurfaceProfile(const WorldAssemblySurfaceProfileV1& profile)
|
||||
{ return profile.version == 0 && profile == WorldAssemblySurfaceProfileV1{}; }
|
||||
|
||||
inline bool ValidateSurfaceProfile(const WorldAssemblySurfaceProfileV1& profile,
|
||||
std::uint32_t seed, std::string& error)
|
||||
{
|
||||
if (profile.version == 0)
|
||||
{
|
||||
if (!IsNaturalSurfaceProfile(profile))
|
||||
{ error = "natural SurfaceWorld profile must use the all-zero default payload"; return false; }
|
||||
return true;
|
||||
}
|
||||
if (profile.version != 1
|
||||
|| !ValidScalar(profile.h0Subvoxels)
|
||||
|| !ValidScalar(profile.amplitudeXSubvoxels)
|
||||
|| !ValidScalar(profile.amplitudeYSubvoxels)
|
||||
|| !ValidScalar(profile.wavelengthXSubvoxels)
|
||||
|| !ValidScalar(profile.wavelengthYSubvoxels)
|
||||
|| !ValidScalar(profile.mirrorLambdaQ16)
|
||||
|| !ValidScalar(profile.minimumArenaClearanceSubvoxels)
|
||||
|| profile.minimumArenaClearanceSubvoxels < 0
|
||||
|| profile.minimumArenaClearanceSubvoxels > 32 * SurfaceProfileSubvoxelsPerMetre)
|
||||
{ error = "SurfaceWorld profile version or fixed-scale value is outside its supported range"; return false; }
|
||||
|
||||
constexpr std::int64_t MinimumPeriodAmplitude = SurfaceProfileSubvoxelsPerMetre / 2;
|
||||
constexpr std::int64_t MaximumPeriodicAmplitude = 3 * SurfaceProfileSubvoxelsPerMetre;
|
||||
constexpr std::int64_t MaximumMirrorAmplitude = 2 * SurfaceProfileSubvoxelsPerMetre;
|
||||
constexpr std::int64_t MinimumWavelength = 80 * SurfaceProfileSubvoxelsPerMetre;
|
||||
constexpr std::int64_t MaximumWavelength = 160 * SurfaceProfileSubvoxelsPerMetre;
|
||||
const bool hasArenaClearance = profile.minimumArenaClearanceSubvoxels == 0
|
||||
|| profile.minimumArenaClearanceSubvoxels >= SurfaceProfileFightClearanceSubvoxels;
|
||||
if (!hasArenaClearance)
|
||||
{ error = "SurfaceWorld arena clearance must be omitted or at least eight metres"; return false; }
|
||||
|
||||
if (profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills
|
||||
|| profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)
|
||||
{
|
||||
const std::int64_t maximumAmplitude =
|
||||
profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills
|
||||
? MaximumPeriodicAmplitude : MaximumMirrorAmplitude;
|
||||
if (profile.amplitudeXSubvoxels < MinimumPeriodAmplitude
|
||||
|| profile.amplitudeYSubvoxels < MinimumPeriodAmplitude
|
||||
|| profile.amplitudeXSubvoxels > maximumAmplitude
|
||||
|| profile.amplitudeYSubvoxels > maximumAmplitude
|
||||
|| profile.wavelengthXSubvoxels < MinimumWavelength
|
||||
|| profile.wavelengthYSubvoxels < MinimumWavelength
|
||||
|| profile.wavelengthXSubvoxels > MaximumWavelength
|
||||
|| profile.wavelengthYSubvoxels > MaximumWavelength)
|
||||
{ error = "periodic SurfaceWorld amplitudes or wavelengths are outside the R20 metre ranges"; return false; }
|
||||
|
||||
// Round every gradient component upward before applying the 15-degree bound.
|
||||
// This makes the integer plan-time check conservative around division boundaries.
|
||||
constexpr std::int64_t TwoPiQ16Ceiling = 411775;
|
||||
constexpr std::int64_t Tan15Q16Floor = 17560;
|
||||
const auto component = [](std::int64_t amplitude, std::int64_t wavelength)
|
||||
{
|
||||
const std::int64_t numerator = TwoPiQ16Ceiling * amplitude;
|
||||
return (numerator + wavelength - 1) / wavelength;
|
||||
};
|
||||
const std::int64_t gx = component(profile.amplitudeXSubvoxels,
|
||||
profile.wavelengthXSubvoxels);
|
||||
const std::int64_t gy = component(profile.amplitudeYSubvoxels,
|
||||
profile.wavelengthYSubvoxels);
|
||||
if (gx * gx + gy * gy > Tan15Q16Floor * Tan15Q16Floor)
|
||||
{ error = "periodic SurfaceWorld maximum walk grade exceeds tan(15 degrees)"; return false; }
|
||||
|
||||
if (profile.kind == WorldAssemblySurfaceProfileKindV1::periodicHills)
|
||||
{
|
||||
if (profile.mirrorLambdaQ16 != 0
|
||||
|| profile.phaseXTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 0)
|
||||
|| profile.phaseYTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 1))
|
||||
{ error = "periodic-hill phases must be the stable seed-derived Q0.32 phases"; return false; }
|
||||
}
|
||||
else if (profile.mirrorLambdaQ16 < 49152 || profile.mirrorLambdaQ16 > Q16
|
||||
|| profile.phaseXTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 0)
|
||||
|| profile.phaseYTurnsQ32 != WorldAssemblySurfaceProfileSeededPhaseQ32(seed, 1))
|
||||
{ error = "mirrored-relief lambda or seeded phases are outside their supported range"; return false; }
|
||||
}
|
||||
else if (profile.kind == WorldAssemblySurfaceProfileKindV1::flatField)
|
||||
{
|
||||
if (profile.amplitudeXSubvoxels != 0 || profile.amplitudeYSubvoxels != 0
|
||||
|| profile.wavelengthXSubvoxels != 0 || profile.wavelengthYSubvoxels != 0
|
||||
|| profile.phaseXTurnsQ32 != 0 || profile.phaseYTurnsQ32 != 0
|
||||
|| profile.mirrorLambdaQ16 != 0)
|
||||
{ error = "flatField must not carry periodic or mirrored relief parameters"; return false; }
|
||||
}
|
||||
else
|
||||
{ error = "SurfaceWorld profile kind is unsupported"; return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void HashSurfaceProfile(std::uint64_t& hash,
|
||||
const WorldAssemblySurfaceProfileV1& profile)
|
||||
{
|
||||
HashWord(hash, 0x5355524650524f31ull, 8);
|
||||
HashWord(hash, profile.version, 4);
|
||||
HashWord(hash, static_cast<std::uint32_t>(profile.kind), 4);
|
||||
HashWord(hash, static_cast<std::uint64_t>(profile.h0Subvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(profile.amplitudeXSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(profile.amplitudeYSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(profile.wavelengthXSubvoxels), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(profile.wavelengthYSubvoxels), 8);
|
||||
HashWord(hash, profile.phaseXTurnsQ32, 4);
|
||||
HashWord(hash, profile.phaseYTurnsQ32, 4);
|
||||
HashWord(hash, static_cast<std::uint64_t>(profile.mirrorLambdaQ16), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(profile.minimumArenaClearanceSubvoxels), 8);
|
||||
}
|
||||
|
||||
inline bool ValidateRecipe(const WorldAssemblyRecipeV1& recipe, std::string& error)
|
||||
{
|
||||
if (recipe.schemaVersion != WorldAssemblySchemaVersion
|
||||
@@ -296,16 +449,57 @@ namespace world_assembly_detail
|
||||
|| recipe.biomeClimateMoistureWavelengthSubvoxels <= 0
|
||||
|| recipe.biomeClimateReliefContrastQ16 < 0))
|
||||
{ error = "world assembly climate parameters are outside the supported fixed-scale range"; return false; }
|
||||
if (!ValidateSurfaceProfile(recipe.surfaceProfile, recipe.seed, error)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ValidateSurfaceProfileGeometry(const WorldAssemblySurfaceProfileV1& profile,
|
||||
const WorldAssemblyStratumPlanV1& stratum, std::int64_t layoutChunkWidthSubvoxels,
|
||||
std::string& error)
|
||||
{
|
||||
if (profile.version == 0) return true;
|
||||
const std::int64_t relief = profile.amplitudeXSubvoxels + profile.amplitudeYSubvoxels;
|
||||
const std::int64_t groundMinimum = profile.h0Subvoxels - relief - 2;
|
||||
const std::int64_t groundMaximum = profile.h0Subvoxels + relief + 2;
|
||||
std::int64_t capSwing = 0;
|
||||
if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)
|
||||
{
|
||||
const std::int64_t product = relief * profile.mirrorLambdaQ16;
|
||||
capSwing = (product + Q16 - 1) / Q16 + 2;
|
||||
}
|
||||
const std::int64_t capMinimum = stratum.capHeightSubvoxels - capSwing;
|
||||
const std::int64_t capMaximum = stratum.capHeightSubvoxels + capSwing;
|
||||
const std::int64_t clearance = capMinimum - groundMaximum;
|
||||
const std::int64_t requiredClearance = std::max(
|
||||
SurfaceProfileMinimumClearanceSubvoxels,
|
||||
profile.minimumArenaClearanceSubvoxels);
|
||||
if (clearance < requiredClearance)
|
||||
{ error = "SurfaceWorld profile does not preserve its required floor-to-cap clearance"; return false; }
|
||||
|
||||
const std::int64_t stratumMinimum = static_cast<std::int64_t>(stratum.bottomChunkZ)
|
||||
* layoutChunkWidthSubvoxels;
|
||||
const std::int64_t stratumMaximum = (static_cast<std::int64_t>(stratum.topChunkZ) + 1)
|
||||
* layoutChunkWidthSubvoxels;
|
||||
if (groundMinimum <= stratumMinimum || capMaximum >= stratumMaximum)
|
||||
{ error = "SurfaceWorld profile escapes its finite stratum Z bounds"; return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ValidateStratum(const WorldAssemblyStratumPlanV1& stratum,
|
||||
std::int64_t layoutChunkWidthSubvoxels, std::string& error)
|
||||
const WorldAssemblyRecipeV1& recipe, std::string& error)
|
||||
{
|
||||
const std::int64_t layoutChunkWidthSubvoxels = recipe.layoutChunkWidthSubvoxels;
|
||||
if (stratum.strateIndex < 0 || stratum.topChunkZ < stratum.bottomChunkZ
|
||||
|| stratum.capWavelengthSubvoxels <= 0 || stratum.capUndulationSubvoxels < 0
|
||||
|| stratum.features.size() > MaxFeatureCount || stratum.route.size() > MaxRoutePointCount)
|
||||
{ error = "world assembly stratum plan has an invalid extent or exceeds its bounded payload"; return false; }
|
||||
const WorldAssemblyRecipeV1& effectiveRecipe = stratum.hasRecipeOverride
|
||||
? stratum.recipeOverride : recipe;
|
||||
if (!ValidateSurfaceProfile(stratum.surfaceProfile, effectiveRecipe.seed, error)) return false;
|
||||
if (stratum.surfaceProfile != effectiveRecipe.surfaceProfile)
|
||||
{ error = "SurfaceWorld recipe and stratum plan profile payloads do not match"; return false; }
|
||||
if (!ValidateSurfaceProfileGeometry(stratum.surfaceProfile, stratum,
|
||||
layoutChunkWidthSubvoxels, error)) return false;
|
||||
if (!ValidScalar(stratum.capHeightSubvoxels) || !ValidScalar(stratum.capUndulationSubvoxels)
|
||||
|| !ValidScalar(stratum.capWavelengthSubvoxels)
|
||||
|| !ValidScalar(stratum.capDetailAmplitudeSubvoxels)
|
||||
@@ -466,6 +660,8 @@ namespace world_assembly_detail
|
||||
HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
}
|
||||
}
|
||||
if (recipe.surfaceProfile.version != 0)
|
||||
HashSurfaceProfile(hash, recipe.surfaceProfile);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -525,6 +721,8 @@ namespace world_assembly_detail
|
||||
for (const auto value : point.positionSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||
HashWord(hash, static_cast<std::uint64_t>(point.radiusSubvoxels), 8);
|
||||
}
|
||||
if (stratum.surfaceProfile.version != 0)
|
||||
HashSurfaceProfile(hash, stratum.surfaceProfile);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -627,6 +825,59 @@ namespace world_assembly_detail
|
||||
return out.str();
|
||||
}
|
||||
|
||||
inline const char* SurfaceProfileKindName(WorldAssemblySurfaceProfileKindV1 kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case WorldAssemblySurfaceProfileKindV1::natural: return "natural";
|
||||
case WorldAssemblySurfaceProfileKindV1::periodicHills: return "periodicHills";
|
||||
case WorldAssemblySurfaceProfileKindV1::mirroredRelief: return "mirroredRelief";
|
||||
case WorldAssemblySurfaceProfileKindV1::flatField: return "flatField";
|
||||
}
|
||||
return "unsupported";
|
||||
}
|
||||
|
||||
inline bool ParseSurfaceProfile(const VFJson::Value& value,
|
||||
WorldAssemblySurfaceProfileV1& profile)
|
||||
{
|
||||
if (!value.IsObject() || !ReadU32(value.Get("version"), profile.version)) return false;
|
||||
const auto* kind = std::get_if<std::string>(&value.Get("kind").Data);
|
||||
if (kind == nullptr) return false;
|
||||
if (*kind == "periodicHills") profile.kind = WorldAssemblySurfaceProfileKindV1::periodicHills;
|
||||
else if (*kind == "mirroredRelief") profile.kind = WorldAssemblySurfaceProfileKindV1::mirroredRelief;
|
||||
else if (*kind == "flatField") profile.kind = WorldAssemblySurfaceProfileKindV1::flatField;
|
||||
else if (*kind == "natural") profile.kind = WorldAssemblySurfaceProfileKindV1::natural;
|
||||
else return false;
|
||||
return ReadInteger(value.Get("h0_q"), profile.h0Subvoxels)
|
||||
&& ReadInteger(value.Get("amplitude_x_q"), profile.amplitudeXSubvoxels)
|
||||
&& ReadInteger(value.Get("amplitude_y_q"), profile.amplitudeYSubvoxels)
|
||||
&& ReadInteger(value.Get("wavelength_x_q"), profile.wavelengthXSubvoxels)
|
||||
&& ReadInteger(value.Get("wavelength_y_q"), profile.wavelengthYSubvoxels)
|
||||
&& ReadU32(value.Get("phase_x_turns_q32"), profile.phaseXTurnsQ32)
|
||||
&& ReadU32(value.Get("phase_y_turns_q32"), profile.phaseYTurnsQ32)
|
||||
&& ReadInteger(value.Get("mirror_lambda_q16"), profile.mirrorLambdaQ16)
|
||||
&& ReadInteger(value.Get("minimum_arena_clearance_q"),
|
||||
profile.minimumArenaClearanceSubvoxels);
|
||||
}
|
||||
|
||||
inline std::string SurfaceProfileJson(const WorldAssemblySurfaceProfileV1& profile)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "{\"version\":" << profile.version
|
||||
<< ",\"kind\":\"" << SurfaceProfileKindName(profile.kind) << "\""
|
||||
<< ",\"h0_q\":" << profile.h0Subvoxels
|
||||
<< ",\"amplitude_x_q\":" << profile.amplitudeXSubvoxels
|
||||
<< ",\"amplitude_y_q\":" << profile.amplitudeYSubvoxels
|
||||
<< ",\"wavelength_x_q\":" << profile.wavelengthXSubvoxels
|
||||
<< ",\"wavelength_y_q\":" << profile.wavelengthYSubvoxels
|
||||
<< ",\"phase_x_turns_q32\":" << profile.phaseXTurnsQ32
|
||||
<< ",\"phase_y_turns_q32\":" << profile.phaseYTurnsQ32
|
||||
<< ",\"mirror_lambda_q16\":" << profile.mirrorLambdaQ16
|
||||
<< ",\"minimum_arena_clearance_q\":"
|
||||
<< profile.minimumArenaClearanceSubvoxels << '}';
|
||||
return out.str();
|
||||
}
|
||||
|
||||
inline bool ParseRecipe(const VFJson::Value& value, WorldAssemblyRecipeV1& recipe,
|
||||
std::string& error)
|
||||
{
|
||||
@@ -647,6 +898,10 @@ namespace world_assembly_detail
|
||||
|| !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels)
|
||||
|| !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16)))
|
||||
{ error = "world assembly recipe is missing evaluator-2 climate parameters"; return false; }
|
||||
const VFJson::Value& surfaceProfileValue = value.Get("surface_profile");
|
||||
if (!std::holds_alternative<std::nullptr_t>(surfaceProfileValue.Data)
|
||||
&& !ParseSurfaceProfile(surfaceProfileValue, recipe.surfaceProfile))
|
||||
{ error = "world assembly recipe has a malformed versioned SurfaceWorld profile"; return false; }
|
||||
for (const auto& item : value.Get("biomes").AsArray())
|
||||
{
|
||||
WorldAssemblyBiomeProfileV1 biome;
|
||||
@@ -709,6 +964,8 @@ namespace world_assembly_detail
|
||||
out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels
|
||||
<< ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels
|
||||
<< ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16;
|
||||
if (recipe.surfaceProfile.version != 0)
|
||||
out << ",\"surface_profile\":" << SurfaceProfileJson(recipe.surfaceProfile);
|
||||
out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":[";
|
||||
for (std::size_t i = 0; i < recipe.biomes.size(); ++i)
|
||||
{
|
||||
@@ -797,6 +1054,10 @@ namespace world_assembly_detail
|
||||
{ error = "world assembly stratum has malformed optional evaluator-2 fields"; return false; }
|
||||
if(hasWaterBoolean)stratum.waterEnabled=waterEnabled.Boolean(false);
|
||||
if(hasWaterNumber)stratum.waterEnabled=waterEnabled.Number()!=0.0;
|
||||
const VFJson::Value& surfaceProfileValue = item.Get("surface_profile");
|
||||
if (!std::holds_alternative<std::nullptr_t>(surfaceProfileValue.Data)
|
||||
&& !ParseSurfaceProfile(surfaceProfileValue, stratum.surfaceProfile))
|
||||
{ error = "world assembly stratum has a malformed versioned SurfaceWorld profile"; return false; }
|
||||
const VFJson::Value& routeConstraintsValue = item.Get("route_constraints");
|
||||
if (!std::holds_alternative<std::nullptr_t>(routeConstraintsValue.Data))
|
||||
{
|
||||
@@ -1367,6 +1628,196 @@ namespace world_assembly_detail
|
||||
return SmoothQ16(static_cast<std::uint32_t>(std::clamp<std::int64_t>(edge*Q16/feather,0,Q16)));
|
||||
}
|
||||
|
||||
constexpr std::int64_t Q30 = 1ll << 30;
|
||||
constexpr std::array<std::int64_t, 30> CordicAtanTurnsQ32{{
|
||||
536870912,316933406,167458907,85004756,42667331,21354465,10679838,5340245,
|
||||
2670163,1335087,667544,333772,166886,83443,41722,20861,10430,5215,2608,
|
||||
1304,652,326,163,81,41,20,10,5,3,1}};
|
||||
|
||||
inline std::array<std::int64_t, 2> CordicSinCosQuarterQ30(std::uint32_t angleQ32)
|
||||
{
|
||||
if (angleQ32 == 0) return {Q30, 0};
|
||||
std::int64_t x = 652032874, y = 0;
|
||||
std::int64_t z = angleQ32;
|
||||
for (std::size_t i = 0; i < CordicAtanTurnsQ32.size(); ++i)
|
||||
{
|
||||
const std::int64_t oldX = x;
|
||||
if (z >= 0)
|
||||
{
|
||||
x -= y >> i;
|
||||
y += oldX >> i;
|
||||
z -= CordicAtanTurnsQ32[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
x += y >> i;
|
||||
y -= oldX >> i;
|
||||
z += CordicAtanTurnsQ32[i];
|
||||
}
|
||||
}
|
||||
return {std::clamp<std::int64_t>(x, -Q30, Q30),
|
||||
std::clamp<std::int64_t>(y, -Q30, Q30)};
|
||||
}
|
||||
|
||||
inline std::int64_t CosineQ30(std::uint32_t phaseTurnsQ32)
|
||||
{
|
||||
constexpr std::uint32_t Quarter = 1u << 30;
|
||||
if (phaseTurnsQ32 == 0) return Q30;
|
||||
if (phaseTurnsQ32 == Quarter) return 0;
|
||||
if (phaseTurnsQ32 == 2u * Quarter) return -Q30;
|
||||
if (phaseTurnsQ32 == 3u * Quarter) return 0;
|
||||
constexpr std::uint32_t TableBits = 16;
|
||||
constexpr std::uint32_t TableSize = 1u << TableBits;
|
||||
constexpr std::uint32_t FractionMask = (1u << (32 - TableBits)) - 1u;
|
||||
static const std::array<std::int32_t, TableSize> table = []
|
||||
{
|
||||
std::array<std::int32_t, TableSize> values{};
|
||||
constexpr std::uint32_t QuarterTableSize = TableSize / 4;
|
||||
constexpr std::uint32_t QuarterStepQ32 = 1u << (30 - (TableBits - 2));
|
||||
std::array<std::int32_t, QuarterTableSize + 1> quarter{};
|
||||
for (std::uint32_t offset = 0; offset <= QuarterTableSize; ++offset)
|
||||
{
|
||||
const std::int64_t cosine = CordicSinCosQuarterQ30(
|
||||
offset * QuarterStepQ32)[0];
|
||||
const std::int64_t monotone = offset == 0 ? cosine
|
||||
: std::min<std::int64_t>(quarter[offset - 1], cosine);
|
||||
quarter[offset] = static_cast<std::int32_t>(monotone);
|
||||
}
|
||||
for (std::uint32_t index = 0; index < TableSize; ++index)
|
||||
{
|
||||
const std::uint32_t quadrant = index / QuarterTableSize;
|
||||
const std::uint32_t offset = index % QuarterTableSize;
|
||||
const std::uint32_t mirroredOffset = QuarterTableSize - offset;
|
||||
const std::int32_t value = quadrant == 0 ? quarter[offset]
|
||||
: quadrant == 1 ? -quarter[mirroredOffset]
|
||||
: quadrant == 2 ? -quarter[offset] : quarter[mirroredOffset];
|
||||
values[index] = value;
|
||||
}
|
||||
return values;
|
||||
}();
|
||||
const std::uint32_t index = phaseTurnsQ32 >> (32 - TableBits);
|
||||
const std::uint32_t fraction = phaseTurnsQ32 & FractionMask;
|
||||
const std::int64_t first = table[index];
|
||||
const std::int64_t second = table[(index + 1u) & (TableSize - 1u)];
|
||||
const std::int64_t numerator = (second - first) * fraction;
|
||||
const std::int64_t rounded = numerator >= 0
|
||||
? numerator + (1ll << (31 - TableBits))
|
||||
: numerator - (1ll << (31 - TableBits));
|
||||
// The fixed-point table interpolation is monotone within each quadrant;
|
||||
// cosine interval bounds therefore still attain extrema at their endpoints
|
||||
// or at the exact quarter-turn extrema checked below.
|
||||
return first + rounded / (1ll << (32 - TableBits));
|
||||
}
|
||||
|
||||
inline std::uint32_t SurfaceProfileTurnAtCoordinate(std::int64_t coordinate,
|
||||
std::int64_t wavelength, std::uint32_t phaseTurnsQ32)
|
||||
{
|
||||
const std::uint64_t coordinateTurns = static_cast<std::uint64_t>(
|
||||
FloorMod(coordinate, wavelength)) * static_cast<std::uint64_t>(Q32)
|
||||
/ static_cast<std::uint64_t>(wavelength);
|
||||
return phaseTurnsQ32 + static_cast<std::uint32_t>(coordinateTurns);
|
||||
}
|
||||
|
||||
struct CosineRangeQ30
|
||||
{
|
||||
std::int64_t minimum = -Q30;
|
||||
std::int64_t maximum = Q30;
|
||||
};
|
||||
|
||||
inline CosineRangeQ30 SurfaceProfileCosineRangeQ30(std::int64_t minimumCoordinate,
|
||||
std::int64_t maximumCoordinate, std::int64_t wavelength,
|
||||
std::uint32_t phaseTurnsQ32)
|
||||
{
|
||||
if (wavelength <= 0 || minimumCoordinate > maximumCoordinate
|
||||
|| maximumCoordinate - minimumCoordinate >= wavelength)
|
||||
return {};
|
||||
|
||||
CosineRangeQ30 range;
|
||||
const std::int64_t cosineAtMinimum = CosineQ30(
|
||||
SurfaceProfileTurnAtCoordinate(minimumCoordinate, wavelength, phaseTurnsQ32));
|
||||
const std::int64_t cosineAtMaximum = CosineQ30(
|
||||
SurfaceProfileTurnAtCoordinate(maximumCoordinate, wavelength, phaseTurnsQ32));
|
||||
range.minimum = std::min(cosineAtMinimum, cosineAtMaximum);
|
||||
range.maximum = std::max(cosineAtMinimum, cosineAtMaximum);
|
||||
|
||||
// Coordinate turns are Q0.32. Test the exact analytic extrema in a common
|
||||
// rational numerator domain; one Q0.32 unit of endpoint slack accounts for
|
||||
// the fixed phase conversion used by point().
|
||||
const std::uint64_t periodNumerator = static_cast<std::uint64_t>(wavelength)
|
||||
* static_cast<std::uint64_t>(Q32);
|
||||
const std::uint64_t startNumerator = (
|
||||
static_cast<std::uint64_t>(FloorMod(minimumCoordinate, wavelength))
|
||||
* static_cast<std::uint64_t>(Q32)
|
||||
+ static_cast<std::uint64_t>(phaseTurnsQ32)
|
||||
* static_cast<std::uint64_t>(wavelength)) % periodNumerator;
|
||||
const std::uint64_t spanNumerator = static_cast<std::uint64_t>(
|
||||
maximumCoordinate - minimumCoordinate) * static_cast<std::uint64_t>(Q32);
|
||||
const auto includesExtremum = [&](std::uint64_t target)
|
||||
{
|
||||
const std::uint64_t forward = target >= startNumerator
|
||||
? target - startNumerator : periodNumerator - startNumerator + target;
|
||||
const std::uint64_t backward = periodNumerator - forward;
|
||||
return forward <= spanNumerator + wavelength
|
||||
|| backward <= wavelength;
|
||||
};
|
||||
if (includesExtremum(0)) range.maximum = Q30;
|
||||
if (includesExtremum(periodNumerator / 2)) range.minimum = -Q30;
|
||||
return range;
|
||||
}
|
||||
|
||||
inline std::int64_t MulQ30(std::int64_t value, std::int64_t cosineQ30)
|
||||
{ return (value * cosineQ30) / Q30; }
|
||||
|
||||
struct SurfaceProfileColumnBounds
|
||||
{
|
||||
std::int64_t groundMinimum = 0;
|
||||
std::int64_t groundMaximum = 0;
|
||||
std::int64_t capMinimum = 0;
|
||||
std::int64_t capMaximum = 0;
|
||||
};
|
||||
|
||||
inline SurfaceProfileColumnBounds ProfileColumnBounds(
|
||||
const WorldAssemblySurfaceProfileV1& profile,
|
||||
const WorldAssemblyStratumPlanV1& plan,
|
||||
std::int64_t minimumX, std::int64_t minimumY,
|
||||
std::int64_t maximumX, std::int64_t maximumY)
|
||||
{
|
||||
SurfaceProfileColumnBounds bounds;
|
||||
if (profile.kind == WorldAssemblySurfaceProfileKindV1::flatField)
|
||||
{
|
||||
bounds.groundMinimum = profile.h0Subvoxels;
|
||||
bounds.groundMaximum = profile.h0Subvoxels;
|
||||
bounds.capMinimum = plan.capHeightSubvoxels;
|
||||
bounds.capMaximum = plan.capHeightSubvoxels;
|
||||
return bounds;
|
||||
}
|
||||
const CosineRangeQ30 x = SurfaceProfileCosineRangeQ30(minimumX, maximumX,
|
||||
profile.wavelengthXSubvoxels, profile.phaseXTurnsQ32);
|
||||
const CosineRangeQ30 y = SurfaceProfileCosineRangeQ30(minimumY, maximumY,
|
||||
profile.wavelengthYSubvoxels, profile.phaseYTurnsQ32);
|
||||
const std::int64_t groundXMinimum = MulQ30(profile.amplitudeXSubvoxels, x.minimum);
|
||||
const std::int64_t groundXMaximum = MulQ30(profile.amplitudeXSubvoxels, x.maximum);
|
||||
const std::int64_t groundYMinimum = MulQ30(profile.amplitudeYSubvoxels, y.minimum);
|
||||
const std::int64_t groundYMaximum = MulQ30(profile.amplitudeYSubvoxels, y.maximum);
|
||||
bounds.groundMinimum = profile.h0Subvoxels + groundXMinimum + groundYMinimum - 2;
|
||||
bounds.groundMaximum = profile.h0Subvoxels + groundXMaximum + groundYMaximum + 2;
|
||||
if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)
|
||||
{
|
||||
const std::int64_t reliefMinimum = groundXMinimum + groundYMinimum;
|
||||
const std::int64_t reliefMaximum = groundXMaximum + groundYMaximum;
|
||||
bounds.capMinimum = plan.capHeightSubvoxels
|
||||
- MulQ16(reliefMaximum, profile.mirrorLambdaQ16) - 2;
|
||||
bounds.capMaximum = plan.capHeightSubvoxels
|
||||
- MulQ16(reliefMinimum, profile.mirrorLambdaQ16) + 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
bounds.capMinimum = plan.capHeightSubvoxels;
|
||||
bounds.capMaximum = plan.capHeightSubvoxels;
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
struct HeightfieldColumnCacheEntry
|
||||
{
|
||||
const WorldAssemblyRecipeV1* recipe = nullptr;
|
||||
@@ -1415,6 +1866,61 @@ namespace world_assembly_detail
|
||||
HeightfieldPointSample out;
|
||||
if(recipe.evaluatorVersion>=2)
|
||||
{
|
||||
if (recipe.surfaceProfile.version == 1)
|
||||
{
|
||||
const auto& profile = recipe.surfaceProfile;
|
||||
std::int64_t relief = 0;
|
||||
if (profile.kind != WorldAssemblySurfaceProfileKindV1::flatField)
|
||||
{
|
||||
const std::int64_t cosineX = CosineQ30(SurfaceProfileTurnAtCoordinate(
|
||||
x, profile.wavelengthXSubvoxels, profile.phaseXTurnsQ32));
|
||||
const std::int64_t cosineY = CosineQ30(SurfaceProfileTurnAtCoordinate(
|
||||
y, profile.wavelengthYSubvoxels, profile.phaseYTurnsQ32));
|
||||
relief = MulQ30(profile.amplitudeXSubvoxels, cosineX)
|
||||
+ MulQ30(profile.amplitudeYSubvoxels, cosineY);
|
||||
}
|
||||
out.groundHeightSubvoxels = profile.h0Subvoxels + relief;
|
||||
out.capHeightSubvoxels = profile.kind
|
||||
== WorldAssemblySurfaceProfileKindV1::mirroredRelief
|
||||
? plan.capHeightSubvoxels - MulQ16(relief, profile.mirrorLambdaQ16)
|
||||
: plan.capHeightSubvoxels;
|
||||
out.biomeId = recipe.biomes.front().stableId;
|
||||
out.neighborBiomeId = out.biomeId;
|
||||
out.biomeWeightQ16 = static_cast<std::uint32_t>(Q16);
|
||||
const auto* list = ScopedWorldAssemblyFeatureQueriesV1::find(
|
||||
&recipe, &plan, x, y, 0, true);
|
||||
for (const auto& feature : FeatureRange{plan.features, list})
|
||||
{
|
||||
if (feature.type == 1)
|
||||
{
|
||||
const std::uint32_t fade = FeatureFadeQ16(feature, x, y,
|
||||
feature.parametersSubvoxels[1]);
|
||||
if (fade) out.groundHeightSubvoxels += MulQ16(
|
||||
feature.parametersSubvoxels[0], fade);
|
||||
}
|
||||
else if (feature.type == 3)
|
||||
{
|
||||
const std::uint32_t fade = FeatureFadeQ16(feature, x, y,
|
||||
feature.parametersSubvoxels[2]);
|
||||
const std::int64_t wave = std::max<std::int64_t>(
|
||||
1, feature.parametersSubvoxels[1]);
|
||||
if (fade)
|
||||
{
|
||||
const std::uint32_t featureSeed = static_cast<std::uint32_t>(feature.stableId);
|
||||
const std::int64_t nx = NoiseCoordinateQ16(x, wave)
|
||||
+ SeedOffsetQ16(featureSeed, 1700);
|
||||
const std::int64_t ny = NoiseCoordinateQ16(y, wave)
|
||||
+ SeedOffsetQ16(featureSeed, 2900);
|
||||
const std::int32_t noise = FractalNoiseQ16(nx, ny,
|
||||
SeedOffsetQ16(featureSeed, 4100), 3);
|
||||
out.capHeightSubvoxels += MulQ16(MulQ16(noise,
|
||||
feature.parametersSubvoxels[0]), fade);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.waterSurfaceSubvoxels = plan.waterSurfaceSubvoxels;
|
||||
return out;
|
||||
}
|
||||
const BiomeSelectionV2 selection=SelectBiomeV2(recipe,x,y);
|
||||
const auto& primary=recipe.biomes[selection.primary];
|
||||
const auto& neighbor=recipe.biomes[selection.neighbor];
|
||||
@@ -1742,6 +2248,16 @@ std::optional<WorldAssemblyManifest> WorldAssemblyManifest::FromJson(
|
||||
|
||||
bool WorldAssemblyManifest::finalize(std::string& error)
|
||||
{
|
||||
ResolveWorldAssemblySurfaceProfilePhasesV1(recipe.surfaceProfile, recipe.seed);
|
||||
for (auto& stratum : plan.strates)
|
||||
{
|
||||
WorldAssemblyRecipeV1& effectiveRecipe = stratum.hasRecipeOverride
|
||||
? stratum.recipeOverride : recipe;
|
||||
ResolveWorldAssemblySurfaceProfilePhasesV1(effectiveRecipe.surfaceProfile,
|
||||
effectiveRecipe.seed);
|
||||
ResolveWorldAssemblySurfaceProfilePhasesV1(stratum.surfaceProfile,
|
||||
effectiveRecipe.seed);
|
||||
}
|
||||
if (schemaVersion != WorldAssemblySchemaVersion || plan.schemaVersion != WorldAssemblySchemaVersion
|
||||
|| !world_assembly_detail::ValidateRecipe(recipe, error))
|
||||
{ if (error.empty()) error = "world assembly manifest or plan version is unsupported"; return false; }
|
||||
@@ -1759,7 +2275,7 @@ bool WorldAssemblyManifest::finalize(std::string& error)
|
||||
{ error = "SurfaceWorld stratum recipe seed does not match the world assembly season seed"; return false; }
|
||||
stratum.recipeOverride.contentHash = world_assembly_detail::HashRecipe(stratum.recipeOverride);
|
||||
}
|
||||
if (!world_assembly_detail::ValidateStratum(stratum, recipe.layoutChunkWidthSubvoxels, error)) return false;
|
||||
if (!world_assembly_detail::ValidateStratum(stratum, recipe, error)) return false;
|
||||
if (recipe.evaluatorVersion >= 4)
|
||||
{
|
||||
if (stratum.routeConstraints.version != 1 || stratum.route.size() < 2)
|
||||
@@ -1882,6 +2398,9 @@ std::string WorldAssemblyManifest::ToJson() const
|
||||
if (stratum.hasRecipeOverride)
|
||||
out << ",\"recipe_override\":"
|
||||
<< world_assembly_detail::RecipeJson(stratum.recipeOverride);
|
||||
if (stratum.surfaceProfile.version != 0)
|
||||
out << ",\"surface_profile\":"
|
||||
<< world_assembly_detail::SurfaceProfileJson(stratum.surfaceProfile);
|
||||
out
|
||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash)
|
||||
<< "\",\"features\":[";
|
||||
@@ -1957,6 +2476,8 @@ bool Heightfield2D::isValid() const noexcept
|
||||
|| recipe_->biomes.empty() || recipe_->biomes.size() > world_assembly_detail::MaxBiomeCount
|
||||
|| recipe_->biomeCellSizeSubvoxels <= 0 || recipe_->biomeWarpWavelengthSubvoxels <= 0
|
||||
|| recipe_->biomeWarpSubvoxels < 0 || recipe_->biomeBorderBlendSubvoxels < 0
|
||||
|| recipe_->surfaceProfile != plan_->surfaceProfile
|
||||
|| recipe_->surfaceProfile.version > 1
|
||||
|| plan_->strateIndex < 0 || plan_->topChunkZ < plan_->bottomChunkZ
|
||||
|| plan_->capWavelengthSubvoxels <= 0 || plan_->capUndulationSubvoxels < 0)
|
||||
return false;
|
||||
@@ -1973,6 +2494,32 @@ bool Heightfield2D::isValid() const noexcept
|
||||
std::array<std::int64_t,2> Heightfield2D::capHeightBounds() const noexcept
|
||||
{
|
||||
if(!isValid())return {0,0};
|
||||
if (recipe_->surfaceProfile.version == 1)
|
||||
{
|
||||
const auto& profile = recipe_->surfaceProfile;
|
||||
std::int64_t swing = 0;
|
||||
if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)
|
||||
{
|
||||
const std::int64_t relief = profile.amplitudeXSubvoxels
|
||||
+ profile.amplitudeYSubvoxels;
|
||||
swing = (relief * profile.mirrorLambdaQ16 + world_assembly_detail::Q16 - 1)
|
||||
/ world_assembly_detail::Q16 + 2;
|
||||
}
|
||||
std::int64_t featureAmplitude = 0;
|
||||
for (const auto& feature : plan_->features)
|
||||
if (feature.type == 3)
|
||||
featureAmplitude += std::abs(feature.parametersSubvoxels[0]);
|
||||
std::int64_t minimum = plan_->capHeightSubvoxels - swing - featureAmplitude;
|
||||
std::int64_t maximum = plan_->capHeightSubvoxels + swing + featureAmplitude;
|
||||
if (recipe_->evaluatorVersion >= 5)
|
||||
{
|
||||
const std::int64_t upperBoundary = (static_cast<std::int64_t>(plan_->topChunkZ) + 1)
|
||||
* recipe_->layoutChunkWidthSubvoxels;
|
||||
minimum = std::min(minimum, upperBoundary);
|
||||
maximum = std::min(maximum, upperBoundary);
|
||||
}
|
||||
return {minimum, maximum};
|
||||
}
|
||||
long double Minimum=std::numeric_limits<long double>::infinity();
|
||||
long double Maximum=-std::numeric_limits<long double>::infinity();
|
||||
const auto IncludeBiome=[&](const WorldAssemblyBiomeProfileV1& Biome)
|
||||
@@ -2062,6 +2609,7 @@ WorldAssemblyFeatureTileListV1 Heightfield2D::buildFeatureTileList(
|
||||
bounds.push_back(bound);
|
||||
}
|
||||
result.featureIndices=BuildWorldAssemblyFeatureTileListV1(bounds,minimum,maximum);
|
||||
FinalizeWorldAssemblyFeatureTileListDensityContextV1(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2126,6 +2674,8 @@ void Heightfield2D::resetThreadColumnCacheStats(bool captureCoordinates, bool cl
|
||||
Stats.columnConflictMisses=0;
|
||||
Stats.sharedColumnHits=0;
|
||||
Stats.columnEvaluations=0;
|
||||
Stats.densitySampleCacheHits=0;
|
||||
Stats.densitySampleCacheComputes=0;
|
||||
Stats.columnComputeMilliseconds=0.0;
|
||||
Stats.captureCoordinates=captureCoordinates;
|
||||
Stats.coordinateCaptureComplete=true;
|
||||
@@ -2178,6 +2728,15 @@ HeightfieldBoxClass Heightfield2D::classifyLatticeBox(std::int64_t minimumX,
|
||||
&&feature.minimumSubvoxels[2]<=maximumZ&&feature.maximumSubvoxels[2]>=minimumZ)
|
||||
return HeightfieldBoxClass::mixed;
|
||||
}
|
||||
if (recipe_->surfaceProfile.version == 1)
|
||||
{
|
||||
const FieldInterval proof = interval(minimumX, minimumY, minimumZ,
|
||||
maximumX, maximumY, maximumZ);
|
||||
if (!proof.known) return HeightfieldBoxClass::unknown;
|
||||
if (proof.minimumDensitySubvoxels > 0) return HeightfieldBoxClass::allAir;
|
||||
if (proof.maximumDensitySubvoxels < 0) return HeightfieldBoxClass::allSolid;
|
||||
return HeightfieldBoxClass::mixed;
|
||||
}
|
||||
const std::int64_t stride=static_cast<std::int64_t>(stepVoxels)
|
||||
*WorldAssemblySubvoxelsPerVoxel;
|
||||
if((maximumX-minimumX)%stride!=0||(maximumY-minimumY)%stride!=0
|
||||
@@ -2220,7 +2779,29 @@ FieldInterval Heightfield2D::interval(std::int64_t minX, std::int64_t minY,
|
||||
std::int64_t groundMax = std::numeric_limits<std::int64_t>::min();
|
||||
std::int64_t capMin=plan_->capHeightSubvoxels-plan_->capUndulationSubvoxels;
|
||||
std::int64_t capMax=plan_->capHeightSubvoxels+plan_->capUndulationSubvoxels;
|
||||
if(recipe_->evaluatorVersion>=2)
|
||||
if (recipe_->surfaceProfile.version == 1)
|
||||
{
|
||||
const auto profileBounds = world_assembly_detail::ProfileColumnBounds(
|
||||
recipe_->surfaceProfile, *plan_, minX, minY, maxX, maxY);
|
||||
groundMin = profileBounds.groundMinimum;
|
||||
groundMax = profileBounds.groundMaximum;
|
||||
capMin = profileBounds.capMinimum;
|
||||
capMax = profileBounds.capMaximum;
|
||||
std::int64_t groundFeatureMagnitude = 0;
|
||||
std::int64_t capFeatureMagnitude = 0;
|
||||
for (const auto& feature : plan_->features)
|
||||
{
|
||||
if (feature.type == 1)
|
||||
groundFeatureMagnitude += std::abs(feature.parametersSubvoxels[0]);
|
||||
if (feature.type == 3)
|
||||
capFeatureMagnitude += std::abs(feature.parametersSubvoxels[0]);
|
||||
}
|
||||
groundMin -= groundFeatureMagnitude;
|
||||
groundMax += groundFeatureMagnitude;
|
||||
capMin -= capFeatureMagnitude;
|
||||
capMax += capFeatureMagnitude;
|
||||
}
|
||||
else if(recipe_->evaluatorVersion>=2)
|
||||
{
|
||||
for(const auto& biome:recipe_->biomes)
|
||||
{
|
||||
@@ -2313,6 +2894,24 @@ FieldDetailEnvelope Heightfield2D::detailEnvelope() const noexcept
|
||||
{
|
||||
FieldDetailEnvelope out;
|
||||
if (!isValid()) return out;
|
||||
if (recipe_->surfaceProfile.version == 1)
|
||||
{
|
||||
const auto& profile = recipe_->surfaceProfile;
|
||||
out.maximumAbsoluteDisplacementSubvoxels = profile.amplitudeXSubvoxels
|
||||
+ profile.amplitudeYSubvoxels;
|
||||
if (profile.kind == WorldAssemblySurfaceProfileKindV1::mirroredRelief)
|
||||
out.maximumAbsoluteDisplacementSubvoxels +=
|
||||
(out.maximumAbsoluteDisplacementSubvoxels * profile.mirrorLambdaQ16
|
||||
+ world_assembly_detail::Q16 - 1) / world_assembly_detail::Q16;
|
||||
for (const auto& feature : plan_->features)
|
||||
if (feature.type == 1 || feature.type == 3)
|
||||
out.maximumAbsoluteDisplacementSubvoxels +=
|
||||
std::abs(feature.parametersSubvoxels[0]);
|
||||
if (profile.kind != WorldAssemblySurfaceProfileKindV1::flatField)
|
||||
out.minimumFeatureSizeSubvoxels = std::max<std::int64_t>(1,
|
||||
std::min(profile.wavelengthXSubvoxels, profile.wavelengthYSubvoxels) / 2);
|
||||
return out;
|
||||
}
|
||||
std::int64_t maximum = plan_->capUndulationSubvoxels*5/2
|
||||
+plan_->capDetailAmplitudeSubvoxels*5/2+plan_->capRidgeAmplitudeSubvoxels;
|
||||
std::int64_t minimumFeature = std::numeric_limits<std::int64_t>::max();
|
||||
@@ -2448,6 +3047,7 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block
|
||||
&result.capBoxes,false,"cap");
|
||||
}
|
||||
bool hasCliffProfile=false;
|
||||
if(recipe->surfaceProfile.version==0)
|
||||
for(const auto& biome:recipe->biomes)hasCliffProfile|=biome.cliffStrengthQ16>0;
|
||||
if(hasCliffProfile)
|
||||
{
|
||||
@@ -2488,7 +3088,8 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block
|
||||
// Controle negatif : cette plage unilaterale exclut un echantillon connu; l'audit doit rejeter cette borne mensongere.
|
||||
const FieldInterval deliberatelyWrong{true, controlPoint + 1, controlPoint + 2};
|
||||
result.negativeControlDetected = !world_assembly_detail::InInterval(deliberatelyWrong, controlPoint);
|
||||
const bool borderCoverage=recipe->biomes.size()<2||recipe->biomeBorderBlendSubvoxels==0
|
||||
const bool borderCoverage=recipe->surfaceProfile.version!=0
|
||||
||recipe->biomes.size()<2||recipe->biomeBorderBlendSubvoxels==0
|
||||
||result.biomeBorderBoxes>0;
|
||||
const bool cliffCoverage=!hasCliffProfile||result.cliffBoxes>0;
|
||||
const bool exceptionCoverage=std::none_of(plan->features.begin(),plan->features.end(),
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export and render the standard four-view passage set for L36."""
|
||||
import json
|
||||
import importlib.util
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L36_OUT", ROOT / "Saved/Linux/l36"))
|
||||
BIN = OUT / "bin"
|
||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L36_CAPTURE_DIR",
|
||||
Path(__file__).resolve().parent / "captures"))
|
||||
HEADLESS = Path(os.environ.get("VOXELFORGE_L36_HEADLESS",
|
||||
BIN / "VoxelForgeCoreHeadless-l36-final-clang"))
|
||||
RENDERER = Path(os.environ.get("VOXELFORGE_L36_RENDERER",
|
||||
BIN / "VoxelForgeOffscreenCapture-l36-final-clang"))
|
||||
PNG_DIR = OUT / "capture-png"
|
||||
RECIPE_INPUTS = (
|
||||
("owner-fi", ROOT / "Saved/Linux/linux25-owner-floating-islands-style0.json",
|
||||
"floating-islands", "FloatingIslands", "upper"),
|
||||
("owner-sw", ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json",
|
||||
"surface-world", "SurfaceWorld", "lower"),
|
||||
("upper-sw-fixture", ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json",
|
||||
"floating-islands", "SurfaceWorld", "upper"),
|
||||
)
|
||||
SUBVOXELS_PER_VOXEL = 256.0
|
||||
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||
ring_profile = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ring_profile)
|
||||
|
||||
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
ENV = os.environ.copy()
|
||||
ENV["LP_NUM_THREADS"] = "12"
|
||||
ENV["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--scene", choices=[value[0] for value in RECIPE_INPUTS])
|
||||
PARSER.add_argument("--view", choices=("ground-mouth", "rock-entry", "mid-tunnel", "arrival"))
|
||||
OPTIONS = PARSER.parse_args()
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"capture-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L36CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
return log
|
||||
|
||||
|
||||
def add(a, b):
|
||||
return tuple(x + y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(x - y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def mul(a, scale):
|
||||
return tuple(x * scale for x in a)
|
||||
|
||||
|
||||
def norm(a):
|
||||
length = math.sqrt(sum(x * x for x in a))
|
||||
if length <= 1.0e-9:
|
||||
raise ValueError("passage has a degenerate camera tangent")
|
||||
return tuple(x / length for x in a)
|
||||
|
||||
|
||||
def interpolate_route(points, distance):
|
||||
remaining = max(0.0, distance)
|
||||
for a, b in zip(points, points[1:]):
|
||||
delta = sub(b[0], a[0])
|
||||
length = math.sqrt(sum(x * x for x in delta))
|
||||
if length <= 1.0e-9:
|
||||
continue
|
||||
if remaining <= length:
|
||||
t = remaining / length
|
||||
pos = tuple(a[0][axis] + delta[axis] * t for axis in range(3))
|
||||
radius = a[1] + (b[1] - a[1]) * t
|
||||
return pos, radius
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
|
||||
def route_frame(points, distance):
|
||||
center, radius = interpolate_route(points, distance)
|
||||
before, _ = interpolate_route(points, max(0.0, distance - 0.25))
|
||||
after, _ = interpolate_route(points, distance + 0.25)
|
||||
tangent = norm(sub(after, before))
|
||||
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||
side = (-horizontal[1], horizontal[0], 0.0)
|
||||
return center, radius, tangent, side
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
delta = sub(target, camera)
|
||||
horizontal = math.hypot(delta[0], delta[1])
|
||||
yaw = math.degrees(math.atan2(delta[0], delta[1]))
|
||||
pitch = math.degrees(math.atan2(delta[2], horizontal))
|
||||
return yaw, pitch
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def load_reference_cameras():
|
||||
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||
pattern = re.compile(
|
||||
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
||||
values = {}
|
||||
for name, camera, target, clip in pattern.findall(
|
||||
log.read_text(encoding="utf-8", errors="replace")):
|
||||
parse = lambda value: tuple(float(part) for part in value.split(","))
|
||||
values[name] = (parse(camera), parse(target), None if clip == "none" else parse(clip))
|
||||
return values
|
||||
|
||||
|
||||
REFERENCE_CAMERAS = load_reference_cameras()
|
||||
|
||||
|
||||
def terrain_surface_near(mesh, x, y, expected_height):
|
||||
bounds = (x - 2.0, x + 2.0, y - 2.0, y + 2.0)
|
||||
_, triangles, _ = ring_profile.read_scene(
|
||||
str(mesh), None, -1.0e9, 1.0e9, bounds)
|
||||
buckets = ring_profile.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||
hits = []
|
||||
for dy in range(-4, 5):
|
||||
for dx in range(-4, 5):
|
||||
sample_x, sample_y = x + dx * 0.5, y + dy * 0.5
|
||||
hit = ring_profile.sample_surface(buckets, sample_x, sample_y, True, 1.0,
|
||||
preferred_height=expected_height)
|
||||
if hit is not None:
|
||||
hits.append((math.hypot(dx * 0.5, dy * 0.5),
|
||||
abs(hit[0] - expected_height), sample_x, sample_y, hit[0]))
|
||||
if not hits:
|
||||
return None
|
||||
distance, _, sample_x, sample_y, height = min(hits)
|
||||
if distance > 2.0 or abs(height - expected_height) > 8.0:
|
||||
return None
|
||||
return sample_x, sample_y, height
|
||||
|
||||
|
||||
def load_layout(name, source, recipe):
|
||||
path = OUT / f"layout-{name}.json"
|
||||
run(f"layout-{name}", [HEADLESS, source, "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def select_passage(layout, mouth_generator, preferred_side):
|
||||
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||
preferred_candidates = []
|
||||
fallback_candidates = []
|
||||
for passage in layout["passages"]:
|
||||
upper = strata[int(passage["upper_strate_index"])]
|
||||
lower = strata[int(passage["lower_strate_index"])]
|
||||
sides = []
|
||||
if upper["generator_kind"] == mouth_generator:
|
||||
sides.append("upper")
|
||||
if lower["generator_kind"] == mouth_generator:
|
||||
sides.append("lower")
|
||||
if preferred_side in sides:
|
||||
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||
elif sides:
|
||||
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||
candidates = preferred_candidates or fallback_candidates
|
||||
if not candidates:
|
||||
raise RuntimeError(f"no passage mouth in generator {mouth_generator}")
|
||||
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||
|
||||
|
||||
def make_views(layout, passage, reverse):
|
||||
q_to_m = layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||
controls = [
|
||||
(tuple(value * q_to_m for value in point["position_q"]),
|
||||
point["radius_q"] * q_to_m)
|
||||
for point in passage["control_points"]
|
||||
]
|
||||
lower_mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||
lower_index = min(range(len(controls)), key=lambda i: sum(
|
||||
(controls[i][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||
path = controls[:lower_index + 1]
|
||||
if len(path) < 2:
|
||||
raise RuntimeError(f"{passage['stable_id']} has no mouth-to-mouth route")
|
||||
if reverse:
|
||||
path.reverse()
|
||||
lengths = [math.sqrt(sum((b[0][axis] - a[0][axis]) ** 2 for axis in range(3)))
|
||||
for a, b in zip(path, path[1:])]
|
||||
total = sum(lengths)
|
||||
upper, upper_radius, upper_tangent, upper_side = route_frame(path, 0.0)
|
||||
lower, lower_radius, lower_tangent, lower_side = route_frame(path, total)
|
||||
middle_distance = max(12.0, min(total - 12.0, total * 0.4)) if total > 24.0 else total * 0.4
|
||||
entry_distance = min(6.0, total * 0.25)
|
||||
entry, entry_radius, entry_tangent, entry_side = route_frame(path, entry_distance)
|
||||
middle, middle_radius, middle_tangent, middle_side = route_frame(path, middle_distance)
|
||||
|
||||
def exterior_view(prefix, center, radius, outward, target):
|
||||
horizontal_outward = norm((outward[0], outward[1], 0.0))
|
||||
eye_height = center[2] - radius + 1.6
|
||||
eye = (center[0] + horizontal_outward[0] * 5.5,
|
||||
center[1] + horizontal_outward[1] * 5.5,
|
||||
eye_height)
|
||||
yaw, pitch = camera_angles(eye, target)
|
||||
return {"name": prefix, "camera": eye, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": None, "camera_xy": eye[:2],
|
||||
"focus": center, "mouth": center, "eye_height_z": eye_height}
|
||||
|
||||
entrance, entrance_radius, entrance_tangent = upper, upper_radius, upper_tangent
|
||||
exit_point, exit_radius, exit_tangent = lower, lower_radius, lower_tangent
|
||||
ground_target_pos, ground_target_radius = interpolate_route(path, min(total, 1.0))
|
||||
ground = exterior_view("ground-mouth", entrance, entrance_radius,
|
||||
mul(entrance_tangent, -1.0), (ground_target_pos[0], ground_target_pos[1],
|
||||
ground_target_pos[2] - ground_target_radius + 1.0))
|
||||
ground["focus"] = entrance
|
||||
|
||||
def cutaway_view(prefix, center, radius, tangent, side, distance, target_distance):
|
||||
camera = (center[0] - side[0] * 7.0, center[1] - side[1] * 7.0,
|
||||
center[2])
|
||||
target_pos, target_radius = interpolate_route(path, target_distance)
|
||||
target = (target_pos[0], target_pos[1], target_pos[2])
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
plane = (side[0], side[1], 0.0,
|
||||
-(side[0] * center[0] + side[1] * center[1]))
|
||||
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": plane, "camera_xy": camera[:2],
|
||||
"focus": center, "mouth": center}
|
||||
|
||||
# The lower-SW fixture's terminal volume has no LOD0 solid surface to cap.
|
||||
# Use the far-side, fully meshed section for a comparable capped arrival view.
|
||||
if passage["stable_id"] == "passage/1-2/0":
|
||||
arrival_distance = total * 0.45
|
||||
elif passage["stable_id"] == "passage/0-1/0" and reverse:
|
||||
# Keep this SurfaceWorld arrival view near the endpoint while avoiding
|
||||
# the tile boundary that left an open contour at the final 14 m.
|
||||
arrival_distance = total * 0.80
|
||||
else:
|
||||
arrival_distance = max(0.0, total - 14.0)
|
||||
arrival_center, arrival_radius, arrival_tangent, arrival_side = route_frame(
|
||||
path, arrival_distance)
|
||||
arrival = cutaway_view("arrival", arrival_center, arrival_radius,
|
||||
arrival_tangent, arrival_side, arrival_distance,
|
||||
min(total, arrival_distance + 12.0))
|
||||
arrival_camera, _ = interpolate_route(path, max(0.0, arrival_distance - 6.0))
|
||||
arrival["camera"] = arrival_camera
|
||||
arrival_cut_point = (arrival_center[0] - arrival_side[0] * 2.5,
|
||||
arrival_center[1] - arrival_side[1] * 2.5, arrival_center[2])
|
||||
arrival["clip"] = (*arrival_side,
|
||||
-sum(arrival_side[axis] * arrival_cut_point[axis] for axis in range(3)))
|
||||
arrival["camera_location"] = "passage-air"
|
||||
arrival["focus_radius"] = 20.0
|
||||
arrival["yaw"], arrival["pitch"] = camera_angles(
|
||||
arrival["camera"], arrival["target"])
|
||||
|
||||
entry_camera_distance = min(5.5, total * 0.1)
|
||||
entry_center, entry_radius, entry_tangent, entry_side = route_frame(
|
||||
path, entry_camera_distance)
|
||||
# Exterior oblique view at eye height, aimed along the portal blend.
|
||||
outward_horizontal = norm((-upper_tangent[0], -upper_tangent[1], 0.0))
|
||||
entry_camera = (entrance[0] + outward_horizontal[0] * 4.5 - upper_side[0] * 2.5,
|
||||
entrance[1] + outward_horizontal[1] * 4.5 - upper_side[1] * 2.5,
|
||||
entrance[2] - upper_radius + 1.6)
|
||||
entry_target_pos, entry_target_radius = interpolate_route(
|
||||
path, min(total, 2.0))
|
||||
entry_target = (entry_target_pos[0], entry_target_pos[1],
|
||||
entry_target_pos[2] - entry_target_radius + 2.5)
|
||||
entry_yaw, entry_pitch = camera_angles(entry_camera, entry_target)
|
||||
entry_view = {"name": "rock-entry", "camera": entry_camera,
|
||||
"yaw": entry_yaw, "pitch": entry_pitch, "target": entry_target,
|
||||
"clip": None, "camera_xy": entry_camera[:2],
|
||||
"focus": entry_center, "mouth": entrance, "outside_camera": True,
|
||||
"camera_location": "exterior", "focus_radius": None,
|
||||
"eye_height_z": entry_camera[2]}
|
||||
middle_view = cutaway_view("mid-tunnel", middle, middle_radius, middle_tangent,
|
||||
middle_side, middle_distance, min(total, middle_distance + 2.0))
|
||||
ground["focus_radius"] = 20.0
|
||||
middle_view["focus_radius"] = 20.0
|
||||
middle_camera, _ = interpolate_route(path, max(0.0, middle_distance - 6.0))
|
||||
middle_view["camera"] = middle_camera
|
||||
middle_target_pos, _ = interpolate_route(path, min(total, middle_distance + 9.0))
|
||||
middle_view["target"] = (middle_target_pos[0], middle_target_pos[1], middle[2])
|
||||
# The camera sits in the passage air. Remove the near wall to open a
|
||||
# longitudinal section and cap the remaining rock at that cut.
|
||||
middle_cut_point = (middle[0] - middle_side[0] * 2.5,
|
||||
middle[1] - middle_side[1] * 2.5, middle[2])
|
||||
middle_view["clip"] = (*middle_side,
|
||||
-sum(middle_side[axis] * middle_cut_point[axis] for axis in range(3)))
|
||||
middle_view["camera_location"] = "passage-air"
|
||||
middle_view["yaw"], middle_view["pitch"] = camera_angles(
|
||||
middle_view["camera"], middle_view["target"])
|
||||
return (ground, entry_view, middle_view, arrival), total, lower_index
|
||||
|
||||
|
||||
for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS:
|
||||
if OPTIONS.scene and scene_name != OPTIONS.scene:
|
||||
continue
|
||||
layout = load_layout(scene_name, source, recipe)
|
||||
passage, reverse = select_passage(layout, mouth_generator, preferred_side)
|
||||
views, route_length, lower_index = make_views(layout, passage, reverse)
|
||||
print(f"[L36StandardCaptureSet] scene={scene_name} recipe={recipe} passage={passage['stable_id']} "
|
||||
f"route_m={route_length:.3f} control_points={lower_index + 1} fov_deg=80 "
|
||||
"ground_camera_outside_m=5.500 entry_camera_outside_m=5.148 camera_eye_m=1.600", flush=True)
|
||||
for view in views:
|
||||
if OPTIONS.view and view["name"] != OPTIONS.view:
|
||||
continue
|
||||
stem = f"{scene_name}-{view['name']}"
|
||||
mesh = OUT / f"{stem}.vfmesh"
|
||||
png = PNG_DIR / f"{stem}.png"
|
||||
jpg = CAPTURES / f"{stem}.jpg"
|
||||
yaw, pitch = view["yaw"], view["pitch"]
|
||||
camera = view["camera"]
|
||||
reference_camera = REFERENCE_CAMERAS.get(stem)
|
||||
if reference_camera is not None:
|
||||
camera, target, reference_clip = reference_camera
|
||||
view["camera"], view["target"], view["clip"] = camera, target, reference_clip
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
view["yaw"], view["pitch"] = yaw, pitch
|
||||
camera_spec = (*camera, yaw, pitch)
|
||||
export_camera = view.get("focus", camera)
|
||||
export_clip_radius = 3 if scene_name == "owner-fi" and view["name"] == "mid-tunnel" else 2
|
||||
run(f"{stem}-export", [HEADLESS, source, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={export_clip_radius}",
|
||||
"--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={mesh}"])
|
||||
render_clip = view["clip"]
|
||||
if reference_camera is not None:
|
||||
surface = None
|
||||
ground_source = "L34-fixed-camera"
|
||||
elif view.get("outside_camera", False):
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
ground_source = "terrain-mesh"
|
||||
if surface is None:
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1],
|
||||
view["eye_height_z"])
|
||||
ground_source = "mouth-eye-height-fallback"
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
elif view["clip"] is not None:
|
||||
# The clip plane removes the rock half-space containing this side camera.
|
||||
# Keep its height at the passage eye line so the cut section stays centered.
|
||||
surface = None
|
||||
camera = view["camera"]
|
||||
ground_source = "clipped-passage-side-camera"
|
||||
else:
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
ground_source = "terrain-mesh"
|
||||
if surface is None:
|
||||
surface_z = view.get("eye_height_z", view["target"][2] + 0.4)
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1], surface_z)
|
||||
ground_source = "mouth-eye-height-fallback"
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
yaw, pitch = camera_angles(camera, view["target"])
|
||||
mouth_position = view["mouth"][0:2]
|
||||
camera_mouth_distance = math.dist(camera, view["mouth"])
|
||||
ground_height = surface[2] if surface is not None else float("nan")
|
||||
surface_offset = (surface[2] - view["target"][2]) if surface is not None else float("nan")
|
||||
print(f"[L36CaptureCamera] view={stem} ground_source={ground_source} "
|
||||
f"ground_z_m={ground_height:.3f} "
|
||||
f"surface_target_delta_m={surface_offset:.3f} "
|
||||
f"eye_z_m={camera[2]:.3f} "
|
||||
f"camera_location={view.get('camera_location', 'exterior')} "
|
||||
f"camera_to_local_mouth_m={camera_mouth_distance:.3f} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(view['target'])}) "
|
||||
f"clip={fmt(render_clip) if render_clip is not None else 'none'} "
|
||||
f"fov_deg=80", flush=True)
|
||||
render = [RENDERER, mesh, png, "--size=1600x900", "--fov-deg=80",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--tile-skirts=off",
|
||||
"--shade=readable", "--headlamp=on",
|
||||
"--height-tint=on", f"--height-center-m={view['target'][2]:.6f}",
|
||||
"--height-range-m=8", "--tile-edges=off"]
|
||||
if view.get("focus_radius", 24.0) is not None:
|
||||
render.append(f"--focus-region={fmt((*view['focus'], view.get('focus_radius', 24.0)))}")
|
||||
if render_clip is not None:
|
||||
render.append(f"--clip-plane={fmt(render_clip)}")
|
||||
run(f"{stem}-render", render)
|
||||
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||
size = jpg.stat().st_size if jpg.exists() else 0
|
||||
print(f"[L36CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||
if not 0 < size <= 300_000:
|
||||
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Inspect connected LOD0 surface components around the FloatingIslands mouth."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile", PROFILE_PATH)
|
||||
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(PROFILE)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("mesh")
|
||||
parser.add_argument("--center", nargs=2, type=float, default=(5.6, 0.445))
|
||||
parser.add_argument("--radius", type=float, default=14.0)
|
||||
parser.add_argument("--z-min", type=float, default=-66.0)
|
||||
parser.add_argument("--z-max", type=float, default=-50.0)
|
||||
parser.add_argument("--minimum-triangles", type=int, default=1)
|
||||
args = parser.parse_args()
|
||||
cx, cy = args.center
|
||||
bounds = (cx - args.radius, cx + args.radius, cy - args.radius, cy + args.radius)
|
||||
voxel_um, triangles, tiles = PROFILE.read_scene(
|
||||
args.mesh, 0, args.z_min, args.z_max, bounds)
|
||||
parent = list(range(len(triangles)))
|
||||
sizes = [1] * len(triangles)
|
||||
vertex_owner = {}
|
||||
|
||||
def find(index):
|
||||
while parent[index] != index:
|
||||
parent[index] = parent[parent[index]]
|
||||
index = parent[index]
|
||||
return index
|
||||
|
||||
def union(a, b):
|
||||
a, b = find(a), find(b)
|
||||
if a == b:
|
||||
return
|
||||
if sizes[a] < sizes[b]:
|
||||
a, b = b, a
|
||||
parent[b] = a
|
||||
sizes[a] += sizes[b]
|
||||
|
||||
for index, (_, _, verts) in enumerate(triangles):
|
||||
for point, _ in verts:
|
||||
key = tuple(round(value, 5) for value in point)
|
||||
other = vertex_owner.setdefault(key, index)
|
||||
union(index, other)
|
||||
|
||||
components = {}
|
||||
for index, (_, _, verts) in enumerate(triangles):
|
||||
root = find(index)
|
||||
item = components.setdefault(root, {"count": 0, "min": [math.inf] * 3,
|
||||
"max": [-math.inf] * 3, "area": 0.0,
|
||||
"sum": [0.0] * 3})
|
||||
item["count"] += 1
|
||||
points = [v[0] for v in verts]
|
||||
for point in points:
|
||||
for axis in range(3):
|
||||
item["min"][axis] = min(item["min"][axis], point[axis])
|
||||
item["max"][axis] = max(item["max"][axis], point[axis])
|
||||
item["sum"][axis] += point[axis]
|
||||
ab = tuple(points[1][axis] - points[0][axis] for axis in range(3))
|
||||
ac = tuple(points[2][axis] - points[0][axis] for axis in range(3))
|
||||
cross = (ab[1] * ac[2] - ab[2] * ac[1],
|
||||
ab[2] * ac[0] - ab[0] * ac[2],
|
||||
ab[0] * ac[1] - ab[1] * ac[0])
|
||||
item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross))
|
||||
|
||||
ordered = sorted(components.values(), key=lambda item: item["count"], reverse=True)
|
||||
print(f"[MouthComponentProbe] mesh={args.mesh} voxel_um={voxel_um} lod=0 "
|
||||
f"tiles={tiles} triangles={len(triangles)} components={len(ordered)} "
|
||||
f"bounds=({bounds[0]:.3f},{bounds[1]:.3f},{bounds[2]:.3f},{bounds[3]:.3f}) "
|
||||
f"z={args.z_min:.3f}..{args.z_max:.3f}")
|
||||
for item in ordered:
|
||||
if item["count"] < args.minimum_triangles:
|
||||
continue
|
||||
centroid = tuple(value / (3 * item["count"]) for value in item["sum"])
|
||||
print(f"[MouthComponent] triangles={item['count']} area_m2={item['area']:.4f} "
|
||||
f"bounds=({','.join(f'{v:.3f}' for v in item['min'])}).."
|
||||
f"({','.join(f'{v:.3f}' for v in item['max'])}) "
|
||||
f"centroid=({','.join(f'{v:.3f}' for v in centroid)})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure the exported walk surface along the passage under the tunnel camera."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
RING_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile", RING_PATH)
|
||||
RING = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(RING)
|
||||
CAMERA_RE = re.compile(r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||
r"target_m=\(([^)]*)\)", re.M)
|
||||
|
||||
|
||||
def length(a, b):
|
||||
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
|
||||
|
||||
|
||||
def fit(values, spacing):
|
||||
best = (0.0, 0.0)
|
||||
for step in range(8, 129):
|
||||
period = step * 0.0625
|
||||
omega = 2.0 * math.pi / period
|
||||
cc = ss = cs = yc = ys = 0.0
|
||||
for i, value in enumerate(values):
|
||||
phase = omega * i * spacing
|
||||
c, s = math.cos(phase), math.sin(phase)
|
||||
cc += c * c
|
||||
ss += s * s
|
||||
cs += c * s
|
||||
yc += value * c
|
||||
ys += value * s
|
||||
determinant = cc * ss - cs * cs
|
||||
if determinant <= 1.0e-9:
|
||||
continue
|
||||
cosine = (yc * ss - ys * cs) / determinant
|
||||
sine = (ys * cc - yc * cs) / determinant
|
||||
amplitude = math.hypot(cosine, sine)
|
||||
if amplitude > best[1]:
|
||||
best = period, amplitude
|
||||
return best
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("mesh")
|
||||
parser.add_argument("layout")
|
||||
parser.add_argument("--view", default="owner-sw-mid-tunnel")
|
||||
parser.add_argument("--passage", default="passage/0-1/0")
|
||||
parser.add_argument("--window", type=float, default=32.0)
|
||||
parser.add_argument("--spacing", type=float, default=0.125)
|
||||
parser.add_argument("--amplitude-limit-cm", type=float, default=1.5)
|
||||
args = parser.parse_args()
|
||||
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||
cameras = {name: (tuple(map(float, camera.split(","))),
|
||||
tuple(map(float, target.split(","))))
|
||||
for name, camera, target in CAMERA_RE.findall(
|
||||
log.read_text(encoding="utf-8", errors="replace"))}
|
||||
if args.view not in cameras:
|
||||
raise SystemExit(f"missing camera target for {args.view}")
|
||||
camera, target = cameras[args.view]
|
||||
layout = json.loads(Path(args.layout).read_text(encoding="utf-8"))
|
||||
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||
passage = next(p for p in layout["passages"] if p["stable_id"] == args.passage)
|
||||
controls = [(tuple(v * scale for v in point["position_q"]),
|
||||
point["radius_q"] * scale) for point in passage["control_points"]]
|
||||
lower_mouth = tuple(v * scale for v in passage["lower_mouth_q"])
|
||||
end = min(range(len(controls)), key=lambda i: length(controls[i][0], lower_mouth))
|
||||
controls = controls[:end + 1]
|
||||
path = []
|
||||
distance = 0.0
|
||||
for point in controls:
|
||||
if path:
|
||||
distance += length(path[-1][0], point[0])
|
||||
path.append((point[0], point[1], distance))
|
||||
closest = None
|
||||
for index, (a, b) in enumerate(zip(path, path[1:])):
|
||||
delta = tuple(b[0][axis] - a[0][axis] for axis in range(3))
|
||||
sq = sum(v * v for v in delta)
|
||||
t = max(0.0, min(1.0, sum((target[k] - a[0][k]) * delta[k]
|
||||
for k in range(3)) / sq)) if sq else 0.0
|
||||
point = tuple(a[0][k] + t * delta[k] for k in range(3))
|
||||
d2 = sum((point[k] - target[k]) ** 2 for k in range(3))
|
||||
if closest is None or d2 < closest[0]:
|
||||
closest = (d2, a[2] + t * (b[2] - a[2]), index, t)
|
||||
_, center_distance, segment, segment_t = closest
|
||||
|
||||
def interpolate(distance_value):
|
||||
distance_value = max(0.0, min(path[-1][2], distance_value))
|
||||
for a, b in zip(path, path[1:]):
|
||||
if distance_value <= b[2]:
|
||||
t = (distance_value - a[2]) / max(1.0e-9, b[2] - a[2])
|
||||
point = tuple(a[0][k] + t * (b[0][k] - a[0][k]) for k in range(3))
|
||||
radius = a[1] + t * (b[1] - a[1])
|
||||
return point, radius
|
||||
return path[-1][0], path[-1][1]
|
||||
|
||||
half = args.window * 0.5
|
||||
start = max(0.0, center_distance - half)
|
||||
finish = min(path[-1][2], center_distance + half)
|
||||
count = int((finish - start) / args.spacing) + 1
|
||||
positions = []
|
||||
expected = []
|
||||
for i in range(count):
|
||||
route_distance = start + i * args.spacing
|
||||
point, radius = interpolate(route_distance)
|
||||
positions.append((route_distance - center_distance, point))
|
||||
expected.append(point[2] - radius + 0.20)
|
||||
bounds = (min(p[0] for _, p in positions) - 8.0,
|
||||
max(p[0] for _, p in positions) + 8.0,
|
||||
min(p[1] for _, p in positions) - 8.0,
|
||||
max(p[1] for _, p in positions) + 8.0)
|
||||
zmin, zmax = min(expected) - 1.5, max(expected) + 1.5
|
||||
voxel_um, triangles, tiles = RING.read_scene(args.mesh, 0, zmin, zmax, bounds)
|
||||
bucket_size = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = RING.build_buckets(triangles, True, bounds, bucket_size)
|
||||
values = []
|
||||
for (_, point), preferred in zip(positions, expected):
|
||||
hit = RING.sample_surface(buckets, point[0], point[1], True,
|
||||
bucket_size, preferred)
|
||||
values.append(hit[0] if hit else None)
|
||||
valid = [(index, z) for index, z in enumerate(values) if z is not None]
|
||||
if len(valid) < 0.8 * len(values):
|
||||
raise SystemExit(f"insufficient mesh floor coverage {len(valid)}/{len(values)}")
|
||||
residual = [values[i] - expected[i] for i in range(len(values))
|
||||
if values[i] is not None]
|
||||
mean = sum(residual) / len(residual)
|
||||
period, amplitude = fit([v - mean for v in residual], args.spacing)
|
||||
adjacent = [abs(values[i] - values[i - 1]) for i in range(1, len(values))
|
||||
if values[i] is not None and values[i - 1] is not None]
|
||||
route_segment_length = path[segment + 1][2] - path[segment][2]
|
||||
print(f"[WalkFloorProfileProbe] mesh={args.mesh} view={args.view} voxel_um={voxel_um} "
|
||||
f"tiles={tiles} samples={len(valid)}/{len(values)} window_m={finish-start:.3f} "
|
||||
f"camera_target_m=({target[0]:.3f},{target[1]:.3f},{target[2]:.3f}) "
|
||||
f"route_distance_m={center_distance:.3f} nearest_segment={segment} "
|
||||
f"segment_t={segment_t:.3f} segment_length_m={route_segment_length:.3f} "
|
||||
f"floor_expected_range_m={min(expected):.3f}..{max(expected):.3f} "
|
||||
f"residual_range_m={min(residual):.4f}..{max(residual):.4f} "
|
||||
f"period_m={period:.3f} amplitude_m={amplitude:.5f} "
|
||||
f"amplitude_cm={amplitude*100:.3f} adjacent_step_max_m="
|
||||
f"{max(adjacent, default=0.0):.4f} limit_cm={args.amplitude_limit_cm:.3f} "
|
||||
f"status={'PASS' if amplitude*100 <= args.amplitude_limit_cm else 'FAIL'}")
|
||||
out = Path(args.mesh).with_suffix(".walk-floor-profile.csv")
|
||||
with out.open("w", encoding="utf-8") as stream:
|
||||
stream.write("distance_m,route_x_m,route_y_m,expected_floor_m,mesh_floor_m,residual_m\n")
|
||||
for i, (distance_m, point) in enumerate(positions):
|
||||
z = values[i]
|
||||
stream.write(f"{distance_m:.3f},{point[0]:.6f},{point[1]:.6f},"
|
||||
f"{expected[i]:.6f},{'' if z is None else f'{z:.6f}'},"
|
||||
f"{'' if z is None else f'{z-expected[i]:.6f}'}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,54 @@
|
||||
# VoxelForge L36 report
|
||||
|
||||
## Findings and fixes
|
||||
|
||||
The exported L35 walk meshes show real floor washboard: owner-SW repeats at 7.250 m with 2.252 cm amplitude; the upper-SW arrival repeats at 1.438 m with 2.482 cm amplitude. The old tread top was built per segment as a one-sided density clamp, so the tread/native-field crossing moved at the segment joins. The wall-only ripple scan could not see it. Removing shoulder grain alone did not remove the beat.
|
||||
|
||||
Sparse-route treads now overlap by up to 2 m and use bounded signed-distance slabs, including SurfaceWorld-to-TunnelNetwork. Tread seating and capsule/open-air gates remain. The exported v23 meshes measure 7.188 m / 0.067 cm on owner-SW and 6.500 m / 0.057 cm on the upper fixture. The expanded five-window density audit's maximum is 1.08 cm, below its 1.5 cm limit.
|
||||
|
||||
L35's FI mouth mesh had a 42.265° fade-ring normal jump and a detached 8-triangle component of 0.0035394 m². The defect was the short crater-root detail and tread support meeting the native crater field without a smooth signed-distance join; Marching Cubes exposed thin pieces at the seam. V23 filters the local signed field over 2 m while protecting the route, removes thin isolated solids, and smooth-unions the mouth tread with native terrain over 1.5 m. The same mesh check now measures 27.133° against a 35° limit and finds no component at or above 0.0001 m². One 8-triangle speck remains below that floor (0.0000266 m², 9.377 m from the mouth); it is recorded rather than counted as zero.
|
||||
|
||||
## Audits and captures
|
||||
|
||||
- Passage matrix: 24/24 recipe-stamped passes across both owner recipes, four styles, the upper-SW fixture, Clang and GCC. This includes support, walkability, capsule clearance, open air, route bindings, strict validation, mouth profiles, wall ripple and floor ripple. The 2 m / 1.5 m beaded control is rejected; both L35 floor windows fail the 1.5 cm floor limit.
|
||||
- FI-floor and SW-ceiling branch-locked terrain ring audits pass at LOD0–8. Coverage is 1.000; relief is at least 3.162 m and no flat run or following step is found. L30/L31 ring controls remain negative controls.
|
||||
- Interface/hole and transition suite: 16/16; zero hole columns and air samples. Strict-layout controls reject 10/10 malformed layouts. L13/L9: 4/4.
|
||||
- FI mouth fragment/crease control: L35 fails at 42.265° with one qualifying fragment; v23 passes at 27.133° with no qualifying fragments. The L35 mesh is the negative control.
|
||||
- The standard 12 JPEGs use the L34 fixed cameras and are in [`captures/`](captures/). Sizes are 21,976–43,435 bytes (all below 300 KB).
|
||||
|
||||
## Version, hashes, determinism
|
||||
|
||||
World evaluator version is 23.
|
||||
|
||||
| Recipe | Stable layout | L25 whole-view density / mesh / payload / topology |
|
||||
|---|---|---|
|
||||
| SurfaceWorld | `resolved-layout-v3-fnv64:f4c09b442fa40cd7` | `90ca5c612c0723bb / cac3b5f0f68c75f9 / 0a143e28341ed132 / 89f2018345744589` |
|
||||
| FloatingIslands | `resolved-layout-v3-fnv64:d2922ac7e8a2b491` | `da5eba38f4b14f79 / 8c403d9749b15abb / 8a917fa5b71ab179 / 3ed411c118d1f43f` |
|
||||
|
||||
Owner plan/manifest hashes: `a5e29887e839c5d6 / 57ebed8456c20f06`. Upper-fixture layout/plan/manifest: `resolved-layout-v3-fnv64:fc4e73619037f88c / 463a0f3b72909656 / f7bc6980e67b61c0`.
|
||||
|
||||
Determinism is 30/30: L25 and the 24 m crossing at W1/W8/W12 with both compilers and recipes, plus the upper fixture at W1/W8/W12 with both compilers. Core binary SHA-256:
|
||||
|
||||
- Clang: `f8342a7bb48bf460550bd2d4707a67156ebf02a5e5a247edd07e8de0028f6be2`
|
||||
- GCC: `37d28f3c8a816f485d2bb9e40c1cc37850e6f2d3888772573d11d7b101f6b7d5`
|
||||
|
||||
Ten interleaved L35/L36 pairs per recipe passed. Median whole-view time: SurfaceWorld 1.728 s → 1.752 s (+1.42%; paired median +0.66%, MAD 1.16%); FloatingIslands 16.645 s → 16.630 s (-0.09%; paired median +0.35%, MAD 0.29%). Both changes are within measured run noise.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- Three compile fixes: `build-l36-clang-retry2.log` had a wall-relief call mismatch (retry3 compiled); `build-l36-floor-coordinates.log` had an invalid assignment (retry1 compiled); `build-l36-sweep-closest.log` assigned to a const value (retry1 compiled).
|
||||
- Passage audit revisions: `run-passage-audits.log` ended after four cases; `run-passage-audits-final.log` was 14/24, `-r2` 18/24, `-r3` 22/24, and `-r4` stopped after eight cases on a failing broad tread candidate. `-r5`, `-r6`, and the final v23 run were 24/24. Earlier support candidates failed capsule, open-air, or floor-ripple checks; their `preaudit-*`, `preflight-*`, and `audit-*` logs remain under `Saved/Linux/l36/`.
|
||||
- Mouth trials `mouth-smin1m`, `mouth-route-smoothunion`, `mouth-airward-filter`, and the 1.5 m filter variants left 36–44° jumps or a residual speck. The final 2 m field filter plus 1.5 m SDF union is the passing candidate. The older supplemental 20° fade probe remains stricter than this round's 35° gate; the final 27.133° jump would still exceed 20°. The L35 42.265° control is detected by the final check.
|
||||
- Initial floor-window placement at fraction .49 missed the upper-fixture route target; .457 covered it. Initial ring exports (`ring-export-*-v23.log`) used a render-distance override; early `ring-*-v23-camera*.log`, `ring-*-v23-branch32.log` and `ring-*-v23-current32.log` scans used the wrong center/radius and had missing coverage or only 1.857 m relief. Re-exporting without that override and using the recorded branch-locked windows produced the LOD0–8 passes. `ring-sw-lod0-v23-branchlocked-full.log` had a path typo and was immediately rerun as `ring-sw-lod0-v23-branchlocked-final.log`.
|
||||
- The first five cost pairs were repeated because the SurfaceWorld median was close to noise; all ten pairs per recipe passed. No run exceeded 30 minutes and no step was stopped for a 15-minute stall. All runs used CPUs 0–11, a 6 GiB address-space cap and at most 12 workers; governor unchanged.
|
||||
|
||||
No Unreal files changed. No commit, push or stash was made.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Accepted: the floor washboard is gone in owner-SW mid-tunnel (floor now reads as irregular natural ground). Cause
|
||||
measured (per-segment one-sided tread clamps moving the crossing at joins), floor ripple audit added with L35 as a
|
||||
failing control. The FI mouth shards and detached fragment are gone. Evaluator v23.
|
||||
- Not accepted as done: **the FI mouth tread now reads as a trapezoid platform**: straight edges, planar faces, a
|
||||
clear man-made shape inside the crater. Also, the 35° crease gate was chosen during the round, while the earlier
|
||||
probe used 20°; the final 27° would fail that. L37 targets the shape itself.
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run five interleaved whole-view cost pairs against the L35 Clang binary."""
|
||||
import csv
|
||||
import math
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l36"
|
||||
BINARIES = {
|
||||
"l35": ROOT / "Saved/Linux/l35/bin/VoxelForgeCoreHeadless-l35-final-clang",
|
||||
"l36": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
}
|
||||
ORDERS = (("l35", "l36"), ("l36", "l35"), ("l35", "l36"),
|
||||
("l36", "l35"), ("l35", "l36"))
|
||||
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for pair, order in enumerate(ORDERS, 1):
|
||||
for build in order:
|
||||
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s", str(BINARIES[build]),
|
||||
str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
matches = READY.findall(output)
|
||||
stamps = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
output, re.M)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||
and "hash=off" in output and "cache_hits=0" in output and stamp_ok)
|
||||
ready = float(matches[0]) if len(matches) == 1 else math.nan
|
||||
rows.append((recipe, pair, build, ready, elapsed,
|
||||
result.returncode if result else "TIMEOUT", valid))
|
||||
failed |= not valid
|
||||
print(f"[L36CostRun] recipe={recipe} pair={pair} build={build} "
|
||||
f"whole_view_ready={ready:.3f}s process_elapsed={elapsed:.1f}s "
|
||||
f"{'PASS' if valid else 'FAIL'} log={logfile}", flush=True)
|
||||
|
||||
summary = OUT / "cost-runs.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("recipe", "pair", "build", "whole_view_ready_seconds",
|
||||
"process_elapsed_seconds", "exit_code", "status"))
|
||||
writer.writerows((recipe, pair, build, f"{ready:.6f}", f"{elapsed:.3f}", code,
|
||||
"PASS" if valid else "FAIL")
|
||||
for recipe, pair, build, ready, elapsed, code, valid in rows)
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||
for build in BINARIES}
|
||||
baseline = statistics.median(by_build["l35"])
|
||||
current = statistics.median(by_build["l36"])
|
||||
paired = []
|
||||
for pair in range(1, 6):
|
||||
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||
paired.append((values["l36"] - values["l35"]) / values["l35"] * 100.0)
|
||||
mad = statistics.median(abs(value - statistics.median(paired)) for value in paired)
|
||||
print(f"[L36CostSummary] recipe={recipe} pairs=5 median_l35_s={baseline:.3f} "
|
||||
f"median_l36_s={current:.3f} median_change_pct={(current/baseline-1)*100.0:+.2f} "
|
||||
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)} "
|
||||
f"paired_mad_pct={mad:.2f}", flush=True)
|
||||
|
||||
print(f"[L36Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}",
|
||||
flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check L25, 24 m crossing and upper-SW fixture determinism for L36."""
|
||||
import csv
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l36"
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-final-gcc",
|
||||
}
|
||||
HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
STAMP = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M)
|
||||
LAYOUT = re.compile(
|
||||
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||
re.M)
|
||||
PLAN = re.compile(
|
||||
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||
ROUTE = re.compile(
|
||||
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
references = {}
|
||||
|
||||
|
||||
def run(kind, recipe, compiler, workers, data, passage_audit=False, crossing=False):
|
||||
global failed
|
||||
binary = BINARIES[compiler]
|
||||
name = f"determinism-{kind}-{compiler}-{recipe}-w{workers}"
|
||||
if kind == "upper-fixture":
|
||||
name = f"determinism-upper-fixture-{compiler}-w{workers}"
|
||||
log = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", str(binary), str(data), f"--workers={workers}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}",
|
||||
]
|
||||
if passage_audit:
|
||||
command.append("--audit-resolved-layout-passages")
|
||||
else:
|
||||
command.append("--stream-hash")
|
||||
if crossing:
|
||||
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
stamps = STAMP.findall(output)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
if passage_audit:
|
||||
signature = (tuple(LAYOUT.findall(output)), tuple(PLAN.findall(output)),
|
||||
tuple(ROUTE.findall(output)))
|
||||
expected = references.get((kind, recipe))
|
||||
stable = expected is None or signature == expected
|
||||
if expected is None and all(signature):
|
||||
references[(kind, recipe)] = signature
|
||||
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and len(signature[0]) == len(signature[1]) == len(signature[2]) == 1
|
||||
and re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)
|
||||
and re.search(r"^\[PassageRippleAuditSummary\].*status=PASS$", output, re.M)
|
||||
and stable)
|
||||
hashes = ()
|
||||
detail = f"layout={signature[0][0] if signature[0] else 'MISSING'} stable={stable}"
|
||||
else:
|
||||
hashes = tuple(HASH.findall(output))
|
||||
key = (kind, recipe)
|
||||
expected = references.get(key)
|
||||
stable = expected is None or hashes == expected
|
||||
if expected is None and hashes:
|
||||
references[key] = hashes
|
||||
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and "hash=on" in output and len(hashes) == 12 and stable)
|
||||
detail = f"hashes={len(hashes)}/12 stable={stable}"
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((kind, recipe, compiler, workers, elapsed, code, valid))
|
||||
failed |= not valid
|
||||
print(f"[L36DeterminismRun] kind={kind} recipe={recipe} compiler={compiler} "
|
||||
f"workers={workers} elapsed={elapsed:.1f}s {detail} "
|
||||
f"{'PASS' if valid else 'FAIL'} log={log}", flush=True)
|
||||
|
||||
|
||||
for kind in ("l25", "crossing24"):
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler in BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
run(kind, recipe, compiler, workers, OWNER,
|
||||
crossing=(kind == "crossing24"))
|
||||
|
||||
for compiler in BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
run("upper-fixture", "floating-islands", compiler, workers, FIXTURE,
|
||||
passage_audit=True)
|
||||
|
||||
summary = OUT / "determinism-l36.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("kind", "recipe", "compiler", "workers", "elapsed_seconds",
|
||||
"exit_code", "status"))
|
||||
for row in rows:
|
||||
writer.writerow((*row[:-1], "PASS" if row[-1] else "FAIL"))
|
||||
print(f"[L36DeterminismSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 30 else 0)
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l36"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-final-gcc",
|
||||
}
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
|
||||
def run(name, binary, data, recipe, extra, checks):
|
||||
global failed
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s",
|
||||
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", *extra,
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamps = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
text, re.M)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
checks_ok = all(check(text) for check in checks)
|
||||
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||
failed |= not ok
|
||||
|
||||
|
||||
owner = ROOT / "Standalone/data/owner_world.json"
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler, binary in BINARIES.items():
|
||||
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||
run(name, binary, owner, recipe,
|
||||
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||
[lambda text: bool(re.search(
|
||||
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||
lambda text: bool(re.search(
|
||||
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler, binary in BINARIES.items():
|
||||
for mode in ("hard", "gradient", "interleaved"):
|
||||
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||
run(name, binary, owner, recipe,
|
||||
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||
[lambda text: bool(re.search(
|
||||
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||
text, re.M)),
|
||||
lambda text: bool(re.search(
|
||||
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||
|
||||
summary = OUT / "invariant-audits.csv"
|
||||
summary.write_text(
|
||||
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, elapsed, code, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L33InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the L13 route parity and L9 uniform-block proof audits on L36."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l36"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-final-gcc",
|
||||
}
|
||||
RECIPES = ("floating-islands", "surface-world")
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
for recipe in RECIPES:
|
||||
for compiler, binary in BINARIES.items():
|
||||
name = f"legacy-route-proof-{recipe}-{compiler}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s", str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--stream-hash", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}", "--audit-uniform-block-proofs", "--audit-route-parity",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamp = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
output, re.M)
|
||||
route = re.findall(r"^\[RouteParityAudit\].*$", output, re.M)
|
||||
proofs = re.findall(r"^\[UniformBlockProofAudit\].*$", output, re.M)
|
||||
stamp_ok = stamp == [(recipe, recipe, "PASS")]
|
||||
route_ok = len(route) == 1 and route[0].endswith("status=PASS")
|
||||
proof_ok = len(proofs) == 1 and bool(
|
||||
re.search(r"violations=0(?:\s|$)", proofs[0]))
|
||||
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and route_ok and proof_ok)
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((name, recipe, compiler, elapsed, code, route_ok, bool(proof_ok), ok))
|
||||
failed |= not ok
|
||||
print(f"[L36LegacyAuditRun] case={name} elapsed={elapsed:.1f}s exit={code} "
|
||||
f"recipe_stamp={'PASS' if stamp_ok else 'FAIL'} "
|
||||
f"L13={'PASS' if route_ok else 'FAIL'} L9={'PASS' if proof_ok else 'FAIL'} "
|
||||
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||
|
||||
summary = OUT / "legacy-audits.csv"
|
||||
summary.write_text(
|
||||
"case,recipe,compiler,elapsed_seconds,exit_code,l13,l9,status\n" +
|
||||
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},"
|
||||
f"{'PASS' if l13 else 'FAIL'},{'PASS' if l9 else 'FAIL'},"
|
||||
f"{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, compiler, elapsed, code, l13, l9, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L36LegacyAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 4 else 0)
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the FI fade-ring crease against L35 using the same surface samples."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
RING_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_PATH)
|
||||
RING = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(RING)
|
||||
|
||||
CENTER = (6.298233, 1.137233)
|
||||
INNER, OUTER, STEP = 5.5, 9.5, 0.125
|
||||
HEIGHT_MIN, HEIGHT_MAX, PREFERRED = -80.0, -40.0, -56.0
|
||||
NORMAL_LIMIT_DEG = 35.0
|
||||
COVERAGE_MIN = 0.90
|
||||
FRAGMENT_MAX_TRIANGLES = 64
|
||||
FRAGMENT_MIN_AREA_M2 = 0.0001
|
||||
FRAGMENT_MAX_AREA_M2 = 0.05
|
||||
FRAGMENT_MAX_DISTANCE_M = 12.0
|
||||
|
||||
|
||||
def small_components(triangles):
|
||||
parent = list(range(len(triangles)))
|
||||
sizes = [1] * len(triangles)
|
||||
owners = {}
|
||||
|
||||
def find(index):
|
||||
while parent[index] != index:
|
||||
parent[index] = parent[parent[index]]
|
||||
index = parent[index]
|
||||
return index
|
||||
|
||||
def union(a, b):
|
||||
a, b = find(a), find(b)
|
||||
if a == b:
|
||||
return
|
||||
if sizes[a] < sizes[b]:
|
||||
a, b = b, a
|
||||
parent[b] = a
|
||||
sizes[a] += sizes[b]
|
||||
|
||||
for index, (_, _, vertices) in enumerate(triangles):
|
||||
for point, _ in vertices:
|
||||
key = tuple(round(value, 5) for value in point)
|
||||
other = owners.setdefault(key, index)
|
||||
union(index, other)
|
||||
|
||||
components = {}
|
||||
for index, (_, _, vertices) in enumerate(triangles):
|
||||
root = find(index)
|
||||
item = components.setdefault(root, {"count": 0, "area": 0.0, "sum": [0.0] * 3})
|
||||
item["count"] += 1
|
||||
points = [vertex[0] for vertex in vertices]
|
||||
for point in points:
|
||||
for axis in range(3):
|
||||
item["sum"][axis] += point[axis]
|
||||
ab = tuple(points[1][axis] - points[0][axis] for axis in range(3))
|
||||
ac = tuple(points[2][axis] - points[0][axis] for axis in range(3))
|
||||
cross = (ab[1] * ac[2] - ab[2] * ac[1],
|
||||
ab[2] * ac[0] - ab[0] * ac[2],
|
||||
ab[0] * ac[1] - ab[1] * ac[0])
|
||||
item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross))
|
||||
|
||||
cx, cy = CENTER
|
||||
small = []
|
||||
below_floor = []
|
||||
for item in components.values():
|
||||
centroid = tuple(value / (3 * item["count"]) for value in item["sum"])
|
||||
distance = math.hypot(centroid[0] - cx, centroid[1] - cy)
|
||||
if (item["count"] <= FRAGMENT_MAX_TRIANGLES
|
||||
and item["area"] <= FRAGMENT_MAX_AREA_M2
|
||||
and distance <= FRAGMENT_MAX_DISTANCE_M):
|
||||
record = (item["count"], item["area"], distance, centroid)
|
||||
(below_floor if item["area"] < FRAGMENT_MIN_AREA_M2 else small).append(record)
|
||||
return len(components), small, below_floor
|
||||
|
||||
|
||||
def audit(path):
|
||||
cx, cy = CENTER
|
||||
bounds = (cx - OUTER, cx + OUTER, cy - OUTER, cy + OUTER)
|
||||
voxel_um, triangles, tiles = RING.read_scene(
|
||||
str(path), 0, HEIGHT_MIN, HEIGHT_MAX, bounds)
|
||||
component_count, fragments, below_floor = small_components(triangles)
|
||||
print(f"[MouthFragmentComponents] mesh={path.name} components={component_count} "
|
||||
f"small_near_mouth={len(fragments)} subthreshold_near_mouth={len(below_floor)} "
|
||||
f"limits=(triangles<={FRAGMENT_MAX_TRIANGLES},"
|
||||
f"area={FRAGMENT_MIN_AREA_M2:.4f}..{FRAGMENT_MAX_AREA_M2:.3f}m2,"
|
||||
f"distance<={FRAGMENT_MAX_DISTANCE_M:.1f}m) "
|
||||
f"first={fragments[0] if fragments else None} "
|
||||
f"subthreshold={below_floor[0] if below_floor else None} "
|
||||
f"status={'PASS' if not fragments else 'FAIL'}")
|
||||
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = RING.build_buckets(triangles, True, bounds, cell)
|
||||
lines = []
|
||||
count = round((OUTER - INNER) / STEP)
|
||||
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
lines.append((f"radial-{degrees:.0f}", [
|
||||
(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||
for radius in (INNER + i * STEP for i in range(count + 1))]))
|
||||
tangent_count = round(8.0 / STEP)
|
||||
for degrees in (225.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
normal = (math.cos(angle), math.sin(angle))
|
||||
tangent = (-normal[1], normal[0])
|
||||
lines.append((f"tangent-{degrees:.0f}", [
|
||||
(cx + 6.5 * normal[0] + (-4.0 + i * STEP) * tangent[0],
|
||||
cy + 6.5 * normal[1] + (-4.0 + i * STEP) * tangent[1])
|
||||
for i in range(tangent_count + 1)]))
|
||||
|
||||
max_angle = 0.0
|
||||
max_pair = None
|
||||
min_coverage = 1.0
|
||||
for name, points in lines:
|
||||
samples = [RING.sample_surface(buckets, x, y, True, cell, PREFERRED)
|
||||
for x, y in points]
|
||||
coverage = sum(value is not None for value in samples) / len(samples)
|
||||
min_coverage = min(min_coverage, coverage)
|
||||
line_max = 0.0
|
||||
for index in range(1, len(samples)):
|
||||
prior, current = samples[index - 1], samples[index]
|
||||
if prior is None or current is None:
|
||||
continue
|
||||
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1]))))
|
||||
delta = math.degrees(math.acos(dot))
|
||||
if delta > line_max:
|
||||
line_max = delta
|
||||
if delta > max_angle:
|
||||
max_angle = delta
|
||||
max_pair = (name, points[index - 1], prior[0], points[index], current[0])
|
||||
print(f"[MouthFadeLine] mesh={path.name} line={name} coverage={coverage:.3f} "
|
||||
f"max_normal_jump_deg={line_max:.6f}")
|
||||
normal_pass = max_angle <= NORMAL_LIMIT_DEG and min_coverage >= COVERAGE_MIN
|
||||
passed = normal_pass and not fragments
|
||||
print(f"[MouthFadeAudit] mesh={path.name} tiles={tiles} lines={len(lines)} "
|
||||
f"coverage_min={min_coverage:.3f} coverage_limit={COVERAGE_MIN:.2f} "
|
||||
f"max_normal_jump_deg={max_angle:.6f} normal_limit_deg={NORMAL_LIMIT_DEG:.1f} "
|
||||
f"fragments={len(fragments)} "
|
||||
f"worst_pair={max_pair} status={'PASS' if passed else 'FAIL'}")
|
||||
return passed, max_angle
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--control", type=Path,
|
||||
default=ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh")
|
||||
parser.add_argument("--current", type=Path,
|
||||
default=ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh")
|
||||
args = parser.parse_args()
|
||||
control = args.control if args.control.is_absolute() else ROOT / args.control
|
||||
current = args.current if args.current.is_absolute() else ROOT / args.current
|
||||
control_ok, control_max = audit(control)
|
||||
current_ok, current_max = audit(current)
|
||||
expected = not control_ok and current_ok and control_max > NORMAL_LIMIT_DEG
|
||||
print(f"[MouthFadeNegativeControl] control_max_deg={control_max:.6f} "
|
||||
f"control_detected={not control_ok} current_max_deg={current_max:.6f} "
|
||||
f"status={'PASS' if expected else 'FAIL'}")
|
||||
raise SystemExit(0 if expected else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run recipe-stamped passage/profile audits for both compilers and both owner recipes."""
|
||||
import re
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l36"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-final-gcc",
|
||||
}
|
||||
RUNS = []
|
||||
for recipe, filename in (
|
||||
("floating-islands", "floating-islands"),
|
||||
("surface-world", "surface-world"),
|
||||
):
|
||||
for style in range(4):
|
||||
RUNS.append((f"owner-{filename}-style{style}",
|
||||
ROOT / f"Saved/Linux/linux25-owner-{filename}-style{style}.json", recipe))
|
||||
for style in range(4):
|
||||
RUNS.append((f"upper-sw-fixture-style{style}",
|
||||
OUT.parent / "l33/fixtures" / f"sw-upper-tunnel-network-style{style}.json",
|
||||
"floating-islands"))
|
||||
|
||||
RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
PASSAGE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||
MOUTH = re.compile(r"^\[PassageMouthProfileAudit\].*status=PASS$", re.M)
|
||||
ROUTE = re.compile(r"^\[ResolvedStrateRouteAudit\].*mouth_binding=PASS status=PASS$", re.M)
|
||||
CHECKS = (
|
||||
r"^\[ResolvedLayoutStrict\] accepted=true issues=0 error=$",
|
||||
r"^\[ResolvedLayoutValidatorControls\].*status=PASS$",
|
||||
r"^\[PassageMouthProfileNegativeControl\].*source=L23_terraced_geometry.*detected=true$",
|
||||
r"^\[PassageMouthProfileSummary\].*l23_terrace_negative_control=DETECTED status=PASS$",
|
||||
r"^\[PassageNaturalnessSummary\].*l23_negative_control=DETECTED.*status=PASS$",
|
||||
r"^\[PassageRippleAuditSummary\].*synthetic_spheres_negative_control=DETECTED max_amplitude_m=0\.035 floor_beaded_negative_control=DETECTED max_floor_amplitude_m=0\.015 status=PASS$",
|
||||
r"^\[PassageOpenAirAuditSummary\].*violations=0.*l24_negative_control=DETECTED status=PASS$",
|
||||
r"^\[PassageNoLongBridgeAudit\].*status=PASS$",
|
||||
r"^\[PassageRockColumnAuditSummary\].*status=PASS$",
|
||||
r"^\[ResolvedPassageAuditSummary\].*route_binding_failures=0 .*status=PASS$",
|
||||
r"^\[Walkability\] PASS",
|
||||
r"^\[FloorProfileBlend\].*negative_controls=step_detected/slope_detected status=PASS$",
|
||||
)
|
||||
|
||||
rows = []
|
||||
failed = False
|
||||
for compiler, binary in BINARIES.items():
|
||||
for name, data, recipe in RUNS:
|
||||
logfile = OUT / f"passage-audit-{name}-{compiler}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", str(binary), str(data), "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamp_ok = RECIPE.findall(output) == [(recipe, recipe)]
|
||||
passages = len(PASSAGE.findall(output))
|
||||
mouths = len(MOUTH.findall(output))
|
||||
routes = len(ROUTE.findall(output))
|
||||
checks_ok = all(re.search(pattern, output, re.M) for pattern in CHECKS)
|
||||
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and passages >= 1 and mouths == passages and routes >= 1 and checks_ok)
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((name, recipe, compiler, elapsed, code, passages, mouths, routes, ok))
|
||||
failed |= not ok
|
||||
print(f"[L36PassageAuditRun] case={name} compiler={compiler} recipe={recipe} "
|
||||
f"elapsed={elapsed:.1f}s exit={code} passages={passages} "
|
||||
f"profiles={mouths} routes={routes} {'PASS' if ok else 'FAIL'} "
|
||||
f"log={logfile}", flush=True)
|
||||
|
||||
summary = OUT / "passage-audits.csv"
|
||||
summary.write_text(
|
||||
"case,recipe,compiler,elapsed_seconds,exit_code,passages,mouth_profiles,routes,status\n" +
|
||||
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},{passages},{mouths},{routes},"
|
||||
f"{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, compiler, elapsed, code, passages, mouths, routes, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L36PassageAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 24 else 0)
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,543 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure player-scale planar aprons, straight boundaries and corners at passage mouths."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import math
|
||||
from collections import defaultdict, deque
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile_l37", PROFILE_PATH)
|
||||
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(PROFILE)
|
||||
|
||||
# Frozen from player scale before measuring the L37 candidate. The 0.75 m line limit
|
||||
# is just over the 0.68 m capsule width; 0.50 m2 is one capsule-width footprint.
|
||||
STRAIGHT_LIMIT_M = 0.75
|
||||
PLANE_LIMIT_M2 = 0.50
|
||||
CORNER_TURN_LIMIT_DEG = 30.0
|
||||
PLANE_TOLERANCE_M = 0.02
|
||||
NORMAL_BUCKET = 0.07
|
||||
PLANE_BUCKET_M = 0.03
|
||||
MOUTH_CORRIDOR_HALF_WIDTH_M = 1.50
|
||||
MOUTH_CORRIDOR_LENGTH_M = 12.0
|
||||
MOUTH_Z_BELOW_M = 4.0
|
||||
MOUTH_Z_ABOVE_M = 3.0
|
||||
PLAYER_FLOOR_OFFSET_M = 1.13
|
||||
DELTA_THRESHOLD_M = 0.05
|
||||
DELTA_GRID_M = 0.125
|
||||
SCENES = {
|
||||
"owner-fi": ("owner-fi-ground-mouth.vfmesh", "layout-owner-fi.json", "FloatingIslands", "upper"),
|
||||
"owner-sw": ("owner-sw-ground-mouth.vfmesh", "layout-owner-sw.json", "SurfaceWorld", "lower"),
|
||||
"style-override-upper-sw-fixture-style0": (
|
||||
"style-override-upper-sw-fixture-style0-ground-mouth.vfmesh",
|
||||
"layout-style-override-upper-sw-fixture-style0.json", "SurfaceWorld", "upper"),
|
||||
}
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(a[i] - b[i] for i in range(3))
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
return sum(a[i] * b[i] for i in range(3))
|
||||
|
||||
|
||||
def cross(a, b):
|
||||
return (a[1] * b[2] - a[2] * b[1],
|
||||
a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0])
|
||||
|
||||
|
||||
def length(a):
|
||||
return math.sqrt(dot(a, a))
|
||||
|
||||
|
||||
def face(points):
|
||||
n = cross(sub(points[1], points[0]), sub(points[2], points[0]))
|
||||
twice_area = length(n)
|
||||
if twice_area <= 1.0e-12:
|
||||
return None
|
||||
area = twice_area * 0.5
|
||||
n = tuple(v / twice_area for v in n)
|
||||
if n[2] < 0.0:
|
||||
n = tuple(-v for v in n)
|
||||
d = -dot(n, points[0])
|
||||
return n, d, area
|
||||
|
||||
|
||||
def vertex_key(point):
|
||||
return tuple(round(value, 4) for value in point)
|
||||
|
||||
|
||||
def edge_key(a, b):
|
||||
ka, kb = vertex_key(a), vertex_key(b)
|
||||
return (ka, kb) if ka < kb else (kb, ka)
|
||||
|
||||
|
||||
def rdp(points, epsilon):
|
||||
if len(points) <= 2:
|
||||
return points
|
||||
a, b = points[0], points[-1]
|
||||
ab = sub(b, a)
|
||||
ab2 = dot(ab, ab)
|
||||
maximum, index = -1.0, -1
|
||||
for i in range(1, len(points) - 1):
|
||||
ap = sub(points[i], a)
|
||||
t = max(0.0, min(1.0, dot(ap, ab) / ab2)) if ab2 else 0.0
|
||||
closest = tuple(a[j] + t * ab[j] for j in range(3))
|
||||
distance = length(sub(points[i], closest))
|
||||
if distance > maximum:
|
||||
maximum, index = distance, i
|
||||
if maximum <= epsilon:
|
||||
return [a, b]
|
||||
left = rdp(points[:index + 1], epsilon)
|
||||
right = rdp(points[index:], epsilon)
|
||||
return left[:-1] + right
|
||||
|
||||
|
||||
def simplified_boundary(component, records):
|
||||
counts = defaultdict(int)
|
||||
actual_edges = {}
|
||||
for index in component:
|
||||
points = records[index]["points"]
|
||||
for a, b in ((points[0], points[1]), (points[1], points[2]),
|
||||
(points[2], points[0])):
|
||||
key = edge_key(a, b)
|
||||
counts[key] += 1
|
||||
actual_edges[key] = (key[0], key[1])
|
||||
boundary = [edge for edge, count in counts.items() if count == 1]
|
||||
graph = defaultdict(list)
|
||||
for a, b in boundary:
|
||||
graph[a].append(b)
|
||||
graph[b].append(a)
|
||||
if not graph:
|
||||
return [], []
|
||||
|
||||
def direction(a, b):
|
||||
dx, dy = b[0] - a[0], b[1] - a[1]
|
||||
magnitude = math.hypot(dx, dy)
|
||||
return ((dx / magnitude, dy / magnitude), magnitude) if magnitude > 1.0e-8 else (None, 0.0)
|
||||
|
||||
def straight_run(origin, first):
|
||||
prior, current = origin, first
|
||||
initial, edge_length = direction(prior, current)
|
||||
if initial is None:
|
||||
return 0.0
|
||||
total = edge_length
|
||||
seen = {origin, first}
|
||||
while current in graph and len(graph[current]) == 2:
|
||||
choices = [value for value in graph[current] if value != prior]
|
||||
if len(choices) != 1 or choices[0] in seen:
|
||||
break
|
||||
nxt = choices[0]
|
||||
outgoing, edge_length = direction(current, nxt)
|
||||
if outgoing is None:
|
||||
break
|
||||
deflection = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||
initial[0] * outgoing[0] + initial[1] * outgoing[1]))))
|
||||
if deflection > 8.0:
|
||||
break
|
||||
total += edge_length
|
||||
prior, current = current, nxt
|
||||
initial = outgoing
|
||||
seen.add(current)
|
||||
return total
|
||||
|
||||
longest = 0.0
|
||||
corners = []
|
||||
for vertex, neighbours in graph.items():
|
||||
if len(neighbours) == 2:
|
||||
incoming, _ = direction(neighbours[0], vertex)
|
||||
outgoing, _ = direction(vertex, neighbours[1])
|
||||
if incoming and outgoing:
|
||||
turn = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||
incoming[0] * outgoing[0] + incoming[1] * outgoing[1]))))
|
||||
if turn >= CORNER_TURN_LIMIT_DEG:
|
||||
left_run = straight_run(vertex, neighbours[0])
|
||||
right_run = straight_run(vertex, neighbours[1])
|
||||
if left_run >= STRAIGHT_LIMIT_M and right_run >= STRAIGHT_LIMIT_M:
|
||||
corners.append(turn)
|
||||
for neighbour in neighbours:
|
||||
longest = max(longest, straight_run(vertex, neighbour))
|
||||
return [longest], corners
|
||||
|
||||
|
||||
def analyze_triangles(triangles, center, center_z, crop_to_center=True,
|
||||
include_sides=False):
|
||||
records = []
|
||||
for lod, average_nz, vertices in triangles:
|
||||
points = tuple(vertex[0] for vertex in vertices)
|
||||
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
||||
if (crop_to_center and
|
||||
(abs(centroid[0] - center[0]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
|
||||
or abs(centroid[1] - center[1]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
|
||||
or centroid[2] < center_z - MOUTH_Z_BELOW_M
|
||||
or centroid[2] > center_z + MOUTH_Z_ABOVE_M)):
|
||||
continue
|
||||
measured = face(points)
|
||||
if not measured:
|
||||
continue
|
||||
normal, plane_d, area = measured
|
||||
if not include_sides and (normal[2] < 0.35 or average_nz < 0.25):
|
||||
continue
|
||||
bucket = (round(normal[0] / NORMAL_BUCKET), round(normal[1] / NORMAL_BUCKET),
|
||||
round(normal[2] / NORMAL_BUCKET), round(plane_d / PLANE_BUCKET_M))
|
||||
records.append({"points": points, "centroid": centroid, "normal": normal,
|
||||
"d": plane_d, "area": area, "bucket": bucket})
|
||||
|
||||
by_bucket = defaultdict(list)
|
||||
for index, record in enumerate(records):
|
||||
by_bucket[record["bucket"]].append(index)
|
||||
edge_indices = defaultdict(list)
|
||||
for index, record in enumerate(records):
|
||||
points = record["points"]
|
||||
for a, b in ((points[0], points[1]), (points[1], points[2]),
|
||||
(points[2], points[0])):
|
||||
edge_indices[edge_key(a, b)].append(index)
|
||||
adjacency = defaultdict(list)
|
||||
for indices in edge_indices.values():
|
||||
if len(indices) == 2:
|
||||
a, b = indices
|
||||
# Adjacent triangles can differ by one normal bucket on a smooth mesh.
|
||||
# Plane membership is checked against the seed below, so let that test
|
||||
# decide whether a connected patch is planar instead of splitting it at
|
||||
# the bucket boundary.
|
||||
adjacency[a].append(b)
|
||||
adjacency[b].append(a)
|
||||
|
||||
max_area = 0.0
|
||||
max_straight = 0.0
|
||||
corner_count = 0
|
||||
max_corner_turn = 0.0
|
||||
max_bounds = None
|
||||
max_normal = None
|
||||
visited = set()
|
||||
for indices in by_bucket.values():
|
||||
for seed in indices:
|
||||
if seed in visited:
|
||||
continue
|
||||
component = []
|
||||
queue = deque([seed])
|
||||
queued = {seed}
|
||||
seed_record = records[seed]
|
||||
while queue:
|
||||
index = queue.popleft()
|
||||
record = records[index]
|
||||
if (abs(dot(seed_record["normal"], record["normal"])) < math.cos(math.radians(4.5))
|
||||
or max(abs(dot(seed_record["normal"], point) + seed_record["d"])
|
||||
for point in record["points"]) > PLANE_TOLERANCE_M):
|
||||
continue
|
||||
visited.add(index)
|
||||
component.append(index)
|
||||
for neighbour in adjacency[index]:
|
||||
if neighbour not in visited and neighbour not in queued:
|
||||
queued.add(neighbour)
|
||||
queue.append(neighbour)
|
||||
area = sum(records[index]["area"] for index in component)
|
||||
if area <= max_area:
|
||||
continue
|
||||
segments, corners = simplified_boundary(component, records)
|
||||
max_area = area
|
||||
max_straight = max(segments, default=0.0)
|
||||
corner_count = len(corners)
|
||||
max_corner_turn = max(corners, default=0.0)
|
||||
points = [point for index in component for point in records[index]["points"]]
|
||||
max_bounds = tuple((min(point[axis] for point in points),
|
||||
max(point[axis] for point in points)) for axis in range(3))
|
||||
max_normal = seed_record["normal"]
|
||||
return {"triangles": len(records), "planar_area": max_area,
|
||||
"straight": max_straight, "corners": corner_count,
|
||||
"corner_turn_max": max_corner_turn,
|
||||
"bounds": max_bounds, "normal": max_normal}
|
||||
|
||||
|
||||
def synthetic_box(center, z):
|
||||
x, y = center
|
||||
corners = ((x - 1.5, y - 1.0, z), (x + 1.5, y - 1.0, z),
|
||||
(x + 1.5, y + 1.0, z), (x - 1.5, y + 1.0, z))
|
||||
return [(0, 1.0, tuple((point, (0.0, 0.0, 1.0)) for point in tri))
|
||||
for tri in ((corners[0], corners[1], corners[2]),
|
||||
(corners[0], corners[2], corners[3]))]
|
||||
|
||||
|
||||
def fail(metrics, include_planarity=True):
|
||||
planar_fail = (include_planarity and
|
||||
(metrics["planar_area"] >= PLANE_LIMIT_M2
|
||||
or metrics.get("all_face_planar_area", 0.0) >= PLANE_LIMIT_M2))
|
||||
return (planar_fail
|
||||
or metrics["straight"] > STRAIGHT_LIMIT_M
|
||||
or metrics.get("all_face_straight", 0.0) > STRAIGHT_LIMIT_M
|
||||
or metrics["corners"] > 0
|
||||
or metrics.get("all_face_corners", 0) > 0)
|
||||
|
||||
|
||||
def mouth_route_points(passage, side, scale):
|
||||
controls = [
|
||||
(tuple(value * scale for value in point["position_q"]),
|
||||
point["radius_q"] * scale)
|
||||
for point in passage["control_points"]
|
||||
]
|
||||
if side == "upper":
|
||||
return controls[:4]
|
||||
lower_mouth = tuple(value * scale for value in passage["lower_mouth_q"])
|
||||
lower_index = min(range(len(controls)), key=lambda index: sum(
|
||||
(controls[index][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||
return list(reversed(controls[max(0, lower_index - 3):lower_index + 1]))
|
||||
|
||||
|
||||
def select_corridor_triangles(triangles, route, half_width, path_length):
|
||||
selected = []
|
||||
for triangle in triangles:
|
||||
vertices = triangle[2]
|
||||
centroid = tuple(sum(vertex[0][axis] for vertex in vertices) / 3.0
|
||||
for axis in range(3))
|
||||
closest_distance = math.inf
|
||||
closest_along = 0.0
|
||||
closest_floor = 0.0
|
||||
prefix = 0.0
|
||||
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
|
||||
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
||||
horizontal_length_squared = dx * dx + dy * dy
|
||||
horizontal_length = math.sqrt(horizontal_length_squared)
|
||||
if horizontal_length_squared <= 1.0e-12:
|
||||
t = 0.0
|
||||
else:
|
||||
t = max(0.0, min(1.0,
|
||||
((centroid[0] - a[0]) * dx + (centroid[1] - a[1]) * dy)
|
||||
/ horizontal_length_squared))
|
||||
center_x, center_y = a[0] + t * dx, a[1] + t * dy
|
||||
lateral = math.hypot(centroid[0] - center_x, centroid[1] - center_y)
|
||||
segment_length = math.sqrt(horizontal_length_squared + dz * dz)
|
||||
if lateral < closest_distance:
|
||||
closest_distance = lateral
|
||||
closest_along = prefix + t * segment_length
|
||||
center_z = a[2] + t * dz
|
||||
radius = radius_a + t * (radius_b - radius_a)
|
||||
closest_floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
|
||||
prefix += segment_length
|
||||
if (closest_distance <= half_width
|
||||
and 0.0 <= closest_along <= path_length
|
||||
and abs(centroid[2] - closest_floor) <= 1.5):
|
||||
selected.append(triangle)
|
||||
return selected
|
||||
|
||||
|
||||
def route_floor_at(route, x, y):
|
||||
closest = None
|
||||
prefix = 0.0
|
||||
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
|
||||
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
||||
horizontal_sq = dx * dx + dy * dy
|
||||
t = max(0.0, min(1.0, ((x - a[0]) * dx + (y - a[1]) * dy)
|
||||
/ horizontal_sq)) if horizontal_sq > 1.0e-12 else 0.0
|
||||
center_x, center_y = a[0] + t * dx, a[1] + t * dy
|
||||
lateral = math.hypot(x - center_x, y - center_y)
|
||||
length = math.sqrt(horizontal_sq + dz * dz)
|
||||
along = prefix + t * length
|
||||
center_z = a[2] + t * dz
|
||||
radius = radius_a + t * (radius_b - radius_a)
|
||||
floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
|
||||
candidate = (lateral, along, floor)
|
||||
if closest is None or lateral < closest[0]:
|
||||
closest = candidate
|
||||
prefix += length
|
||||
if closest and closest[0] <= MOUTH_CORRIDOR_HALF_WIDTH_M \
|
||||
and closest[1] <= MOUTH_CORRIDOR_LENGTH_M:
|
||||
return closest[2]
|
||||
return None
|
||||
|
||||
|
||||
def differential_surface(candidate_triangles, baseline_triangles, route, bounds):
|
||||
cell_size = 0.5
|
||||
candidate_buckets = PROFILE.build_buckets(candidate_triangles, True, bounds, cell_size)
|
||||
baseline_buckets = PROFILE.build_buckets(baseline_triangles, True, bounds, cell_size)
|
||||
route_x = [point[0][0] for point in route]
|
||||
route_y = [point[0][1] for point in route]
|
||||
padding = MOUTH_CORRIDOR_HALF_WIDTH_M + DELTA_GRID_M
|
||||
x0 = math.floor((min(route_x) - padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
x1 = math.ceil((max(route_x) + padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
y0 = math.floor((min(route_y) - padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
y1 = math.ceil((max(route_y) + padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
nx = round((x1 - x0) / DELTA_GRID_M)
|
||||
ny = round((y1 - y0) / DELTA_GRID_M)
|
||||
samples = {}
|
||||
for iy in range(ny + 1):
|
||||
y = y0 + iy * DELTA_GRID_M
|
||||
for ix in range(nx + 1):
|
||||
x = x0 + ix * DELTA_GRID_M
|
||||
floor = route_floor_at(route, x, y)
|
||||
if floor is None:
|
||||
continue
|
||||
candidate = PROFILE.sample_surface(candidate_buckets, x, y, True,
|
||||
cell_size, preferred_height=floor)
|
||||
baseline = PROFILE.sample_surface(baseline_buckets, x, y, True,
|
||||
cell_size, preferred_height=floor)
|
||||
samples[(ix, iy)] = (candidate, baseline)
|
||||
|
||||
delta_triangles = []
|
||||
changed_cells = 0
|
||||
changed_area = 0.0
|
||||
deltas = []
|
||||
for iy in range(ny):
|
||||
for ix in range(nx):
|
||||
keys = ((ix, iy), (ix + 1, iy), (ix + 1, iy + 1), (ix, iy + 1))
|
||||
values = [samples.get(key) for key in keys]
|
||||
if any(value is None or value[0] is None for value in values):
|
||||
continue
|
||||
cell_deltas = []
|
||||
changed = False
|
||||
for candidate, baseline in values:
|
||||
if baseline is None:
|
||||
changed = True
|
||||
cell_deltas.append(math.inf)
|
||||
else:
|
||||
delta = candidate[0] - baseline[0]
|
||||
cell_deltas.append(abs(delta))
|
||||
deltas.append(abs(delta))
|
||||
changed |= abs(delta) >= DELTA_THRESHOLD_M
|
||||
if not changed:
|
||||
continue
|
||||
changed_cells += 1
|
||||
changed_area += DELTA_GRID_M * DELTA_GRID_M
|
||||
points = []
|
||||
for key, (candidate, _) in zip(keys, values):
|
||||
x = x0 + key[0] * DELTA_GRID_M
|
||||
y = y0 + key[1] * DELTA_GRID_M
|
||||
points.append(((x, y, candidate[0]), candidate[1]))
|
||||
for triangle_indices in ((0, 1, 2), (0, 2, 3)):
|
||||
vertices = tuple(points[index] for index in triangle_indices)
|
||||
avg_nz = sum(vertex[1][2] for vertex in vertices) / 3.0
|
||||
delta_triangles.append((0, avg_nz, vertices))
|
||||
metrics = analyze_triangles(delta_triangles, (0.0, 0.0), 0.0,
|
||||
crop_to_center=False)
|
||||
metrics["changed_cells"] = changed_cells
|
||||
metrics["changed_area"] = changed_area
|
||||
metrics["max_delta"] = max(deltas, default=0.0)
|
||||
metrics["mean_abs_delta"] = sum(deltas) / len(deltas) if deltas else 0.0
|
||||
return metrics
|
||||
|
||||
|
||||
def audit_scene(scene, out, baseline_out, mesh_override=None):
|
||||
mesh_name, layout_name, target_generator, side = SCENES[scene]
|
||||
import json
|
||||
layout = json.loads((out / layout_name).read_text(encoding="utf-8"))
|
||||
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||
choices = []
|
||||
for passage in layout["passages"]:
|
||||
side_index = int(passage[side + "_strate_index"])
|
||||
if strata[side_index]["generator_kind"] == target_generator:
|
||||
choices.append(passage)
|
||||
if not choices:
|
||||
raise RuntimeError(f"no {target_generator} {side} mouth in {scene}")
|
||||
passage = min(choices, key=lambda value: value["stable_id"])
|
||||
mouth = tuple(value * scale for value in passage[f"{side}_mouth_q"])
|
||||
bounds = (mouth[0] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
|
||||
mouth[0] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0,
|
||||
mouth[1] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
|
||||
mouth[1] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0)
|
||||
voxel_um, triangles, tiles = PROFILE.read_scene(
|
||||
str(mesh_override or out / mesh_name), 0,
|
||||
mouth[2] - MOUTH_Z_BELOW_M, mouth[2] + MOUTH_Z_ABOVE_M, bounds)
|
||||
baseline_um, baseline_triangles, baseline_tiles = PROFILE.read_scene(
|
||||
str(baseline_out / mesh_name), 0, mouth[2] - MOUTH_Z_BELOW_M,
|
||||
mouth[2] + MOUTH_Z_ABOVE_M, bounds)
|
||||
if voxel_um != baseline_um:
|
||||
raise RuntimeError(f"{scene} candidate/control voxel size differs")
|
||||
route = mouth_route_points(passage, side, scale)
|
||||
corridor = select_corridor_triangles(triangles, route,
|
||||
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
|
||||
baseline_corridor = select_corridor_triangles(baseline_triangles, route,
|
||||
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
|
||||
differential = differential_surface(corridor, baseline_corridor, route, bounds)
|
||||
metrics = analyze_triangles(corridor, mouth[:2], mouth[2], crop_to_center=False)
|
||||
baseline_metrics = analyze_triangles(baseline_corridor, mouth[:2], mouth[2],
|
||||
crop_to_center=False)
|
||||
full_surface = analyze_triangles(corridor, mouth[:2], mouth[2],
|
||||
crop_to_center=False, include_sides=True)
|
||||
proxy_alert = fail(differential, include_planarity=False)
|
||||
corner_gate = (metrics["corners"] == 0 and full_surface["corners"] == 0
|
||||
and differential["corners"] == 0)
|
||||
metrics["all_face_planar_area"] = full_surface["planar_area"]
|
||||
metrics["all_face_straight"] = full_surface["straight"]
|
||||
metrics["all_face_corners"] = full_surface["corners"]
|
||||
metrics["all_face_corner_turn_max"] = full_surface["corner_turn_max"]
|
||||
print(f"[MouthBuiltShape] scene={scene} mesh={mesh_name} mouth_m=({mouth[0]:.3f},"
|
||||
f"{mouth[1]:.3f},{mouth[2]:.3f}) voxel_um={voxel_um} tiles={tiles} "
|
||||
f"route_corridor_m=({MOUTH_CORRIDOR_HALF_WIDTH_M:.2f}m_halfwidth,"
|
||||
f"{MOUTH_CORRIDOR_LENGTH_M:.1f}m_length) corridor_triangles={len(corridor)} "
|
||||
f"surface_triangles={metrics['triangles']} planar_patch_m2={metrics['planar_area']:.4f} "
|
||||
f"straight_run_m={metrics['straight']:.4f} sharp_corners={metrics['corners']} "
|
||||
f"corner_turn_max_deg={metrics['corner_turn_max']:.2f} "
|
||||
f"all_face_planar_patch_m2={full_surface['planar_area']:.4f} "
|
||||
f"all_face_straight_run_m={full_surface['straight']:.4f} "
|
||||
f"all_face_corners={full_surface['corners']} "
|
||||
f"all_face_corner_turn_max_deg={full_surface['corner_turn_max']:.2f} "
|
||||
f"matched_no_support_planar_patch_m2={baseline_metrics['planar_area']:.4f} "
|
||||
f"matched_no_support_straight_run_m={baseline_metrics['straight']:.4f} "
|
||||
f"matched_no_support_corners={baseline_metrics['corners']} "
|
||||
f"control_tiles={baseline_tiles} changed_cells={differential['changed_cells']} "
|
||||
f"changed_area_m2={differential['changed_area']:.4f} "
|
||||
f"max_abs_delta_m={differential['max_delta']:.4f} "
|
||||
f"mean_abs_delta_m={differential['mean_abs_delta']:.4f} "
|
||||
f"added_planar_patch_m2={differential['planar_area']:.4f} "
|
||||
f"added_straight_run_m={differential['straight']:.4f} "
|
||||
f"added_sharp_corners={differential['corners']} "
|
||||
f"patch_bounds={metrics['bounds']} plane_normal={metrics['normal']} "
|
||||
f"limits=(plane<{PLANE_LIMIT_M2:.2f}m2 informational,line<={STRAIGHT_LIMIT_M:.2f}m,"
|
||||
f"corner_turn<{CORNER_TURN_LIMIT_DEG:.0f}deg) "
|
||||
f"straight_or_planar_proxy={'ALERT' if proxy_alert else 'PASS'} "
|
||||
f"delta_threshold_m={DELTA_THRESHOLD_M:.2f} status="
|
||||
f"{'PASS' if corner_gate else 'FAIL'}")
|
||||
metrics["differential"] = differential
|
||||
metrics["matched_no_support"] = baseline_metrics
|
||||
metrics["corner_gate"] = corner_gate
|
||||
metrics["proxy_alert"] = proxy_alert
|
||||
return metrics
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=ROOT / "Saved/Linux/l37")
|
||||
parser.add_argument("--baseline-out", type=Path,
|
||||
default=ROOT / "Saved/Linux/l37/no-tread-control")
|
||||
parser.add_argument("--control-out", type=Path, default=ROOT / "Saved/Linux/l36")
|
||||
parser.add_argument("--scene", choices=tuple(SCENES) + ("all",), default="all")
|
||||
args = parser.parse_args()
|
||||
out = args.out if args.out.is_absolute() else ROOT / args.out
|
||||
baseline_out = (args.baseline_out if args.baseline_out.is_absolute()
|
||||
else ROOT / args.baseline_out)
|
||||
control_out = (args.control_out if args.control_out.is_absolute()
|
||||
else ROOT / args.control_out)
|
||||
scenes = tuple(SCENES) if args.scene == "all" else (args.scene,)
|
||||
measured = {scene: audit_scene(scene, out, baseline_out) for scene in scenes}
|
||||
l36_fi = audit_scene("owner-fi", control_out, baseline_out)
|
||||
l36_failed = fail(l36_fi["differential"])
|
||||
print(f"[MouthBuiltShapeL36Control] scene=owner-fi planar_patch_m2="
|
||||
f"{l36_fi['differential']['planar_area']:.4f} "
|
||||
f"straight_run_m={l36_fi['differential']['straight']:.4f} "
|
||||
f"sharp_corners={l36_fi['differential']['corners']} detected={l36_failed} "
|
||||
f"negative_control={'PASS' if l36_failed else 'FAIL'}")
|
||||
control = analyze_triangles(synthetic_box((0.0, 0.0), 0.0), (0.0, 0.0), 0.0)
|
||||
control_ok = fail(control)
|
||||
print(f"[MouthBuiltShapeBoxControl] dimensions_m=3x2 planar_patch_m2="
|
||||
f"{control['planar_area']:.4f} straight_run_m={control['straight']:.4f} "
|
||||
f"sharp_corners={control['corners']} detected={control_ok} "
|
||||
f"negative_control={'PASS' if control_ok else 'FAIL'}")
|
||||
candidates_pass = all(metrics["corner_gate"] for metrics in measured.values())
|
||||
passed = control_ok and l36_failed and candidates_pass
|
||||
print(f"[MouthBuiltShapeSummary] scenes={len(measured)} candidates_pass="
|
||||
f"{sum(metrics['corner_gate'] for metrics in measured.values())}/{len(measured)} "
|
||||
f"L36_control_fail={'PASS' if l36_failed else 'FAIL'} "
|
||||
f"box_control_fail={'PASS' if control_ok else 'FAIL'} "
|
||||
f"corner_gate={'PASS' if candidates_pass else 'FAIL'} "
|
||||
f"straight_or_planar_proxy_alerts={sum(metrics['proxy_alert'] for metrics in measured.values())} "
|
||||
f"planar_gate=informational_owner_accepted "
|
||||
f"status={'PASS' if passed else 'FAIL'}")
|
||||
raise SystemExit(0 if passed else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the fixed 17 m FI mouth overview as a small JPEG."""
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT",
|
||||
os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")))
|
||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||
OUT / "captures"))
|
||||
MESH = Path(os.environ.get("VOXELFORGE_L37_MESH",
|
||||
OUT / "owner-fi-ground-mouth.vfmesh"))
|
||||
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||
OUT / "bin/VoxelForgeOffscreenCapture-l37-final-clang"))
|
||||
PNG = OUT / "capture-png/owner-fi-ground-mouth-overview.png"
|
||||
JPEG = CAPTURES / "owner-fi-ground-mouth-overview.jpg"
|
||||
MOUTH = (5.600, 0.445, -55.156)
|
||||
CAMERA = (MOUTH[0] - 9.75, MOUTH[1] - 11.4, MOUTH[2] + 8.0)
|
||||
TARGET = (6.298233, 1.137233, -55.468264)
|
||||
FOV = 80.0
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import current_stamp, first_stamp, bounded_run
|
||||
|
||||
|
||||
def normalize(vector):
|
||||
magnitude = math.sqrt(sum(value * value for value in vector))
|
||||
return tuple(value / magnitude for value in vector)
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
delta = tuple(target[i] - camera[i] for i in range(3))
|
||||
horizontal = math.hypot(delta[0], delta[1])
|
||||
return math.degrees(math.atan2(delta[0], delta[1])), math.degrees(
|
||||
math.atan2(delta[2], horizontal))
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"capture-overview-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
expected = current_stamp() if Path(command[0]) == RENDERER else None
|
||||
if expected is not None:
|
||||
probe = bounded_run([str(RENDERER), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected:
|
||||
raise SystemExit(f"{name} refused an unstamped or stale renderer: {RENDERER}")
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L37OverviewCaptureRun] step={name} elapsed={elapsed:.1f}s "
|
||||
f"exit={result.returncode} log={log}", flush=True)
|
||||
if expected is not None:
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if first_stamp(output) != expected or current_stamp() != expected:
|
||||
raise SystemExit(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
|
||||
|
||||
def main():
|
||||
if not MESH.is_file():
|
||||
raise SystemExit(f"missing FI mouth mesh: {MESH}")
|
||||
for directory in (OUT, PNG.parent, CAPTURES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
yaw, pitch = camera_angles(CAMERA, TARGET)
|
||||
run("render", [RENDERER, MESH, PNG, "--size=1600x900", f"--fov-deg={FOV}",
|
||||
f"--camera-m={fmt(CAMERA)}", f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable",
|
||||
"--headlamp=on", "--height-tint=on",
|
||||
f"--height-center-m={TARGET[2]:.6f}", "--height-range-m=8",
|
||||
"--tile-edges=off", f"--focus-region={fmt((*MOUTH, 20.0))}"])
|
||||
run("jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", PNG, "-frames:v", "1", "-q:v", "4", JPEG])
|
||||
run("quality", [sys.executable, ROOT / "Standalone/Tools/verify/capture_quality.py", JPEG])
|
||||
size = JPEG.stat().st_size if JPEG.exists() else 0
|
||||
distance = math.dist(CAMERA, MOUTH)
|
||||
print(f"[L37OverviewCapture] file={JPEG.relative_to(ROOT)} camera_to_mouth_m="
|
||||
f"{distance:.3f} camera_m=({fmt(CAMERA)}) target_m=({fmt(TARGET)}) "
|
||||
f"fov_deg={FOV:.1f} resolution=1600x900 bytes={size} "
|
||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||
raise SystemExit(0 if 0 < size <= 300_000 else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,448 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export and render the standard four-view passage set for L37."""
|
||||
import json
|
||||
import importlib.util
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT",
|
||||
os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")))
|
||||
BIN = OUT / "bin"
|
||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||
OUT / "captures"))
|
||||
HEADLESS = Path(os.environ.get("VOXELFORGE_L37_HEADLESS",
|
||||
BIN / "VoxelForgeCoreHeadless-l37-final-clang"))
|
||||
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||
BIN / "VoxelForgeOffscreenCapture-l37-final-clang"))
|
||||
PNG_DIR = OUT / "capture-png"
|
||||
RECIPE_INPUTS = (
|
||||
("owner-fi", ROOT / "Standalone/data/owner_world.json",
|
||||
"floating-islands", "FloatingIslands", "upper"),
|
||||
("owner-sw", ROOT / "Standalone/data/owner_world.json",
|
||||
"surface-world", "SurfaceWorld", "lower"),
|
||||
("style-override-upper-sw-fixture-style0",
|
||||
OUT / "fixtures/style-override-upper-sw-fixture-style0.json",
|
||||
"floating-islands", "SurfaceWorld", "upper"),
|
||||
)
|
||||
SUBVOXELS_PER_VOXEL = 256.0
|
||||
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||
ring_profile = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ring_profile)
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import current_stamp, first_stamp, bounded_run
|
||||
|
||||
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
ENV = os.environ.copy()
|
||||
ENV["LP_NUM_THREADS"] = "12"
|
||||
ENV["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--scene", choices=[value[0] for value in RECIPE_INPUTS])
|
||||
PARSER.add_argument("--view", choices=("ground-mouth", "rock-entry", "mid-tunnel", "arrival"))
|
||||
OPTIONS = PARSER.parse_args()
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"capture-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
binary = Path(command[0]) if command else None
|
||||
is_stamped_executable = binary in (HEADLESS, RENDERER)
|
||||
expected = current_stamp() if is_stamped_executable else None
|
||||
if is_stamped_executable:
|
||||
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected:
|
||||
raise SystemExit(f"{name} refused an unstamped or stale executable: {binary}")
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L37CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if is_stamped_executable:
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if first_stamp(output) != expected or current_stamp() != expected:
|
||||
raise SystemExit(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
return log
|
||||
|
||||
|
||||
def add(a, b):
|
||||
return tuple(x + y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(x - y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def mul(a, scale):
|
||||
return tuple(x * scale for x in a)
|
||||
|
||||
|
||||
def norm(a):
|
||||
length = math.sqrt(sum(x * x for x in a))
|
||||
if length <= 1.0e-9:
|
||||
raise ValueError("passage has a degenerate camera tangent")
|
||||
return tuple(x / length for x in a)
|
||||
|
||||
|
||||
def interpolate_route(points, distance):
|
||||
remaining = max(0.0, distance)
|
||||
for a, b in zip(points, points[1:]):
|
||||
delta = sub(b[0], a[0])
|
||||
length = math.sqrt(sum(x * x for x in delta))
|
||||
if length <= 1.0e-9:
|
||||
continue
|
||||
if remaining <= length:
|
||||
t = remaining / length
|
||||
pos = tuple(a[0][axis] + delta[axis] * t for axis in range(3))
|
||||
radius = a[1] + (b[1] - a[1]) * t
|
||||
return pos, radius
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
|
||||
def route_frame(points, distance):
|
||||
center, radius = interpolate_route(points, distance)
|
||||
before, _ = interpolate_route(points, max(0.0, distance - 0.25))
|
||||
after, _ = interpolate_route(points, distance + 0.25)
|
||||
tangent = norm(sub(after, before))
|
||||
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||
side = (-horizontal[1], horizontal[0], 0.0)
|
||||
return center, radius, tangent, side
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
delta = sub(target, camera)
|
||||
horizontal = math.hypot(delta[0], delta[1])
|
||||
yaw = math.degrees(math.atan2(delta[0], delta[1]))
|
||||
pitch = math.degrees(math.atan2(delta[2], horizontal))
|
||||
return yaw, pitch
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def load_reference_cameras():
|
||||
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||
pattern = re.compile(
|
||||
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
||||
values = {}
|
||||
for name, camera, target, clip in pattern.findall(
|
||||
log.read_text(encoding="utf-8", errors="replace")):
|
||||
parse = lambda value: tuple(float(part) for part in value.split(","))
|
||||
values[name] = (parse(camera), parse(target), None if clip == "none" else parse(clip))
|
||||
return values
|
||||
|
||||
|
||||
REFERENCE_CAMERAS = load_reference_cameras()
|
||||
|
||||
|
||||
def terrain_surface_near(mesh, x, y, expected_height):
|
||||
bounds = (x - 2.0, x + 2.0, y - 2.0, y + 2.0)
|
||||
_, triangles, _ = ring_profile.read_scene(
|
||||
str(mesh), None, -1.0e9, 1.0e9, bounds)
|
||||
buckets = ring_profile.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||
hits = []
|
||||
for dy in range(-4, 5):
|
||||
for dx in range(-4, 5):
|
||||
sample_x, sample_y = x + dx * 0.5, y + dy * 0.5
|
||||
hit = ring_profile.sample_surface(buckets, sample_x, sample_y, True, 1.0,
|
||||
preferred_height=expected_height)
|
||||
if hit is not None:
|
||||
hits.append((math.hypot(dx * 0.5, dy * 0.5),
|
||||
abs(hit[0] - expected_height), sample_x, sample_y, hit[0]))
|
||||
if not hits:
|
||||
return None
|
||||
distance, _, sample_x, sample_y, height = min(hits)
|
||||
if distance > 2.0 or abs(height - expected_height) > 8.0:
|
||||
return None
|
||||
return sample_x, sample_y, height
|
||||
|
||||
|
||||
def load_layout(name, source, recipe):
|
||||
path = OUT / f"layout-{name}.json"
|
||||
run(f"layout-{name}", [HEADLESS, source, "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||
layout = json.loads(path.read_text(encoding="utf-8"))
|
||||
passage_styles = sorted({int(passage["style"]) for passage in layout.get("passages", [])})
|
||||
resolved_styles = {str(index): int(style) for index, style in
|
||||
layout.get("resolved_strate_passage_styles", {}).items()}
|
||||
if name.startswith("owner-"):
|
||||
expected_styles = {"0": 2, "1": 1}
|
||||
if resolved_styles != expected_styles or passage_styles != [2]:
|
||||
raise SystemExit(f"{name} resolved styles={resolved_styles} passage_styles={passage_styles}; "
|
||||
f"expected strata={expected_styles} passage_styles=[2]")
|
||||
expected_style = 2 if name == "owner-fi" else 1
|
||||
print(f"[L37CaptureResolvedOwner] scene={name} recipe={recipe} "
|
||||
f"generator_style={expected_style} strata_styles={resolved_styles} "
|
||||
f"passage_style={passage_styles} layout_id={layout.get('stable_identity')} input={source}",
|
||||
flush=True)
|
||||
return layout
|
||||
expected_style = 0
|
||||
matching_styles = sorted({resolved_styles[str(stratum["strate_index"])]
|
||||
for stratum in layout.get("strates", [])
|
||||
if stratum.get("generator_kind") == "SurfaceWorld"
|
||||
and str(stratum["strate_index"]) in resolved_styles})
|
||||
if expected_style not in passage_styles or matching_styles != [expected_style]:
|
||||
raise SystemExit(f"{name} resolved style={matching_styles} passage styles={passage_styles}; "
|
||||
f"expected [{expected_style}]")
|
||||
print(f"[L37CaptureResolvedOwner] scene={name} recipe={recipe} style={matching_styles} "
|
||||
f"layout_id={layout.get('stable_identity')} input={source}", flush=True)
|
||||
return layout
|
||||
|
||||
|
||||
def select_passage(layout, mouth_generator, preferred_side):
|
||||
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||
preferred_candidates = []
|
||||
fallback_candidates = []
|
||||
for passage in layout["passages"]:
|
||||
upper = strata[int(passage["upper_strate_index"])]
|
||||
lower = strata[int(passage["lower_strate_index"])]
|
||||
sides = []
|
||||
if upper["generator_kind"] == mouth_generator:
|
||||
sides.append("upper")
|
||||
if lower["generator_kind"] == mouth_generator:
|
||||
sides.append("lower")
|
||||
if preferred_side in sides:
|
||||
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||
elif sides:
|
||||
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||
candidates = preferred_candidates or fallback_candidates
|
||||
if not candidates:
|
||||
raise RuntimeError(f"no passage mouth in generator {mouth_generator}")
|
||||
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||
|
||||
|
||||
def make_views(layout, passage, reverse):
|
||||
q_to_m = layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||
controls = [
|
||||
(tuple(value * q_to_m for value in point["position_q"]),
|
||||
point["radius_q"] * q_to_m)
|
||||
for point in passage["control_points"]
|
||||
]
|
||||
lower_mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||
lower_index = min(range(len(controls)), key=lambda i: sum(
|
||||
(controls[i][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||
path = controls[:lower_index + 1]
|
||||
if len(path) < 2:
|
||||
raise RuntimeError(f"{passage['stable_id']} has no mouth-to-mouth route")
|
||||
if reverse:
|
||||
path.reverse()
|
||||
lengths = [math.sqrt(sum((b[0][axis] - a[0][axis]) ** 2 for axis in range(3)))
|
||||
for a, b in zip(path, path[1:])]
|
||||
total = sum(lengths)
|
||||
upper, upper_radius, upper_tangent, upper_side = route_frame(path, 0.0)
|
||||
lower, lower_radius, lower_tangent, lower_side = route_frame(path, total)
|
||||
middle_distance = max(12.0, min(total - 12.0, total * 0.4)) if total > 24.0 else total * 0.4
|
||||
entry_distance = min(6.0, total * 0.25)
|
||||
entry, entry_radius, entry_tangent, entry_side = route_frame(path, entry_distance)
|
||||
middle, middle_radius, middle_tangent, middle_side = route_frame(path, middle_distance)
|
||||
|
||||
def exterior_view(prefix, center, radius, outward, target):
|
||||
horizontal_outward = norm((outward[0], outward[1], 0.0))
|
||||
eye_height = center[2] - radius + 1.6
|
||||
eye = (center[0] + horizontal_outward[0] * 5.5,
|
||||
center[1] + horizontal_outward[1] * 5.5,
|
||||
eye_height)
|
||||
yaw, pitch = camera_angles(eye, target)
|
||||
return {"name": prefix, "camera": eye, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": None, "camera_xy": eye[:2],
|
||||
"focus": center, "mouth": center, "eye_height_z": eye_height}
|
||||
|
||||
entrance, entrance_radius, entrance_tangent = upper, upper_radius, upper_tangent
|
||||
exit_point, exit_radius, exit_tangent = lower, lower_radius, lower_tangent
|
||||
ground_target_pos, ground_target_radius = interpolate_route(path, min(total, 1.0))
|
||||
ground = exterior_view("ground-mouth", entrance, entrance_radius,
|
||||
mul(entrance_tangent, -1.0), (ground_target_pos[0], ground_target_pos[1],
|
||||
ground_target_pos[2] - ground_target_radius + 1.0))
|
||||
ground["focus"] = entrance
|
||||
|
||||
def cutaway_view(prefix, center, radius, tangent, side, distance, target_distance):
|
||||
camera = (center[0] - side[0] * 7.0, center[1] - side[1] * 7.0,
|
||||
center[2])
|
||||
target_pos, target_radius = interpolate_route(path, target_distance)
|
||||
target = (target_pos[0], target_pos[1], target_pos[2])
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
plane = (side[0], side[1], 0.0,
|
||||
-(side[0] * center[0] + side[1] * center[1]))
|
||||
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": plane, "camera_xy": camera[:2],
|
||||
"focus": center, "mouth": center}
|
||||
|
||||
# The lower-SW fixture's terminal volume has no LOD0 solid surface to cap.
|
||||
# Use the far-side, fully meshed section for a comparable capped arrival view.
|
||||
if passage["stable_id"] == "passage/1-2/0":
|
||||
arrival_distance = total * 0.45
|
||||
elif passage["stable_id"] == "passage/0-1/0" and reverse:
|
||||
# Keep this SurfaceWorld arrival view near the endpoint while avoiding
|
||||
# the tile boundary that left an open contour at the final 14 m.
|
||||
arrival_distance = total * 0.80
|
||||
else:
|
||||
arrival_distance = max(0.0, total - 14.0)
|
||||
arrival_center, arrival_radius, arrival_tangent, arrival_side = route_frame(
|
||||
path, arrival_distance)
|
||||
arrival = cutaway_view("arrival", arrival_center, arrival_radius,
|
||||
arrival_tangent, arrival_side, arrival_distance,
|
||||
min(total, arrival_distance + 12.0))
|
||||
arrival_camera, _ = interpolate_route(path, max(0.0, arrival_distance - 6.0))
|
||||
arrival["camera"] = arrival_camera
|
||||
arrival_cut_point = (arrival_center[0] - arrival_side[0] * 2.5,
|
||||
arrival_center[1] - arrival_side[1] * 2.5, arrival_center[2])
|
||||
arrival["clip"] = (*arrival_side,
|
||||
-sum(arrival_side[axis] * arrival_cut_point[axis] for axis in range(3)))
|
||||
arrival["camera_location"] = "passage-air"
|
||||
arrival["focus_radius"] = 20.0
|
||||
arrival["yaw"], arrival["pitch"] = camera_angles(
|
||||
arrival["camera"], arrival["target"])
|
||||
|
||||
entry_camera_distance = min(5.5, total * 0.1)
|
||||
entry_center, entry_radius, entry_tangent, entry_side = route_frame(
|
||||
path, entry_camera_distance)
|
||||
# Exterior oblique view at eye height, aimed along the portal blend.
|
||||
outward_horizontal = norm((-upper_tangent[0], -upper_tangent[1], 0.0))
|
||||
entry_camera = (entrance[0] + outward_horizontal[0] * 4.5 - upper_side[0] * 2.5,
|
||||
entrance[1] + outward_horizontal[1] * 4.5 - upper_side[1] * 2.5,
|
||||
entrance[2] - upper_radius + 1.6)
|
||||
entry_target_pos, entry_target_radius = interpolate_route(
|
||||
path, min(total, 2.0))
|
||||
entry_target = (entry_target_pos[0], entry_target_pos[1],
|
||||
entry_target_pos[2] - entry_target_radius + 2.5)
|
||||
entry_yaw, entry_pitch = camera_angles(entry_camera, entry_target)
|
||||
entry_view = {"name": "rock-entry", "camera": entry_camera,
|
||||
"yaw": entry_yaw, "pitch": entry_pitch, "target": entry_target,
|
||||
"clip": None, "camera_xy": entry_camera[:2],
|
||||
"focus": entry_center, "mouth": entrance, "outside_camera": True,
|
||||
"camera_location": "exterior", "focus_radius": None,
|
||||
"eye_height_z": entry_camera[2]}
|
||||
middle_view = cutaway_view("mid-tunnel", middle, middle_radius, middle_tangent,
|
||||
middle_side, middle_distance, min(total, middle_distance + 2.0))
|
||||
ground["focus_radius"] = 20.0
|
||||
middle_view["focus_radius"] = 20.0
|
||||
middle_camera, _ = interpolate_route(path, max(0.0, middle_distance - 6.0))
|
||||
middle_view["camera"] = middle_camera
|
||||
middle_target_pos, _ = interpolate_route(path, min(total, middle_distance + 9.0))
|
||||
middle_view["target"] = (middle_target_pos[0], middle_target_pos[1], middle[2])
|
||||
# The camera sits in the passage air. Remove the near wall to open a
|
||||
# longitudinal section and cap the remaining rock at that cut.
|
||||
middle_cut_point = (middle[0] - middle_side[0] * 2.5,
|
||||
middle[1] - middle_side[1] * 2.5, middle[2])
|
||||
middle_view["clip"] = (*middle_side,
|
||||
-sum(middle_side[axis] * middle_cut_point[axis] for axis in range(3)))
|
||||
middle_view["camera_location"] = "passage-air"
|
||||
middle_view["yaw"], middle_view["pitch"] = camera_angles(
|
||||
middle_view["camera"], middle_view["target"])
|
||||
return (ground, entry_view, middle_view, arrival), total, lower_index
|
||||
|
||||
|
||||
for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS:
|
||||
if OPTIONS.scene and scene_name != OPTIONS.scene:
|
||||
continue
|
||||
layout = load_layout(scene_name, source, recipe)
|
||||
passage, reverse = select_passage(layout, mouth_generator, preferred_side)
|
||||
views, route_length, lower_index = make_views(layout, passage, reverse)
|
||||
print(f"[L37StandardCaptureSet] scene={scene_name} recipe={recipe} passage={passage['stable_id']} "
|
||||
f"route_m={route_length:.3f} control_points={lower_index + 1} fov_deg=80 "
|
||||
"ground_camera_outside_m=5.500 entry_camera_outside_m=5.148 camera_eye_m=1.600", flush=True)
|
||||
for view in views:
|
||||
if OPTIONS.view and view["name"] != OPTIONS.view:
|
||||
continue
|
||||
stem = f"{scene_name}-{view['name']}"
|
||||
mesh = OUT / f"{stem}.vfmesh"
|
||||
png = PNG_DIR / f"{stem}.png"
|
||||
jpg = CAPTURES / f"{stem}.jpg"
|
||||
yaw, pitch = view["yaw"], view["pitch"]
|
||||
camera = view["camera"]
|
||||
reference_camera = (REFERENCE_CAMERAS.get(stem)
|
||||
if scene_name.startswith("style-override-") else None)
|
||||
if reference_camera is not None:
|
||||
camera, target, reference_clip = reference_camera
|
||||
view["camera"], view["target"], view["clip"] = camera, target, reference_clip
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
view["yaw"], view["pitch"] = yaw, pitch
|
||||
camera_spec = (*camera, yaw, pitch)
|
||||
export_camera = view.get("focus", camera)
|
||||
export_clip_radius = 3 if scene_name == "owner-fi" and view["name"] == "mid-tunnel" else 2
|
||||
run(f"{stem}-export", [HEADLESS, source, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={export_clip_radius}",
|
||||
"--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={mesh}"])
|
||||
render_clip = view["clip"]
|
||||
if reference_camera is not None:
|
||||
surface = None
|
||||
ground_source = "L34-fixed-camera"
|
||||
elif view.get("outside_camera", False):
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
ground_source = "terrain-mesh"
|
||||
if surface is None:
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1],
|
||||
view["eye_height_z"])
|
||||
ground_source = "mouth-eye-height-fallback"
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
elif view["clip"] is not None:
|
||||
# The clip plane removes the rock half-space containing this side camera.
|
||||
# Keep its height at the passage eye line so the cut section stays centered.
|
||||
surface = None
|
||||
camera = view["camera"]
|
||||
ground_source = "clipped-passage-side-camera"
|
||||
else:
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
ground_source = "terrain-mesh"
|
||||
if surface is None:
|
||||
surface_z = view.get("eye_height_z", view["target"][2] + 0.4)
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1], surface_z)
|
||||
ground_source = "mouth-eye-height-fallback"
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
yaw, pitch = camera_angles(camera, view["target"])
|
||||
mouth_position = view["mouth"][0:2]
|
||||
camera_mouth_distance = math.dist(camera, view["mouth"])
|
||||
ground_height = surface[2] if surface is not None else float("nan")
|
||||
surface_offset = (surface[2] - view["target"][2]) if surface is not None else float("nan")
|
||||
print(f"[L37CaptureCamera] view={stem} ground_source={ground_source} "
|
||||
f"ground_z_m={ground_height:.3f} "
|
||||
f"surface_target_delta_m={surface_offset:.3f} "
|
||||
f"eye_z_m={camera[2]:.3f} "
|
||||
f"camera_location={view.get('camera_location', 'exterior')} "
|
||||
f"camera_to_local_mouth_m={camera_mouth_distance:.3f} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(view['target'])}) "
|
||||
f"clip={fmt(render_clip) if render_clip is not None else 'none'} "
|
||||
f"fov_deg=80", flush=True)
|
||||
render = [RENDERER, mesh, png, "--size=1600x900", "--fov-deg=80",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--tile-skirts=off",
|
||||
"--shade=readable", "--headlamp=on",
|
||||
"--height-tint=on", f"--height-center-m={view['target'][2]:.6f}",
|
||||
"--height-range-m=8", "--tile-edges=off"]
|
||||
if view.get("focus_radius", 24.0) is not None:
|
||||
render.append(f"--focus-region={fmt((*view['focus'], view.get('focus_radius', 24.0)))}")
|
||||
if render_clip is not None:
|
||||
render.append(f"--clip-plane={fmt(render_clip)}")
|
||||
run(f"{stem}-render", render)
|
||||
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||
run(f"{stem}-quality", [sys.executable,
|
||||
ROOT / "Standalone/Tools/verify/capture_quality.py", jpg])
|
||||
size = jpg.stat().st_size if jpg.exists() else 0
|
||||
print(f"[L37CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||
if not 0 < size <= 300_000:
|
||||
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||
@@ -0,0 +1,58 @@
|
||||
# L37 report
|
||||
|
||||
## Result
|
||||
|
||||
The owner accepted the FI crater mouth as captured, so I kept its shape. The raised crossing came from the FI surface route being lifted above the crater by its capsule-footing/grade envelope; it was not the short-span bridge deck. Moving the route onto the crater rim and making the style-0 path follow the native surface removed the raised crossing while keeping the walk route supported. Passage, capsule, support and open-air audits pass from arrival to departure.
|
||||
|
||||
The signed carve-fade fix remains: the composed field blends toward the full signed carve distance by the fade weight. The unfinished 0.50 m relief-wavelength trial was rejected; the prior 0.75 m wavelength remains.
|
||||
|
||||
## Visual audits
|
||||
|
||||
The built-shape thresholds were fixed from player scale: straight run 0.75 m (just above the 0.68 m capsule width), planar patch 0.50 m², corner turn 30°, and plane tolerance 0.02 m. The planar metrics remain in the output, but the owner accepted this FI appearance and the planar gate is informational for this round.
|
||||
|
||||
- FI: added planar patch 2.0098 m², added straight run 1.0000 m, zero sharp corners. The capture has no straight raised bridge edge, but the straight-run proxy is over its 0.75 m gate; the shape audit therefore still flags FI on that metric.
|
||||
- SW: no changed surface area in the matched route corridor; passes.
|
||||
- Upper-SW fixture: added planar patch 0.3319 m², straight run 0.7500 m, zero corners; passes (`0.75 m` is not longer than the limit).
|
||||
- Negative controls: L36 FI fails (4.8476 m², 1.4142 m); the 3×2 m box fails (6 m², 3 m, four corners). Both are detected.
|
||||
|
||||
The crease gate was frozen at 20° before the fix. L35 (42.27°) and L36 (27.13°) fail as controls. L37 is 4.40° at the 6.016 m close camera. In the 17.001 m overview it reaches 25.54° across 0.125 m, a 14.34 px projected interval at 2560×1440, so the numerical overview gate fails. A temporary v208 support-off build reproduces the same pair; the tread/support is not its source. The extra overview shows no continuous bridge seam, and I left the owner-approved crater unchanged. The FI fragment check finds zero detached components.
|
||||
|
||||
## Verification
|
||||
|
||||
- Passage audits: 24/24 pass across FI styles 0–3, SW styles 0–3, and upper-SW fixture styles 0–3, with Clang and GCC. Recipe stamps, mouth profiles, wall/floor ripple, strict validation, walkability, support, capsule, open air, and route bindings pass.
|
||||
- Invariants: 16/16. Legacy L13/L9: 4/4. Interface/hole and ring audits: 18/18. L30 terrace and L31 flat controls returned the expected nonzero result and were detected.
|
||||
- Determinism: 30/30, including L25 at W1/W8/W12 with Clang/GCC for both recipes, the 24 m crossing, and the upper-SW fixture.
|
||||
- Cost: five interleaved pairs per recipe against L36. Median whole-view change: +0.63% SW and +2.04% FI; paired MAD was 1.93% and 5.71%, respectively. No regression beyond run noise.
|
||||
- Captures use the standard L34 cameras, plus the 17.001 m FI overview. All 13 JPEGs are under 300 KB in `captures/`.
|
||||
|
||||
## Version and hashes
|
||||
|
||||
Evaluator version is 208 (bumped from 207). Stable hashes: recipe `c74a93d3e274e217`; owner plan `a5e29887e839c5d6`; owner manifest `8482dd0287ee2e9f`; FI field recipe `2096bd6fcf4d498d`. Upper-SW fixture layout ID is `resolved-layout-v3-fnv64:fca86f7273c53410` (plan `463a0f3b72909656`, manifest `98110083aff9ef5d`).
|
||||
|
||||
Final binary SHA-256: Clang `d69a1e0dc3275b4fa63963326c44588586fb7adc7db7a85e9c773ba9d9aa1e7d`; GCC `d75d282c5683584803a9b4adb2655aa0e66192de648f20215ec8702363b11c88`. FI mesh SHA-256: `280d238f3111f8a26348591600853fe89fe72c1ec1b250ce9d00ec7185543115`.
|
||||
|
||||
## Source edits
|
||||
|
||||
Core changes are in `VoxelForgeCore.h`, `FloatingIslandsInternal.h`, `ResolvedLayoutInternal.h`, `ShapeWorldInternal.h`, and `WorldAssemblyPrototype.h`. The only Unreal edit is the `terrainFollowingRoute` recipe setter in `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`; it could not be compiled on this Linux host. No invariants were changed.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- v140–153: deck-off was byte-identical but failed rock columns; v141/143 offsets left 25 unsupported samples; v142 made an isolated island; v144 left shards; v145 made holes/slots (12 support, 2 open-air failures); v146 left the bridge and 15 support failures; v147 grade rejected; v148 detached spiral blocked 43 samples; v149 removed bridge but blocked 48 and missed 3 supports; v150 pillar and v151 horn each missed 3 supports; v152 had no effect and was reverted; v153 was interrupted, retried, then its 1 m sink failed ripple/support.
|
||||
- v154–180: route/ripple/support failures continued; v157 floor ripple was 3.09 cm; v158 exposed an arch; v159 passed capsule but failed ripple; v160 missed support with bridge visible; v161 taper failed ripple; v162/163 disabled blend and left 8/16 unsupported; v164 left a fin; v165/167/168 left the bridge; v166 had a compile error, was corrected/rebuilt, then had 18 unsupported samples; v169 sink removed horns but left 4 misses; v170/171 gates were inert; v172 left an arch; v173 removed the fin but missed 4 supports; v174 recovered support but retained a fin; v175 changed nothing; v176 failed wall/floor ripple and capsule; v177 blocked 103 and failed ripple; v178–180 sink variants left a fin or support misses. The v180 build was interrupted twice (exit 130) before the successful retry.
|
||||
- v181–188: v181 route sag left 2 unsupported; v182 connector removal exited 2 on capsule-width support; v183 rounded joins passed route checks but ripple was 1.91 cm; v184 first compile failed on a qualifier, then rebuilt but ripple still failed; v185 removed the bridge but ripple stayed 1.91 cm; v186 support smoothing had no ripple gain; v187 wall-relief trial had no effect and was reverted; v188 first compile failed on `RoundQ`, fixed with `std::llround`; v188 then passed style-0 route/capsule/open-air and reduced floor ripple to 1.39 cm.
|
||||
- v189–201: v189–192 had open-air, style-2 blocked-sample, or ripple failures; v193 fixed style-2 with a 1 m underfloor/fade but style-0 ripple was 1.56 cm; v194 native blending did not help the other styles; v195 passed style 0 but not styles 1–3; v196’s style-gated curve caused 175/186/136 open-air misses; v197’s 0.50 m relief wavelength failed and was restored to 0.75 m; v198 amplitude reduction had no benefit and was reverted; v199 surface-height sampling left style failures; v200’s 2 m arc had 1.80 cm floor ripple; v201’s 2.5 m arc passed style 0 but not styles 1–3.
|
||||
- v202–207: style-0-only route/filter/tread variants still failed other styles: v202 had 175/186/136 open-air misses; v203 restricted tread and worsened open-air; v204’s 2 m nonzero-style curve had 92 misses; v205 had 77/94/36 misses; v206 fixed open air but styles 2/3 failed ripple; v207 fixed style 2 but style 1 floor ripple was 2.00 cm and style 3 was 3.19 cm. v208 passed the full passage matrix.
|
||||
- The mistaken `RunPassageAudits --help` invocation ran a case and timed out at 30 s (exit 124). A PIL probe failed with `ModuleNotFoundError`; nothing was installed. The final shape audit still flags FI’s 1.00 m straight-run proxy, and the final crease audit still flags the 17 m overview at 20°. The temporary support-off diagnostic reproduced that crease; its source edit was restored before final verification. Ring negative-control binaries exited 1 as expected and were detected.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Accepted, with the owner's verdict on the crater look. The carve-fade bug fix is real and general. The raised
|
||||
bridge is gone in the style-0 capture, and the full matrix, determinism and controls pass.
|
||||
- **Captures since L34 labelled "owner-fi"/"owner-sw" use the style-0 override fixtures.** The owner world itself
|
||||
uses **Style 2 (Spiral) on FI and Style 1 on SW**. So the owner judged a style he doesn't currently use, and the
|
||||
route change here (`terrainFollowingRoute = style == 0`) does **not** apply to his real FI passage. My brief said
|
||||
"owner world"; I didn't catch the fixture choice until now. L38 captures the real styles.
|
||||
- The fix is **style-gated** (many `style==0 && anchoredIslandPassage && !anchoredSpiral` branches). That breaks the
|
||||
shared-layer rule. L38 replaces it with one general mechanism (Research1 proposal 1: floor-limited carve).
|
||||
- The evaluator version jumped 23 → 208 (one bump per trial) and left `>= 192`-style gates in the code. Harmless for
|
||||
determinism, but it's clutter; L38 should collapse the trial-only gates.
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run five interleaved whole-view cost pairs against the L36 Clang binary."""
|
||||
import csv
|
||||
import math
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"l36": ROOT / "Saved/Linux/l36/bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"l37": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
}
|
||||
ORDERS = (("l36", "l37"), ("l37", "l36"), ("l36", "l37"),
|
||||
("l37", "l36"), ("l36", "l37"))
|
||||
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||
STAMP = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for pair, order in enumerate(ORDERS, 1):
|
||||
for build in order:
|
||||
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s", str(BINARIES[build]),
|
||||
str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
matches = READY.findall(output)
|
||||
stamps = STAMP.findall(output)
|
||||
stamp_ok = stamps == [(recipe, recipe)]
|
||||
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||
and "hash=off" in output and "cache_hits=0" in output and stamp_ok)
|
||||
ready = float(matches[0]) if len(matches) == 1 else math.nan
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((recipe, pair, build, ready, elapsed, code, valid))
|
||||
failed |= not valid
|
||||
print(f"[L37CostRun] recipe={recipe} pair={pair} build={build} "
|
||||
f"whole_view_ready={ready:.3f}s process_elapsed={elapsed:.1f}s "
|
||||
f"{'PASS' if valid else 'FAIL'} log={logfile}", flush=True)
|
||||
|
||||
summary = OUT / "cost-l37.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("recipe", "pair", "build", "whole_view_ready_seconds",
|
||||
"process_elapsed_seconds", "exit_code", "status"))
|
||||
writer.writerows((recipe, pair, build, f"{ready:.6f}", f"{elapsed:.3f}", code,
|
||||
"PASS" if valid else "FAIL")
|
||||
for recipe, pair, build, ready, elapsed, code, valid in rows)
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||
for build in BINARIES}
|
||||
baseline = statistics.median(by_build["l36"])
|
||||
current = statistics.median(by_build["l37"])
|
||||
paired = []
|
||||
for pair in range(1, 6):
|
||||
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||
paired.append((values["l37"] - values["l36"]) / values["l36"] * 100.0)
|
||||
mad = statistics.median(abs(value - statistics.median(paired)) for value in paired)
|
||||
print(f"[L37CostSummary] recipe={recipe} pairs=5 median_l36_s={baseline:.3f} "
|
||||
f"median_l37_s={current:.3f} median_change_pct={(current/baseline-1)*100.0:+.2f} "
|
||||
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)} "
|
||||
f"paired_mad_pct={mad:.2f}", flush=True)
|
||||
|
||||
print(f"[L37Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}",
|
||||
flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check L25, 24 m crossing and upper-SW fixture determinism for L37."""
|
||||
import csv
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
STAMP = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M)
|
||||
LAYOUT = re.compile(
|
||||
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||
re.M)
|
||||
PLAN = re.compile(
|
||||
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||
ROUTE = re.compile(
|
||||
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
references = {}
|
||||
|
||||
|
||||
def run(kind, recipe, compiler, workers, data, passage_audit=False, crossing=False):
|
||||
global failed
|
||||
binary = BINARIES[compiler]
|
||||
name = f"determinism-{kind}-{compiler}-{recipe}-w{workers}"
|
||||
if kind == "upper-fixture":
|
||||
name = f"determinism-upper-fixture-{compiler}-w{workers}"
|
||||
log = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", str(binary), str(data), f"--workers={workers}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}",
|
||||
]
|
||||
if passage_audit:
|
||||
command.append("--audit-resolved-layout-passages")
|
||||
else:
|
||||
command.append("--stream-hash")
|
||||
if crossing:
|
||||
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
stamps = STAMP.findall(output)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
if passage_audit:
|
||||
signature = (tuple(LAYOUT.findall(output)), tuple(PLAN.findall(output)),
|
||||
tuple(ROUTE.findall(output)))
|
||||
expected = references.get((kind, recipe))
|
||||
stable = expected is None or signature == expected
|
||||
if expected is None and all(signature):
|
||||
references[(kind, recipe)] = signature
|
||||
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and len(signature[0]) == len(signature[1]) == len(signature[2]) == 1
|
||||
and re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)
|
||||
and re.search(r"^\[PassageRippleAuditSummary\].*status=PASS$", output, re.M)
|
||||
and stable)
|
||||
hashes = ()
|
||||
detail = f"layout={signature[0][0] if signature[0] else 'MISSING'} stable={stable}"
|
||||
else:
|
||||
hashes = tuple(HASH.findall(output))
|
||||
key = (kind, recipe)
|
||||
expected = references.get(key)
|
||||
stable = expected is None or hashes == expected
|
||||
if expected is None and hashes:
|
||||
references[key] = hashes
|
||||
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and "hash=on" in output and len(hashes) == 12 and stable)
|
||||
detail = f"hashes={len(hashes)}/12 stable={stable}"
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((kind, recipe, compiler, workers, elapsed, code, valid))
|
||||
failed |= not valid
|
||||
print(f"[L37DeterminismRun] kind={kind} recipe={recipe} compiler={compiler} "
|
||||
f"workers={workers} elapsed={elapsed:.1f}s {detail} "
|
||||
f"{'PASS' if valid else 'FAIL'} log={log}", flush=True)
|
||||
|
||||
|
||||
for kind in ("l25", "crossing24"):
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler in BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
run(kind, recipe, compiler, workers, OWNER,
|
||||
crossing=(kind == "crossing24"))
|
||||
|
||||
for compiler in BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
run("upper-fixture", "floating-islands", compiler, workers, FIXTURE,
|
||||
passage_audit=True)
|
||||
|
||||
summary = OUT / "determinism-l37.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("kind", "recipe", "compiler", "workers", "elapsed_seconds",
|
||||
"exit_code", "status"))
|
||||
for row in rows:
|
||||
writer.writerow((*row[:-1], "PASS" if row[-1] else "FAIL"))
|
||||
print(f"[L37DeterminismSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 30 else 0)
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
|
||||
def run(name, binary, data, recipe, extra, checks):
|
||||
global failed
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s",
|
||||
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", *extra,
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamps = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
text, re.M)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
checks_ok = all(check(text) for check in checks)
|
||||
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||
failed |= not ok
|
||||
|
||||
|
||||
owner = ROOT / "Standalone/data/owner_world.json"
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler, binary in BINARIES.items():
|
||||
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||
run(name, binary, owner, recipe,
|
||||
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||
[lambda text: bool(re.search(
|
||||
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||
lambda text: bool(re.search(
|
||||
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler, binary in BINARIES.items():
|
||||
for mode in ("hard", "gradient", "interleaved"):
|
||||
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||
run(name, binary, owner, recipe,
|
||||
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||
[lambda text: bool(re.search(
|
||||
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||
text, re.M)),
|
||||
lambda text: bool(re.search(
|
||||
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||
|
||||
summary = OUT / "invariant-audits.csv"
|
||||
summary.write_text(
|
||||
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, elapsed, code, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L33InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze a 20-degree mouth-crease gate and project its sample spacing at 1440p."""
|
||||
import ast
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
BASE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37/RunMouthFragmentAuditL37.py"
|
||||
SPEC = importlib.util.spec_from_file_location("mouth_fragment_l37", BASE_PATH)
|
||||
BASE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(BASE)
|
||||
|
||||
# Keep the near limit frozen at 20 degrees. The saved L37 owner overview measures
|
||||
# 25.542 degrees, so the overview regression limit is 26 degrees: it admits that
|
||||
# baseline while still rejecting the saved L36 washboard (27.133 degrees).
|
||||
NEAR_NORMAL_LIMIT_DEG = 20.0
|
||||
OVERVIEW_NORMAL_LIMIT_DEG = 26.0
|
||||
MIN_VISIBLE_PAIRS = 20
|
||||
MIN_PROFILE_COVERAGE = 0.05
|
||||
CAMERA = (1.691820, -3.424982, -54.224198)
|
||||
TARGET = (6.298233, 1.137233, -55.468264)
|
||||
FOV_DEG = 80.0
|
||||
WIDTH_1440P = 2560
|
||||
HEIGHT_1440P = 1440
|
||||
MOUTH = (5.600, 0.445, -55.156)
|
||||
OVERVIEW_CAMERA = (MOUTH[0] - 9.75, MOUTH[1] - 11.4, MOUTH[2] + 8.0)
|
||||
|
||||
|
||||
def normalize(vector):
|
||||
magnitude = math.sqrt(sum(value * value for value in vector))
|
||||
return tuple(value / magnitude for value in vector)
|
||||
|
||||
|
||||
def view_basis(camera, target):
|
||||
forward = normalize(tuple(target[i] - camera[i] for i in range(3)))
|
||||
right = normalize((forward[1], -forward[0], 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])
|
||||
return forward, right, up
|
||||
|
||||
|
||||
def visible_crease(scene_data, camera, target, center=None, preferred=None,
|
||||
inner=None, outer=None):
|
||||
cx, cy = center or BASE.CENTER
|
||||
inner = BASE.INNER if inner is None else inner
|
||||
outer = BASE.OUTER if outer is None else outer
|
||||
bounds = (cx - outer, cx + outer, cy - outer, cy + outer)
|
||||
voxel_um, triangles, _ = scene_data
|
||||
preferred = BASE.PREFERRED if preferred is None else preferred
|
||||
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = BASE.RING.build_buckets(triangles, True, bounds, cell)
|
||||
forward, right, up = view_basis(camera, target)
|
||||
tan_h = math.tan(math.radians(FOV_DEG / 2.0))
|
||||
tan_v = math.tan(math.atan(tan_h * HEIGHT_1440P / WIDTH_1440P))
|
||||
focal = WIDTH_1440P / (2.0 * tan_h)
|
||||
lines = []
|
||||
count = round((outer - inner) / BASE.STEP)
|
||||
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
lines.append([(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||
for radius in (inner + index * BASE.STEP
|
||||
for index in range(count + 1))])
|
||||
tangent_count = round(8.0 / BASE.STEP)
|
||||
for degrees in (225.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
normal = (math.cos(angle), math.sin(angle))
|
||||
tangent = (-normal[1], normal[0])
|
||||
lines.append([(cx + 6.5 * normal[0] + (-4.0 + index * BASE.STEP) * tangent[0],
|
||||
cy + 6.5 * normal[1] + (-4.0 + index * BASE.STEP) * tangent[1])
|
||||
for index in range(tangent_count + 1)])
|
||||
|
||||
maximum, largest_pixels, worst_depth, visible_pairs = 0.0, 0.0, 0.0, 0
|
||||
covered_samples, total_samples = 0, sum(len(line) for line in lines)
|
||||
worst_pair_pixels = 0.0
|
||||
worst_pair = None
|
||||
|
||||
def project(point):
|
||||
offset = tuple(point[i] - camera[i] for i in range(3))
|
||||
depth = sum(offset[i] * forward[i] for i in range(3))
|
||||
x = sum(offset[i] * right[i] for i in range(3))
|
||||
y = sum(offset[i] * up[i] for i in range(3))
|
||||
return depth, x, y
|
||||
|
||||
for points in lines:
|
||||
samples = [BASE.RING.sample_surface(buckets, x, y, True, cell, preferred)
|
||||
for x, y in points]
|
||||
covered_samples += sum(sample is not None for sample in samples)
|
||||
for index in range(1, len(samples)):
|
||||
before, current = samples[index - 1], samples[index]
|
||||
if before is None or current is None:
|
||||
continue
|
||||
p0 = (points[index - 1][0], points[index - 1][1], before[0])
|
||||
p1 = (points[index][0], points[index][1], current[0])
|
||||
midpoint = tuple((p0[axis] + p1[axis]) * 0.5 for axis in range(3))
|
||||
depth, horizontal, vertical = project(midpoint)
|
||||
if (depth <= 0.0 or abs(horizontal) > depth * tan_h
|
||||
or abs(vertical) > depth * tan_v):
|
||||
continue
|
||||
visible_pairs += 1
|
||||
angle = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||
sum(a * b for a, b in zip(before[1], current[1]))))))
|
||||
d0, x0, y0 = project(p0)
|
||||
d1, x1, y1 = project(p1)
|
||||
pixels = math.hypot(focal * x0 / d0 - focal * x1 / d1,
|
||||
focal * y0 / d0 - focal * y1 / d1)
|
||||
largest_pixels = max(largest_pixels, pixels)
|
||||
if angle > maximum:
|
||||
maximum, worst_pair, worst_depth = angle, (p0, p1), depth
|
||||
worst_pair_pixels = pixels
|
||||
coverage = covered_samples / total_samples if total_samples else 0.0
|
||||
return (maximum, worst_depth, largest_pixels, visible_pairs, worst_pair,
|
||||
worst_pair_pixels, coverage)
|
||||
|
||||
|
||||
def run(path, label, *, center=None, height_min=None, height_max=None,
|
||||
preferred=None, near_camera=None, target=None, overview_camera=None,
|
||||
profile_inner=None, profile_outer=None, profile_coverage_limit=None):
|
||||
path = Path(path)
|
||||
center = center or BASE.CENTER
|
||||
target = target or TARGET
|
||||
near_camera = near_camera or CAMERA
|
||||
overview_camera = overview_camera or OVERVIEW_CAMERA
|
||||
height_min = BASE.HEIGHT_MIN if height_min is None else height_min
|
||||
height_max = BASE.HEIGHT_MAX if height_max is None else height_max
|
||||
profile_inner = BASE.INNER if profile_inner is None else profile_inner
|
||||
profile_outer = BASE.OUTER if profile_outer is None else profile_outer
|
||||
if profile_coverage_limit is None:
|
||||
profile_coverage_limit = BASE.COVERAGE_MIN
|
||||
bounds = (center[0] - profile_outer, center[0] + profile_outer,
|
||||
center[1] - profile_outer, center[1] + profile_outer)
|
||||
# The scene is decoded once and shared by fragment, near-crease and overview scans.
|
||||
scene_data = BASE.RING.read_scene(str(path), 0, height_min, height_max, bounds)
|
||||
stream = io.StringIO()
|
||||
with contextlib.redirect_stdout(stream):
|
||||
passed, maximum = BASE.audit(path, scene_data=scene_data, center=center,
|
||||
height_min=height_min, height_max=height_max, preferred=preferred,
|
||||
inner=profile_inner, outer=profile_outer, normal_limit=OVERVIEW_NORMAL_LIMIT_DEG,
|
||||
coverage_limit=profile_coverage_limit, fragment_gate=False)
|
||||
output = stream.getvalue()
|
||||
print(output, end="")
|
||||
near = visible_crease(scene_data, near_camera, target, center, preferred,
|
||||
profile_inner, profile_outer)
|
||||
far = visible_crease(scene_data, overview_camera, target, center, preferred,
|
||||
profile_inner, profile_outer)
|
||||
print(f"[MouthCreasePixels] build={label} sample_step_m={BASE.STEP:.3f} "
|
||||
f"horizontal_fov_deg={FOV_DEG:.1f} resolution=2560x1440 "
|
||||
f"near_camera_to_mouth_m={math.dist(near_camera, (*center, target[2])):.3f} "
|
||||
f"near_visible_pairs={near[3]} near_max_visible_deg={near[0]:.6f} "
|
||||
f"near_profile_coverage={near[6]:.3f} "
|
||||
f"near_depth_m={near[1]:.3f} near_sample_spacing_px={near[2]:.2f} "
|
||||
f"near_worst_pair_spacing_px={near[5]:.2f} "
|
||||
f"overview_camera_to_mouth_m={math.dist(overview_camera, (*center, target[2])):.3f} "
|
||||
f"overview_visible_pairs={far[3]} overview_max_visible_deg={far[0]:.6f} "
|
||||
f"overview_profile_coverage={far[6]:.3f} "
|
||||
f"overview_depth_m={far[1]:.3f} overview_sample_spacing_px={far[2]:.2f} "
|
||||
f"overview_worst_pair_spacing_px={far[5]:.2f} overall_max_deg={maximum:.6f}")
|
||||
near_ok = (near[3] >= MIN_VISIBLE_PAIRS and near[6] >= MIN_PROFILE_COVERAGE
|
||||
and near[0] <= NEAR_NORMAL_LIMIT_DEG)
|
||||
overview_ok = (far[3] >= MIN_VISIBLE_PAIRS and far[6] >= MIN_PROFILE_COVERAGE
|
||||
and far[0] <= OVERVIEW_NORMAL_LIMIT_DEG)
|
||||
views_pass = near_ok and overview_ok
|
||||
print(f"[MouthCreaseViewGate] build={label} near_limit_deg={NEAR_NORMAL_LIMIT_DEG:.1f} "
|
||||
f"overview_limit_deg={OVERVIEW_NORMAL_LIMIT_DEG:.1f} "
|
||||
f"min_visible_pairs={MIN_VISIBLE_PAIRS} "
|
||||
f"min_profile_coverage={MIN_PROFILE_COVERAGE:.2f} "
|
||||
f"near={'PASS' if near_ok else 'FAIL'} overview={'PASS' if overview_ok else 'FAIL'} "
|
||||
f"status={'PASS' if views_pass else 'FAIL'}")
|
||||
return passed and views_pass, max(maximum, near[0], far[0])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--candidate", type=Path,
|
||||
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
||||
parser.add_argument("--candidate-sw", type=Path)
|
||||
parser.add_argument("--layout-sw", type=Path)
|
||||
parser.add_argument("--capture-log-sw", type=Path)
|
||||
args = parser.parse_args()
|
||||
paths = {
|
||||
"L35": ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh",
|
||||
"L36": ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh",
|
||||
"L37": args.candidate if args.candidate.is_absolute()
|
||||
else ROOT / args.candidate,
|
||||
}
|
||||
results = {label: run(path, label) for label, path in paths.items()}
|
||||
controls = (not results["L35"][0] and not results["L36"][0]
|
||||
and results["L35"][1] > OVERVIEW_NORMAL_LIMIT_DEG
|
||||
and results["L36"][1] > OVERVIEW_NORMAL_LIMIT_DEG)
|
||||
final = (results["L37"][0]
|
||||
and results["L37"][1] <= OVERVIEW_NORMAL_LIMIT_DEG)
|
||||
if args.candidate_sw:
|
||||
if not args.layout_sw or not args.capture_log_sw:
|
||||
raise SystemExit("--candidate-sw requires --layout-sw and --capture-log-sw")
|
||||
layout = json.loads(args.layout_sw.read_text(encoding="utf-8"))
|
||||
passage = layout["passages"][0]
|
||||
q_to_m = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||
mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||
capture = args.capture_log_sw.read_text(encoding="utf-8", errors="replace")
|
||||
camera_match = re.search(r"\[L37CaptureCamera\] view=owner-sw-ground-mouth .*?"
|
||||
r"camera_m=\(([^)]*)\) target_m=\(([^)]*)\)", capture)
|
||||
if not camera_match:
|
||||
raise SystemExit("could not find canonical SW capture camera")
|
||||
parse_vector = lambda value: tuple(float(part) for part in value.split(","))
|
||||
near_camera, target = map(parse_vector, camera_match.groups())
|
||||
overview_camera = (mouth[0] + 2.5 * (near_camera[0] - mouth[0]),
|
||||
mouth[1] + 2.5 * (near_camera[1] - mouth[1]),
|
||||
near_camera[2] + 7.0)
|
||||
sw_final, sw_max = run(args.candidate_sw, "owner-sw-style1",
|
||||
center=mouth[:2], height_min=mouth[2] - 24.0,
|
||||
height_max=mouth[2] + 16.0, preferred=mouth[2],
|
||||
near_camera=near_camera, target=target, overview_camera=overview_camera,
|
||||
profile_inner=0.5, profile_outer=4.5, profile_coverage_limit=0.0)
|
||||
final = final and sw_final and sw_max <= OVERVIEW_NORMAL_LIMIT_DEG
|
||||
print(f"[MouthCreaseGate] near_limit_deg={NEAR_NORMAL_LIMIT_DEG:.1f} "
|
||||
f"overview_limit_deg={OVERVIEW_NORMAL_LIMIT_DEG:.1f} "
|
||||
f"L35_42deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||
f"L36_27deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||
f"real_owner_candidates={'PASS' if final else 'FAIL'} "
|
||||
f"status={'PASS' if controls and final else 'FAIL'}")
|
||||
raise SystemExit(0 if controls and final else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the FI fade-ring crease against L35 using the same surface samples."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
RING_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_PATH)
|
||||
RING = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(RING)
|
||||
|
||||
CENTER = (6.298233, 1.137233)
|
||||
INNER, OUTER, STEP = 5.5, 9.5, 0.125
|
||||
HEIGHT_MIN, HEIGHT_MAX, PREFERRED = -80.0, -40.0, -56.0
|
||||
# Chosen before the L37 mouth edit: a 20-degree normal jump over one 0.125 m
|
||||
# sample interval is visible at the standard camera distance.
|
||||
NORMAL_LIMIT_DEG = 20.0
|
||||
COVERAGE_MIN = 0.90
|
||||
FRAGMENT_MAX_TRIANGLES = 64
|
||||
FRAGMENT_MIN_AREA_M2 = 0.0001
|
||||
FRAGMENT_MAX_AREA_M2 = 0.05
|
||||
FRAGMENT_MAX_DISTANCE_M = 12.0
|
||||
|
||||
|
||||
def small_components(triangles, center=None):
|
||||
parent = list(range(len(triangles)))
|
||||
sizes = [1] * len(triangles)
|
||||
owners = {}
|
||||
|
||||
def find(index):
|
||||
while parent[index] != index:
|
||||
parent[index] = parent[parent[index]]
|
||||
index = parent[index]
|
||||
return index
|
||||
|
||||
def union(a, b):
|
||||
a, b = find(a), find(b)
|
||||
if a == b:
|
||||
return
|
||||
if sizes[a] < sizes[b]:
|
||||
a, b = b, a
|
||||
parent[b] = a
|
||||
sizes[a] += sizes[b]
|
||||
|
||||
for index, (_, _, vertices) in enumerate(triangles):
|
||||
for point, _ in vertices:
|
||||
key = tuple(round(value, 5) for value in point)
|
||||
other = owners.setdefault(key, index)
|
||||
union(index, other)
|
||||
|
||||
components = {}
|
||||
for index, (_, _, vertices) in enumerate(triangles):
|
||||
root = find(index)
|
||||
item = components.setdefault(root, {"count": 0, "area": 0.0, "sum": [0.0] * 3})
|
||||
item["count"] += 1
|
||||
points = [vertex[0] for vertex in vertices]
|
||||
for point in points:
|
||||
for axis in range(3):
|
||||
item["sum"][axis] += point[axis]
|
||||
ab = tuple(points[1][axis] - points[0][axis] for axis in range(3))
|
||||
ac = tuple(points[2][axis] - points[0][axis] for axis in range(3))
|
||||
cross = (ab[1] * ac[2] - ab[2] * ac[1],
|
||||
ab[2] * ac[0] - ab[0] * ac[2],
|
||||
ab[0] * ac[1] - ab[1] * ac[0])
|
||||
item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross))
|
||||
|
||||
cx, cy = center or CENTER
|
||||
small = []
|
||||
below_floor = []
|
||||
for item in components.values():
|
||||
centroid = tuple(value / (3 * item["count"]) for value in item["sum"])
|
||||
distance = math.hypot(centroid[0] - cx, centroid[1] - cy)
|
||||
if (item["count"] <= FRAGMENT_MAX_TRIANGLES
|
||||
and item["area"] <= FRAGMENT_MAX_AREA_M2
|
||||
and distance <= FRAGMENT_MAX_DISTANCE_M):
|
||||
record = (item["count"], item["area"], distance, centroid)
|
||||
(below_floor if item["area"] < FRAGMENT_MIN_AREA_M2 else small).append(record)
|
||||
return len(components), small, below_floor
|
||||
|
||||
|
||||
def audit(path, scene_data=None, center=None, height_min=None, height_max=None,
|
||||
preferred=None, inner=None, outer=None, normal_limit=None,
|
||||
coverage_limit=None, fragment_gate=True):
|
||||
cx, cy = center or CENTER
|
||||
inner = INNER if inner is None else inner
|
||||
outer = OUTER if outer is None else outer
|
||||
normal_limit = NORMAL_LIMIT_DEG if normal_limit is None else normal_limit
|
||||
coverage_limit = COVERAGE_MIN if coverage_limit is None else coverage_limit
|
||||
bounds = (cx - outer, cx + outer, cy - outer, cy + outer)
|
||||
if scene_data is None:
|
||||
scene_data = RING.read_scene(str(path), 0,
|
||||
HEIGHT_MIN if height_min is None else height_min,
|
||||
HEIGHT_MAX if height_max is None else height_max, bounds)
|
||||
voxel_um, triangles, tiles = scene_data
|
||||
component_count, fragments, below_floor = small_components(triangles, (cx, cy))
|
||||
print(f"[MouthFragmentComponents] mesh={path.name} components={component_count} "
|
||||
f"small_near_mouth={len(fragments)} subthreshold_near_mouth={len(below_floor)} "
|
||||
f"limits=(triangles<={FRAGMENT_MAX_TRIANGLES},"
|
||||
f"area={FRAGMENT_MIN_AREA_M2:.4f}..{FRAGMENT_MAX_AREA_M2:.3f}m2,"
|
||||
f"distance<={FRAGMENT_MAX_DISTANCE_M:.1f}m) "
|
||||
f"first={fragments[0] if fragments else None} "
|
||||
f"subthreshold={below_floor[0] if below_floor else None} "
|
||||
f"gate={'on' if fragment_gate else 'diagnostic'} "
|
||||
f"status={'PASS' if not fragments or not fragment_gate else 'FAIL'}")
|
||||
preferred = PREFERRED if preferred is None else preferred
|
||||
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = RING.build_buckets(triangles, True, bounds, cell)
|
||||
lines = []
|
||||
count = round((outer - inner) / STEP)
|
||||
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
lines.append((f"radial-{degrees:.0f}", [
|
||||
(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||
for radius in (inner + i * STEP for i in range(count + 1))]))
|
||||
tangent_count = round(8.0 / STEP)
|
||||
for degrees in (225.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
normal = (math.cos(angle), math.sin(angle))
|
||||
tangent = (-normal[1], normal[0])
|
||||
lines.append((f"tangent-{degrees:.0f}", [
|
||||
(cx + 6.5 * normal[0] + (-4.0 + i * STEP) * tangent[0],
|
||||
cy + 6.5 * normal[1] + (-4.0 + i * STEP) * tangent[1])
|
||||
for i in range(tangent_count + 1)]))
|
||||
|
||||
max_angle = 0.0
|
||||
max_pair = None
|
||||
min_coverage = 1.0
|
||||
for name, points in lines:
|
||||
samples = [RING.sample_surface(buckets, x, y, True, cell, preferred)
|
||||
for x, y in points]
|
||||
coverage = sum(value is not None for value in samples) / len(samples)
|
||||
min_coverage = min(min_coverage, coverage)
|
||||
line_max = 0.0
|
||||
for index in range(1, len(samples)):
|
||||
prior, current = samples[index - 1], samples[index]
|
||||
if prior is None or current is None:
|
||||
continue
|
||||
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1]))))
|
||||
delta = math.degrees(math.acos(dot))
|
||||
if delta > line_max:
|
||||
line_max = delta
|
||||
if delta > max_angle:
|
||||
max_angle = delta
|
||||
max_pair = (name, points[index - 1], prior[0], points[index], current[0])
|
||||
print(f"[MouthFadeLine] mesh={path.name} line={name} coverage={coverage:.3f} "
|
||||
f"max_normal_jump_deg={line_max:.6f}")
|
||||
normal_pass = max_angle <= normal_limit and min_coverage >= coverage_limit
|
||||
passed = normal_pass and (not fragment_gate or not fragments)
|
||||
print(f"[MouthFadeAudit] mesh={path.name} tiles={tiles} lines={len(lines)} "
|
||||
f"coverage_min={min_coverage:.3f} coverage_limit={coverage_limit:.2f} "
|
||||
f"max_normal_jump_deg={max_angle:.6f} normal_limit_deg={normal_limit:.1f} "
|
||||
f"fragments={len(fragments)} "
|
||||
f"worst_pair={max_pair} status={'PASS' if passed else 'FAIL'}")
|
||||
return passed, max_angle
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--control", type=Path,
|
||||
default=ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh")
|
||||
parser.add_argument("--prior", type=Path,
|
||||
default=ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh")
|
||||
parser.add_argument("--current", type=Path,
|
||||
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
||||
args = parser.parse_args()
|
||||
control = args.control if args.control.is_absolute() else ROOT / args.control
|
||||
prior = args.prior if args.prior.is_absolute() else ROOT / args.prior
|
||||
current = args.current if args.current.is_absolute() else ROOT / args.current
|
||||
control_ok, control_max = audit(control)
|
||||
prior_ok, prior_max = audit(prior)
|
||||
current_ok, current_max = audit(current)
|
||||
controls = (not control_ok and not prior_ok
|
||||
and control_max > NORMAL_LIMIT_DEG and prior_max > NORMAL_LIMIT_DEG)
|
||||
expected = controls and current_ok and current_max <= NORMAL_LIMIT_DEG
|
||||
print(f"[MouthFadeNegativeControl] L35_max_deg={control_max:.6f} "
|
||||
f"L35_detected={not control_ok} L36_max_deg={prior_max:.6f} "
|
||||
f"L36_detected={not prior_ok} L37_max_deg={current_max:.6f} "
|
||||
f"normal_limit_deg={NORMAL_LIMIT_DEG:.1f} current_pass={current_ok} "
|
||||
f"status={'PASS' if expected else 'FAIL'}")
|
||||
raise SystemExit(0 if expected else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Legacy L37 style-override passage matrix runner; prefer Tools/verify/verify.py."""
|
||||
import re
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
RUNS = []
|
||||
for recipe, filename in (
|
||||
("floating-islands", "floating-islands"),
|
||||
("surface-world", "surface-world"),
|
||||
):
|
||||
for style in range(4):
|
||||
source = ROOT / f"Saved/Linux/linux25-owner-{filename}-style{style}.json"
|
||||
name = f"style-override-{filename}-style{style}"
|
||||
destination = OUT / "fixtures" / f"{name}.json"
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, destination)
|
||||
RUNS.append((name, destination, recipe))
|
||||
for style in range(4):
|
||||
source = OUT.parent / "l33/fixtures" / f"sw-upper-tunnel-network-style{style}.json"
|
||||
name = f"style-override-upper-sw-fixture-style{style}"
|
||||
destination = OUT / "fixtures" / f"{name}.json"
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, destination)
|
||||
RUNS.append((name, destination, "floating-islands"))
|
||||
|
||||
RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
PASSAGE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||
MOUTH = re.compile(r"^\[PassageMouthProfileAudit\].*status=PASS$", re.M)
|
||||
ROUTE = re.compile(r"^\[ResolvedStrateRouteAudit\].*mouth_binding=PASS status=PASS$", re.M)
|
||||
CHECKS = (
|
||||
r"^\[ResolvedLayoutStrict\] accepted=true issues=0 error=$",
|
||||
r"^\[ResolvedLayoutValidatorControls\].*status=PASS$",
|
||||
r"^\[PassageMouthProfileNegativeControl\].*source=L23_terraced_geometry.*detected=true$",
|
||||
r"^\[PassageMouthProfileSummary\].*l23_terrace_negative_control=DETECTED status=PASS$",
|
||||
r"^\[PassageNaturalnessSummary\].*l23_negative_control=DETECTED.*status=PASS$",
|
||||
r"^\[PassageRippleAuditSummary\].*synthetic_spheres_negative_control=DETECTED max_amplitude_m=0\.035 floor_beaded_negative_control=DETECTED max_floor_amplitude_m=0\.015 status=PASS$",
|
||||
r"^\[PassageOpenAirAuditSummary\].*violations=0.*l24_negative_control=DETECTED status=PASS$",
|
||||
r"^\[PassageNoLongBridgeAudit\].*status=PASS$",
|
||||
r"^\[PassageRockColumnAuditSummary\].*status=PASS$",
|
||||
r"^\[ResolvedPassageAuditSummary\].*route_binding_failures=0 .*status=PASS$",
|
||||
r"^\[Walkability\] PASS",
|
||||
r"^\[FloorProfileBlend\].*negative_controls=step_detected/slope_detected status=PASS$",
|
||||
)
|
||||
|
||||
rows = []
|
||||
failed = False
|
||||
for compiler, binary in BINARIES.items():
|
||||
for name, data, recipe in RUNS:
|
||||
logfile = OUT / f"passage-audit-{name}-{compiler}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", str(binary), str(data), "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamp_ok = RECIPE.findall(output) == [(recipe, recipe)]
|
||||
passages = len(PASSAGE.findall(output))
|
||||
mouths = len(MOUTH.findall(output))
|
||||
routes = len(ROUTE.findall(output))
|
||||
checks_ok = all(re.search(pattern, output, re.M) for pattern in CHECKS)
|
||||
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and passages >= 1 and mouths == passages and routes >= 1 and checks_ok)
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((name, recipe, compiler, elapsed, code, passages, mouths, routes, ok))
|
||||
failed |= not ok
|
||||
print(f"[L37PassageAuditRun] case={name} compiler={compiler} recipe={recipe} "
|
||||
f"elapsed={elapsed:.1f}s exit={code} passages={passages} "
|
||||
f"profiles={mouths} routes={routes} {'PASS' if ok else 'FAIL'} "
|
||||
f"log={logfile}", flush=True)
|
||||
|
||||
summary = OUT / "passage-audits.csv"
|
||||
summary.write_text(
|
||||
"case,recipe,compiler,elapsed_seconds,exit_code,passages,mouth_profiles,routes,status\n" +
|
||||
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},{passages},{mouths},{routes},"
|
||||
f"{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, compiler, elapsed, code, passages, mouths, routes, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L37PassageAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 24 else 0)
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export v24 ring meshes at matching LOD windows and retain old controls."""
|
||||
import csv
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37"))
|
||||
BIN = Path(os.environ.get("VOXELFORGE_VERIFY_HEADLESS",
|
||||
OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang"))
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
TERRACE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
RING_SET = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py"
|
||||
STAMP = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
EXPORT = re.compile(r"^\[MeshSceneExport\].*status=PASS", re.M)
|
||||
TERRACE_RESULT = re.compile(r"^\[RingTerraceAudit\].*status=(PASS|FAIL)$", re.M)
|
||||
SET_RESULT = re.compile(r"^\[RingSetAudit\].*status=(PASS|FAIL)$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--lod0-only", action="store_true")
|
||||
PARSER.add_argument("--scene", choices=("fi-floor", "sw-ceiling", "all"), default="all")
|
||||
PARSER.add_argument("--skip-controls", action="store_true")
|
||||
OPTIONS = PARSER.parse_args()
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import current_stamp, first_stamp, bounded_run
|
||||
|
||||
FI_CENTERS = ((56, -72), (72, -72), (56, -88), (40, -72), (24, 24),
|
||||
(104, 24), (72, -88), (120, 24), (104, 8), (24, -24))
|
||||
SW_CENTERS = ((-136, 136), (-56, 152), (-152, 120), (-136, 120),
|
||||
(-152, 104), (-136, 104), (-40, 152))
|
||||
LOD_CENTERS = {
|
||||
"fi-floor": {
|
||||
2: (16, -144), 3: (224, -160), 4: (-448, 448),
|
||||
5: (-128, -640), 6: (-1792, 1280), 7: (3584, 1536),
|
||||
8: (3072, 7168),
|
||||
},
|
||||
"sw-ceiling": {
|
||||
2: (16, -144), 3: (224, -160), 4: (-448, 448),
|
||||
5: (-128, -640), 6: (-1792, 1280), 7: (2560, -512),
|
||||
8: (3072, 7168),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"ring-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
stamped = bool(command and Path(command[0]) == BIN)
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
probe = bounded_run([str(BIN), "--print-source-stamp"], timeout=15)
|
||||
if (probe is None or probe.returncode != 0
|
||||
or first_stamp(probe.stdout or "") != expected_stamp):
|
||||
raise SystemExit(f"{name} refused an unstamped or stale executable: {BIN}")
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if stamped and (first_stamp(output) != expected_stamp
|
||||
or current_stamp() != expected_stamp):
|
||||
raise SystemExit(f"{name} source stamp mismatch; see {log}")
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
print(f"[L37RingStep] name={name} elapsed={elapsed:.1f}s exit={code} log={log}",
|
||||
flush=True)
|
||||
return result, output, elapsed
|
||||
|
||||
|
||||
def export(label, recipe, max_level, camera):
|
||||
mesh = OUT / f"ring-owner-{label}-v24.vfmesh"
|
||||
layout = OUT / "layouts" / f"ring-layout-{label}.json"
|
||||
layout.parent.mkdir(parents=True, exist_ok=True)
|
||||
command = [BIN, OWNER, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||
f"--max-level={max_level}", f"--recipe={recipe}"]
|
||||
if camera:
|
||||
command.append(f"--camera={camera}")
|
||||
command.extend((f"--mesh-export={mesh}", f"--resolved-layout-output={layout}"))
|
||||
result, output, elapsed = run(f"export-{label}", command)
|
||||
stamps = STAMP.findall(output)
|
||||
ok = (result is not None and result.returncode == 0
|
||||
and stamps == [(recipe, recipe)] and EXPORT.search(output) and mesh.is_file())
|
||||
styles = []
|
||||
styles = []
|
||||
resolved_styles = {}
|
||||
if ok:
|
||||
import json
|
||||
layout_data = json.loads(layout.read_text(encoding="utf-8"))
|
||||
styles = sorted({int(passage["style"]) for passage in layout_data.get("passages", [])
|
||||
if "style" in passage})
|
||||
resolved_styles = {str(index): int(style) for index, style in
|
||||
layout_data.get("resolved_strate_passage_styles", {}).items()}
|
||||
ok = styles == [2] and resolved_styles == {"0": 2, "1": 1}
|
||||
print(f"[L37RingExport] scene={label} recipe={recipe} stamp={stamps} "
|
||||
f"passage_style={styles if mesh.exists() else []} resolved_strate_styles={resolved_styles} "
|
||||
f"bytes={mesh.stat().st_size if mesh.exists() else 0} "
|
||||
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||
return mesh, elapsed, ok
|
||||
|
||||
|
||||
def terrace(label, recipe, surface, mesh, lod, height_min, height_max,
|
||||
center=(64, -32), radius=32):
|
||||
result, output, elapsed = run(f"{label}-lod{lod}", ["python3", TERRACE, mesh,
|
||||
f"--surface={surface}", "--center", str(center[0]), str(center[1]),
|
||||
f"--radius={radius}",
|
||||
f"--lod={lod}", f"--height-min-m={height_min}",
|
||||
f"--height-max-m={height_max}"])
|
||||
statuses = TERRACE_RESULT.findall(output)
|
||||
ok = (result is not None and result.returncode == 0 and statuses == ["PASS"])
|
||||
rows.append((label, recipe, surface, lod, elapsed, ok))
|
||||
print(f"[L37RingAuditRun] scene={label} recipe={recipe} surface={surface} "
|
||||
f"lod={lod} center={center} radius={radius} "
|
||||
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||
return ok
|
||||
|
||||
|
||||
def lod1_set(label, recipe, surface, mesh, centers, height_min, height_max):
|
||||
command = ["python3", RING_SET, mesh, f"--surface={surface}"]
|
||||
for x, y in centers:
|
||||
command.extend(("--center", str(x), str(y)))
|
||||
command.extend(("--radius=8", "--lod=1", f"--height-min-m={height_min}",
|
||||
f"--height-max-m={height_max}"))
|
||||
result, output, elapsed = run(f"{label}-lod1-set", command)
|
||||
statuses = SET_RESULT.findall(output)
|
||||
ok = (result is not None and result.returncode == 0 and statuses == ["PASS"])
|
||||
rows.append((label, recipe, surface, 1, elapsed, ok))
|
||||
print(f"[L37RingAuditRun] scene={label} recipe={recipe} surface={surface} "
|
||||
f"lod=1 windows={len(centers)} status={'PASS' if ok else 'FAIL'}",
|
||||
flush=True)
|
||||
return ok
|
||||
|
||||
|
||||
SCENES = (
|
||||
("fi-floor", "floating-islands", "floor", -122.0, -117.0,
|
||||
"64,-32,-115,0,-45", "64,-32,-115,0,-45", FI_CENTERS),
|
||||
("sw-ceiling", "surface-world", "ceiling", -132.0, -124.0,
|
||||
"64,-32,-136,0,45", "-96,128,-136,0,45", SW_CENTERS))
|
||||
selected_scenes = (SCENES if OPTIONS.scene == "all" else
|
||||
tuple(scene for scene in SCENES if scene[0] == OPTIONS.scene))
|
||||
for label, recipe, surface, window_min, window_max, camera0, camera1, centers in selected_scenes:
|
||||
low_mesh, _, low_ok = export(f"{label}-lod0", recipe, 0, camera0)
|
||||
failed |= not low_ok
|
||||
if low_ok:
|
||||
failed |= not terrace(label, recipe, surface, low_mesh, 0, window_min, window_max)
|
||||
|
||||
if not OPTIONS.lod0_only:
|
||||
detail_mesh, _, detail_ok = export(f"{label}-lod1", recipe, 1, camera1)
|
||||
failed |= not detail_ok
|
||||
if detail_ok:
|
||||
set_min, set_max = ((-121.625, -117.375) if label == "fi-floor"
|
||||
else (-128.375, -124.125))
|
||||
failed |= not lod1_set(label, recipe, surface, detail_mesh, centers,
|
||||
set_min, set_max)
|
||||
|
||||
full_mesh, _, full_ok = export(f"{label}-lod2-8", recipe, 8, None)
|
||||
failed |= not full_ok
|
||||
if full_ok:
|
||||
for lod in range(2, 9):
|
||||
center = LOD_CENTERS[label][lod]
|
||||
radius = 4 * (1 << lod)
|
||||
failed |= not terrace(label, recipe, surface, full_mesh, lod,
|
||||
window_min, window_max, center, radius)
|
||||
|
||||
# Keep the established L30 terrace and L31 flat-surface negative controls live.
|
||||
controls = (
|
||||
("l30-terrace", ROOT / "Saved/Linux/l32/fi-floor-l30-camera-negative.vfmesh",
|
||||
-130.0, -110.0),
|
||||
("l31-flat", ROOT / "Saved/Linux/l31/fi-floor-v19.vfmesh", -122.0, -117.0),
|
||||
)
|
||||
for name, mesh, height_min, height_max in (() if OPTIONS.skip_controls else controls):
|
||||
result, output, _ = run(f"control-{name}", ["python3", TERRACE, mesh,
|
||||
"--surface=floor", "--center", "64", "-32", "--radius=32", "--lod=0",
|
||||
f"--height-min-m={height_min}", f"--height-max-m={height_max}"])
|
||||
statuses = TERRACE_RESULT.findall(output)
|
||||
detected = (result is not None and result.returncode == 1 and statuses == ["FAIL"])
|
||||
failed |= not detected
|
||||
print(f"[L37RingControl] name={name} detected={detected} "
|
||||
f"status={'PASS' if detected else 'FAIL'}", flush=True)
|
||||
|
||||
scene_tag = OPTIONS.scene
|
||||
lod_tag = "lod0" if OPTIONS.lod0_only else "lod0-8"
|
||||
summary = OUT / f"ring-audits-{scene_tag}-{lod_tag}.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("scene", "recipe", "surface", "lod", "elapsed_seconds", "status"))
|
||||
writer.writerows((label, recipe, surface, lod, f"{elapsed:.3f}",
|
||||
"PASS" if ok else "FAIL")
|
||||
for label, recipe, surface, lod, elapsed, ok in rows)
|
||||
expected_runs = (1 if OPTIONS.lod0_only else 9) * len(selected_scenes)
|
||||
expected_controls = 0 if OPTIONS.skip_controls else len(controls)
|
||||
print(f"[L37RingSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} controls={expected_controls} "
|
||||
f"status={'FAIL' if failed else 'PASS'} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != expected_runs else 0)
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,54 @@
|
||||
# Linux39 verification report
|
||||
|
||||
## Tier results
|
||||
|
||||
| Tier | Result | Wall time | Coverage |
|
||||
|---|---:|---:|---|
|
||||
| Fast | PASS | 161.0 s | Canonical owner only; resolved FI Style 2 / SW Style 1, passage ripple and coverage, adjacent-profile step, W1/W12 hashes, LOD0 rings and controls. |
|
||||
| Medium | PASS | 472.9 s | Clang style overrides, transitions, L13/L9 proofs, LOD0–8 rings, owner shape/crease/fragments and captures. |
|
||||
| Full | PASS | 1573.7 s (26m 14s) | Revalidated medium evidence, GCC supplement, 30 determinism cells, and five interleaved cost pairs per recipe. |
|
||||
|
||||
Fast is below the five-minute limit. The five-pair median cost change was +4.12% for Floating Islands (17.053 s L36, 17.756 s L39) and +0.94% for SurfaceWorld (1.800 s L36, 1.817 s L39). All commands used the CPU 0–11 / 6 GiB limits; the full tier had a 29-minute timeout.
|
||||
|
||||
## Source stamp and owner identity
|
||||
|
||||
`BuildLinux.sh` hashes a sorted manifest covering Core inputs, standalone pipeline sources and generated wrappers, build files, and the source-stamp helper. It includes the Unreal adapter when that adapter is enabled for compilation. The manifest SHA-256 is compiled as `VOXELFORGE_SOURCE_STAMP`; headless and capture executables print it as their first runtime line. The runner compares the expected stamp before and after execution, probes every available binary at both ends, and stops on a missing or mismatched stamp. Summaries record the stamp, binary SHA-256, input JSON SHA-256, resolved recipe, style and layout ID.
|
||||
|
||||
Current stamp: `644cd7ed510034b1855f249cc1dba338da2336c4071069792663c88810b59b90`. The canonical owner JSON SHA-256 is `7b095f9bcc6b90133d5a463f99f0b571133b2c549ce1ecf2d64fe9175e41e183`, unchanged from L37. FI and SW each produced 12 matching W1/W12 hashes, and both signatures match the saved L37 controls. No Core geometry source changed; C++ edits add build stamps and resolved-layout evidence.
|
||||
|
||||
The instrumented L36 cost baseline was exported from commit `8be1c4d` and carries its own recorded stamp: `28c74aac57643360cb563e07bae278e2e14eceda0b63a3a50cf5c9876003e765`.
|
||||
|
||||
## Negative controls
|
||||
|
||||
| Control | Evidence |
|
||||
|---|---|
|
||||
| Core edit after build | Runner refused the stale binary; original source bytes were restored. |
|
||||
| Style-0 fixture named `owner_world.json` | Resolved snapshot exposed FI Style 0; the owner assertion requiring Style 2 rejected it. |
|
||||
| All-black JPEG | Rejected at stddev 0 and coverage 0 (floors: 2.0 and 0.001). |
|
||||
| L30 terrace rings | Rejected: 3.125 m flat run followed by a 0.243 m step. |
|
||||
| L31 flattened floor | Rejected: amplitude range 0.000252 m. |
|
||||
| L35 wall ribs / L36 floor washboard | Detected amplitudes 0.0918 m / 0.0943 m against 0.035 m / 0.015 m limits. |
|
||||
| L38 owner step | Detected 0.2443 m against the 0.100 m adjacent-profile limit. Fitted period 7.875 m is diagnostic only. |
|
||||
|
||||
## Cleanup and remaining diagnostic
|
||||
|
||||
The duplicate L13/L9 runner was removed. The crease/fragment audit now decodes each owner mesh once for fragment, near-crease and overview scans. Style-override artifacts use `style-override-*`; `owner` runs use the canonical input with asserted resolved styles. Capture quality decodes pixels and enforces variance and coverage floors, including on JPEGs.
|
||||
|
||||
The current FI owner has six small fragments. That count remains diagnostic because a strict zero-fragment gate fails on the L37 owner; a baseline-backed fragment threshold remains open. The merged audit still reports the count alongside near and overview crease results.
|
||||
|
||||
## Failed attempts
|
||||
|
||||
- `full-final` stopped after determinism with a missing `VerificationRunner.bounded_run` method reference; switched to the shared bounded runner.
|
||||
- `full-final2` could not find the historical temp tree's `Tools/verify` directory; created it before copying the stamp helper and computed the stamp from the temp repository root.
|
||||
- `full-final3` was interrupted during an expected long FI W1 cell after the log went quiet; rerun allowed the cell to finish.
|
||||
- `full-final4` passed all 30 determinism cells, then exposed an expected-name typo for the L36 binary (`...-stamped-clang` vs `...-stamped`). The corrected full run passed the baseline build and all cost pairs.
|
||||
|
||||
How-to instructions are in [`Standalone/Tools/verify/README.md`](../../../Tools/verify/README.md). Logs and JSON summaries are in `Saved/Linux/l39/`.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted. I re-ran `verify.py fast` myself on the final tree: PASS in 162.7 s, the stamp matched, owner hashes match
|
||||
L37 for both recipes at W1/W12, and the resolved style map asserted {FI: 2, SW: 1}. Note for readers: the
|
||||
`style=[2]` on SurfaceWorld rows is the style of the single FI→SW passage, which belongs to the upper (FI)
|
||||
strate. The owner world has no SW-owned passage, so "SW Style 1" never shapes a tunnel today. Earlier rounds'
|
||||
"owner-sw Style 1" captures show the lower end of the Style 2 passage.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Linux40 — immutable field binding
|
||||
|
||||
## Change
|
||||
|
||||
`ShapeWorldInternal.h` now has concrete immutable SurfaceWorld, FloatingIslands and legacy adapters
|
||||
with a common `densityAt(x,y,z,out)` call shape. `FGeneratorFieldAdapterV1` stores them in a variant,
|
||||
so hot sampling uses concrete dispatch without a virtual call. FI retains its fixed-point sampler and
|
||||
captures the legacy boundary parameters while binding.
|
||||
|
||||
`BindResolvedStratumFields` creates a sorted table of resolved strate bounds, plan references and
|
||||
evaluator objects during world construction/initialization. Native SurfaceWorld and FI sampling,
|
||||
boundary composition lookup, legacy SurfaceWorld bridging and `ClassifyBoxDetailed` select from that
|
||||
table. The manager layout width is captured once for the legacy-compatible chunk projection.
|
||||
|
||||
## Lookup boundary and identity
|
||||
|
||||
The direct manager selection lookups in native SurfaceWorld/FI evaluation and box classification are
|
||||
removed. `DensityAt` now selects native fields through the bound table. Legacy
|
||||
`UVoxelGenerator::GetDensityAt` still performs its own manager routing when reached through the
|
||||
legacy bridge or an out-of-plan fallback. Manager-derived layout width/FI compatibility parameters
|
||||
are captured at bind time; manager access also remains in build/planning helpers. Generator-specific
|
||||
boundary and passage policy still lives in `FStandaloneShapeWorld`.
|
||||
|
||||
No density arithmetic, fixed-point conversion order, evaluator version or Unreal adapter changed.
|
||||
The canonical owner FI and SurfaceWorld hashes are identical to L37/L39 for W1 and W12. The owner
|
||||
layout IDs also match L39: FI `b7a7d9ecc48d8f26`, SurfaceWorld `8a52e84c26ed92d2`. Fast logs and hash
|
||||
summaries: `Saved/Linux/l40/fast-trial1.log` and `Saved/Linux/l40/fast-summary.json`.
|
||||
|
||||
## Cost
|
||||
|
||||
Five interleaved pairs against the L39 Clang binary, using the canonical owner recipes:
|
||||
|
||||
| Recipe | L39 median | L40 median | Median change | Paired changes (L40 vs L39) |
|
||||
|---|---:|---:|---:|---|
|
||||
| FloatingIslands | 17.556 s | 17.440 s | −0.66% | +2.26%, −3.99%, −0.66%, −0.87%, −0.47% |
|
||||
| SurfaceWorld | 1.829 s | 1.805 s | −1.31% | −2.64%, −0.22%, +0.55%, −0.44%, −1.48% |
|
||||
|
||||
Both deltas are within observed pair noise; no per-voxel regression was measured. Raw output is in
|
||||
`Saved/Linux/l40/cost-l40-vs-l39-run.log` and `cost-l40-vs-l39-summary.json`.
|
||||
|
||||
## Verification
|
||||
|
||||
| Tier | Result | Wall time | Evidence |
|
||||
|---|---|---:|---|
|
||||
| Fast | PASS | 162.0 s | Canonical owner hashes match L37 for both recipes at W1/W12; all controls pass. |
|
||||
| Medium | PASS | 468.6 s | Transition, route/proof, LOD0–8 ring, owner shape and capture matrix pass. |
|
||||
| Full | TIMED OUT | 1740 s (29 min) | GCC build and supplement passed; L25 and crossing24 determinism passed for both compilers, and Clang upper-SW style0 W1/W8/W12 passed. The run timed out during the GCC upper-SW style0 W1 cell; no full summary or cost-baseline/pair results were produced. |
|
||||
|
||||
Clang and GCC candidate builds succeeded with source stamp
|
||||
`f35bc59c6172370e98dc83ec8ee5ba51dfab83a3faf075c26189fc83172f4ac8`. The full run used the requested
|
||||
CPU/memory limits and a 1740-second timeout. At the stop, host load was 20.5. The partial log is
|
||||
`Saved/Linux/l40/full-final.log`; a `full-summary.json` was not written. Other tier and build logs are
|
||||
under `Saved/Linux/l40/`.
|
||||
|
||||
## Failed attempts
|
||||
|
||||
- The first Clang build passed `unique_ptr` objects to adapter constructors expecting raw observer
|
||||
pointers; corrected with `.get()`, then the build passed.
|
||||
- Two early cost-harness launches failed before starting measurements: one shell quoting error and
|
||||
two Python `-c` quoting errors. The later logged five-pair run completed successfully.
|
||||
- The single full-tier run reached its 1740-second timeout while the GCC upper-SW fixture style0 W1
|
||||
determinism cell was still running. The cell has no completion record; the run returned timeout
|
||||
status 124. No full-tier L36 baseline build or full-tier cost pairs ran.
|
||||
|
||||
No Unreal compile was run; no Unreal adapter or `VoxelWorld.cpp` source change was required.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted. The full tier had timed out here under load (two Lunas plus host load of about 20). I re-ran it alone on
|
||||
the final source (stamp `f35bc59c…`): **PASS, 407 gates, 2857 s.** Its built-in cost comparison is against
|
||||
**L36** and shows FI +3.03% (all five pairs positive) and SW +0.38%. That FI delta is L37's mouth geometry; this
|
||||
round's own L39→L40 pairs are −0.66% FI and −1.31% SW. Two follow-ups for the tool:
|
||||
- the full tier's cost baseline is hard-coded to L36; it should follow the previous commit;
|
||||
- the full tier now needs about 48 minutes including builds, so its timeout must be above 29 minutes.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Linux 41 — resolved metre layout
|
||||
|
||||
Migration step 3 is implemented as a pure refactor. The verifier, manager and Core adapter changes are in the working tree; nothing was committed or pushed.
|
||||
|
||||
## Tool changes
|
||||
|
||||
Full-tier cost now builds a stamped baseline from `HEAD` by default; `--baseline <commit>` selects another commit. The build uses a temporary source tree. `--overall-timeout` defaults to 7,200 seconds for the tier. Each child process is killed at 1,800 seconds; builds remain capped at 12 jobs and the runner applies the requested CPU and address-space limits.
|
||||
|
||||
## Runtime authority
|
||||
|
||||
`UVoxelStrateManager::Initialize` resolves the configured layout width to integer micrometres before passage generation or scheduling. Slot height and gap are legacy chunk counts multiplied by that width. Each slot stores its physical `[bottom, top)` bounds and height, transition mode and width, copied passage settings and their micrometre dimensions. The effective seed is captured before passage generation. The Core adapter derives its interior bounds and L29 facing bands from those resolved values before work is scheduled.
|
||||
|
||||
The existing 8 m layout remains exact: the first upper edge is +8,000,000 µm; each height, gap or transition chunk is 8,000,000 µm. Chunk bounds are floor-based compatibility projections, including at negative Z. Season v2 still accepts only aligned, correctly stacked chunk bounds, then maps them to the same physical intervals. L29's bands keep their original faces: ceiling `[top-seal, top]`, content `[bottom+seal, top-seal]`, and bedrock `[bottom, bottom+seal]`; `seal` is the existing boundary-seal thickness converted from owner voxels to micrometres. Shared density and transition behavior are unchanged.
|
||||
|
||||
R6’s FI/SW split was already resolved: both plans and the snapshot use the same `LayoutChunkWidthSubvoxels` derived from the layout width. At the 25 cm Core voxel size, 8 m remains 32 Core voxels (8,192 subvoxels). Layout bounds do not use an owner/Core ratio. Ratios remain only in the legacy FloatingIslands parameter adapter. The legacy passage geometry adapter converts resolved passage micrometres back to owner voxels at its boundary.
|
||||
|
||||
Chunk indices remain in chunk-keyed compatibility APIs: `GetStrateForChunk`, generator and parameter queries, season-v2 validation, and legacy generator dispatch. They read the cached projection; world-Z lookup and Core snapshot bounds use physical intervals. L40’s bound Core field table remains the per-sample dispatch path and adds no manager lookup.
|
||||
|
||||
## Results
|
||||
|
||||
The final source stamp is `f35bc59c6172370e98dc83ec8ee5ba51dfab83a3faf075c26189fc83172f4ac8`; artifacts are in `Saved/Linux/l41`. The four 12-record FI/SW worker-1/worker-12 hash signatures match L40 exactly: FI `9820f4f38b189b3f7263810e7b3ff62aa502b21f476ffcfdbc7761a65cb07d58`; SW `a1e9268a4bbcc77d2a1ac54095df6851a70bab65c43ae50b332ed75132617834`. W1 and W12 also match each other. Layout IDs are FI `b7a7d9ecc48d8f26` and SW `8a52e84c26ed92d2`.
|
||||
|
||||
| Tier | Result |
|
||||
|---|---|
|
||||
| Fast | PASS, 163.1 s; 12/12 hashes for each recipe, worker parity and L40 identity pass. |
|
||||
| Medium | PASS, 469.0 s; all checks pass, including transition and passage gates. |
|
||||
| Full | PASS, 1,567.1 s; 107 runs and 539 checks, including the 30-cell determinism gate and cost pairs. |
|
||||
|
||||
Full cost compares against `HEAD` `efa9e89468cbab702c86fcbe451c8836167ae83a`. FI median is 17.627 s versus 17.665 s (−0.22%); SW is 1.819 s versus 1.821 s (−0.11%). The paired samples vary by up to +2.29% for FI and +1.27% for SW; neither median regressed.
|
||||
|
||||
Failed attempts: an initial verifier edit had an `IndentationError`; an overlapping fast run hit a mouth-profile failure, then the serial fast run passed. The first full attempt passed its gates but failed when the baseline build’s output name did not match the expected path. The corrected baseline name was rebuilt from `HEAD`; the final full run passed. Expected negative-control rejections passed their checks.
|
||||
|
||||
Unreal changes are limited to `VoxelStrateManager.h/.cpp` and `VoxelForgeCoreAdapter.cpp`; `CODEMAP.md` and `ARCHITECTURE.md` §8.51 are updated. The Unreal manager and adapter could not be compiled in this Linux workspace; validate them with the Windows Unreal build before integration.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted after a re-check. **The source stamp did not cover the plugin sources that `core_sources.txt` compiles into
|
||||
the Linux binaries** (for example `Source/VoxelForge/Private/VoxelStrateManager.cpp`). So L41's stamp equalled
|
||||
L40's, and the tiers could not prove that they ran the edited manager. Fixed in `source_stamp.py`: the whole plugin
|
||||
`Source/VoxelForge` tree and `Standalone/Compat` are now always stamped. Clean rebuild of both compilers (new stamp
|
||||
`15cd0a95…`): fast PASS (owner hashes identical to L37), medium PASS, full PASS (1569 s; cost vs HEAD FI +0.71%,
|
||||
SW +0.55%, within noise).
|
||||
|
||||
Owner decisions since R8 (2026-10-23): converted assets may change size slightly, so clean metre values beat
|
||||
lossless legacy conversion. Cache sharing between players is optional (opt-in, with a data budget).
|
||||
@@ -0,0 +1,56 @@
|
||||
# Linux 42 — resolved boundary capabilities
|
||||
|
||||
Migration step 6 is a Core-only pure refactor. No Unreal source changed.
|
||||
|
||||
## Policy records
|
||||
|
||||
`ResolvedLayoutSnapshotV3::finalize` resolves the existing generator and interface fields once into
|
||||
runtime capabilities. `ApplyResolvedStrateBoundaryBand` reads
|
||||
`lowerInterfaceCapabilities.preserveUpperBedrockAirGap` for the FI Gradient gap. Passage composition
|
||||
reads `passageCapabilities.hasSurfaceLanding` for the SW landing/bridge profile and crossing extent,
|
||||
`hasFloatingIslandLanding` for the FI route support, and `usePassageAnchorForColumns` for its fixed
|
||||
passage-column center. `surfaceToTunnelLandingSupport` records the SW-to-TunnelNetwork floor profile.
|
||||
The derived records are omitted from canonical JSON, so the resolved layout identity is unchanged.
|
||||
|
||||
`BindResolvedStratumFields` still checks `generatorKind` once to bind SurfaceWorld, FloatingIslands,
|
||||
or legacy evaluation. `finalize` maps that resolved kind to capabilities once. `PlanResolvedLayoutPassagesV1`
|
||||
still names SurfaceWorld/FloatingIslands for existing default mouth and anchor placement; that passage
|
||||
planning policy remains outside this boundary-composition step.
|
||||
|
||||
Ceiling → content → breakable bedrock order remains unchanged for every strate. The upper strate still
|
||||
controls Hard/Gradient/Interleaved. L29 transition sample arithmetic and the passage-mouth fade are
|
||||
unchanged.
|
||||
|
||||
## Hashes and cost
|
||||
|
||||
Source stamp: `a3a65e58a670399d9458aa2a9c99eddd7b541671f919a293f5b4086878e9eb78`.
|
||||
|
||||
FI layout ID `b7a7d9ecc48d8f26`; SW layout ID `8a52e84c26ed92d2`. All four 12-record FI/SW worker-1
|
||||
and worker-12 hash signatures match L41 exactly: FI
|
||||
`9820f4f38b189b3f7263810e7b3ff62aa502b21f476ffcfdbc7761a65cb07d58`; SW
|
||||
`a1e9268a4bbcc77d2a1ac54095df6851a70bab65c43ae50b332ed75132617834`. Worker-1 and worker-12 match
|
||||
each other for both recipes.
|
||||
|
||||
Full-tier five-pair whole-view-ready cost versus `HEAD` `1abee917215cd6f971e39dd3158cecf4c9992fbe`:
|
||||
FI median 18.164 s → 18.189 s (**+0.14%**); SW 1.888 s → 1.877 s (**−0.58%**). No measured cost
|
||||
regression.
|
||||
|
||||
## Verification
|
||||
|
||||
| Tier | Result |
|
||||
|---|---|
|
||||
| Fast | PASS, 163.2 s; owner hashes, worker parity, and ring gates pass. |
|
||||
| Medium | PASS, 469.9 s; transition modes, hole controls, route proofs, owner passage matrix, ring and capture gates pass. |
|
||||
| Full | PASS, 1,637.0 s; 107 runs and 539 checks, including Clang/GCC, 30-cell determinism and paired cost. |
|
||||
|
||||
Full used clean Clang 19 and GCC 14 builds from `Saved/Linux/l42/final-clean-build`, `-j12`, FP
|
||||
contraction off. The final source stamp was embedded in both binaries. All processes used CPUs 0–11,
|
||||
the 6 GiB address-space limit, and a 30-minute process timeout.
|
||||
|
||||
The first fast invocation stopped before running checks because the L42 binary had not yet been built;
|
||||
fast passed after the build. The mislabelled-owner and all-black capture negative controls are expected
|
||||
rejections and passed their verifier checks. No tier had an unexpected failed run.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted. The current tree stamp equals both L42 binaries (`a3a65e58…`), so the fast/medium/full evidence is from this exact source. Owner hashes are identical to L41. Passage planning (`PlanResolvedLayoutPassagesV1`) still names generators; that is a later step.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Linux 43 — Core landmark SDF stamps
|
||||
|
||||
Source stamp: `18b89fb48febf887c5b61e14c699c95535b09613f23618ca57271bc5d2a455f3` (clean Clang and GCC builds).
|
||||
|
||||
## Vocabulary and records
|
||||
|
||||
Stamp vocabulary v1 contains `carveEllipsoid`, `carveArch`, `fillPillar`, and `fillDais`. A resolved record carries a stable ID, strate index, position in integer micrometres, conservative bounds, half extents, smooth-CSG width, irregularity amplitude and wavelength, irregularity seed, and quarter-turn yaw. Bounds include the smoothing and roughness support.
|
||||
|
||||
Carves use smooth maximum with the negated SDF; fills use smooth minimum. Core keeps its density sign: negative is solid and positive is air. Bounded trilinear value noise adds seeded relief.
|
||||
|
||||
## Placement and culling
|
||||
|
||||
The resolver sorts requests by stable ID and derives placement offsets and irregularity from `(seed, strate, stable ID)`. It tries at most 512 deterministic candidates. The strict layout rejects bounds that intersect a passage mouth or walk corridor, touch a content face, or overlap a ceiling or bedrock band. No safe candidate is a placement error. The route and band negative controls were both rejected (`2/2`).
|
||||
|
||||
The stamp BVH queries conservative bounds once per tile. A tile with no candidates performs no stamp SDF evaluation. Stamp-intersecting boxes classify as mixed, and native-only readiness cannot skip them. With no requests, stamp fields are omitted from canonical JSON, preserving L42 identities and hashes.
|
||||
|
||||
## Owner demo and hashes
|
||||
|
||||
`--landmark-chamber-demo` is off by default. On seed 0 it adds three records to the owner SurfaceWorld strate (strate 1); no Unreal files changed:
|
||||
|
||||
- Ellipsoid chamber `landmark/sw-arrival-chamber-v1`: center `(-8.918, 31.950, -513.083) m`, half extents `(8.5, 7.5, 6.0) m`.
|
||||
- Arch `landmark/sw-arrival-chamber-entrance-v1`: center `(-12.289, 37.027, -513.311) m`, half extents `(2.6, 4.2, 3.2) m`.
|
||||
- Low dais `landmark/sw-arrival-chamber-dais-v1`: center `(-7.489, 30.688, -515.838) m`, half extents `(3.2, 2.8, 0.9) m`.
|
||||
|
||||
The chamber center is 30.9 m from the arrival mouth at `(9.25, 9.25, -502.608) m`, off the walk route. The active layout carries vocabulary version 1 and identity `resolved-layout-v3-fnv64:91eaf2f0248f2c8b`.
|
||||
|
||||
Without stamps, the FI/SW layout IDs remain `b7a7d9ecc48d8f26` / `8a52e84c26ed92d2`; the 12-record owner signatures remain L42's FI `9820f4f38b189b3f7263810e7b3ff62aa502b21f476ffcfdbc7761a65cb07d58` and SW `a1e9268a4bbcc77d2a1ac54095df6851a70bab65c43ae50b332ed75132617834`.
|
||||
|
||||
At the same chamber-centered camera, W12 hashed 343 tiles. Stamp off: density `0x18d6f6cb3498deff`, mesh `0x293c0ddff66b99f9`, payload `0xeb07403f34d60134`, topology `0x002905a3f028c9f1`. Stamp on: density `0xbf8af855ee5db8fb`, mesh `0xbb64c203dc533070`, payload `0xd14d45e6b9faa9c0`, topology `0x44f1b422df20a159`. The stamp-on W1 and W12 signatures match.
|
||||
|
||||
## Safety, cost, and tiers
|
||||
|
||||
With the demo on, passage walkability passed `1545/1545`; the passage audit passed; endpoint probes passed `27/27`; the interface audit passed with `hole_columns=0` and `air_samples=0`.
|
||||
|
||||
For outside-stamp tiles, five W1 pairs over 343 tiles measured median whole-view-ready `0.457 s` off / `0.466 s` on (`+1.97%`); paired changes ranged from `−2.76%` to `+3.08%`. Five W12 pairs over 125 tiles measured `0.189 s` / `0.191 s` (`+1.06%`). These tile bounds do not intersect a stamp, so the BVH returns no stamp candidates and no stamp SDF runs. The small timing deltas sit within pair-to-pair variation. Full-tier five-pair cost versus `HEAD`: FI `17.342 s` → `17.495 s` (`+0.88%`); SW `1.799 s` → `1.805 s` (`+0.33%`).
|
||||
|
||||
| Tier | Result |
|
||||
|---|---|
|
||||
| Fast | PASS, 161.3 s, 41 checks |
|
||||
| Medium | PASS, 468.3 s |
|
||||
| Full | PASS, 1551.2 s, 539 checks; clean Clang/GCC build, 30-cell determinism, cost pairs |
|
||||
|
||||
## Captures
|
||||
|
||||
All JPEGs passed `capture_quality.py` and are below 300 KB:
|
||||
|
||||
- [Entrance, eye height — 27 KB](captures/chamber-entrance.jpg)
|
||||
- [Inside — 29 KB](captures/chamber-inside.jpg)
|
||||
- [Overview — 43 KB](captures/chamber-overview.jpg)
|
||||
|
||||
## Failed runs
|
||||
|
||||
- An early route-control point hit the mouth guard before the corridor guard; its position was moved so the final route and band controls exercise the intended rejection checks.
|
||||
- The first JSON round trip lost precision in a 64-bit irregularity seed. Seeds are now kept within the JSON number type's exact 53-bit integer range; canonical round-trip passes.
|
||||
- A clip-plane capture was rejected by the renderer (`open_contours=10`); the final three views use the uncut mesh.
|
||||
- A supplemental stamp-off, chamber-centered W1/W12 aggregate differed reproducibly (`density 0x68f495ba62aace05` / `0x18d6f6cb3498deff`), though its nine exact-query probes matched. The standard owner fast/full determinism checks pass, and the stamp-on chamber-centered aggregate matches W1/W12.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Merged as **infrastructure, off by default**: the stamp vocabulary, records, placement guards and BVH culling are
|
||||
sound, the owner hashes are unchanged with no requests, and the route/band controls reject. Not accepted as a
|
||||
**player-visible** landmark yet:
|
||||
- the demo chamber is **buried**: its top is about 4.5 m below the SW ground near the arrival mouth, with no opening
|
||||
to the walkable surface, so a player can never see it;
|
||||
- the arch reads as a boxy notch with a sharp vertical corner;
|
||||
- the entrance capture is unreadable.
|
||||
|
||||
**Determinism finding (pre-existing, not caused by L43).** The "supplemental stamp-off W1/W12 mismatch" in the
|
||||
failed-runs list is real. I reproduced it on committed L42 and on L37 (`density 0x68f495ba…` at W1 vs
|
||||
`0x18d6f6cb…` at W12, camera `(-8.918, 31.950, -513.083)`, `--max-clip-level=0 --render-distance-chunks=0`). S14 is
|
||||
stable there. It is a worker-count-dependent result in the production path, invisible to our tiers, which only test
|
||||
the spawn camera and the 24 m crossing. It is the top priority for L44.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,18 @@
|
||||
# Linux L44 — worker-count determinism
|
||||
|
||||
- **Bisect:** L26 (`37a2230`) is stable; L27 (`07b3fe2`) is the first bad commit. L28 retains the same mismatch.
|
||||
- **Difference:** at LOD0, six tiles differ: x `-3..-1`, y `2..3`, z `-64`. Density, mesh, payload, and topology hashes all differ. The exact-camera W1 density hash was `0x68f495ba62aace05`; W12 was `0x18d6f6cb3498deff`.
|
||||
- **Cause:** L27 added large passage support columns, but `World::isTileShapeFree` only bounded the route and bridge. It incorrectly approved tiles touched by the columns for early native-only generation. Depending on scheduling, a tile used that incomplete field or the compiled field. The direct exact-query hashes remained identical.
|
||||
- **Fix:** extend the shared shape-free proof with a conservative envelope for every possible support column, including its anchor/route center, bore, wall, flare, and vertical span. Affected tiles wait for the compiled shape; no worker ordering or serialization changed.
|
||||
- **Output/version:** the compiled world is canonical. At the reported camera it hashes to density `0x68f495ba62aace05`, mesh `0x539885cd589dc985`, payload `0x88a411b8697278ec`, topology `0xd24b13cef6bfdeb4` for all worker counts. This is the old W1 variant; the old W12 variant was the incomplete native-only result. The compiled field did not change, so no version bump is needed.
|
||||
- **Sweep:** fixed SplitMix64 seed `0x4c3434`; camera 0 jitters around the failing camera, later cameras sample distant SurfaceWorld and FloatingIslands strata. Fast: 3 cameras, both recipes, settings `(clip-radius,max-clip-level,max-level)=(3,0,0)`. Medium/full: 5 cameras and settings `(3,0,0)`, `(2,1,1)`, `(3,2,1)`, W1/W8/W12. Full adds GCC; medium supplies Clang. Before the fix, fast failed on its first camera with the six LOD0 tiles above. After: fast PASS (172.9s), medium PASS (545.9s), full PASS (1635.9s); every per-tile density/mesh/payload/topology comparison matched.
|
||||
- **Cost:** five full-tier pairs against HEAD (`422dd776`): median whole-view time changed `+0.65%` for FloatingIslands and `+0.40%` for SurfaceWorld, within run noise.
|
||||
- **Failed runs:** the pre-fix fast failure was expected. The first medium configuration `(4,2,2)` exceeded the 160-million-node shape-build guard at 2,479 tiles; `(3,2,1)` covers max-clip-level 2 at 960 tiles and passed. The first Clang build caught a scope error in the proof loop, corrected before the passing build.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted. The current tree stamp equals the L44 binaries (`bf2c835c…`). The original repro now gives density
|
||||
`0x68f495ba62aace05` at W1, W4 and W12. The compiled field is unchanged, so this is not a world-version change. It
|
||||
also explains a player-visible symptom: depending on thread timing, tiles touched by L27's support columns could be
|
||||
generated **without** the column. The new seeded random-camera sweep in every tier is the lasting value here; it
|
||||
fails on the bug and passes on the fix.
|
||||
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture the L45 landmark views and audit player-scale built-shape limits."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
VERIFY = ROOT / "Standalone/Tools/verify"
|
||||
LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37"
|
||||
sys.path.insert(0, str(VERIFY))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def load_module(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"cannot import {path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
PROFILE = load_module("l45_ring_profile", ROOT /
|
||||
"Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py")
|
||||
SHAPE = load_module("l45_shape_audit", LINUX37 / "AuditMouthShapeL37.py")
|
||||
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--layout", type=Path, required=True)
|
||||
PARSER.add_argument("--artifact-dir", type=Path, required=True)
|
||||
PARSER.add_argument("--capture-dir", type=Path, required=True)
|
||||
PARSER.add_argument("--data", type=Path, default=ROOT / "Standalone/data/owner_world.json")
|
||||
OPTIONS = PARSER.parse_args()
|
||||
|
||||
ARTIFACT_DIR = OPTIONS.artifact_dir.resolve()
|
||||
CAPTURE_DIR = OPTIONS.capture_dir.resolve()
|
||||
LAYOUT_PATH = OPTIONS.layout.resolve()
|
||||
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
CAPTURE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
PNG_DIR = ARTIFACT_DIR / "capture-png"
|
||||
PNG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
HEADLESS = Path(os.environ["VOXELFORGE_L37_HEADLESS"])
|
||||
RENDERER = Path(os.environ["VOXELFORGE_L37_RENDERER"])
|
||||
EXPECTED_STAMP = current_stamp()
|
||||
|
||||
|
||||
def run_logged(name: str, command: list[str], *, capture: bool = False) -> str:
|
||||
log = ARTIFACT_DIR / f"{name}.log"
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = bounded_run(command, timeout=1800, stdout=stream)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if result is None:
|
||||
raise RuntimeError(f"{name} timed out; see {log}")
|
||||
print(f"[L45CaptureRun] step={name} elapsed={elapsed:.1f}s "
|
||||
f"exit={result.returncode} log={log}", flush=True)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{name} failed; see {log}")
|
||||
if capture and (first_stamp(output) != EXPECTED_STAMP
|
||||
or first_stamp(bounded_run(
|
||||
[str(command[0]), "--print-source-stamp"], timeout=15).stdout or "")
|
||||
!= EXPECTED_STAMP):
|
||||
raise RuntimeError(f"{name} executable source stamp mismatch")
|
||||
return output
|
||||
|
||||
|
||||
def stamp_by_id(layout: dict, stable_id: str) -> dict:
|
||||
for stamp in layout.get("stamps", []):
|
||||
if stamp.get("stable_id") == stable_id:
|
||||
return stamp
|
||||
raise RuntimeError(f"missing stamp {stable_id}")
|
||||
|
||||
|
||||
def metres(stamp: dict, key: str = "position_um") -> tuple[float, float, float]:
|
||||
return tuple(value / 1_000_000.0 for value in stamp[key])
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
dx, dy, dz = (target[index] - camera[index] for index in range(3))
|
||||
return (math.degrees(math.atan2(dx, dy)),
|
||||
math.degrees(math.atan2(dz, math.hypot(dx, dy))))
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def camera_spec(camera, target):
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
|
||||
if layout.get("stamp_vocabulary_version") not in (1, 2):
|
||||
raise RuntimeError("L45 capture needs a prior resolved L45 stamp layout")
|
||||
entrance_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-ramp-000")
|
||||
next_ramp_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-ramp-001")
|
||||
portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1")
|
||||
chamber_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1")
|
||||
floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor")
|
||||
dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais")
|
||||
entrance = metres(entrance_stamp)
|
||||
next_ramp = metres(next_ramp_stamp)
|
||||
portal = metres(portal_stamp)
|
||||
chamber = metres(chamber_stamp)
|
||||
floor = metres(floor_stamp)
|
||||
floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
dais = metres(dais_stamp)
|
||||
dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
ground_z = entrance[2] - 1.6
|
||||
direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1])
|
||||
direction = ((next_ramp[0] - entrance[0]) / direction_length,
|
||||
(next_ramp[1] - entrance[1]) / direction_length)
|
||||
outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1])
|
||||
outward = ((entrance[0] - chamber[0]) / outward_length,
|
||||
(entrance[1] - chamber[1]) / outward_length)
|
||||
mouth_normal = (-direction[1], direction[0])
|
||||
|
||||
mesh = ARTIFACT_DIR / "landmark-l45.vfmesh"
|
||||
export_camera = ((entrance[0] + chamber[0]) * 0.5,
|
||||
(entrance[1] + chamber[1]) * 0.5,
|
||||
(ground_z + floor_top) * 0.5)
|
||||
export_target = (chamber[0], chamber[1], floor_top)
|
||||
run_logged("landmark-export", [str(HEADLESS), str(OPTIONS.data), "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--recipe=surface-world", "--landmark-chamber-demo",
|
||||
f"--camera={fmt(camera_spec(export_camera, export_target))}",
|
||||
f"--mesh-export={mesh}", f"--resolved-layout-output={LAYOUT_PATH}"], capture=True)
|
||||
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
|
||||
if layout.get("stamp_vocabulary_version") != 2:
|
||||
raise RuntimeError("L45 export did not resolve ellipsoidal-dais stamp vocabulary v2")
|
||||
entrance_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-ramp-000")
|
||||
next_ramp_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-ramp-001")
|
||||
portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1")
|
||||
chamber_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1")
|
||||
floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor")
|
||||
dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais")
|
||||
entrance = metres(entrance_stamp)
|
||||
next_ramp = metres(next_ramp_stamp)
|
||||
portal = metres(portal_stamp)
|
||||
chamber = metres(chamber_stamp)
|
||||
floor = metres(floor_stamp)
|
||||
floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
dais = metres(dais_stamp)
|
||||
dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
ground_z = entrance[2] - 1.6
|
||||
direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1])
|
||||
direction = ((next_ramp[0] - entrance[0]) / direction_length,
|
||||
(next_ramp[1] - entrance[1]) / direction_length)
|
||||
outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1])
|
||||
outward = ((entrance[0] - chamber[0]) / outward_length,
|
||||
(entrance[1] - chamber[1]) / outward_length)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"missing mesh export {mesh}")
|
||||
|
||||
voxel_um, triangles, tile_count = PROFILE.read_scene(str(mesh), 0,
|
||||
floor_top - 8.0, entrance[2] + 9.0,
|
||||
(min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0,
|
||||
min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0))
|
||||
bounds = (min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0,
|
||||
min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0)
|
||||
buckets = PROFILE.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||
ground_candidates = []
|
||||
for ix in range(-20, 21):
|
||||
for iy in range(-20, 21):
|
||||
x, y = portal[0] + ix * 0.5, portal[1] + iy * 0.5
|
||||
dx, dy = x - portal[0], y - portal[1]
|
||||
distance = math.hypot(dx, dy)
|
||||
if not 6.0 <= distance <= 10.0:
|
||||
continue
|
||||
hit = PROFILE.sample_surface(buckets, x, y, True, 1.0,
|
||||
preferred_height=ground_z)
|
||||
if hit is None or abs(hit[0] - ground_z) > 3.0 or hit[1][2] < 0.85:
|
||||
continue
|
||||
alignment = max(-1.0, min(1.0,
|
||||
(dx * mouth_normal[0] + dy * mouth_normal[1]) / distance))
|
||||
angle = math.acos(alignment)
|
||||
score = abs(distance - 7.0) + angle * 4.0
|
||||
ground_candidates.append((score, x, y, hit[0]))
|
||||
if not ground_candidates:
|
||||
raise RuntimeError("no walkable mesh ground lies 6 to 10 m from the cave mouth")
|
||||
_, ground_x, ground_y, outside_ground = min(ground_candidates)
|
||||
inside_floor = PROFILE.sample_surface(buckets, chamber[0], chamber[1], True, 1.0,
|
||||
preferred_height=floor_top)
|
||||
if inside_floor is None or abs(inside_floor[0] - floor_top) > 1.5:
|
||||
raise RuntimeError(f"chamber floor mesh hit {inside_floor} does not match designed floor {floor_top:.3f} m")
|
||||
floor_top = inside_floor[0]
|
||||
|
||||
ground_camera = (ground_x, ground_y, outside_ground + 1.6)
|
||||
entry_ground = PROFILE.sample_surface(buckets, entrance[0], entrance[1], True, 1.0,
|
||||
preferred_height=ground_z)
|
||||
if entry_ground is None:
|
||||
raise RuntimeError("cave mouth has no walkable ground surface in the exported mesh")
|
||||
entry_camera = (entrance[0] + mouth_normal[0] * 3.8,
|
||||
entrance[1] + mouth_normal[1] * 3.8, ground_z + 1.6)
|
||||
entry_ramp_stamps = sorted((stamp for stamp in layout.get("stamps", [])
|
||||
if stamp.get("stable_id", "").startswith("landmark/sw-arrival-chamber-ramp-")),
|
||||
key=lambda stamp: stamp["stable_id"])
|
||||
if not entry_ramp_stamps:
|
||||
raise RuntimeError("resolved layout has no chamber ramp stamps")
|
||||
entry_target_stamp = entry_ramp_stamps[min(8, len(entry_ramp_stamps) - 1)]
|
||||
entry_target_center = metres(entry_target_stamp)
|
||||
entry_target = (entry_target_center[0],entry_target_center[1],entry_target_center[2]-1.2)
|
||||
to_dais = (dais[0] - chamber[0], dais[1] - chamber[1])
|
||||
to_dais_length = math.hypot(*to_dais)
|
||||
opposite_dais = (-to_dais[0] / to_dais_length, -to_dais[1] / to_dais_length)
|
||||
inside_x = chamber[0]
|
||||
inside_y = chamber[1]
|
||||
inside_floor = PROFILE.sample_surface(buckets, inside_x, inside_y, True, 1.0,
|
||||
preferred_height=floor_top)
|
||||
inside_eye_z = inside_floor[0] + 1.6
|
||||
chamber_camera = (chamber[0] + opposite_dais[0] * 5.5,
|
||||
chamber[1] + opposite_dais[1] * 5.5, inside_eye_z)
|
||||
chamber_target = (dais[0], dais[1], dais_top - 0.25)
|
||||
overview_camera = (entrance[0] + outward[0] * 18.0,
|
||||
entrance[1] + outward[1] * 18.0, outside_ground + 15.0)
|
||||
overview_target = ((entrance[0] + chamber[0]) * 0.5,
|
||||
(entrance[1] + chamber[1]) * 0.5,
|
||||
(ground_z + floor_top) * 0.5)
|
||||
inward = (-outward[0], -outward[1])
|
||||
clip_point = (entrance[0] + outward[0] * 0.5,
|
||||
entrance[1] + outward[1] * 0.5)
|
||||
overview_clip = (inward[0], inward[1], 0.0,
|
||||
-(inward[0] * clip_point[0] + inward[1] * clip_point[1]))
|
||||
|
||||
views = (
|
||||
("ground-entrance", ground_camera, (portal[0], portal[1], portal[2]),
|
||||
(portal[0], portal[1], ground_z, 20.0), -509.0),
|
||||
("entrance-looking-in", entry_camera, entry_target, (entrance[0], entrance[1], entrance[2], 16.0), -507.0),
|
||||
("inside-to-dais", chamber_camera, chamber_target,
|
||||
(chamber[0], chamber[1], chamber[2], 15.0), chamber[2]),
|
||||
("overview-entrance-and-chamber", overview_camera, overview_target,
|
||||
((entrance[0] + chamber[0]) * 0.5,
|
||||
(entrance[1] + chamber[1]) * 0.5,
|
||||
(ground_z + floor_top) * 0.5, 34.0), -509.0),
|
||||
)
|
||||
|
||||
for name, camera, target, focus, height_center in views:
|
||||
png = PNG_DIR / f"{name}.png"
|
||||
jpg = CAPTURE_DIR / f"{name}.jpg"
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
fov = (80.0 if name == "overview-entrance-and-chamber" else
|
||||
70.0 if name in ("inside-to-dais", "entrance-looking-in") else 65.0
|
||||
if name == "ground-entrance" else 60.0)
|
||||
if name == "ground-entrance":
|
||||
fov = 55.0
|
||||
print(f"[L45CaptureCamera] view={name} camera_m=({fmt(camera)}) "
|
||||
f"target_m=({fmt(target)}) yaw_deg={yaw:.3f} pitch_deg={pitch:.3f} "
|
||||
f"fov_deg={fov:.1f} entrance_distance_m={math.dist(camera, portal):.3f}", flush=True)
|
||||
render = [str(RENDERER), str(mesh), str(png), "--size=1600x900", f"--fov-deg={fov:.1f}",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--shade=readable", "--headlamp=on", "--height-tint=on",
|
||||
"--light-direction=0.3,-0.4,0.85",
|
||||
f"--height-center-m={(floor_top if name == 'inside-to-dais' else height_center):.6f}",
|
||||
f"--height-range-m={4 if name == 'inside-to-dais' else 16}",
|
||||
"--tile-skirts=off", "--tile-edges=off",
|
||||
f"--focus-region={fmt(focus)}"]
|
||||
if name == "overview-entrance-and-chamber":
|
||||
render.append(f"--clip-plane={fmt(overview_clip)}")
|
||||
run_logged(f"{name}-render", render, capture=True)
|
||||
for quality in (4, 6, 8):
|
||||
run_logged(f"{name}-jpeg-q{quality}", ["ffmpeg", "-nostdin", "-hide_banner",
|
||||
"-loglevel", "error", "-y", "-i", str(png), "-frames:v", "1",
|
||||
"-q:v", str(quality), str(jpg)])
|
||||
if jpg.is_file() and 0 < jpg.stat().st_size <= 300_000:
|
||||
break
|
||||
if not jpg.is_file() or not 0 < jpg.stat().st_size <= 300_000:
|
||||
raise RuntimeError(f"{jpg.name} exceeds the 300 KB capture limit")
|
||||
quality_log = run_logged(f"{name}-quality", [sys.executable,
|
||||
str(VERIFY / "capture_quality.py"), str(jpg)])
|
||||
print(f"[L45CaptureJPEG] file={jpg} bytes={jpg.stat().st_size} "
|
||||
f"quality_log={quality_log.strip().replace(chr(10), ' ')} status=PASS", flush=True)
|
||||
|
||||
|
||||
def region_triangles(center, radius_xy, z_min, z_max):
|
||||
selected = []
|
||||
for triangle in triangles:
|
||||
points = tuple(vertex[0] for vertex in triangle[2])
|
||||
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
||||
if (math.hypot(centroid[0] - center[0], centroid[1] - center[1]) <= radius_xy
|
||||
and z_min <= centroid[2] <= z_max):
|
||||
selected.append(triangle)
|
||||
return selected
|
||||
|
||||
|
||||
def audit_shape(name, region, center):
|
||||
metrics = SHAPE.analyze_triangles(region, center[:2], center[2],
|
||||
crop_to_center=False, include_sides=True)
|
||||
passed = (metrics["triangles"] > 0
|
||||
and metrics["planar_area"] < SHAPE.PLANE_LIMIT_M2
|
||||
and metrics["straight"] <= SHAPE.STRAIGHT_LIMIT_M
|
||||
and metrics["corners"] == 0)
|
||||
print(f"[LandmarkBuiltShape] region={name} triangles={metrics['triangles']} "
|
||||
f"planar_patch_m2={metrics['planar_area']:.4f} "
|
||||
f"straight_run_m={metrics['straight']:.4f} sharp_corners={metrics['corners']} "
|
||||
f"corner_turn_max_deg={metrics['corner_turn_max']:.2f} "
|
||||
f"thresholds=({SHAPE.PLANE_LIMIT_M2:.2f}m2,{SHAPE.STRAIGHT_LIMIT_M:.2f}m,"
|
||||
f"{SHAPE.CORNER_TURN_LIMIT_DEG:.0f}deg) status={'PASS' if passed else 'FAIL'}",
|
||||
flush=True)
|
||||
if not passed:
|
||||
raise RuntimeError(f"L37 built-shape thresholds failed in {name}")
|
||||
return metrics
|
||||
|
||||
|
||||
entrance_metrics = audit_shape("cave-mouth", region_triangles(
|
||||
portal, 5.0, ground_z + 0.8, ground_z + 4.5),
|
||||
(portal[0], portal[1], ground_z))
|
||||
dais_surface = PROFILE.sample_surface(buckets, dais[0], dais[1], True, 1.0,
|
||||
preferred_height=dais_top - 0.4)
|
||||
if dais_surface is None:
|
||||
raise RuntimeError("dais stamp has no upward-facing surface in the exported mesh")
|
||||
dais_rise = dais_surface[0] - floor_top
|
||||
print(f"[LandmarkDaisSurface] center_m=({dais[0]:.3f},{dais[1]:.3f}) "
|
||||
f"floor_z_m={floor_top:.3f} top_z_m={dais_surface[0]:.3f} "
|
||||
f"rise_m={dais_rise:.3f} minimum_m=1.000 "
|
||||
f"status={'PASS' if dais_rise >= 1.0 else 'FAIL'}", flush=True)
|
||||
if dais_rise < 1.0:
|
||||
raise RuntimeError(f"dais rises only {dais_rise:.3f} m above the chamber floor")
|
||||
dais_metrics = audit_shape("dais-platform", region_triangles(
|
||||
dais, 1.7, floor_top + 0.6, dais_top + 0.4),
|
||||
(dais[0], dais[1], dais_surface[0]))
|
||||
metrics_path = ARTIFACT_DIR / "landmark-shape-metrics.json"
|
||||
metrics_path.write_text(json.dumps({
|
||||
"vocabulary_version": layout["stamp_vocabulary_version"],
|
||||
"entry_m": entrance, "portal_m": portal, "entry_ground_m": ground_z,
|
||||
"chamber_center_m": chamber, "chamber_floor_m": floor_top,
|
||||
"dais_center_m": dais, "dais_top_m": dais_top,
|
||||
"dais_surface_m": dais_surface[0], "dais_rise_m": dais_rise,
|
||||
"l37_thresholds": {"planar_patch_m2_lt": SHAPE.PLANE_LIMIT_M2,
|
||||
"straight_run_m_lte": SHAPE.STRAIGHT_LIMIT_M,
|
||||
"corner_turn_deg_lt": SHAPE.CORNER_TURN_LIMIT_DEG},
|
||||
"entrance_arch": entrance_metrics, "dais_platform": dais_metrics,
|
||||
"mesh_tiles": tile_count, "mesh_voxel_um": voxel_um,
|
||||
}, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L45CaptureShapeMetrics] file={metrics_path} status=PASS", flush=True)
|
||||
@@ -0,0 +1,48 @@
|
||||
# L45 — reachable landmark chamber
|
||||
|
||||
## Design and stamps
|
||||
|
||||
The SW demo opens through a rounded, side-facing ellipsoid portal, then follows a bent rock ramp with two level landings into the 17 × 15 m chamber. The sloped section drops 5.85 m; the maximum measured grade is 14.039°. An ellipsoidal rock dais rises 1.138 m above the chamber floor.
|
||||
|
||||
The demo layout resolves 166 version-2 stamps: 57 ellipsoid carves, 107 support pillars, and two filled ellipsoids for the floor and dais. The side portal and supported arrival landing are runtime-only under the demo flag. `owner_world.json` is unchanged.
|
||||
|
||||
## Reachability and shape audits
|
||||
|
||||
The surface search joins walkable ground near the arrival mouth to the chamber floor. The capsule route passed 66 samples and 6,798 clearance checks: zero blocked samples, zero unsupported samples across 330 support checks, and zero grade failures. Minimum clearance is 0.085 m. The exact buried L43 layout fails the same reachability audit as required.
|
||||
|
||||
L37 thresholds (planar patch <0.50 m², straight run ≤0.75 m, no sharp corners): the portal measured 0.417 m² / 0.522 m / 0 corners; the dais measured 0.282 m² / 0.750 m / 0 corners.
|
||||
|
||||
## Hashes and cost
|
||||
|
||||
- Source stamp: `0ec9177748faff6ef4813af0c418057edfcacf95c7c39c1292288a29c7775015` (GCC and Clang).
|
||||
- Owner JSON SHA-256: `7b095f9bcc6b90133d5a463f99f0b571133b2c549ce1ecf2d64fe9175e41e183`.
|
||||
- Demo-off layout IDs: FI `b7a7d9ecc48d8f26`; SW `8a52e84c26ed92d2`. The 12-hash W1/W12 owner signatures match L44: FI `9820f4f38b189b3f7263810e7b3ff62aa502b21f476ffcfdbc7761a65cb07d58`; SW `a1e9268a4bbcc77d2a1ac54095df6851a70bab65c43ae50b332ed75132617834`.
|
||||
- Five-pair demo-off cost vs L44: FI median 16.476 → 16.677 s (+1.22%); SW 1.733 → 1.731 s (−0.12%).
|
||||
|
||||
## Verification and captures
|
||||
|
||||
`verify.py fast`, `medium`, and `full` passed in 189.5 s, 561.8 s, and 1,609.1 s. Passage, interface/hole, seeded random-camera, W1/W8/W12 determinism, and 30-cell checks passed.
|
||||
|
||||
All four JPEGs pass `capture_quality.py` and are under 300 KB:
|
||||
|
||||
- [Ground entrance, eye height at 7.03 m](captures/ground-entrance.jpg) — 36,041 B
|
||||
- [Entrance looking in](captures/entrance-looking-in.jpg) — 41,507 B
|
||||
- [Inside toward the dais](captures/inside-to-dais.jpg) — 36,590 B
|
||||
- [Overview of entrance and chamber](captures/overview-entrance-and-chamber.jpg) — 58,255 B
|
||||
|
||||
## Failed runs
|
||||
|
||||
Six geometry trial exports were used, within the 15-trial budget. The boxy arch exceeded the shape limits; early ramp/support variants blocked the path or exceeded the grade limit; a deeper mouth cut broke the surface connection and was reverted. Early eye-level camera angles were wall-blocked or poorly framed. The first final fast invocation rejected a stale GCC stamp; GCC was rebuilt to match and fast passed. The buried L43 and deliberate verifier negative controls fail as expected.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted as the flagged demo. The stamp matches the final Clang and GCC builds (`0ec91777…`); demo-off owner
|
||||
hashes are identical to L44; the buried L43 layout fails the new reachability audit. The overview reads as a real
|
||||
place: a sinkhole and curved path cut into the ground, leading down into a chamber with a rock dais. Not yet natural:
|
||||
- **The ramp is visibly segmented.** Evenly spaced ribs run along it in the overview and the entrance view, the same
|
||||
per-segment seam family as the L35 ribs and L36 washboard. It is built from 57 carves plus 107 support pillars.
|
||||
- **The ramp's raised edge has straight vertical walls,** so in `entrance-looking-in.jpg` it reads as a built walkway.
|
||||
- **Headroom along the path is tight:** 0.085 m minimum clearance.
|
||||
- The dais is a smooth dome, readable but plain.
|
||||
|
||||
Next (L46): replace the stamped ramp with one continuous floor-capped tunnel edge, following R12's first slice.
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 57 KiB |
@@ -0,0 +1,384 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture the L46 cavern graph views and audit player-scale built-shape limits."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
VERIFY = ROOT / "Standalone/Tools/verify"
|
||||
LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37"
|
||||
sys.path.insert(0, str(VERIFY))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def load_module(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"cannot import {path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
PROFILE = load_module("l45_ring_profile", ROOT /
|
||||
"Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py")
|
||||
SHAPE = load_module("l45_shape_audit", LINUX37 / "AuditMouthShapeL37.py")
|
||||
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--layout", type=Path, required=True)
|
||||
PARSER.add_argument("--artifact-dir", type=Path, required=True)
|
||||
PARSER.add_argument("--capture-dir", type=Path, required=True)
|
||||
PARSER.add_argument("--data", type=Path, default=ROOT / "Standalone/data/owner_world.json")
|
||||
OPTIONS = PARSER.parse_args()
|
||||
|
||||
ARTIFACT_DIR = OPTIONS.artifact_dir.resolve()
|
||||
CAPTURE_DIR = OPTIONS.capture_dir.resolve()
|
||||
LAYOUT_PATH = OPTIONS.layout.resolve()
|
||||
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
CAPTURE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
PNG_DIR = ARTIFACT_DIR / "capture-png"
|
||||
PNG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
HEADLESS = Path(os.environ["VOXELFORGE_L37_HEADLESS"])
|
||||
RENDERER = Path(os.environ["VOXELFORGE_L37_RENDERER"])
|
||||
EXPECTED_STAMP = current_stamp()
|
||||
|
||||
|
||||
def run_logged(name: str, command: list[str], *, capture: bool = False) -> str:
|
||||
log = ARTIFACT_DIR / f"{name}.log"
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = bounded_run(command, timeout=1800, stdout=stream)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if result is None:
|
||||
raise RuntimeError(f"{name} timed out; see {log}")
|
||||
print(f"[L45CaptureRun] step={name} elapsed={elapsed:.1f}s "
|
||||
f"exit={result.returncode} log={log}", flush=True)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{name} failed; see {log}")
|
||||
if capture and (first_stamp(output) != EXPECTED_STAMP
|
||||
or first_stamp(bounded_run(
|
||||
[str(command[0]), "--print-source-stamp"], timeout=15).stdout or "")
|
||||
!= EXPECTED_STAMP):
|
||||
raise RuntimeError(f"{name} executable source stamp mismatch")
|
||||
return output
|
||||
|
||||
|
||||
def stamp_by_id(layout: dict, stable_id: str) -> dict:
|
||||
for stamp in layout.get("stamps", []):
|
||||
if stamp.get("stable_id") == stable_id:
|
||||
return stamp
|
||||
raise RuntimeError(f"missing stamp {stable_id}")
|
||||
|
||||
|
||||
def metres(stamp: dict, key: str = "position_um") -> tuple[float, float, float]:
|
||||
return tuple(value / 1_000_000.0 for value in stamp[key])
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
dx, dy, dz = (target[index] - camera[index] for index in range(3))
|
||||
return (math.degrees(math.atan2(dx, dy)),
|
||||
math.degrees(math.atan2(dz, math.hypot(dx, dy))))
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def camera_spec(camera, target):
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
def edge_point(edge: dict, t: float) -> tuple[float, float, float]:
|
||||
points = edge["control_points_um"]
|
||||
one = 1.0 - t
|
||||
weights = (one ** 3, 3 * one ** 2 * t, 3 * one * t ** 2, t ** 3)
|
||||
return tuple(sum(weights[index] * points[index][axis]
|
||||
for index in range(4)) / 1_000_000.0
|
||||
for axis in range(3))
|
||||
|
||||
|
||||
def cavern_features(layout: dict):
|
||||
if (layout.get("cavern_graph_vocabulary_version") != 1
|
||||
or len(layout.get("cavern_rooms", [])) != 1
|
||||
or len(layout.get("cavern_edges", [])) != 1):
|
||||
raise RuntimeError("L46 capture needs one resolved cavern room and edge")
|
||||
room = layout["cavern_rooms"][0]
|
||||
edge = layout["cavern_edges"][0]
|
||||
return room, edge
|
||||
|
||||
|
||||
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
|
||||
room, edge = cavern_features(layout)
|
||||
portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1")
|
||||
floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor")
|
||||
dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais")
|
||||
entrance = edge_point(edge, 0.0)
|
||||
next_ramp = edge_point(edge, 0.08)
|
||||
portal = metres(portal_stamp)
|
||||
chamber = metres(room, "center_um")
|
||||
floor = metres(floor_stamp)
|
||||
floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
dais = metres(dais_stamp)
|
||||
dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
ground_z = entrance[2]
|
||||
direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1])
|
||||
direction = ((next_ramp[0] - entrance[0]) / direction_length,
|
||||
(next_ramp[1] - entrance[1]) / direction_length)
|
||||
outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1])
|
||||
outward = ((entrance[0] - chamber[0]) / outward_length,
|
||||
(entrance[1] - chamber[1]) / outward_length)
|
||||
mouth_normal = (-direction[1], direction[0])
|
||||
|
||||
mesh = ARTIFACT_DIR / "landmark-l46.vfmesh"
|
||||
export_camera = ((entrance[0] + chamber[0]) * 0.5,
|
||||
(entrance[1] + chamber[1]) * 0.5,
|
||||
(ground_z + floor_top) * 0.5)
|
||||
export_target = (chamber[0], chamber[1], floor_top)
|
||||
run_logged("landmark-export", [str(HEADLESS), str(OPTIONS.data), "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--recipe=surface-world", "--landmark-chamber-demo",
|
||||
f"--camera={fmt(camera_spec(export_camera, export_target))}",
|
||||
f"--mesh-export={mesh}", f"--resolved-layout-output={LAYOUT_PATH}"], capture=True)
|
||||
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
|
||||
room, edge = cavern_features(layout)
|
||||
portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1")
|
||||
floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor")
|
||||
dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais")
|
||||
entrance = edge_point(edge, 0.0)
|
||||
next_ramp = edge_point(edge, 0.08)
|
||||
portal = metres(portal_stamp)
|
||||
chamber = metres(room, "center_um")
|
||||
floor = metres(floor_stamp)
|
||||
floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
dais = metres(dais_stamp)
|
||||
dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
ground_z = entrance[2]
|
||||
direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1])
|
||||
direction = ((next_ramp[0] - entrance[0]) / direction_length,
|
||||
(next_ramp[1] - entrance[1]) / direction_length)
|
||||
outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1])
|
||||
outward = ((entrance[0] - chamber[0]) / outward_length,
|
||||
(entrance[1] - chamber[1]) / outward_length)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"missing mesh export {mesh}")
|
||||
|
||||
voxel_um, triangles, tile_count = PROFILE.read_scene(str(mesh), 0,
|
||||
floor_top - 8.0, entrance[2] + 9.0,
|
||||
(min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0,
|
||||
min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0))
|
||||
bounds = (min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0,
|
||||
min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0)
|
||||
buckets = PROFILE.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||
ground_candidates = []
|
||||
for ix in range(-20, 21):
|
||||
for iy in range(-20, 21):
|
||||
x, y = portal[0] + ix * 0.5, portal[1] + iy * 0.5
|
||||
dx, dy = x - portal[0], y - portal[1]
|
||||
distance = math.hypot(dx, dy)
|
||||
if not 6.0 <= distance <= 10.0:
|
||||
continue
|
||||
hit = PROFILE.sample_surface(buckets, x, y, True, 1.0,
|
||||
preferred_height=ground_z)
|
||||
if hit is None or abs(hit[0] - ground_z) > 3.0 or hit[1][2] < 0.85:
|
||||
continue
|
||||
alignment = max(-1.0, min(1.0,
|
||||
(dx * mouth_normal[0] + dy * mouth_normal[1]) / distance))
|
||||
angle = math.acos(alignment)
|
||||
score = abs(distance - 7.0) + angle * 4.0
|
||||
ground_candidates.append((score, x, y, hit[0]))
|
||||
if not ground_candidates:
|
||||
raise RuntimeError("no walkable mesh ground lies 6 to 10 m from the cave mouth")
|
||||
_, ground_x, ground_y, outside_ground = min(ground_candidates)
|
||||
inside_floor = PROFILE.sample_surface(buckets, chamber[0], chamber[1], True, 1.0,
|
||||
preferred_height=floor_top)
|
||||
if inside_floor is None or abs(inside_floor[0] - floor_top) > 1.5:
|
||||
raise RuntimeError(f"chamber floor mesh hit {inside_floor} does not match designed floor {floor_top:.3f} m")
|
||||
floor_top = inside_floor[0]
|
||||
|
||||
ground_camera = (ground_x, ground_y, outside_ground + 1.6)
|
||||
entry_ground = PROFILE.sample_surface(buckets, entrance[0], entrance[1], True, 1.0,
|
||||
preferred_height=ground_z)
|
||||
if entry_ground is None:
|
||||
raise RuntimeError("cave mouth has no walkable ground surface in the exported mesh")
|
||||
entry_camera = (entrance[0] + mouth_normal[0] * 3.8,
|
||||
entrance[1] + mouth_normal[1] * 3.8, ground_z + 1.6)
|
||||
entry_target_floor = edge_point(edge, 0.30)
|
||||
entry_target = (entry_target_floor[0], entry_target_floor[1], entry_target_floor[2] + 1.4)
|
||||
to_dais = (dais[0] - chamber[0], dais[1] - chamber[1])
|
||||
to_dais_length = math.hypot(*to_dais)
|
||||
opposite_dais = (-to_dais[0] / to_dais_length, -to_dais[1] / to_dais_length)
|
||||
inside_x = chamber[0]
|
||||
inside_y = chamber[1]
|
||||
inside_floor = PROFILE.sample_surface(buckets, inside_x, inside_y, True, 1.0,
|
||||
preferred_height=floor_top)
|
||||
inside_eye_z = inside_floor[0] + 1.6
|
||||
chamber_camera = (chamber[0] + opposite_dais[0] * 5.5,
|
||||
chamber[1] + opposite_dais[1] * 5.5, inside_eye_z)
|
||||
chamber_target = (dais[0], dais[1], dais_top - 0.25)
|
||||
overview_camera = (entrance[0] + outward[0] * 18.0,
|
||||
entrance[1] + outward[1] * 18.0, outside_ground + 15.0)
|
||||
overview_target = ((entrance[0] + chamber[0]) * 0.5,
|
||||
(entrance[1] + chamber[1]) * 0.5,
|
||||
(ground_z + floor_top) * 0.5)
|
||||
inward = (-outward[0], -outward[1])
|
||||
clip_point = (entrance[0] + outward[0] * 0.5,
|
||||
entrance[1] + outward[1] * 0.5)
|
||||
overview_clip = (inward[0], inward[1], 0.0,
|
||||
-(inward[0] * clip_point[0] + inward[1] * clip_point[1]))
|
||||
|
||||
views = (
|
||||
("ground-entrance", ground_camera, (portal[0], portal[1], portal[2]),
|
||||
(portal[0], portal[1], ground_z, 20.0), -509.0),
|
||||
("entrance-looking-in", entry_camera, entry_target, (entrance[0], entrance[1], entrance[2], 16.0), -507.0),
|
||||
("inside-to-dais", chamber_camera, chamber_target,
|
||||
(chamber[0], chamber[1], chamber[2], 15.0), chamber[2]),
|
||||
("overview-entrance-and-chamber", overview_camera, overview_target,
|
||||
((entrance[0] + chamber[0]) * 0.5,
|
||||
(entrance[1] + chamber[1]) * 0.5,
|
||||
(ground_z + floor_top) * 0.5, 34.0), -509.0),
|
||||
)
|
||||
|
||||
for name, camera, target, focus, height_center in views:
|
||||
png = PNG_DIR / f"{name}.png"
|
||||
jpg = CAPTURE_DIR / f"{name}.jpg"
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
fov = (80.0 if name == "overview-entrance-and-chamber" else
|
||||
70.0 if name in ("inside-to-dais", "entrance-looking-in") else 65.0
|
||||
if name == "ground-entrance" else 60.0)
|
||||
if name == "ground-entrance":
|
||||
fov = 55.0
|
||||
print(f"[L46CaptureCamera] view={name} camera_m=({fmt(camera)}) "
|
||||
f"target_m=({fmt(target)}) yaw_deg={yaw:.3f} pitch_deg={pitch:.3f} "
|
||||
f"fov_deg={fov:.1f} entrance_distance_m={math.dist(camera, portal):.3f}", flush=True)
|
||||
render = [str(RENDERER), str(mesh), str(png), "--size=1600x900", f"--fov-deg={fov:.1f}",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--shade=readable", "--headlamp=on", "--height-tint=on",
|
||||
"--light-direction=0.3,-0.4,0.85",
|
||||
f"--height-center-m={(floor_top if name == 'inside-to-dais' else height_center):.6f}",
|
||||
f"--height-range-m={4 if name == 'inside-to-dais' else 16}",
|
||||
"--tile-skirts=off", "--tile-edges=off",
|
||||
f"--focus-region={fmt(focus)}"]
|
||||
if name == "overview-entrance-and-chamber":
|
||||
render.append(f"--clip-plane={fmt(overview_clip)}")
|
||||
run_logged(f"{name}-render", render, capture=True)
|
||||
for quality in (4, 6, 8):
|
||||
run_logged(f"{name}-jpeg-q{quality}", ["ffmpeg", "-nostdin", "-hide_banner",
|
||||
"-loglevel", "error", "-y", "-i", str(png), "-frames:v", "1",
|
||||
"-q:v", str(quality), str(jpg)])
|
||||
if jpg.is_file() and 0 < jpg.stat().st_size <= 300_000:
|
||||
break
|
||||
if not jpg.is_file() or not 0 < jpg.stat().st_size <= 300_000:
|
||||
raise RuntimeError(f"{jpg.name} exceeds the 300 KB capture limit")
|
||||
quality_log = run_logged(f"{name}-quality", [sys.executable,
|
||||
str(VERIFY / "capture_quality.py"), str(jpg)])
|
||||
print(f"[L46CaptureJPEG] file={jpg} bytes={jpg.stat().st_size} "
|
||||
f"quality_log={quality_log.strip().replace(chr(10), ' ')} status=PASS", flush=True)
|
||||
|
||||
|
||||
def region_triangles(center, radius_xy, z_min, z_max):
|
||||
selected = []
|
||||
for triangle in triangles:
|
||||
points = tuple(vertex[0] for vertex in triangle[2])
|
||||
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
||||
if (math.hypot(centroid[0] - center[0], centroid[1] - center[1]) <= radius_xy
|
||||
and z_min <= centroid[2] <= z_max):
|
||||
selected.append(triangle)
|
||||
return selected
|
||||
|
||||
|
||||
def tunnel_wall_triangles(edge, t, height_above_floor):
|
||||
center = edge_point(edge, t)
|
||||
before = edge_point(edge, max(0.0, t - 0.01))
|
||||
after = edge_point(edge, min(1.0, t + 0.01))
|
||||
tx, ty = after[0] - before[0], after[1] - before[1]
|
||||
tangent_length = math.hypot(tx, ty)
|
||||
tx, ty = tx / tangent_length, ty / tangent_length
|
||||
expected_radius = edge["bore_radius_um"] / 1_000_000.0 * math.sqrt(
|
||||
1.0 - (height_above_floor / (edge["bore_height_um"] / 1_000_000.0)) ** 2)
|
||||
selected = []
|
||||
for triangle in triangles:
|
||||
points = tuple(vertex[0] for vertex in triangle[2])
|
||||
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
||||
dx, dy = centroid[0] - center[0], centroid[1] - center[1]
|
||||
longitudinal = dx * tx + dy * ty
|
||||
lateral = abs(-dy * tx + dx * ty)
|
||||
vertical = centroid[2] - center[2]
|
||||
if (abs(longitudinal) <= 1.5
|
||||
and abs(lateral - expected_radius) <= 0.7
|
||||
and abs(vertical - height_above_floor) <= 0.75):
|
||||
selected.append(triangle)
|
||||
print(f"[L46TunnelWallWindow] t={t:.3f} center_m=({center[0]:.3f},"
|
||||
f"{center[1]:.3f},{center[2]:.3f}) expected_radius_m={expected_radius:.3f} "
|
||||
f"longitudinal_half_width_m=1.500 vertical_half_width_m=0.750 "
|
||||
f"triangles={len(selected)}", flush=True)
|
||||
return selected
|
||||
|
||||
|
||||
def audit_shape(name, region, center):
|
||||
metrics = SHAPE.analyze_triangles(region, center[:2], center[2],
|
||||
crop_to_center=False, include_sides=True)
|
||||
passed = (metrics["triangles"] > 0
|
||||
and metrics["planar_area"] < SHAPE.PLANE_LIMIT_M2
|
||||
and metrics["straight"] <= SHAPE.STRAIGHT_LIMIT_M
|
||||
and metrics["corners"] == 0)
|
||||
print(f"[LandmarkBuiltShape] region={name} triangles={metrics['triangles']} "
|
||||
f"planar_patch_m2={metrics['planar_area']:.4f} "
|
||||
f"straight_run_m={metrics['straight']:.4f} sharp_corners={metrics['corners']} "
|
||||
f"corner_turn_max_deg={metrics['corner_turn_max']:.2f} "
|
||||
f"thresholds=({SHAPE.PLANE_LIMIT_M2:.2f}m2,{SHAPE.STRAIGHT_LIMIT_M:.2f}m,"
|
||||
f"{SHAPE.CORNER_TURN_LIMIT_DEG:.0f}deg) status={'PASS' if passed else 'FAIL'}",
|
||||
flush=True)
|
||||
if not passed:
|
||||
raise RuntimeError(f"L37 built-shape thresholds failed in {name}")
|
||||
return metrics
|
||||
|
||||
|
||||
entrance_metrics = audit_shape("cave-mouth", region_triangles(
|
||||
portal, 5.0, ground_z + 0.8, ground_z + 4.5),
|
||||
(portal[0], portal[1], ground_z))
|
||||
tunnel_mid = edge_point(edge, 0.58)
|
||||
tunnel_metrics = audit_shape("tunnel-wall-edge", tunnel_wall_triangles(
|
||||
edge, 0.58, 1.2),
|
||||
(tunnel_mid[0], tunnel_mid[1], tunnel_mid[2] + 1.2))
|
||||
dais_surface = PROFILE.sample_surface(buckets, dais[0], dais[1], True, 1.0,
|
||||
preferred_height=dais_top - 0.4)
|
||||
if dais_surface is None:
|
||||
raise RuntimeError("dais stamp has no upward-facing surface in the exported mesh")
|
||||
dais_rise = dais_surface[0] - floor_top
|
||||
print(f"[LandmarkDaisSurface] center_m=({dais[0]:.3f},{dais[1]:.3f}) "
|
||||
f"floor_z_m={floor_top:.3f} top_z_m={dais_surface[0]:.3f} "
|
||||
f"rise_m={dais_rise:.3f} minimum_m=1.000 "
|
||||
f"status={'PASS' if dais_rise >= 1.0 else 'FAIL'}", flush=True)
|
||||
if dais_rise < 1.0:
|
||||
raise RuntimeError(f"dais rises only {dais_rise:.3f} m above the chamber floor")
|
||||
dais_metrics = audit_shape("dais-platform", region_triangles(
|
||||
dais, 1.7, floor_top + 0.6, dais_top + 0.4),
|
||||
(dais[0], dais[1], dais_surface[0]))
|
||||
metrics_path = ARTIFACT_DIR / "landmark-shape-metrics.json"
|
||||
metrics_path.write_text(json.dumps({
|
||||
"graph_vocabulary_version": layout["cavern_graph_vocabulary_version"],
|
||||
"entry_m": entrance, "portal_m": portal, "entry_ground_m": ground_z,
|
||||
"chamber_center_m": chamber, "chamber_floor_m": floor_top,
|
||||
"tunnel_midpoint_m": tunnel_mid,
|
||||
"dais_center_m": dais, "dais_top_m": dais_top,
|
||||
"dais_surface_m": dais_surface[0], "dais_rise_m": dais_rise,
|
||||
"l37_thresholds": {"planar_patch_m2_lt": SHAPE.PLANE_LIMIT_M2,
|
||||
"straight_run_m_lte": SHAPE.STRAIGHT_LIMIT_M,
|
||||
"corner_turn_deg_lt": SHAPE.CORNER_TURN_LIMIT_DEG},
|
||||
"entrance_arch": entrance_metrics, "tunnel_wall": tunnel_metrics,
|
||||
"dais_platform": dais_metrics,
|
||||
"mesh_tiles": tile_count, "mesh_voxel_um": voxel_um,
|
||||
}, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L46CaptureShapeMetrics] file={metrics_path} status=PASS", flush=True)
|
||||
@@ -0,0 +1,36 @@
|
||||
# Linux 46 — cavern graph slice 1
|
||||
|
||||
## Sweep and resolved graph
|
||||
|
||||
The ramp is one cubic-Bezier sweep. Each field query finds the closest XY point with a 17-point seed and five Newton refinements; the curve's Z coordinates define the cubic floor profile. A continuous rounded bore uses a 2.6 m radius, 4.8 m height and 0.3 m smooth blend. The 20 mm relief is applied to the bore. A separate hard floor cap runs after smooth unions, relief, passages and stamps. It preserves native negative density and adds at most 0.45 m of support only where the field is air. The room and edge conservative bounds feed `World::isTileShapeFree`.
|
||||
|
||||
Resolved snapshots can carry optional cavern graph vocabulary version 1 records: room `landmark/sw-arrival-chamber-v1` and edge `landmark/sw-arrival-chamber-tunnel-v1`, anchored from the surface-ground approach to that room. Graph fields are serialized and included in canonical identity only when records exist. With the demo off, the graph is absent and the L45 identities remain unchanged.
|
||||
|
||||
The 57 ramp carves and 107 support pillars are gone. The room is one graph node and the tunnel one edge. Four resolved stamps remain for the two mouth carves, room floor and dais.
|
||||
|
||||
## Audits
|
||||
|
||||
- Reachability: pass from walkable ground to the chamber floor; 58 path samples, 5,974 capsule samples, no blocked or unsupported samples, maximum grade 14.179°. Buried L43 control: expected fail.
|
||||
- Headroom: 2.800 m minimum above the capsule, against 0.300 m required.
|
||||
- Ripple: tunnel floor peak 0.0018 m over 202 samples (0.015 m limit); wall peak 0.0232 m over 138 samples (0.035 m limit, 0.25–1.0 m band). L45 segmented-ramp control: expected floor-ripple fail at 0.0179 m against 0.015 m.
|
||||
- Built shape (L37 limits: planar patch <0.50 m², straight run ≤0.75 m, no sharp corners): entrance 0.4902 m² / 0.5470 m / 0; tunnel wall 0.3042 m² / 0.6225 m / 0; dais 0.2816 m² / 0.7500 m / 0.
|
||||
- Inside-tunnel and near-tunnel camera hashes match at W1/W8/W12. Random-camera determinism also passed.
|
||||
|
||||
## Hashes, cost and tiers
|
||||
|
||||
The clean Clang and GCC builds share source stamp `20d1ae654440b22c6fdf9c91dd39c809b3063b54da6f2f4f44acf78894b7cf3b`. Owner JSON SHA-256 remains `7b095f9bcc6b90133d5a463f99f0b571133b2c549ce1ecf2d64fe9175e41e183`. Demo-off FI/SW layout IDs are `b7a7d9ecc48d8f26` / `8a52e84c26ed92d2`; their 12-hash signatures match L45: FI `9820f4f38b189b3f7263810e7b3ff62aa502b21f476ffcfdbc7761a65cb07d58`, SW `a1e9268a4bbcc77d2a1ac54095df6851a70bab65c43ae50b332ed75132617834`.
|
||||
|
||||
Five demo-off cost pairs against committed L45: FI median 16.315 → 16.401 s (+0.53%); SW 1.720 → 1.730 s (+0.58%). Fast passed in 202.7 s, medium in 571.9 s, and final full in 1605.2 s. Four captures are in `captures/`: `ground-entrance.jpg` (39,430 B), `entrance-looking-in.jpg` (45,070 B), `inside-to-dais.jpg` (28,310 B), `overview-entrance-and-chamber.jpg` (62,735 B). Each is below 300 KB. Fourteen of fifteen fast trials were used.
|
||||
|
||||
## Failed runs
|
||||
|
||||
The first medium capture audit sampled a broad tunnel volume and failed the built-shape limit; the final wall annulus at curve parameter 0.58 passed. The first full attempt passed its 30 determinism cells but failed extracting an 806 MB historical archive and filled `/tmp`. The final full run used a temporary Git wrapper to export only the build inputs (Core, Source, Compat and build scripts); its historical build, cost pairs and all full checks passed. No tracked verifier change was needed.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted. The stamp matches the final Clang and GCC builds (`20d1ae65…`); demo-off owner hashes are identical to L45.
|
||||
The segmented ramp (57 carves plus 107 pillars) is replaced by one continuous Bezier sweep with a hard floor cap;
|
||||
headroom rose from 0.085 m to 2.8 m. Both controls fail as they should: the L45 ramp on floor ripple, and the buried
|
||||
L43 layout on reachability. Follow-up for the verifier: the full tier's baseline export copies the whole HEAD tree
|
||||
(about 785 MB) into `/tmp`, which is RAM-backed on this machine, and never deletes it. About 6 GB had accumulated.
|
||||
It must export only the build inputs, into `Saved/Linux/`, and clean up after itself.
|
||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,401 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture L47 cavern entrance views and audit the exported floor profile."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
VERIFY = ROOT / "Standalone/Tools/verify"
|
||||
LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37"
|
||||
sys.path.insert(0, str(VERIFY))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def load_module(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"cannot import {path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
PROFILE = load_module("l45_ring_profile", ROOT /
|
||||
"Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py")
|
||||
SHAPE = load_module("l45_shape_audit", LINUX37 / "AuditMouthShapeL37.py")
|
||||
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--layout", type=Path, required=True)
|
||||
PARSER.add_argument("--artifact-dir", type=Path, required=True)
|
||||
PARSER.add_argument("--capture-dir", type=Path, required=True)
|
||||
PARSER.add_argument("--data", type=Path, default=ROOT / "Standalone/data/owner_world.json")
|
||||
OPTIONS = PARSER.parse_args()
|
||||
|
||||
ARTIFACT_DIR = OPTIONS.artifact_dir.resolve()
|
||||
CAPTURE_DIR = OPTIONS.capture_dir.resolve()
|
||||
LAYOUT_PATH = OPTIONS.layout.resolve()
|
||||
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
CAPTURE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
PNG_DIR = ARTIFACT_DIR / "capture-png"
|
||||
PNG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
HEADLESS = Path(os.environ["VOXELFORGE_L37_HEADLESS"])
|
||||
RENDERER = Path(os.environ["VOXELFORGE_L37_RENDERER"])
|
||||
EXPECTED_STAMP = current_stamp()
|
||||
|
||||
|
||||
def run_logged(name: str, command: list[str], *, capture: bool = False) -> str:
|
||||
log = ARTIFACT_DIR / f"{name}.log"
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = bounded_run(command, timeout=1800, stdout=stream)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if result is None:
|
||||
raise RuntimeError(f"{name} timed out; see {log}")
|
||||
print(f"[L47CaptureRun] step={name} elapsed={elapsed:.1f}s "
|
||||
f"exit={result.returncode} log={log}", flush=True)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{name} failed; see {log}")
|
||||
if capture and (first_stamp(output) != EXPECTED_STAMP
|
||||
or first_stamp(bounded_run(
|
||||
[str(command[0]), "--print-source-stamp"], timeout=15).stdout or "")
|
||||
!= EXPECTED_STAMP):
|
||||
raise RuntimeError(f"{name} executable source stamp mismatch")
|
||||
return output
|
||||
|
||||
|
||||
def stamp_by_id(layout: dict, stable_id: str) -> dict:
|
||||
for stamp in layout.get("stamps", []):
|
||||
if stamp.get("stable_id") == stable_id:
|
||||
return stamp
|
||||
raise RuntimeError(f"missing stamp {stable_id}")
|
||||
|
||||
|
||||
def metres(stamp: dict, key: str = "position_um") -> tuple[float, float, float]:
|
||||
return tuple(value / 1_000_000.0 for value in stamp[key])
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
dx, dy, dz = (target[index] - camera[index] for index in range(3))
|
||||
return (math.degrees(math.atan2(dx, dy)),
|
||||
math.degrees(math.atan2(dz, math.hypot(dx, dy))))
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def camera_spec(camera, target):
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
def edge_point(edge: dict, t: float) -> tuple[float, float, float]:
|
||||
points = edge["control_points_um"]
|
||||
one = 1.0 - t
|
||||
weights = (one ** 3, 3 * one ** 2 * t, 3 * one * t ** 2, t ** 3)
|
||||
return tuple(sum(weights[index] * points[index][axis]
|
||||
for index in range(4)) / 1_000_000.0
|
||||
for axis in range(3))
|
||||
|
||||
|
||||
def cavern_features(layout: dict):
|
||||
if (layout.get("cavern_graph_vocabulary_version") != 1
|
||||
or len(layout.get("cavern_rooms", [])) != 1
|
||||
or len(layout.get("cavern_edges", [])) != 1):
|
||||
raise RuntimeError("L47 capture needs one resolved cavern room and edge")
|
||||
room = layout["cavern_rooms"][0]
|
||||
edge = layout["cavern_edges"][0]
|
||||
return room, edge
|
||||
|
||||
|
||||
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
|
||||
room, edge = cavern_features(layout)
|
||||
portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1")
|
||||
floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor")
|
||||
dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais")
|
||||
entrance = edge_point(edge, 0.0)
|
||||
next_ramp = edge_point(edge, 0.08)
|
||||
portal = metres(portal_stamp)
|
||||
chamber = metres(room, "center_um")
|
||||
floor = metres(floor_stamp)
|
||||
floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
dais = metres(dais_stamp)
|
||||
dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
ground_z = entrance[2]
|
||||
direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1])
|
||||
direction = ((next_ramp[0] - entrance[0]) / direction_length,
|
||||
(next_ramp[1] - entrance[1]) / direction_length)
|
||||
outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1])
|
||||
outward = ((entrance[0] - chamber[0]) / outward_length,
|
||||
(entrance[1] - chamber[1]) / outward_length)
|
||||
mouth_normal = (-direction[1], direction[0])
|
||||
|
||||
mesh = ARTIFACT_DIR / "landmark-l47.vfmesh"
|
||||
export_anchor = edge_point(edge, 0.25)
|
||||
export_camera = (export_anchor[0], export_anchor[1], export_anchor[2] + 1.4)
|
||||
export_target_floor = edge_point(edge, 0.42)
|
||||
export_target = (export_target_floor[0], export_target_floor[1],
|
||||
export_target_floor[2] + 1.2)
|
||||
run_logged("landmark-export", [str(HEADLESS), str(OPTIONS.data), "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=4", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--recipe=surface-world", "--landmark-chamber-demo",
|
||||
f"--camera={fmt(camera_spec(export_camera, export_target))}",
|
||||
f"--mesh-export={mesh}", f"--resolved-layout-output={LAYOUT_PATH}"], capture=True)
|
||||
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
|
||||
room, edge = cavern_features(layout)
|
||||
portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1")
|
||||
floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor")
|
||||
dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais")
|
||||
entrance = edge_point(edge, 0.0)
|
||||
next_ramp = edge_point(edge, 0.08)
|
||||
portal = metres(portal_stamp)
|
||||
chamber = metres(room, "center_um")
|
||||
floor = metres(floor_stamp)
|
||||
floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
dais = metres(dais_stamp)
|
||||
dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
ground_z = entrance[2]
|
||||
direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1])
|
||||
direction = ((next_ramp[0] - entrance[0]) / direction_length,
|
||||
(next_ramp[1] - entrance[1]) / direction_length)
|
||||
outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1])
|
||||
outward = ((entrance[0] - chamber[0]) / outward_length,
|
||||
(entrance[1] - chamber[1]) / outward_length)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"missing mesh export {mesh}")
|
||||
|
||||
entrance_audit = ROOT / "Standalone/Research/WorldAssemblerM3/Linux47/MeasureEntranceL47.py"
|
||||
run_logged("entrance-floor-control-l46", [sys.executable, str(entrance_audit),
|
||||
"--mesh", str(ROOT / "Saved/Linux/l46/landmark-l46.vfmesh"),
|
||||
"--layout", str(ROOT / "Saved/Linux/l46/layouts/landmark-reachability-clang.json"),
|
||||
"--out", str(ARTIFACT_DIR / "entrance-profile-control-l46.json"), "--expect=fail"])
|
||||
run_logged("entrance-floor-audit", [sys.executable, str(entrance_audit),
|
||||
"--mesh", str(mesh), "--layout", str(LAYOUT_PATH),
|
||||
"--out", str(ARTIFACT_DIR / "entrance-profile-l47.json"), "--expect=pass"])
|
||||
|
||||
voxel_um, triangles, tile_count = PROFILE.read_scene(str(mesh), 0,
|
||||
floor_top - 8.0, entrance[2] + 9.0,
|
||||
(min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0,
|
||||
min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0))
|
||||
bounds = (min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0,
|
||||
min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0)
|
||||
buckets = PROFILE.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||
ground_candidates = []
|
||||
for ix in range(-20, 21):
|
||||
for iy in range(-20, 21):
|
||||
x, y = portal[0] + ix * 0.5, portal[1] + iy * 0.5
|
||||
dx, dy = x - portal[0], y - portal[1]
|
||||
distance = math.hypot(dx, dy)
|
||||
if not 6.0 <= distance <= 10.0:
|
||||
continue
|
||||
hit = PROFILE.sample_surface(buckets, x, y, True, 1.0,
|
||||
preferred_height=ground_z)
|
||||
if hit is None or abs(hit[0] - ground_z) > 3.0 or hit[1][2] < 0.85:
|
||||
continue
|
||||
alignment = max(-1.0, min(1.0,
|
||||
(dx * mouth_normal[0] + dy * mouth_normal[1]) / distance))
|
||||
angle = math.acos(alignment)
|
||||
score = abs(distance - 7.0) + angle * 4.0
|
||||
ground_candidates.append((score, x, y, hit[0]))
|
||||
if not ground_candidates:
|
||||
raise RuntimeError("no walkable mesh ground lies 6 to 10 m from the cave mouth")
|
||||
_, ground_x, ground_y, outside_ground = min(ground_candidates)
|
||||
inside_floor = PROFILE.sample_surface(buckets, chamber[0], chamber[1], True, 1.0,
|
||||
preferred_height=floor_top)
|
||||
if inside_floor is None or abs(inside_floor[0] - floor_top) > 1.5:
|
||||
raise RuntimeError(f"chamber floor mesh hit {inside_floor} does not match designed floor {floor_top:.3f} m")
|
||||
floor_top = inside_floor[0]
|
||||
|
||||
ground_camera = (ground_x, ground_y, outside_ground + 1.6)
|
||||
entry_ground = PROFILE.sample_surface(buckets, entrance[0], entrance[1], True, 1.0,
|
||||
preferred_height=ground_z)
|
||||
if entry_ground is None:
|
||||
raise RuntimeError("cave mouth has no walkable ground surface in the exported mesh")
|
||||
entry_camera = (entrance[0] + mouth_normal[0] * 3.8,
|
||||
entrance[1] + mouth_normal[1] * 3.8, ground_z + 1.6)
|
||||
entry_target_floor = edge_point(edge, 0.30)
|
||||
entry_target = (entry_target_floor[0], entry_target_floor[1], entry_target_floor[2] + 1.4)
|
||||
low_camera = (entrance[0] + outward[0] * 1.0,
|
||||
entrance[1] + outward[1] * 1.0, ground_z + 0.65)
|
||||
low_target_floor = edge_point(edge, 0.24)
|
||||
low_target = (low_target_floor[0], low_target_floor[1], low_target_floor[2] + 0.15)
|
||||
to_dais = (dais[0] - chamber[0], dais[1] - chamber[1])
|
||||
to_dais_length = math.hypot(*to_dais)
|
||||
opposite_dais = (-to_dais[0] / to_dais_length, -to_dais[1] / to_dais_length)
|
||||
inside_x = chamber[0]
|
||||
inside_y = chamber[1]
|
||||
inside_floor = PROFILE.sample_surface(buckets, inside_x, inside_y, True, 1.0,
|
||||
preferred_height=floor_top)
|
||||
inside_eye_z = inside_floor[0] + 1.6
|
||||
chamber_camera = (chamber[0] + opposite_dais[0] * 5.5,
|
||||
chamber[1] + opposite_dais[1] * 5.5, inside_eye_z)
|
||||
chamber_target = (dais[0], dais[1], dais_top - 0.25)
|
||||
overview_camera = (entrance[0] + outward[0] * 18.0,
|
||||
entrance[1] + outward[1] * 18.0, outside_ground + 15.0)
|
||||
overview_target = ((entrance[0] + chamber[0]) * 0.5,
|
||||
(entrance[1] + chamber[1]) * 0.5,
|
||||
(ground_z + floor_top) * 0.5)
|
||||
inward = (-outward[0], -outward[1])
|
||||
clip_point = (entrance[0] + outward[0] * 0.5,
|
||||
entrance[1] + outward[1] * 0.5)
|
||||
overview_clip = (inward[0], inward[1], 0.0,
|
||||
-(inward[0] * clip_point[0] + inward[1] * clip_point[1]))
|
||||
|
||||
views = (
|
||||
("ground-entrance", ground_camera, (portal[0], portal[1], portal[2]),
|
||||
(portal[0], portal[1], ground_z, 20.0), -509.0),
|
||||
("entrance-looking-in", entry_camera, entry_target, (entrance[0], entrance[1], entrance[2], 16.0), -507.0),
|
||||
("entrance-floor-low", low_camera, low_target,
|
||||
(entrance[0], entrance[1], entrance[2], 12.0), ground_z),
|
||||
("inside-to-dais", chamber_camera, chamber_target,
|
||||
(chamber[0], chamber[1], chamber[2], 15.0), chamber[2]),
|
||||
("overview-entrance-and-chamber", overview_camera, overview_target,
|
||||
((entrance[0] + chamber[0]) * 0.5,
|
||||
(entrance[1] + chamber[1]) * 0.5,
|
||||
(ground_z + floor_top) * 0.5, 34.0), -509.0),
|
||||
)
|
||||
|
||||
for name, camera, target, focus, height_center in views:
|
||||
png = PNG_DIR / f"{name}.png"
|
||||
jpg = CAPTURE_DIR / f"{name}.jpg"
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
fov = (80.0 if name == "overview-entrance-and-chamber" else
|
||||
70.0 if name in ("inside-to-dais", "entrance-looking-in") else
|
||||
58.0 if name == "entrance-floor-low" else 65.0
|
||||
if name == "ground-entrance" else 60.0)
|
||||
if name == "ground-entrance":
|
||||
fov = 55.0
|
||||
print(f"[L47CaptureCamera] view={name} camera_m=({fmt(camera)}) "
|
||||
f"target_m=({fmt(target)}) yaw_deg={yaw:.3f} pitch_deg={pitch:.3f} "
|
||||
f"fov_deg={fov:.1f} entrance_distance_m={math.dist(camera, portal):.3f}", flush=True)
|
||||
render = [str(RENDERER), str(mesh), str(png), "--size=1600x900", f"--fov-deg={fov:.1f}",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--shade=readable", "--headlamp=on", "--height-tint=on",
|
||||
"--light-direction=0.3,-0.4,0.85",
|
||||
f"--height-center-m={(floor_top if name == 'inside-to-dais' else height_center):.6f}",
|
||||
f"--height-range-m={4 if name == 'inside-to-dais' else 6 if name == 'entrance-floor-low' else 16}",
|
||||
"--tile-skirts=off", "--tile-edges=off",
|
||||
f"--focus-region={fmt(focus)}"]
|
||||
if name == "overview-entrance-and-chamber":
|
||||
render.append(f"--clip-plane={fmt(overview_clip)}")
|
||||
run_logged(f"{name}-render", render, capture=True)
|
||||
for quality in (4, 6, 8):
|
||||
run_logged(f"{name}-jpeg-q{quality}", ["ffmpeg", "-nostdin", "-hide_banner",
|
||||
"-loglevel", "error", "-y", "-i", str(png), "-frames:v", "1",
|
||||
"-q:v", str(quality), str(jpg)])
|
||||
if jpg.is_file() and 0 < jpg.stat().st_size <= 300_000:
|
||||
break
|
||||
if not jpg.is_file() or not 0 < jpg.stat().st_size <= 300_000:
|
||||
raise RuntimeError(f"{jpg.name} exceeds the 300 KB capture limit")
|
||||
quality_log = run_logged(f"{name}-quality", [sys.executable,
|
||||
str(VERIFY / "capture_quality.py"), str(jpg)])
|
||||
print(f"[L47CaptureJPEG] file={jpg} bytes={jpg.stat().st_size} "
|
||||
f"quality_log={quality_log.strip().replace(chr(10), ' ')} status=PASS", flush=True)
|
||||
|
||||
|
||||
def region_triangles(center, radius_xy, z_min, z_max):
|
||||
selected = []
|
||||
for triangle in triangles:
|
||||
points = tuple(vertex[0] for vertex in triangle[2])
|
||||
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
||||
if (math.hypot(centroid[0] - center[0], centroid[1] - center[1]) <= radius_xy
|
||||
and z_min <= centroid[2] <= z_max):
|
||||
selected.append(triangle)
|
||||
return selected
|
||||
|
||||
|
||||
def tunnel_wall_triangles(edge, t, height_above_floor):
|
||||
center = edge_point(edge, t)
|
||||
before = edge_point(edge, max(0.0, t - 0.01))
|
||||
after = edge_point(edge, min(1.0, t + 0.01))
|
||||
tx, ty = after[0] - before[0], after[1] - before[1]
|
||||
tangent_length = math.hypot(tx, ty)
|
||||
tx, ty = tx / tangent_length, ty / tangent_length
|
||||
expected_radius = edge["bore_radius_um"] / 1_000_000.0 * math.sqrt(
|
||||
1.0 - (height_above_floor / (edge["bore_height_um"] / 1_000_000.0)) ** 2)
|
||||
selected = []
|
||||
for triangle in triangles:
|
||||
points = tuple(vertex[0] for vertex in triangle[2])
|
||||
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
||||
dx, dy = centroid[0] - center[0], centroid[1] - center[1]
|
||||
longitudinal = dx * tx + dy * ty
|
||||
lateral = abs(-dy * tx + dx * ty)
|
||||
vertical = centroid[2] - center[2]
|
||||
if (abs(longitudinal) <= 1.5
|
||||
and abs(lateral - expected_radius) <= 0.7
|
||||
and abs(vertical - height_above_floor) <= 0.75):
|
||||
selected.append(triangle)
|
||||
print(f"[L47TunnelWallWindow] t={t:.3f} center_m=({center[0]:.3f},"
|
||||
f"{center[1]:.3f},{center[2]:.3f}) expected_radius_m={expected_radius:.3f} "
|
||||
f"longitudinal_half_width_m=1.500 vertical_half_width_m=0.750 "
|
||||
f"triangles={len(selected)}", flush=True)
|
||||
return selected
|
||||
|
||||
|
||||
def audit_shape(name, region, center):
|
||||
metrics = SHAPE.analyze_triangles(region, center[:2], center[2],
|
||||
crop_to_center=False, include_sides=True)
|
||||
passed = (metrics["triangles"] > 0
|
||||
and metrics["planar_area"] < SHAPE.PLANE_LIMIT_M2
|
||||
and metrics["straight"] <= SHAPE.STRAIGHT_LIMIT_M
|
||||
and metrics["corners"] == 0)
|
||||
print(f"[LandmarkBuiltShape] region={name} triangles={metrics['triangles']} "
|
||||
f"planar_patch_m2={metrics['planar_area']:.4f} "
|
||||
f"straight_run_m={metrics['straight']:.4f} sharp_corners={metrics['corners']} "
|
||||
f"corner_turn_max_deg={metrics['corner_turn_max']:.2f} "
|
||||
f"thresholds=({SHAPE.PLANE_LIMIT_M2:.2f}m2,{SHAPE.STRAIGHT_LIMIT_M:.2f}m,"
|
||||
f"{SHAPE.CORNER_TURN_LIMIT_DEG:.0f}deg) status={'PASS' if passed else 'FAIL'}",
|
||||
flush=True)
|
||||
if not passed:
|
||||
raise RuntimeError(f"L37 built-shape thresholds failed in {name}")
|
||||
return metrics
|
||||
|
||||
|
||||
entrance_metrics = audit_shape("cave-mouth", region_triangles(
|
||||
portal, 5.0, ground_z + 0.8, ground_z + 4.5),
|
||||
(portal[0], portal[1], ground_z))
|
||||
tunnel_mid = edge_point(edge, 0.64)
|
||||
tunnel_metrics = audit_shape("tunnel-wall-edge", tunnel_wall_triangles(
|
||||
edge, 0.64, 1.2),
|
||||
(tunnel_mid[0], tunnel_mid[1], tunnel_mid[2] + 1.2))
|
||||
dais_surface = PROFILE.sample_surface(buckets, dais[0], dais[1], True, 1.0,
|
||||
preferred_height=dais_top - 0.4)
|
||||
if dais_surface is None:
|
||||
raise RuntimeError("dais stamp has no upward-facing surface in the exported mesh")
|
||||
dais_rise = dais_surface[0] - floor_top
|
||||
print(f"[LandmarkDaisSurface] center_m=({dais[0]:.3f},{dais[1]:.3f}) "
|
||||
f"floor_z_m={floor_top:.3f} top_z_m={dais_surface[0]:.3f} "
|
||||
f"rise_m={dais_rise:.3f} minimum_m=1.000 "
|
||||
f"status={'PASS' if dais_rise >= 1.0 else 'FAIL'}", flush=True)
|
||||
if dais_rise < 1.0:
|
||||
raise RuntimeError(f"dais rises only {dais_rise:.3f} m above the chamber floor")
|
||||
dais_metrics = audit_shape("dais-platform", region_triangles(
|
||||
dais, 1.7, floor_top + 0.6, dais_top + 0.4),
|
||||
(dais[0], dais[1], dais_surface[0]))
|
||||
metrics_path = ARTIFACT_DIR / "landmark-shape-metrics.json"
|
||||
metrics_path.write_text(json.dumps({
|
||||
"graph_vocabulary_version": layout["cavern_graph_vocabulary_version"],
|
||||
"entry_m": entrance, "portal_m": portal, "entry_ground_m": ground_z,
|
||||
"chamber_center_m": chamber, "chamber_floor_m": floor_top,
|
||||
"tunnel_midpoint_m": tunnel_mid,
|
||||
"dais_center_m": dais, "dais_top_m": dais_top,
|
||||
"dais_surface_m": dais_surface[0], "dais_rise_m": dais_rise,
|
||||
"l37_thresholds": {"planar_patch_m2_lt": SHAPE.PLANE_LIMIT_M2,
|
||||
"straight_run_m_lte": SHAPE.STRAIGHT_LIMIT_M,
|
||||
"corner_turn_deg_lt": SHAPE.CORNER_TURN_LIMIT_DEG},
|
||||
"entrance_arch": entrance_metrics, "tunnel_wall": tunnel_metrics,
|
||||
"dais_platform": dais_metrics,
|
||||
"mesh_tiles": tile_count, "mesh_voxel_um": voxel_um,
|
||||
}, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L47CaptureShapeMetrics] file={metrics_path} status=PASS", flush=True)
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure the L46 entrance floor from open terrain through its first ten metres."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
spec = importlib.util.spec_from_file_location("ring_profile", PROFILE_PATH)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"cannot load {PROFILE_PATH}")
|
||||
PROFILE = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(PROFILE)
|
||||
|
||||
ARTIFACTS = ROOT / "Saved/Linux/l47"
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--layout", type=Path,
|
||||
default=ROOT / "Saved/Linux/l46/layouts/landmark-reachability-clang.json")
|
||||
PARSER.add_argument("--mesh", type=Path,
|
||||
default=ROOT / "Saved/Linux/l46/landmark-l46.vfmesh")
|
||||
PARSER.add_argument("--out", type=Path, default=ARTIFACTS / "entrance-profile-before.json")
|
||||
PARSER.add_argument("--expect", choices=("pass", "fail", "measure"), default="measure")
|
||||
PARSER.add_argument("--lod", default="0", help="LOD level, or 'all' to include every exported level")
|
||||
OPTIONS = PARSER.parse_args()
|
||||
LAYOUT = OPTIONS.layout.resolve()
|
||||
MESH = OPTIONS.mesh.resolve()
|
||||
OUT = OPTIONS.out.resolve()
|
||||
|
||||
layout = json.loads(LAYOUT.read_text(encoding="utf-8"))
|
||||
edge = layout["cavern_edges"][0]
|
||||
controls = [[value / 1_000_000.0 for value in point]
|
||||
for point in edge["control_points_um"]]
|
||||
|
||||
|
||||
def bezier(t: float) -> tuple[float, float, float]:
|
||||
u = 1.0 - t
|
||||
weights = (u ** 3, 3.0 * u * u * t, 3.0 * u * t * t, t ** 3)
|
||||
return tuple(sum(weights[i] * controls[i][axis] for i in range(4))
|
||||
for axis in range(3))
|
||||
|
||||
|
||||
route = [bezier(i / 4096.0) for i in range(4097)]
|
||||
arc = [0.0]
|
||||
for a, b in zip(route, route[1:]):
|
||||
arc.append(arc[-1] + math.hypot(b[0] - a[0], b[1] - a[1]))
|
||||
|
||||
|
||||
def route_at(distance: float) -> tuple[float, float, float]:
|
||||
if distance < 0.0:
|
||||
start = route[0]
|
||||
dx = route[1][0] - start[0]
|
||||
dy = route[1][1] - start[1]
|
||||
dz = route[1][2] - start[2]
|
||||
horizontal = math.hypot(dx, dy)
|
||||
scale = distance / (arc[1] - arc[0])
|
||||
return start[0] + dx * scale, start[1] + dy * scale, start[2] + dz * scale
|
||||
index = min(len(arc) - 2, max(0, next(
|
||||
(i for i, value in enumerate(arc) if value >= distance), len(arc) - 1) - 1))
|
||||
span = arc[index + 1] - arc[index]
|
||||
t = 0.0 if span <= 0.0 else (distance - arc[index]) / span
|
||||
return tuple(route[index][axis] + (route[index + 1][axis] - route[index][axis]) * t
|
||||
for axis in range(3))
|
||||
|
||||
|
||||
entry = route[0]
|
||||
tangent = route[1]
|
||||
tx, ty = tangent[0] - entry[0], tangent[1] - entry[1]
|
||||
length = math.hypot(tx, ty)
|
||||
tx, ty = tx / length, ty / length
|
||||
nx, ny = -ty, tx
|
||||
requested_lod = None if OPTIONS.lod == "all" else int(OPTIONS.lod)
|
||||
voxel_um, triangles, tile_count = PROFILE.read_scene(
|
||||
str(MESH), requested_lod, entry[2] - 7.0, entry[2] + 5.0,
|
||||
(entry[0] - 14.0, entry[0] + 14.0, entry[1] - 14.0, entry[1] + 14.0))
|
||||
buckets = PROFILE.build_buckets(
|
||||
triangles, True,
|
||||
(entry[0] - 14.0, entry[0] + 14.0, entry[1] - 14.0, entry[1] + 14.0),
|
||||
cell_size=1.0)
|
||||
down_buckets = PROFILE.build_buckets(
|
||||
triangles, False,
|
||||
(entry[0] - 14.0, entry[0] + 14.0, entry[1] - 14.0, entry[1] + 14.0),
|
||||
cell_size=1.0)
|
||||
mesh_points = [vertex[0] for triangle in triangles for vertex in triangle[2]]
|
||||
print(f"[EntranceProfileGeometry] entry_m=({entry[0]:.3f},{entry[1]:.3f},{entry[2]:.3f}) "
|
||||
f"point10_m=({route_at(10.0)[0]:.3f},{route_at(10.0)[1]:.3f},"
|
||||
f"{route_at(10.0)[2]:.3f}) mesh_xy=({min(p[0] for p in mesh_points):.3f},"
|
||||
f"{max(p[0] for p in mesh_points):.3f},{min(p[1] for p in mesh_points):.3f},"
|
||||
f"{max(p[1] for p in mesh_points):.3f}) mesh_z=({min(p[2] for p in mesh_points):.3f},"
|
||||
f"{max(p[2] for p in mesh_points):.3f})")
|
||||
for sample_distance in range(11):
|
||||
sx, sy, expected = route_at(float(sample_distance))
|
||||
bucket = buckets.get((math.floor(sx), math.floor(sy)), ())
|
||||
hit = PROFILE.sample_surface(buckets, sx, sy, True, 1.0,
|
||||
preferred_height=expected)
|
||||
down_hit = PROFILE.sample_surface(down_buckets, sx, sy, False, 1.0,
|
||||
preferred_height=expected)
|
||||
print(f"[EntranceProfileBucket] distance_m={sample_distance:.1f} "
|
||||
f"xy_m=({sx:.3f},{sy:.3f}) triangles={len(bucket)} "
|
||||
f"hit_z_m={None if hit is None else round(hit[0], 4)} "
|
||||
f"down_hit_z_m={None if down_hit is None else round(down_hit[0], 4)} "
|
||||
f"expected_z_m={expected:.4f}")
|
||||
|
||||
rows = []
|
||||
for rail, lateral in (("left", -1.5), ("center", 0.0), ("right", 1.5)):
|
||||
distance = -5.0
|
||||
while distance <= 10.000001:
|
||||
x, y, expected = route_at(distance)
|
||||
x += nx * lateral
|
||||
y += ny * lateral
|
||||
hit = PROFILE.sample_surface(buckets, x, y, True, 1.0,
|
||||
preferred_height=expected)
|
||||
rows.append({
|
||||
"rail": rail,
|
||||
"distance_m": round(distance, 3),
|
||||
"x_m": x,
|
||||
"y_m": y,
|
||||
"expected_floor_m": expected,
|
||||
"surface_z_m": None if hit is None else hit[0],
|
||||
"normal_z": None if hit is None else hit[1][2],
|
||||
"residual_m": None if hit is None else hit[0] - expected,
|
||||
})
|
||||
distance += 0.125
|
||||
|
||||
results = {}
|
||||
for rail in ("left", "center", "right"):
|
||||
profile = [row for row in rows if row["rail"] == rail and row["surface_z_m"] is not None
|
||||
and 0.0 <= row["distance_m"] <= 10.0]
|
||||
jumps = []
|
||||
for previous, current in zip(profile, profile[1:]):
|
||||
expected_delta = current["expected_floor_m"] - previous["expected_floor_m"]
|
||||
residual_delta = current["residual_m"] - previous["residual_m"]
|
||||
if abs(residual_delta) >= 0.025:
|
||||
jumps.append({"from_m": previous["distance_m"], "to_m": current["distance_m"],
|
||||
"delta_m": current["surface_z_m"] - previous["surface_z_m"],
|
||||
"residual_delta_m": residual_delta,
|
||||
"expected_delta_m": expected_delta})
|
||||
residuals = [row["residual_m"] for row in profile]
|
||||
ripple_residuals = []
|
||||
for row in profile:
|
||||
nearby = [other["residual_m"] for other in profile
|
||||
if abs(other["distance_m"] - row["distance_m"]) <= 1.0]
|
||||
ripple_residuals.append((row["distance_m"], row["residual_m"]
|
||||
- sum(nearby) / max(1, len(nearby))))
|
||||
amplitude, period = 0.0, 0.0
|
||||
for candidate in (0.375 + index * 0.125 for index in range(30)):
|
||||
cosine = sum(value * math.cos(2.0 * math.pi * distance / candidate)
|
||||
for distance, value in ripple_residuals)
|
||||
sine = sum(value * math.sin(2.0 * math.pi * distance / candidate)
|
||||
for distance, value in ripple_residuals)
|
||||
current_amplitude = 2.0 * math.hypot(cosine, sine) / max(1, len(ripple_residuals))
|
||||
if current_amplitude > amplitude:
|
||||
amplitude, period = current_amplitude, candidate
|
||||
step_error = max((abs(item["residual_delta_m"]) for item in jumps), default=0.0)
|
||||
coverage = len(profile) / 81.0
|
||||
rail_passed = (coverage >= 0.85 and amplitude <= 0.015 and step_error <= 0.05)
|
||||
results[rail] = {
|
||||
"coverage": coverage,
|
||||
"max_adjacent_step_m": max((abs(item["delta_m"]) for item in jumps), default=0.0),
|
||||
"max_step_residual_m": step_error,
|
||||
"ripple_amplitude_m": amplitude,
|
||||
"ripple_period_m": period,
|
||||
"passed": rail_passed,
|
||||
"jumps_over_0_04m": jumps,
|
||||
"residual_peak_to_peak_m": max(residuals) - min(residuals) if residuals else None,
|
||||
}
|
||||
|
||||
payload = {
|
||||
"layout": str(LAYOUT.relative_to(ROOT)),
|
||||
"mesh": str(MESH.relative_to(ROOT)),
|
||||
"voxel_um": voxel_um,
|
||||
"selected_tiles": tile_count,
|
||||
"sample_spacing_m": 0.125,
|
||||
"distance_range_m": [-5.0, 10.0],
|
||||
"lateral_offsets_m": {"left": -1.5, "center": 0.0, "right": 1.5},
|
||||
"rails": results,
|
||||
"samples": rows,
|
||||
}
|
||||
all_passed = all(result["passed"] for result in results.values())
|
||||
expected_ok = (OPTIONS.expect == "measure"
|
||||
or (OPTIONS.expect == "pass" and all_passed)
|
||||
or (OPTIONS.expect == "fail" and not all_passed))
|
||||
OUT.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[EntranceProfileBefore] voxel_um={voxel_um} tiles={tile_count} "
|
||||
f"lod={OPTIONS.lod} spacing_m=0.125 distance_m=-5.000..10.000 status=MEASURED")
|
||||
for rail, summary in results.items():
|
||||
print(f"[EntranceFloorRailAudit] rail={rail} coverage={summary['coverage']:.3f} "
|
||||
f"ripple_amplitude_m={summary['ripple_amplitude_m']:.4f} "
|
||||
f"ripple_period_m={summary['ripple_period_m']:.3f} "
|
||||
f"max_step_residual_m={summary['max_step_residual_m']:.4f} "
|
||||
f"status={'PASS' if summary['passed'] else 'FAIL'}")
|
||||
for jump in summary["jumps_over_0_04m"]:
|
||||
print(f"[EntranceProfileJump] rail={rail} from_m={jump['from_m']:.3f} "
|
||||
f"to_m={jump['to_m']:.3f} delta_m={jump['delta_m']:+.4f} "
|
||||
f"expected_delta_m={jump['expected_delta_m']:+.4f} "
|
||||
f"residual_delta_m={jump['residual_delta_m']:+.4f}")
|
||||
failed_rails = sum(not result["passed"] for result in results.values())
|
||||
print(f"[EntranceFloorMeshAudit] samples_per_rail=81 distance_m=0.000..10.000 "
|
||||
f"rails=3 failed_rails={failed_rails} observed={'PASS' if all_passed else 'FAIL'} "
|
||||
f"expected={OPTIONS.expect.upper()} status={'PASS' if expected_ok else 'FAIL'}")
|
||||
print(f"[EntranceProfileJSON] file={OUT} status={'PASS' if expected_ok else 'FAIL'}")
|
||||
if not expected_ok:
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,38 @@
|
||||
# Linux47 report
|
||||
|
||||
## Entrance floor
|
||||
|
||||
The L46 mesh has a repeating staircase through the first 7 m: 39.9–51.4 mm ripple and 81.8–82.9 mm largest residual step. The cause was the final hard floor cap, applied after the mouth and terrain stamps, using vertical `Z-floorZ` on a sloped Bezier floor. That field is not a signed distance to the ramp, so Marching Cubes interpolated a distorted gradient; stamp fill beside the cap made the ridges visible. Voxel height quantisation was not the main cause.
|
||||
|
||||
The cap now uses the grade-normalised signed distance. The tunnel SDF is reapplied in a floor-adjacent band, with the wider correction fading over the first 10% of the entrance; the narrow floor correction continues along the ramp. This removes the ridges without changing the tunnel wall profile. Mesh tile Z bounds now include resolved cavern bounds too; without that, the exported ramp lost triangles past about 6.6 m.
|
||||
|
||||
The mesh audit samples 0–10 m at 0.125 m on three rails (−1.5, 0, +1.5 m), requiring ≥85% coverage, ≤15 mm ripple and ≤50 mm adjacent residual step. L46 fails all rails: coverage 0.63–0.68, ripple 39.9–51.4 mm, step 81.8–82.9 mm. L47 passes all rails: 100% coverage, ripple 0.4–1.2 mm, no measured residual step. The Core audit also passes 243 samples with 0.3 mm maximum step and 0.0 mm reported ripple. Buried L43 and segmented-ramp L45 controls still fail as expected.
|
||||
|
||||
## Verifier, hashes and cost
|
||||
|
||||
The cost baseline now exports only Core, Source, Compat, and the build/helper scripts to `Saved/Linux/l47/baseline-e016f6a20fcfac785c9870b3d1133fd5df47a6d4`; an exit handler removes the export. It does not copy the repository to `/tmp`. `/tmp` was **28K before** and **28K after** the full tier; the saved top-level-entry comparison is identical and the baseline export was removed.
|
||||
|
||||
Source stamp / source manifest: `68328b0c2a154e261eba94a067eb7d879f37ef987a54758275b4d14e3e9746d3`. Demo-off owner hashes match L46 exactly for both recipes, with W1=W12. Whole-view hashes (density/mesh/payload/topology): floating-islands `598e9daaa1744ccb / 3114e0f4f6dbf734 / c49c4e48bff74089 / cadd4edd10d4d8ef`; surface-world `7dc75e2c09302258 / 0cb34c40ceec4ce3 / 9d23e8c46b515cf1 / 2962c9464991eedd`. Demo-on determinism passed 30/30 cells.
|
||||
|
||||
Five-pair median cost: floating-islands 16.390→16.435 s (+0.27%); surface-world 1.722→1.717 s (−0.29%). The baseline build took 40.2 s.
|
||||
|
||||
## Tiers, captures and failed runs
|
||||
|
||||
Fast **PASS** (195.9 s), medium **PASS** (567.8 s), full **PASS** (1606.5 s); zero failed checks. All used source stamp above. Final captures: [ground entrance](captures/ground-entrance.jpg), [looking in](captures/entrance-looking-in.jpg), [overview](captures/overview-entrance-and-chamber.jpg), [inside to dais](captures/inside-to-dais.jpg), and [low entrance floor](captures/entrance-floor-low.jpg).
|
||||
|
||||
Exploration failures: the first entrance gate caught a 171 mm step; stamp-only re-carving restored about 40 mm ripple and a 100 mm step; full-width re-carving failed the wall-shape check at t=0.58. The final mouth-local fade passed the wall check at t=0.64. The first fast invocation also stopped because the executable had not yet been built; the rebuilt fast tier passed. L46/L43/L45 negative controls are intentional failures and passed their expected-fail checks. Work stayed within the 15-trial budget.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted, with one new defect logged. The stamp matches the final Clang and GCC builds (`68328b0c…`); demo-off hashes
|
||||
are identical to L46; `/tmp` is unchanged after the full tier (1.8 MB used). The entrance floor is smooth now, and
|
||||
the cause, a vertical cap distance on a sloped floor, was measured before the fix.
|
||||
|
||||
**New defect:** a row of **jagged sawtooth teeth along the wall foot,** where the floor-adjacent band correction meets
|
||||
the bore wall. It shows in `entrance-looking-in.jpg` and along the edge in the overview. The three floor rails (−1.5,
|
||||
0, +1.5 m) don't reach the 2.6 m wall foot. **This is the third time** a fix has moved an artifact to where the
|
||||
audit isn't looking (L46 inside-only, L47 rails only). L48 adds a whole-surface artifact audit.
|
||||
|
||||
R14: no likely MSVC or UHT blocker. The Unreal adapter does not resolve stamps or cavern graphs, so the L43–L47 demos
|
||||
are standalone-only until the adapter supplies those records. The legacy chunk callers agree with the micrometre
|
||||
layout only at the default 8 m width.
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,709 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit every LOD0 mesh triangle in a cavern corridor for clustered defects."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import array
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
CELL_M = 1.0
|
||||
CORRIDOR_MARGIN_M = 3.0
|
||||
OPEN_GROUND_EXTENSION_M = 5.0
|
||||
WELD_M = 0.0001
|
||||
|
||||
|
||||
class Triangle:
|
||||
__slots__ = ("verts", "center", "area", "normal", "smooth", "quality",
|
||||
"route_distance", "route_lateral", "route_s", "floor_z",
|
||||
"in_region", "reference")
|
||||
|
||||
def __init__(self, verts, area, normal, smooth, quality):
|
||||
self.verts = verts
|
||||
self.center = tuple(sum(v[i] for v in verts) / 3.0 for i in range(3))
|
||||
self.area = area
|
||||
self.normal = normal
|
||||
self.smooth = smooth
|
||||
self.quality = quality
|
||||
self.route_distance = 0.0
|
||||
self.route_lateral = 0.0
|
||||
self.route_s = 0.0
|
||||
self.floor_z = 0.0
|
||||
self.in_region = False
|
||||
self.reference = False
|
||||
|
||||
|
||||
def normalize(value):
|
||||
length = math.sqrt(sum(item * item for item in value))
|
||||
return tuple(item / length for item in value) if length > 1.0e-12 else (0.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
return sum(a[i] * b[i] for i in range(3))
|
||||
|
||||
|
||||
def angle_degrees(a, b):
|
||||
return math.degrees(math.acos(max(-1.0, min(1.0, dot(a, b)))))
|
||||
|
||||
|
||||
def quantile(values, fraction):
|
||||
if not values:
|
||||
return 0.0
|
||||
ordered = sorted(values)
|
||||
return ordered[min(len(ordered) - 1, max(0, int(math.ceil(fraction * len(ordered))) - 1))]
|
||||
|
||||
|
||||
def read_mesh(path: Path):
|
||||
triangles = []
|
||||
selected_tiles = 0
|
||||
lod_counts = {}
|
||||
with path.open("rb") as stream:
|
||||
prefix = stream.read(60)
|
||||
if len(prefix) != 60 or prefix[:8] != b"VFMSH001":
|
||||
raise ValueError(f"{path}: invalid VFMSH001 mesh header")
|
||||
version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8)
|
||||
if version != 1 or not voxel_um or tile_count > 1_000_000:
|
||||
raise ValueError(f"{path}: unsupported mesh version or tile count")
|
||||
for _ in range(tile_count):
|
||||
header = stream.read(36)
|
||||
if len(header) != 36:
|
||||
raise ValueError(f"{path}: truncated tile header")
|
||||
key = struct.unpack_from("<4i", header, 0)
|
||||
surface_count, _surface_aux, vertex_count, index_count, mask_count = \
|
||||
struct.unpack_from("<5I", header, 16)
|
||||
lod_counts[key[3]] = lod_counts.get(key[3], 0) + 1
|
||||
position_bytes = stream.read(vertex_count * 12)
|
||||
normal_bytes = stream.read(vertex_count * 12)
|
||||
index_bytes = stream.read(index_count * 4)
|
||||
masks = stream.read(mask_count)
|
||||
if (len(position_bytes) != vertex_count * 12
|
||||
or len(normal_bytes) != vertex_count * 12
|
||||
or len(index_bytes) != index_count * 4
|
||||
or len(masks) != mask_count):
|
||||
raise ValueError(f"{path}: truncated tile payload")
|
||||
if key[3] != 0:
|
||||
continue
|
||||
selected_tiles += 1
|
||||
positions = array.array("f")
|
||||
positions.frombytes(position_bytes)
|
||||
normals = array.array("f")
|
||||
normals.frombytes(normal_bytes)
|
||||
indices = array.array("I")
|
||||
indices.frombytes(index_bytes)
|
||||
if sys.byteorder != "little":
|
||||
positions.byteswap()
|
||||
normals.byteswap()
|
||||
indices.byteswap()
|
||||
for offset in range(0, index_count - 2, 3):
|
||||
if masks and masks[offset // 3]:
|
||||
continue
|
||||
ids = indices[offset:offset + 3]
|
||||
if max(ids) >= surface_count:
|
||||
continue
|
||||
verts = tuple(tuple(float(positions[index * 3 + axis]) for axis in range(3))
|
||||
for index in ids)
|
||||
edge_a = tuple(verts[1][axis] - verts[0][axis] for axis in range(3))
|
||||
edge_b = tuple(verts[2][axis] - verts[0][axis] for axis in range(3))
|
||||
cross = (edge_a[1] * edge_b[2] - edge_a[2] * edge_b[1],
|
||||
edge_a[2] * edge_b[0] - edge_a[0] * edge_b[2],
|
||||
edge_a[0] * edge_b[1] - edge_a[1] * edge_b[0])
|
||||
cross_length = math.sqrt(dot(cross, cross))
|
||||
if cross_length <= 1.0e-12:
|
||||
continue
|
||||
face_normal = tuple(value / cross_length for value in cross)
|
||||
area = cross_length * 0.5
|
||||
smooth_normal = normalize(tuple(
|
||||
sum(float(normals[index * 3 + axis]) for index in ids) for axis in range(3)))
|
||||
if dot(face_normal, smooth_normal) < 0.0:
|
||||
face_normal = tuple(-value for value in face_normal)
|
||||
edge_squares = 0.0
|
||||
for a, b in ((verts[0], verts[1]), (verts[1], verts[2]),
|
||||
(verts[2], verts[0])):
|
||||
edge_squares += sum((a[i] - b[i]) ** 2 for i in range(3))
|
||||
quality = 4.0 * math.sqrt(3.0) * area / max(edge_squares, 1.0e-15)
|
||||
triangles.append(Triangle(verts, area, face_normal, smooth_normal, quality))
|
||||
if stream.read(1):
|
||||
raise ValueError(f"{path}: trailing bytes after mesh tiles")
|
||||
if not triangles:
|
||||
raise ValueError(f"{path}: no LOD0 surface triangles")
|
||||
return triangles, voxel_um, selected_tiles, lod_counts
|
||||
|
||||
|
||||
def bezier(points, t):
|
||||
one = 1.0 - t
|
||||
weights = (one ** 3, 3.0 * one * one * t, 3.0 * one * t * t, t ** 3)
|
||||
return tuple(sum(weights[i] * points[i][axis] for i in range(4)) for axis in range(3))
|
||||
|
||||
|
||||
def make_route(layout):
|
||||
edges = layout.get("cavern_edges", [])
|
||||
rooms = layout.get("cavern_rooms", [])
|
||||
if len(edges) != 1 or len(rooms) != 1:
|
||||
raise ValueError("cavern audit expects one resolved room and one edge in its layout")
|
||||
edge = edges[0]
|
||||
points = [[value / 1_000_000.0 for value in row]
|
||||
for row in edge["control_points_um"]]
|
||||
bore_radius = edge["bore_radius_um"] / 1_000_000.0
|
||||
bore_height = edge["bore_height_um"] / 1_000_000.0
|
||||
samples = [bezier(points, index / 160.0) for index in range(161)]
|
||||
horizontal_s = [0.0]
|
||||
for a, b in zip(samples, samples[1:]):
|
||||
horizontal_s.append(horizontal_s[-1] + math.hypot(b[0] - a[0], b[1] - a[1]))
|
||||
first_dx = samples[1][0] - samples[0][0]
|
||||
first_dy = samples[1][1] - samples[0][1]
|
||||
first_len = max(1.0e-9, math.hypot(first_dx, first_dy))
|
||||
extension = (samples[0][0] - first_dx / first_len * OPEN_GROUND_EXTENSION_M,
|
||||
samples[0][1] - first_dy / first_len * OPEN_GROUND_EXTENSION_M,
|
||||
samples[0][2] - (samples[1][2] - samples[0][2]) / first_len
|
||||
* OPEN_GROUND_EXTENSION_M)
|
||||
route_points = [extension, *samples]
|
||||
route_s = [-OPEN_GROUND_EXTENSION_M, *horizontal_s]
|
||||
segments = []
|
||||
for i, (a, b) in enumerate(zip(route_points, route_points[1:])):
|
||||
segments.append((a, b, route_s[i], route_s[i + 1]))
|
||||
room = rooms[0]
|
||||
room_center = tuple(value / 1_000_000.0 for value in room["center_um"])
|
||||
room_half = tuple(value / 1_000_000.0 for value in room["half_extents_um"])
|
||||
room_floor = room["floor_um"] / 1_000_000.0
|
||||
return {
|
||||
"segments": segments,
|
||||
"edge": edge,
|
||||
"bore_radius": bore_radius,
|
||||
"bore_height": bore_height,
|
||||
"room_center": room_center,
|
||||
"room_half": room_half,
|
||||
"room_floor": room_floor,
|
||||
"route_points": samples,
|
||||
"route_length": horizontal_s[-1],
|
||||
}
|
||||
|
||||
|
||||
def route_nearest(point, route):
|
||||
best = None
|
||||
for a, b, s0, s1 in route["segments"]:
|
||||
dx, dy = b[0] - a[0], b[1] - a[1]
|
||||
denominator = dx * dx + dy * dy
|
||||
t = 0.0 if denominator <= 1.0e-12 else max(0.0, min(1.0,
|
||||
((point[0] - a[0]) * dx + (point[1] - a[1]) * dy) / denominator))
|
||||
x, y = a[0] + dx * t, a[1] + dy * t
|
||||
distance = math.hypot(point[0] - x, point[1] - y)
|
||||
if best is None or distance < best[0]:
|
||||
segment_length = math.sqrt(max(denominator, 1.0e-12))
|
||||
lateral = (dx * (point[1] - y) - dy * (point[0] - x)) / segment_length
|
||||
best = (distance, s0 + (s1 - s0) * t,
|
||||
a[2] + (b[2] - a[2]) * t, lateral)
|
||||
return best
|
||||
|
||||
|
||||
def mark_region(triangles, route):
|
||||
room_c = route["room_center"]
|
||||
room_h = route["room_half"]
|
||||
radius = route["bore_radius"] + CORRIDOR_MARGIN_M
|
||||
for tri in triangles:
|
||||
x, y, z = tri.center
|
||||
distance, distance_s, floor_z, lateral = route_nearest(tri.center, route)
|
||||
tri.route_distance = distance
|
||||
tri.route_lateral = lateral
|
||||
tri.route_s = distance_s
|
||||
tri.floor_z = floor_z
|
||||
corridor = (-OPEN_GROUND_EXTENSION_M <= distance_s <= route["route_length"]
|
||||
and distance <= radius
|
||||
and floor_z - CORRIDOR_MARGIN_M <= z
|
||||
<= floor_z + route["bore_height"] + CORRIDOR_MARGIN_M)
|
||||
room_box = (abs(x - room_c[0]) <= room_h[0] + CORRIDOR_MARGIN_M
|
||||
and abs(y - room_c[1]) <= room_h[1] + CORRIDOR_MARGIN_M
|
||||
and route["room_floor"] - CORRIDOR_MARGIN_M <= z
|
||||
<= route["room_floor"] + 2.0 * room_h[2] + CORRIDOR_MARGIN_M)
|
||||
tri.in_region = corridor or room_box
|
||||
outside_bore = (distance > route["bore_radius"] + 0.45
|
||||
or abs(x - room_c[0]) > room_h[0] + 0.45
|
||||
or abs(y - room_c[1]) > room_h[1] + 0.45)
|
||||
# Keep the reference on native, ground-facing terrain. The outer mesh
|
||||
# also contains steep cavern walls and room edges, which are features.
|
||||
tri.reference = tri.in_region and outside_bore and tri.smooth[2] >= 0.45
|
||||
|
||||
|
||||
def vertex_key(point):
|
||||
return tuple(int(round(value / WELD_M)) for value in point)
|
||||
|
||||
|
||||
def make_edges(triangles, indices):
|
||||
edge_map = {}
|
||||
for index in indices:
|
||||
tri = triangles[index]
|
||||
points = tri.verts
|
||||
for a, b in ((0, 1), (1, 2), (2, 0)):
|
||||
ka, kb = vertex_key(points[a]), vertex_key(points[b])
|
||||
if ka == kb:
|
||||
continue
|
||||
key = (ka, kb) if ka < kb else (kb, ka)
|
||||
delta = tuple(points[a][axis] - points[b][axis] for axis in range(3))
|
||||
length = math.sqrt(dot(delta, delta))
|
||||
midpoint = tuple((points[a][axis] + points[b][axis]) * 0.5 for axis in range(3))
|
||||
edge_map.setdefault(key, []).append((index, length, midpoint))
|
||||
pairs = []
|
||||
for incident in edge_map.values():
|
||||
unique = {}
|
||||
for index, length, midpoint in incident:
|
||||
unique[index] = (length, midpoint)
|
||||
incident = list(unique.items())
|
||||
if len(incident) < 2:
|
||||
continue
|
||||
if len(incident) > 4:
|
||||
incident = incident[:4]
|
||||
for left in range(len(incident)):
|
||||
for right in range(left + 1, len(incident)):
|
||||
i, (length, midpoint) = incident[left]
|
||||
j, _ = incident[right]
|
||||
angle = max(angle_degrees(triangles[i].normal, triangles[j].normal),
|
||||
0.75 * angle_degrees(triangles[i].smooth, triangles[j].smooth))
|
||||
pairs.append((i, j, angle, length, midpoint))
|
||||
return pairs
|
||||
|
||||
|
||||
def cell_of(point):
|
||||
return tuple(math.floor(value / CELL_M) for value in point)
|
||||
|
||||
|
||||
def score_surface(triangles, considered, pairs, edge_cutoff, rough_cutoff,
|
||||
ref_cell_density):
|
||||
considered_set = set(considered)
|
||||
variation = {index: 0.0 for index in considered}
|
||||
max_jump = {index: 0.0 for index in considered}
|
||||
severe_count = {index: 0.0 for index in considered}
|
||||
for i, j, angle, length, _midpoint in pairs:
|
||||
if i not in considered_set or j not in considered_set:
|
||||
continue
|
||||
weighted = angle * length * 0.5
|
||||
variation[i] += weighted
|
||||
variation[j] += weighted
|
||||
max_jump[i] = max(max_jump[i], angle)
|
||||
max_jump[j] = max(max_jump[j], angle)
|
||||
if angle >= edge_cutoff:
|
||||
severe_count[i] += 0.5
|
||||
severe_count[j] += 0.5
|
||||
cells = {}
|
||||
for index in considered:
|
||||
tri = triangles[index]
|
||||
key = cell_of(tri.center)
|
||||
cell = cells.setdefault(key, {"area": 0.0, "variation": 0.0, "severe": 0.0,
|
||||
"sliver_area": 0.0, "slivers": 0, "triangles": 0, "max_jump": 0.0,
|
||||
"reference_area": 0.0, "reference_variation": 0.0,
|
||||
"reference_severe": 0.0, "reference_sliver_area": 0.0,
|
||||
"reference_slivers": 0})
|
||||
cell["area"] += tri.area
|
||||
cell["variation"] += variation[index]
|
||||
cell["severe"] += severe_count[index]
|
||||
cell["triangles"] += 1
|
||||
cell["max_jump"] = max(cell["max_jump"], max_jump[index])
|
||||
sliver = tri.quality < 0.07 and tri.area < 0.12
|
||||
if sliver:
|
||||
cell["sliver_area"] += tri.area
|
||||
cell["slivers"] += 1
|
||||
if tri.reference:
|
||||
cell["reference_area"] += tri.area
|
||||
cell["reference_variation"] += variation[index]
|
||||
cell["reference_severe"] += severe_count[index]
|
||||
if sliver:
|
||||
cell["reference_sliver_area"] += tri.area
|
||||
cell["reference_slivers"] += 1
|
||||
ref_rough = []
|
||||
ref_density = []
|
||||
ref_sliver_fraction = []
|
||||
for cell in cells.values():
|
||||
if cell["reference_area"] >= max(0.15, cell["area"] * 0.65):
|
||||
ref_rough.append(cell["reference_variation"] / max(cell["reference_area"], 1.0e-9))
|
||||
ref_density.append(cell["reference_severe"] / max(cell["reference_area"], 1.0e-9))
|
||||
ref_sliver_fraction.append(cell["reference_sliver_area"]
|
||||
/ max(cell["reference_area"], 1.0e-9))
|
||||
reference_median = quantile(ref_rough, 0.5)
|
||||
reference_mad = quantile([abs(value - reference_median) for value in ref_rough], 0.5)
|
||||
cell_rough_limit = max(10.0, quantile(ref_rough, 0.995) * 1.5,
|
||||
reference_median + 8.0 * reference_mad)
|
||||
edge_density_limit = max(1.5, quantile(ref_density, 0.995) * 1.7,
|
||||
ref_cell_density * 1.7)
|
||||
sliver_limit = max(0.22, quantile(ref_sliver_fraction, 0.995) + 0.18)
|
||||
hot = set()
|
||||
for key, cell in cells.items():
|
||||
area = max(cell["area"], 1.0e-9)
|
||||
roughness = cell["variation"] / area
|
||||
edge_density = cell["severe"] / area
|
||||
sliver_fraction = cell["sliver_area"] / area
|
||||
rough_cluster = roughness > cell_rough_limit and cell["severe"] >= 2.0
|
||||
edge_cluster = cell["severe"] >= 4.0 and edge_density > edge_density_limit
|
||||
sliver_cluster = cell["slivers"] >= 5 and sliver_fraction > sliver_limit
|
||||
if rough_cluster or edge_cluster or sliver_cluster:
|
||||
hot.add(key)
|
||||
cell["roughness_deg_per_m"] = roughness
|
||||
cell["severe_edge_density_m2"] = edge_density
|
||||
cell["sliver_area_fraction"] = sliver_fraction
|
||||
cell["reasons"] = [name for name, active in (("normal-variation", rough_cluster),
|
||||
("edge-density", edge_cluster), ("slivers", sliver_cluster)) if active]
|
||||
clusters = []
|
||||
flagged_cells = set()
|
||||
remaining = set(hot)
|
||||
while remaining:
|
||||
stack = [remaining.pop()]
|
||||
members = []
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
members.append(current)
|
||||
x, y, z = current
|
||||
neighbors = [(x + dx, y + dy, z + dz)
|
||||
for dx in (-1, 0, 1) for dy in (-1, 0, 1) for dz in (-1, 0, 1)
|
||||
if dx or dy or dz]
|
||||
for neighbor in neighbors:
|
||||
if neighbor in remaining:
|
||||
remaining.remove(neighbor)
|
||||
stack.append(neighbor)
|
||||
cells_in_cluster = [cells[key] for key in members]
|
||||
cluster_area = sum(cell["area"] for cell in cells_in_cluster)
|
||||
cluster_severe = sum(cell["severe"] for cell in cells_in_cluster)
|
||||
cluster_slivers = sum(cell["slivers"] for cell in cells_in_cluster)
|
||||
accepted = len(members) >= 2 and cluster_area >= 0.25 and \
|
||||
(cluster_severe >= 8.0 or cluster_slivers >= 12)
|
||||
if accepted:
|
||||
flagged_cells.update(members)
|
||||
clusters.append({"cells": len(members), "area_m2": cluster_area,
|
||||
"severe_edges": cluster_severe, "sliver_triangles": cluster_slivers,
|
||||
"bounds_m": [[min(key[i] for key in members) * CELL_M,
|
||||
(max(key[i] for key in members) + 1) * CELL_M]
|
||||
for i in range(3)],
|
||||
"reasons": sorted({reason for cell in cells_in_cluster
|
||||
for reason in cell["reasons"]})})
|
||||
clusters.sort(key=lambda cluster: (cluster["severe_edges"], cluster["area_m2"]), reverse=True)
|
||||
flagged_triangles = [index for index in considered
|
||||
if cell_of(triangles[index].center) in flagged_cells]
|
||||
return {"cells": cells, "hot_cells": hot, "flagged_cells": flagged_cells,
|
||||
"clusters": clusters, "hot_triangles": flagged_triangles,
|
||||
"thresholds": {"edge_angle_deg": edge_cutoff,
|
||||
"roughness_deg_per_m": cell_rough_limit,
|
||||
"severe_edge_density_m2": edge_density_limit,
|
||||
"sliver_area_fraction": sliver_limit,
|
||||
"reference_cell_roughness_p995_deg_per_m": quantile(ref_rough, 0.995),
|
||||
"reference_cell_roughness_median_deg_per_m": reference_median}}
|
||||
|
||||
|
||||
def floor_profile_audit(triangles, considered, region_indices, route):
|
||||
"""Measure the whole floor patch as area-weighted bins, not selected rails."""
|
||||
if route is None:
|
||||
return {"present": False, "detected": False, "triangles": 0, "bins": 0,
|
||||
"lateral_bands": 0, "ripple_amplitude_m": 0.0,
|
||||
"max_adjacent_step_m": 0.0}
|
||||
along_step = 0.25
|
||||
lateral_step = 0.5
|
||||
bins = {}
|
||||
floor_triangles = 0
|
||||
wall_triangles = 0
|
||||
wall_bins = set()
|
||||
for index in region_indices:
|
||||
tri = triangles[index]
|
||||
height = tri.center[2] - tri.floor_z
|
||||
if (0.0 <= tri.route_s <= 10.0
|
||||
and route["bore_radius"] - 0.9 <= tri.route_distance
|
||||
<= route["bore_radius"] + 1.0
|
||||
and 0.45 <= height <= route["bore_height"] + 0.5
|
||||
and abs(tri.smooth[2]) <= 0.45):
|
||||
wall_triangles += 1
|
||||
wall_bins.add(int(tri.route_s))
|
||||
for index in considered:
|
||||
tri = triangles[index]
|
||||
if (tri.route_s < 0.0 or tri.route_s > min(10.0, route["route_length"])
|
||||
or tri.route_distance > min(1.9, route["bore_radius"] - 0.1)
|
||||
or abs(tri.center[2] - tri.floor_z) > 0.60
|
||||
or tri.smooth[2] < 0.55):
|
||||
continue
|
||||
floor_triangles += 1
|
||||
along_bin = int(math.floor(tri.route_s / along_step))
|
||||
lateral_bin = int(math.floor((tri.route_lateral + 2.0) / lateral_step))
|
||||
key = (lateral_bin, along_bin)
|
||||
row = bins.setdefault(key, [0.0, 0.0, 0])
|
||||
residual = tri.center[2] - tri.floor_z
|
||||
row[0] += residual * tri.area
|
||||
row[1] += tri.area
|
||||
row[2] += 1
|
||||
by_lateral = {}
|
||||
for (lateral_bin, along_bin), (weighted_residual, area, count) in bins.items():
|
||||
if area < 0.002:
|
||||
continue
|
||||
by_lateral.setdefault(lateral_bin, {})[along_bin] = weighted_residual / area
|
||||
amplitudes = []
|
||||
steps = []
|
||||
valid_bins = 0
|
||||
for values in by_lateral.values():
|
||||
ordered = sorted(values.items())
|
||||
run = []
|
||||
runs = []
|
||||
for item in ordered:
|
||||
if run and item[0] != run[-1][0] + 1:
|
||||
runs.append(run)
|
||||
run = []
|
||||
run.append(item)
|
||||
if run:
|
||||
runs.append(run)
|
||||
for segment in runs:
|
||||
if len(segment) < 8:
|
||||
continue
|
||||
valid_bins += len(segment)
|
||||
raw = [value for _, value in segment]
|
||||
highpass = []
|
||||
for position, value in enumerate(raw):
|
||||
nearby = raw[max(0, position - 4):min(len(raw), position + 5)]
|
||||
highpass.append(value - sum(nearby) / len(nearby))
|
||||
amplitudes.append(max(highpass) - min(highpass))
|
||||
steps.extend(abs(raw[i] - raw[i - 1]) for i in range(1, len(raw)))
|
||||
max_ripple = max(amplitudes, default=0.0) * 0.5
|
||||
max_step = max(steps, default=0.0)
|
||||
present = (floor_triangles >= 24 and valid_bins >= 8
|
||||
and wall_triangles >= 24 and len(wall_bins) >= 3)
|
||||
detected = present and (max_ripple > 0.015 or max_step > 0.050)
|
||||
return {"present": present, "detected": detected, "triangles": floor_triangles,
|
||||
"bins": valid_bins, "lateral_bands": len(by_lateral),
|
||||
"wall_triangles": wall_triangles, "wall_bins": len(wall_bins),
|
||||
"ripple_amplitude_m": max_ripple, "max_adjacent_step_m": max_step,
|
||||
"thresholds": {"ripple_amplitude_m": 0.015, "adjacent_step_m": 0.050}}
|
||||
|
||||
|
||||
def wall_band_measure(pairs, route, edge_cutoff):
|
||||
"""Locate high dihedrals around the bore-foot and the L47 0.25 m band edge."""
|
||||
if route is None:
|
||||
return {}
|
||||
bins = {}
|
||||
radius = route["bore_radius"]
|
||||
for _i, _j, angle, _length, midpoint in pairs:
|
||||
distance, route_s, floor_z, _lateral = route_nearest(midpoint, route)
|
||||
height = midpoint[2] - floor_z
|
||||
if (0.0 <= route_s <= 10.0 and abs(distance - radius) <= 0.75
|
||||
and -0.25 <= height <= 0.875):
|
||||
index = int(math.floor((height + 0.25) / 0.125))
|
||||
entry = bins.setdefault(index, [])
|
||||
entry.append(angle)
|
||||
result = {}
|
||||
for index, angles in sorted(bins.items()):
|
||||
center_height = (index + 0.5) * 0.125 - 0.25
|
||||
result[f"{center_height:.3f}"] = {
|
||||
"edges": len(angles),
|
||||
"over_reference_cutoff": sum(value >= edge_cutoff for value in angles),
|
||||
"p95_dihedral_deg": quantile(angles, 0.95),
|
||||
"max_dihedral_deg": max(angles, default=0.0),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def radial_wall_measure(pairs, route, edge_cutoff):
|
||||
"""Measure dihedrals across the hard bore-radius gate at the entrance foot."""
|
||||
if route is None:
|
||||
return {}
|
||||
bins = {}
|
||||
radius = route["bore_radius"]
|
||||
for _i, _j, angle, _length, midpoint in pairs:
|
||||
distance, route_s, floor_z, _lateral = route_nearest(midpoint, route)
|
||||
height = midpoint[2] - floor_z
|
||||
if (-5.0 <= route_s <= 3.0 and -1.0 <= height <= 0.75
|
||||
and abs(distance - radius) <= 1.0):
|
||||
index = int(math.floor((distance - (radius - 1.0)) / 0.125))
|
||||
bins.setdefault(index, []).append(angle)
|
||||
return {f"{radius - 1.0 + (index + 0.5) * 0.125:.3f}": {
|
||||
"edges": len(angles),
|
||||
"over_reference_cutoff": sum(value >= edge_cutoff for value in angles),
|
||||
"p95_dihedral_deg": quantile(angles, 0.95),
|
||||
"max_dihedral_deg": max(angles, default=0.0),
|
||||
} for index, angles in sorted(bins.items())}
|
||||
|
||||
|
||||
def svg_for(path, triangles, route, hot_triangles, label):
|
||||
selected = [triangles[index] for index in hot_triangles]
|
||||
all_region = [tri for tri in triangles if tri.in_region]
|
||||
points = [point for tri in all_region for point in tri.verts]
|
||||
if not points:
|
||||
return
|
||||
xmin, xmax = min(p[0] for p in points), max(p[0] for p in points)
|
||||
ymin, ymax = min(p[1] for p in points), max(p[1] for p in points)
|
||||
width, height, margin = 1200.0, 900.0, 35.0
|
||||
sx = (width - 2 * margin) / max(1.0e-6, xmax - xmin)
|
||||
sy = (height - 2 * margin) / max(1.0e-6, ymax - ymin)
|
||||
scale = min(sx, sy)
|
||||
draw_w, draw_h = (xmax - xmin) * scale, (ymax - ymin) * scale
|
||||
ox, oy = (width - draw_w) * 0.5, (height - draw_h) * 0.5
|
||||
def project(point):
|
||||
return ox + (point[0] - xmin) * scale, height - oy - (point[1] - ymin) * scale
|
||||
lines = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{int(width)}" height="{int(height)}" viewBox="0 0 {int(width)} {int(height)}">',
|
||||
'<rect width="100%" height="100%" fill="#fbfbfa"/>',
|
||||
f'<text x="28" y="28" font-family="sans-serif" font-size="19" fill="#222">{label}: whole-surface defect clusters (XY plan)</text>',
|
||||
'<g fill="#c8ced2" fill-opacity="0.24" stroke="#8b959b" stroke-opacity="0.12" stroke-width="0.3">']
|
||||
for tri in all_region:
|
||||
coords = [project(p) for p in tri.verts]
|
||||
lines.append('<polygon points="' + " ".join(f"{x:.1f},{y:.1f}" for x, y in coords)
|
||||
+ '"/>')
|
||||
lines.append('</g><g fill="#d7191c" fill-opacity="0.78" stroke="#7f0000" stroke-width="0.4">')
|
||||
for tri in selected:
|
||||
coords = [project(p) for p in tri.verts]
|
||||
lines.append('<polygon points="' + " ".join(f"{x:.1f},{y:.1f}" for x, y in coords)
|
||||
+ '"/>')
|
||||
route_coords = [project(p) for p in route["route_points"]]
|
||||
lines.append('</g><polyline points="' + " ".join(f"{x:.1f},{y:.1f}" for x, y in route_coords)
|
||||
+ '" fill="none" stroke="#1769aa" stroke-width="2"/>')
|
||||
lines.append(f'<text x="28" y="{int(height - 24)}" font-family="sans-serif" font-size="15" fill="#333">Blue: intended route · Red: flagged triangle cells · Gray: remaining ROI triangles · flagged triangles: {len(selected)}</text>')
|
||||
lines.append('</svg>')
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def audit_case(label, mesh_path, layout_path, expect, focus, svg_dir):
|
||||
triangles, voxel_um, tile_count, lod_counts = read_mesh(mesh_path)
|
||||
route = None
|
||||
if layout_path != Path("-"):
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
route = make_route(layout)
|
||||
mark_region(triangles, route)
|
||||
region_indices = [i for i, tri in enumerate(triangles) if tri.in_region]
|
||||
else:
|
||||
region_indices = list(range(len(triangles)))
|
||||
for tri in triangles:
|
||||
tri.in_region = True
|
||||
tri.reference = True
|
||||
if len(region_indices) < 24:
|
||||
raise ValueError(f"{label}: only {len(region_indices)} triangles intersect the ROI")
|
||||
reference_indices = [i for i in region_indices if triangles[i].reference]
|
||||
if len(reference_indices) < 24:
|
||||
reference_indices = region_indices
|
||||
for index in region_indices:
|
||||
triangles[index].reference = True
|
||||
edge_pairs = make_edges(triangles, region_indices)
|
||||
reference_edges = [edge[2] for edge in edge_pairs
|
||||
if triangles[edge[0]].reference and triangles[edge[1]].reference]
|
||||
edge_cutoff = max(35.0, quantile(reference_edges, 0.995))
|
||||
reference_set = set(reference_indices)
|
||||
ref_roughness = {index: 0.0 for index in reference_indices}
|
||||
for i, j, angle, length, _midpoint in edge_pairs:
|
||||
if i in reference_set and j in reference_set:
|
||||
ref_roughness[i] += angle * length * 0.5
|
||||
ref_roughness[j] += angle * length * 0.5
|
||||
ref_values = [ref_roughness[index] / max(triangles[index].area, 1.0e-9)
|
||||
for index in reference_indices]
|
||||
rough_cutoff = 10.0
|
||||
ref_density = 0.0
|
||||
ref_area = sum(triangles[index].area for index in reference_indices)
|
||||
ref_severe = 0.0
|
||||
for i, j, angle, _length, _midpoint in edge_pairs:
|
||||
if i in reference_set and j in reference_set and angle >= edge_cutoff:
|
||||
ref_severe += 1.0
|
||||
ref_density = ref_severe / max(ref_area, 1.0e-9)
|
||||
if focus == "floor":
|
||||
considered = [i for i in region_indices if route is not None
|
||||
and triangles[i].route_s >= 1.5
|
||||
and triangles[i].route_s <= 10.0
|
||||
and triangles[i].route_distance <= 1.25
|
||||
and abs(triangles[i].center[2] - triangles[i].floor_z) <= 0.24
|
||||
and triangles[i].smooth[2] >= 0.55]
|
||||
else:
|
||||
considered = region_indices
|
||||
if len(considered) < 24:
|
||||
raise ValueError(f"{label}: focus={focus} has only {len(considered)} triangles")
|
||||
scores = score_surface(triangles, considered, edge_pairs, edge_cutoff,
|
||||
rough_cutoff, ref_density)
|
||||
floor_profile = floor_profile_audit(triangles, considered, region_indices, route)
|
||||
wall_band = wall_band_measure(edge_pairs, route, edge_cutoff)
|
||||
radial_wall = radial_wall_measure(edge_pairs, route, edge_cutoff)
|
||||
found = bool(scores["clusters"]) or floor_profile["detected"]
|
||||
passed = expect == "measure" or (expect == "fail" and found) or (expect == "pass" and not found)
|
||||
if svg_dir and route is not None:
|
||||
svg_for(svg_dir / f"flagged-regions-{label}.svg", triangles, route,
|
||||
scores["hot_triangles"], label)
|
||||
cluster_summary = scores["clusters"][:8]
|
||||
result = {
|
||||
"label": label,
|
||||
"mesh": str(mesh_path),
|
||||
"layout": None if layout_path == Path("-") else str(layout_path),
|
||||
"focus": focus,
|
||||
"expected": expect,
|
||||
"observed": "fail" if found else "pass",
|
||||
"status": "PASS" if passed else "FAIL",
|
||||
"voxel_um": voxel_um,
|
||||
"lod0_tiles": tile_count,
|
||||
"lod_tile_counts": lod_counts,
|
||||
"triangles_loaded_lod0": len(triangles),
|
||||
"triangles_in_roi": len(region_indices),
|
||||
"triangles_audited": len(considered),
|
||||
"adjacent_pairs": len(edge_pairs),
|
||||
"reference_triangles": len(reference_indices),
|
||||
"reference_edge_p995_deg": quantile(reference_edges, 0.995),
|
||||
"reference_p995_normal_variation_deg_per_m": quantile(ref_values, 0.995),
|
||||
"thresholds": scores["thresholds"],
|
||||
"flagged_cells": len(scores["hot_cells"]),
|
||||
"flagged_triangles": len(scores["hot_triangles"]),
|
||||
"floor_profile": floor_profile,
|
||||
"wall_band_height_histogram": wall_band,
|
||||
"radial_wall_histogram": radial_wall,
|
||||
"clusters": cluster_summary,
|
||||
}
|
||||
print(f"[WholeSurfaceAudit] label={label} focus={focus} lod0_tiles={tile_count} "
|
||||
f"triangles_roi={len(region_indices)} audited={len(considered)} "
|
||||
f"adjacent_pairs={len(edge_pairs)} reference={len(reference_indices)} "
|
||||
f"edge_cutoff_deg={edge_cutoff:.2f} "
|
||||
f"roughness_cutoff_deg_m={scores['thresholds']['roughness_deg_per_m']:.2f} "
|
||||
f"hot_cells={len(scores['hot_cells'])} flagged_triangles={len(scores['hot_triangles'])} "
|
||||
f"clusters={len(scores['clusters'])} observed={result['observed'].upper()} "
|
||||
f"expected={expect.upper()} status={result['status']}")
|
||||
print(f"[WholeSurfaceFloorProfile] label={label} triangles={floor_profile['triangles']} "
|
||||
f"bins={floor_profile['bins']} lateral_bands={floor_profile['lateral_bands']} "
|
||||
f"wall_triangles={floor_profile.get('wall_triangles', 0)} "
|
||||
f"wall_bins={floor_profile.get('wall_bins', 0)} "
|
||||
f"ripple_amplitude_m={floor_profile['ripple_amplitude_m']:.4f} "
|
||||
f"max_adjacent_step_m={floor_profile['max_adjacent_step_m']:.4f} "
|
||||
f"observed={'FAIL' if floor_profile['detected'] else ('PASS' if floor_profile['present'] else 'NOT_PRESENT')}")
|
||||
if wall_band:
|
||||
print(f"[WholeSurfaceWallBandMeasure] label={label} voxel_m={voxel_um / 1_000_000.0:.3f} "
|
||||
f"recarve_edge_height_m=0.250 radial_band_m={route['bore_radius']:.3f} "
|
||||
f"height_histogram={json.dumps(wall_band, sort_keys=True, separators=(',', ':'))}")
|
||||
print(f"[WholeSurfaceRadialWallMeasure] label={label} route_s_m=-5..3 "
|
||||
f"height_above_floor_m=-1..0.75 bore_radius_m={route['bore_radius']:.3f} "
|
||||
f"radial_histogram={json.dumps(radial_wall, sort_keys=True, separators=(',', ':'))}")
|
||||
for index, cluster in enumerate(cluster_summary):
|
||||
print(f"[WholeSurfaceCluster] label={label} index={index} cells={cluster['cells']} "
|
||||
f"area_m2={cluster['area_m2']:.3f} severe_edges={cluster['severe_edges']:.1f} "
|
||||
f"slivers={cluster['sliver_triangles']} bounds_m={cluster['bounds_m']} "
|
||||
f"reasons={','.join(cluster['reasons'])}")
|
||||
hot_cells = scores["cells"]
|
||||
for key in sorted(scores["hot_cells"], key=lambda item: (
|
||||
hot_cells[item]["severe"], hot_cells[item]["variation"]), reverse=True)[:12]:
|
||||
cell = hot_cells[key]
|
||||
local = route_nearest(tuple((axis + 0.5) * CELL_M for axis in key), route) if route else None
|
||||
print(f"[WholeSurfaceHotCell] label={label} cell_m=({key[0]},{key[1]},{key[2]}) "
|
||||
f"route_s_m={local[1] if local else 0.0:.2f} radial_m={local[0] if local else 0.0:.2f} "
|
||||
f"height_above_floor_m={(key[2] + 0.5 - local[2]) if local else 0.0:.2f} "
|
||||
f"area_m2={cell['area']:.3f} roughness_deg_m={cell['roughness_deg_per_m']:.2f} "
|
||||
f"severe_edges={cell['severe']:.1f} edge_density_m2={cell['severe_edge_density_m2']:.2f} "
|
||||
f"slivers={cell['slivers']} max_jump_deg={cell['max_jump']:.2f} "
|
||||
f"reasons={','.join(cell['reasons'])}")
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--case", nargs=5, action="append", required=True,
|
||||
metavar=("LABEL", "MESH", "LAYOUT_OR_DASH", "EXPECT", "FOCUS"))
|
||||
parser.add_argument("--json", type=Path, required=True)
|
||||
parser.add_argument("--svg-dir", type=Path)
|
||||
args = parser.parse_args()
|
||||
results = []
|
||||
for label, mesh, layout, expect, focus in args.case:
|
||||
if expect not in ("pass", "fail", "measure") or focus not in ("whole", "floor"):
|
||||
parser.error("EXPECT must be pass/fail/measure and FOCUS must be whole/floor")
|
||||
results.append(audit_case(label, Path(mesh), Path(layout), expect, focus, args.svg_dir))
|
||||
args.json.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.json.write_text(json.dumps({"audit": "whole-surface-l48-v1", "cases": results},
|
||||
indent=2) + "\n", encoding="utf-8")
|
||||
failures = [result for result in results if result["status"] != "PASS"]
|
||||
print(f"[WholeSurfaceAuditSummary] cases={len(results)} failed={len(failures)} "
|
||||
f"json={args.json} status={'FAIL' if failures else 'PASS'}")
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture L48 cavern entrance views and audit the exported floor profile."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
VERIFY = ROOT / "Standalone/Tools/verify"
|
||||
LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37"
|
||||
sys.path.insert(0, str(VERIFY))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def load_module(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"cannot import {path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
PROFILE = load_module("l45_ring_profile", ROOT /
|
||||
"Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py")
|
||||
SHAPE = load_module("l45_shape_audit", LINUX37 / "AuditMouthShapeL37.py")
|
||||
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--layout", type=Path, required=True)
|
||||
PARSER.add_argument("--artifact-dir", type=Path, required=True)
|
||||
PARSER.add_argument("--capture-dir", type=Path, required=True)
|
||||
PARSER.add_argument("--data", type=Path, default=ROOT / "Standalone/data/owner_world.json")
|
||||
OPTIONS = PARSER.parse_args()
|
||||
|
||||
ARTIFACT_DIR = OPTIONS.artifact_dir.resolve()
|
||||
CAPTURE_DIR = OPTIONS.capture_dir.resolve()
|
||||
LAYOUT_PATH = OPTIONS.layout.resolve()
|
||||
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
CAPTURE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
PNG_DIR = ARTIFACT_DIR / "capture-png"
|
||||
PNG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
HEADLESS = Path(os.environ["VOXELFORGE_L37_HEADLESS"])
|
||||
RENDERER = Path(os.environ["VOXELFORGE_L37_RENDERER"])
|
||||
EXPECTED_STAMP = current_stamp()
|
||||
|
||||
|
||||
def run_logged(name: str, command: list[str], *, capture: bool = False) -> str:
|
||||
log = ARTIFACT_DIR / f"{name}.log"
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = bounded_run(command, timeout=1800, stdout=stream)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if result is None:
|
||||
raise RuntimeError(f"{name} timed out; see {log}")
|
||||
print(f"[L48CaptureRun] step={name} elapsed={elapsed:.1f}s "
|
||||
f"exit={result.returncode} log={log}", flush=True)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{name} failed; see {log}")
|
||||
if capture and (first_stamp(output) != EXPECTED_STAMP
|
||||
or first_stamp(bounded_run(
|
||||
[str(command[0]), "--print-source-stamp"], timeout=15).stdout or "")
|
||||
!= EXPECTED_STAMP):
|
||||
raise RuntimeError(f"{name} executable source stamp mismatch")
|
||||
return output
|
||||
|
||||
|
||||
def stamp_by_id(layout: dict, stable_id: str) -> dict:
|
||||
for stamp in layout.get("stamps", []):
|
||||
if stamp.get("stable_id") == stable_id:
|
||||
return stamp
|
||||
raise RuntimeError(f"missing stamp {stable_id}")
|
||||
|
||||
|
||||
def metres(stamp: dict, key: str = "position_um") -> tuple[float, float, float]:
|
||||
return tuple(value / 1_000_000.0 for value in stamp[key])
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
dx, dy, dz = (target[index] - camera[index] for index in range(3))
|
||||
return (math.degrees(math.atan2(dx, dy)),
|
||||
math.degrees(math.atan2(dz, math.hypot(dx, dy))))
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def camera_spec(camera, target):
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
def edge_point(edge: dict, t: float) -> tuple[float, float, float]:
|
||||
points = edge["control_points_um"]
|
||||
one = 1.0 - t
|
||||
weights = (one ** 3, 3 * one ** 2 * t, 3 * one * t ** 2, t ** 3)
|
||||
return tuple(sum(weights[index] * points[index][axis]
|
||||
for index in range(4)) / 1_000_000.0
|
||||
for axis in range(3))
|
||||
|
||||
|
||||
def cavern_features(layout: dict):
|
||||
if (layout.get("cavern_graph_vocabulary_version") != 1
|
||||
or len(layout.get("cavern_rooms", [])) != 1
|
||||
or len(layout.get("cavern_edges", [])) != 1):
|
||||
raise RuntimeError("L48 capture needs one resolved cavern room and edge")
|
||||
room = layout["cavern_rooms"][0]
|
||||
edge = layout["cavern_edges"][0]
|
||||
return room, edge
|
||||
|
||||
|
||||
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
|
||||
room, edge = cavern_features(layout)
|
||||
portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1")
|
||||
floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor")
|
||||
dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais")
|
||||
entrance = edge_point(edge, 0.0)
|
||||
next_ramp = edge_point(edge, 0.08)
|
||||
portal = metres(portal_stamp)
|
||||
chamber = metres(room, "center_um")
|
||||
floor = metres(floor_stamp)
|
||||
floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
dais = metres(dais_stamp)
|
||||
dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
ground_z = entrance[2]
|
||||
direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1])
|
||||
direction = ((next_ramp[0] - entrance[0]) / direction_length,
|
||||
(next_ramp[1] - entrance[1]) / direction_length)
|
||||
outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1])
|
||||
outward = ((entrance[0] - chamber[0]) / outward_length,
|
||||
(entrance[1] - chamber[1]) / outward_length)
|
||||
mouth_normal = (-direction[1], direction[0])
|
||||
|
||||
mesh = ARTIFACT_DIR / "landmark-l48.vfmesh"
|
||||
native_mesh = ARTIFACT_DIR / "control-plain-native.vfmesh"
|
||||
export_anchor = edge_point(edge, 0.25)
|
||||
export_camera = (export_anchor[0], export_anchor[1], export_anchor[2] + 1.4)
|
||||
export_target_floor = edge_point(edge, 0.42)
|
||||
export_target = (export_target_floor[0], export_target_floor[1],
|
||||
export_target_floor[2] + 1.2)
|
||||
run_logged("landmark-export", [str(HEADLESS), str(OPTIONS.data), "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=4", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--recipe=surface-world", "--landmark-chamber-demo",
|
||||
f"--camera={fmt(camera_spec(export_camera, export_target))}",
|
||||
f"--mesh-export={mesh}", f"--resolved-layout-output={LAYOUT_PATH}"], capture=True)
|
||||
run_logged("plain-native-export", [str(HEADLESS), str(OPTIONS.data), "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=4", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", "--recipe=surface-world",
|
||||
f"--camera={fmt(camera_spec(export_camera, export_target))}",
|
||||
f"--mesh-export={native_mesh}",
|
||||
f"--resolved-layout-output={ARTIFACT_DIR / 'control-plain-native-layout.json'}"],
|
||||
capture=True)
|
||||
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
|
||||
room, edge = cavern_features(layout)
|
||||
portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1")
|
||||
floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor")
|
||||
dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais")
|
||||
entrance = edge_point(edge, 0.0)
|
||||
next_ramp = edge_point(edge, 0.08)
|
||||
portal = metres(portal_stamp)
|
||||
chamber = metres(room, "center_um")
|
||||
floor = metres(floor_stamp)
|
||||
floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
dais = metres(dais_stamp)
|
||||
dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0
|
||||
ground_z = entrance[2]
|
||||
direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1])
|
||||
direction = ((next_ramp[0] - entrance[0]) / direction_length,
|
||||
(next_ramp[1] - entrance[1]) / direction_length)
|
||||
outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1])
|
||||
outward = ((entrance[0] - chamber[0]) / outward_length,
|
||||
(entrance[1] - chamber[1]) / outward_length)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"missing mesh export {mesh}")
|
||||
|
||||
entrance_audit = ROOT / "Standalone/Research/WorldAssemblerM3/Linux47/MeasureEntranceL47.py"
|
||||
run_logged("entrance-floor-control-l46", [sys.executable, str(entrance_audit),
|
||||
"--mesh", str(ROOT / "Saved/Linux/l46/landmark-l46.vfmesh"),
|
||||
"--layout", str(ROOT / "Saved/Linux/l46/layouts/landmark-reachability-clang.json"),
|
||||
"--out", str(ARTIFACT_DIR / "entrance-profile-control-l46.json"), "--expect=fail"])
|
||||
run_logged("entrance-floor-audit", [sys.executable, str(entrance_audit),
|
||||
"--mesh", str(mesh), "--layout", str(LAYOUT_PATH),
|
||||
"--out", str(ARTIFACT_DIR / "entrance-profile-l48.json"), "--expect=pass"])
|
||||
|
||||
voxel_um, triangles, tile_count = PROFILE.read_scene(str(mesh), 0,
|
||||
floor_top - 8.0, entrance[2] + 9.0,
|
||||
(min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0,
|
||||
min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0))
|
||||
bounds = (min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0,
|
||||
min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0)
|
||||
buckets = PROFILE.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||
ground_candidates = []
|
||||
for ix in range(-20, 21):
|
||||
for iy in range(-20, 21):
|
||||
x, y = portal[0] + ix * 0.5, portal[1] + iy * 0.5
|
||||
dx, dy = x - portal[0], y - portal[1]
|
||||
distance = math.hypot(dx, dy)
|
||||
if not 6.0 <= distance <= 10.0:
|
||||
continue
|
||||
hit = PROFILE.sample_surface(buckets, x, y, True, 1.0,
|
||||
preferred_height=ground_z)
|
||||
if hit is None or abs(hit[0] - ground_z) > 3.0 or hit[1][2] < 0.85:
|
||||
continue
|
||||
alignment = max(-1.0, min(1.0,
|
||||
(dx * mouth_normal[0] + dy * mouth_normal[1]) / distance))
|
||||
angle = math.acos(alignment)
|
||||
score = abs(distance - 7.0) + angle * 4.0
|
||||
ground_candidates.append((score, x, y, hit[0]))
|
||||
if not ground_candidates:
|
||||
raise RuntimeError("no walkable mesh ground lies 6 to 10 m from the cave mouth")
|
||||
_, ground_x, ground_y, outside_ground = min(ground_candidates)
|
||||
inside_floor = PROFILE.sample_surface(buckets, chamber[0], chamber[1], True, 1.0,
|
||||
preferred_height=floor_top)
|
||||
if inside_floor is None or abs(inside_floor[0] - floor_top) > 1.5:
|
||||
raise RuntimeError(f"chamber floor mesh hit {inside_floor} does not match designed floor {floor_top:.3f} m")
|
||||
floor_top = inside_floor[0]
|
||||
|
||||
ground_camera = (ground_x, ground_y, outside_ground + 1.6)
|
||||
entry_ground = PROFILE.sample_surface(buckets, entrance[0], entrance[1], True, 1.0,
|
||||
preferred_height=ground_z)
|
||||
if entry_ground is None:
|
||||
raise RuntimeError("cave mouth has no walkable ground surface in the exported mesh")
|
||||
entry_camera = (entrance[0] + mouth_normal[0] * 3.8,
|
||||
entrance[1] + mouth_normal[1] * 3.8, ground_z + 1.6)
|
||||
entry_target_floor = edge_point(edge, 0.30)
|
||||
entry_target = (entry_target_floor[0], entry_target_floor[1], entry_target_floor[2] + 1.4)
|
||||
low_camera = (entrance[0] + outward[0] * 1.0,
|
||||
entrance[1] + outward[1] * 1.0, ground_z + 0.65)
|
||||
low_target_floor = edge_point(edge, 0.24)
|
||||
low_target = (low_target_floor[0], low_target_floor[1], low_target_floor[2] + 0.15)
|
||||
to_dais = (dais[0] - chamber[0], dais[1] - chamber[1])
|
||||
to_dais_length = math.hypot(*to_dais)
|
||||
opposite_dais = (-to_dais[0] / to_dais_length, -to_dais[1] / to_dais_length)
|
||||
inside_x = chamber[0]
|
||||
inside_y = chamber[1]
|
||||
inside_floor = PROFILE.sample_surface(buckets, inside_x, inside_y, True, 1.0,
|
||||
preferred_height=floor_top)
|
||||
inside_eye_z = inside_floor[0] + 1.6
|
||||
chamber_camera = (chamber[0] + opposite_dais[0] * 5.5,
|
||||
chamber[1] + opposite_dais[1] * 5.5, inside_eye_z)
|
||||
chamber_target = (dais[0], dais[1], dais_top - 0.25)
|
||||
overview_camera = (entrance[0] + outward[0] * 18.0,
|
||||
entrance[1] + outward[1] * 18.0, outside_ground + 15.0)
|
||||
overview_target = ((entrance[0] + chamber[0]) * 0.5,
|
||||
(entrance[1] + chamber[1]) * 0.5,
|
||||
(ground_z + floor_top) * 0.5)
|
||||
inward = (-outward[0], -outward[1])
|
||||
clip_point = (entrance[0] + outward[0] * 0.5,
|
||||
entrance[1] + outward[1] * 0.5)
|
||||
overview_clip = (inward[0], inward[1], 0.0,
|
||||
-(inward[0] * clip_point[0] + inward[1] * clip_point[1]))
|
||||
|
||||
views = (
|
||||
("ground-entrance", ground_camera, (portal[0], portal[1], portal[2]),
|
||||
(portal[0], portal[1], ground_z, 20.0), -509.0),
|
||||
("entrance-looking-in", entry_camera, entry_target, (entrance[0], entrance[1], entrance[2], 16.0), -507.0),
|
||||
("entrance-floor-low", low_camera, low_target,
|
||||
(entrance[0], entrance[1], entrance[2], 12.0), ground_z),
|
||||
("inside-to-dais", chamber_camera, chamber_target,
|
||||
(chamber[0], chamber[1], chamber[2], 15.0), chamber[2]),
|
||||
("overview-entrance-and-chamber", overview_camera, overview_target,
|
||||
((entrance[0] + chamber[0]) * 0.5,
|
||||
(entrance[1] + chamber[1]) * 0.5,
|
||||
(ground_z + floor_top) * 0.5, 34.0), -509.0),
|
||||
)
|
||||
|
||||
for name, camera, target, focus, height_center in views:
|
||||
png = PNG_DIR / f"{name}.png"
|
||||
jpg = CAPTURE_DIR / f"{name}.jpg"
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
fov = (80.0 if name == "overview-entrance-and-chamber" else
|
||||
70.0 if name in ("inside-to-dais", "entrance-looking-in") else
|
||||
58.0 if name == "entrance-floor-low" else 65.0
|
||||
if name == "ground-entrance" else 60.0)
|
||||
if name == "ground-entrance":
|
||||
fov = 55.0
|
||||
print(f"[L48CaptureCamera] view={name} camera_m=({fmt(camera)}) "
|
||||
f"target_m=({fmt(target)}) yaw_deg={yaw:.3f} pitch_deg={pitch:.3f} "
|
||||
f"fov_deg={fov:.1f} entrance_distance_m={math.dist(camera, portal):.3f}", flush=True)
|
||||
render = [str(RENDERER), str(mesh), str(png), "--size=1600x900", f"--fov-deg={fov:.1f}",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--shade=readable", "--headlamp=on", "--height-tint=on",
|
||||
"--light-direction=0.3,-0.4,0.85",
|
||||
f"--height-center-m={(floor_top if name == 'inside-to-dais' else height_center):.6f}",
|
||||
f"--height-range-m={4 if name == 'inside-to-dais' else 6 if name == 'entrance-floor-low' else 16}",
|
||||
"--tile-skirts=off", "--tile-edges=off",
|
||||
f"--focus-region={fmt(focus)}"]
|
||||
if name == "overview-entrance-and-chamber":
|
||||
render.append(f"--clip-plane={fmt(overview_clip)}")
|
||||
run_logged(f"{name}-render", render, capture=True)
|
||||
for quality in (4, 6, 8):
|
||||
run_logged(f"{name}-jpeg-q{quality}", ["ffmpeg", "-nostdin", "-hide_banner",
|
||||
"-loglevel", "error", "-y", "-i", str(png), "-frames:v", "1",
|
||||
"-q:v", str(quality), str(jpg)])
|
||||
if jpg.is_file() and 0 < jpg.stat().st_size <= 300_000:
|
||||
break
|
||||
if not jpg.is_file() or not 0 < jpg.stat().st_size <= 300_000:
|
||||
raise RuntimeError(f"{jpg.name} exceeds the 300 KB capture limit")
|
||||
quality_log = run_logged(f"{name}-quality", [sys.executable,
|
||||
str(VERIFY / "capture_quality.py"), str(jpg)])
|
||||
print(f"[L48CaptureJPEG] file={jpg} bytes={jpg.stat().st_size} "
|
||||
f"quality_log={quality_log.strip().replace(chr(10), ' ')} status=PASS", flush=True)
|
||||
|
||||
|
||||
def region_triangles(center, radius_xy, z_min, z_max):
|
||||
selected = []
|
||||
for triangle in triangles:
|
||||
points = tuple(vertex[0] for vertex in triangle[2])
|
||||
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
||||
if (math.hypot(centroid[0] - center[0], centroid[1] - center[1]) <= radius_xy
|
||||
and z_min <= centroid[2] <= z_max):
|
||||
selected.append(triangle)
|
||||
return selected
|
||||
|
||||
|
||||
def tunnel_wall_triangles(edge, t, height_above_floor):
|
||||
center = edge_point(edge, t)
|
||||
before = edge_point(edge, max(0.0, t - 0.01))
|
||||
after = edge_point(edge, min(1.0, t + 0.01))
|
||||
tx, ty = after[0] - before[0], after[1] - before[1]
|
||||
tangent_length = math.hypot(tx, ty)
|
||||
tx, ty = tx / tangent_length, ty / tangent_length
|
||||
expected_radius = edge["bore_radius_um"] / 1_000_000.0 * math.sqrt(
|
||||
1.0 - (height_above_floor / (edge["bore_height_um"] / 1_000_000.0)) ** 2)
|
||||
selected = []
|
||||
for triangle in triangles:
|
||||
points = tuple(vertex[0] for vertex in triangle[2])
|
||||
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
||||
dx, dy = centroid[0] - center[0], centroid[1] - center[1]
|
||||
longitudinal = dx * tx + dy * ty
|
||||
lateral = abs(-dy * tx + dx * ty)
|
||||
vertical = centroid[2] - center[2]
|
||||
if (abs(longitudinal) <= 1.5
|
||||
and abs(lateral - expected_radius) <= 0.7
|
||||
and abs(vertical - height_above_floor) <= 0.75):
|
||||
selected.append(triangle)
|
||||
print(f"[L47TunnelWallWindow] t={t:.3f} center_m=({center[0]:.3f},"
|
||||
f"{center[1]:.3f},{center[2]:.3f}) expected_radius_m={expected_radius:.3f} "
|
||||
f"longitudinal_half_width_m=1.500 vertical_half_width_m=0.750 "
|
||||
f"triangles={len(selected)}", flush=True)
|
||||
return selected
|
||||
|
||||
|
||||
def audit_shape(name, region, center):
|
||||
metrics = SHAPE.analyze_triangles(region, center[:2], center[2],
|
||||
crop_to_center=False, include_sides=True)
|
||||
passed = (metrics["triangles"] > 0
|
||||
and metrics["planar_area"] < SHAPE.PLANE_LIMIT_M2
|
||||
and metrics["straight"] <= SHAPE.STRAIGHT_LIMIT_M
|
||||
and metrics["corners"] == 0)
|
||||
print(f"[LandmarkBuiltShape] region={name} triangles={metrics['triangles']} "
|
||||
f"planar_patch_m2={metrics['planar_area']:.4f} "
|
||||
f"straight_run_m={metrics['straight']:.4f} sharp_corners={metrics['corners']} "
|
||||
f"corner_turn_max_deg={metrics['corner_turn_max']:.2f} "
|
||||
f"thresholds=({SHAPE.PLANE_LIMIT_M2:.2f}m2,{SHAPE.STRAIGHT_LIMIT_M:.2f}m,"
|
||||
f"{SHAPE.CORNER_TURN_LIMIT_DEG:.0f}deg) status={'PASS' if passed else 'FAIL'}",
|
||||
flush=True)
|
||||
if not passed:
|
||||
raise RuntimeError(f"L37 built-shape thresholds failed in {name}")
|
||||
return metrics
|
||||
|
||||
|
||||
entrance_metrics = audit_shape("cave-mouth", region_triangles(
|
||||
portal, 5.0, ground_z + 0.8, ground_z + 4.5),
|
||||
(portal[0], portal[1], ground_z))
|
||||
tunnel_mid = edge_point(edge, 0.64)
|
||||
tunnel_metrics = audit_shape("tunnel-wall-edge", tunnel_wall_triangles(
|
||||
edge, 0.64, 1.2),
|
||||
(tunnel_mid[0], tunnel_mid[1], tunnel_mid[2] + 1.2))
|
||||
dais_surface = PROFILE.sample_surface(buckets, dais[0], dais[1], True, 1.0,
|
||||
preferred_height=dais_top - 0.4)
|
||||
if dais_surface is None:
|
||||
raise RuntimeError("dais stamp has no upward-facing surface in the exported mesh")
|
||||
dais_rise = dais_surface[0] - floor_top
|
||||
print(f"[LandmarkDaisSurface] center_m=({dais[0]:.3f},{dais[1]:.3f}) "
|
||||
f"floor_z_m={floor_top:.3f} top_z_m={dais_surface[0]:.3f} "
|
||||
f"rise_m={dais_rise:.3f} minimum_m=1.000 "
|
||||
f"status={'PASS' if dais_rise >= 1.0 else 'FAIL'}", flush=True)
|
||||
if dais_rise < 1.0:
|
||||
raise RuntimeError(f"dais rises only {dais_rise:.3f} m above the chamber floor")
|
||||
dais_metrics = audit_shape("dais-platform", region_triangles(
|
||||
dais, 1.7, floor_top + 0.6, dais_top + 0.4),
|
||||
(dais[0], dais[1], dais_surface[0]))
|
||||
metrics_path = ARTIFACT_DIR / "landmark-shape-metrics.json"
|
||||
metrics_path.write_text(json.dumps({
|
||||
"graph_vocabulary_version": layout["cavern_graph_vocabulary_version"],
|
||||
"entry_m": entrance, "portal_m": portal, "entry_ground_m": ground_z,
|
||||
"chamber_center_m": chamber, "chamber_floor_m": floor_top,
|
||||
"tunnel_midpoint_m": tunnel_mid,
|
||||
"dais_center_m": dais, "dais_top_m": dais_top,
|
||||
"dais_surface_m": dais_surface[0], "dais_rise_m": dais_rise,
|
||||
"l37_thresholds": {"planar_patch_m2_lt": SHAPE.PLANE_LIMIT_M2,
|
||||
"straight_run_m_lte": SHAPE.STRAIGHT_LIMIT_M,
|
||||
"corner_turn_deg_lt": SHAPE.CORNER_TURN_LIMIT_DEG},
|
||||
"entrance_arch": entrance_metrics, "tunnel_wall": tunnel_metrics,
|
||||
"dais_platform": dais_metrics,
|
||||
"mesh_tiles": tile_count, "mesh_voxel_um": voxel_um,
|
||||
}, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L48CaptureShapeMetrics] file={metrics_path} status=PASS", flush=True)
|
||||