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 | ||
|
|
ba017077d1 | ||
|
|
49e29fcd36 | ||
|
|
b7a47bef95 | ||
|
|
d0158c6019 | ||
|
|
28069f0d98 | ||
|
|
103d8b471e | ||
|
|
63c7e563d0 |
No files matched your search
@@ -1883,8 +1883,9 @@ Each record separates the nominal ceiling, content, and bedrock bands. A strict
|
|||||||
sequence for every strate and requires bedrock(N) to touch the next strate's ceiling(N+1). The two
|
sequence for every strate and requires bedrock(N) to touch the next strate's ceiling(N+1). The two
|
||||||
bands remain separately owned. Gradient and Interleaved interfaces may declare interpenetration only
|
bands remain separately owned. Gradient and Interleaved interfaces may declare interpenetration only
|
||||||
inside their explicit transition zone; Hard has zero effective width. The manager's `TransitionBlendChunks`
|
inside their explicit transition zone; Hard has zero effective width. The manager's `TransitionBlendChunks`
|
||||||
is expressed in owner-chunk metres. Interleaved's existing two-chunk domain warp extends its possible
|
is expressed in owner-chunk metres. L21 recorded the older Interleaved allowance as blend width plus
|
||||||
fingers beyond the blend width and is recorded separately. Compatibility validation reports current
|
warp amplitude; L29 now treats the declared blend width as the complete physical crossing allowance.
|
||||||
|
Domain warp changes finger placement, but cannot extend overlap beyond that width. Compatibility validation reports current
|
||||||
gaps, overlaps, ambiguities, inactive/missing bands, and unhonoured transitions while accepting them;
|
gaps, overlaps, ambiguities, inactive/missing bands, and unhonoured transitions while accepting them;
|
||||||
strict validation rejects them. Core dispatches per-strate recipe fields without applying that
|
strict validation rejects them. Core dispatches per-strate recipe fields without applying that
|
||||||
parameter blend, even though the legacy manager helper implements it. Both band types default diggable,
|
parameter blend, even though the legacy manager helper implements it. Both band types default diggable,
|
||||||
@@ -1943,6 +1944,7 @@ opens a band; the support strip stays below the player capsule. The lower mouth
|
|||||||
monotone descent from the short route-bound landing tail, whose height may vary with the route.
|
monotone descent from the short route-bound landing tail, whose height may vary with the route.
|
||||||
Both route and passage audits sample the 0.34 m by 1.76 m capsule, support probes, and grade; the
|
Both route and passage audits sample the 0.34 m by 1.76 m capsule, support probes, and grade; the
|
||||||
negative control compiles the same world without passage carving and must detect a blocked capsule.
|
negative control compiles the same world without passage carving and must detect a blocked capsule.
|
||||||
|
|
||||||
The L23 shared-field change is `WorldAssemblyEvaluatorVersion=9`. It does not change the L22 bands
|
The L23 shared-field change is `WorldAssemblyEvaluatorVersion=9`. It does not change the L22 bands
|
||||||
or any generator's field away from a passage. The L23 report includes exact-query/hash comparisons,
|
or any generator's field away from a passage. The L23 report includes exact-query/hash comparisons,
|
||||||
the blocked control, captures, and the Windows adapter checklist. §8.10's cache, proof, streaming,
|
the blocked control, captures, and the Windows adapter checklist. §8.10's cache, proof, streaming,
|
||||||
@@ -2051,6 +2053,245 @@ proof bounds preserve the L27 output.
|
|||||||
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md)
|
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md)
|
||||||
records counts, parity, costs and audit caveats.
|
records counts, parity, costs and audit caveats.
|
||||||
|
|
||||||
|
### 8.46 Honored bedrock/ceiling transitions — World Assembler L29
|
||||||
|
|
||||||
|
The upper strate's `EVoxelStrateTransition` controls the shared interface between its bedrock band
|
||||||
|
and the next strate's ceiling band in the Core composition path. `TransitionBlendChunks` is converted
|
||||||
|
using the resolved layout chunk width (8 m by default). Hard preserves the flat interface. Gradient
|
||||||
|
moves the shared surface with a smooth 2D hash field whose peak-to-peak range is at most one quarter
|
||||||
|
of the declared width. Interleaved uses a seed/interface/coordinate-keyed, domain-warped 3D hash field
|
||||||
|
to set actual overlap between the facing bands from zero to the declared width. The two faces move
|
||||||
|
inward by half that overlap; the warp changes finger placement without increasing the crossing
|
||||||
|
allowance. Both bands keep authored thickness, and their solid intervals touch or overlap in every
|
||||||
|
XY column. Core does not blend unrelated generator fields.
|
||||||
|
|
||||||
|
Each boundary band keeps at least **two Core voxels** of solid thickness (0.5 m at the default 25 cm
|
||||||
|
voxel size). Strict validation rejects thinner bands and declared interpenetration beyond the
|
||||||
|
transition width. The world audit samples a 512 m square XY area, measures actual crossing depth and
|
||||||
|
minimum band thickness, and sweeps every non-passage column through both bands at half-voxel spacing.
|
||||||
|
Passage columns are excluded from the hole sweep. Near an interface mouth, the shared face returns to
|
||||||
|
its nominal height inside the mouth radius plus a 1 m support margin, then fades back to the selected
|
||||||
|
transition over 2 m so the authored landing floor remains supported. Negative controls reject a
|
||||||
|
transition one micrometre wider than declared and a band one micrometre below the minimum. See
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md)
|
||||||
|
for audit measurements, hashes, timings, captures and run history.
|
||||||
|
|
||||||
|
### 8.47 Ground-bound SurfaceWorld departure routes — World Assembler M3 Linux L33
|
||||||
|
|
||||||
|
An intra-strate SurfaceWorld route follows the resolved terrain ground profile. Its route-point Z
|
||||||
|
is the ground height plus the player's half-height; the route does not descend to the strate's
|
||||||
|
bedrock boundary. Bedrock belongs to the separate inter-strate passage, which begins at the
|
||||||
|
SurfaceWorld departure mouth on the ground and tunnels outward and down through native rock and
|
||||||
|
the boundary into the next strate.
|
||||||
|
|
||||||
|
The old fixed candidate envelope used a 64 m chord and 16 m centre spacing. On the L33 upper-SW
|
||||||
|
fixture, replaying that envelope sampled ground from −507.767 to −464.857 m; 366/648 candidate paths
|
||||||
|
exceeded the 15° grade limit. The worst failures clustered near the outer west/northwest terrain
|
||||||
|
shoulder (up to 79.971°) and the steep approach slopes around the origin. Candidate diagnostics
|
||||||
|
separate grade failures from endpoints outside the bounded arrival/departure placement disks.
|
||||||
|
The bounded search now limits its longest chord to the two mouth radii minus a 3 m clearance
|
||||||
|
margin, checks centres every 8 m, and ranks endpoints near the route anchors. It finds a 21.5 m
|
||||||
|
route from (−2.75, 0) to (18.75, 0), sampled on ground and accepted by the full capsule/support/
|
||||||
|
grade audit.
|
||||||
|
|
||||||
|
The SurfaceWorld departure passage starts at the ground mouth with a 1.5 m portal, then smoothsteps
|
||||||
|
to a 3.5 m rock tunnel over 9 m. Its ring is sized and oriented outward from the mouth, so the
|
||||||
|
approach does not cut back under the walk route. The `FloatingIslands → SurfaceWorld → TunnelNetwork`
|
||||||
|
fixture passes all four passage styles, including both interfaces, capsule clearance, support,
|
||||||
|
grade, walkability, naturalness, open-air, column ends, long-bridge and route-binding audits.
|
||||||
|
|
||||||
|
`FloorProfileBlend` previously compared each curved floor profile with its straight chord and
|
||||||
|
applied a 2-voxel limit. That is a curvature measurement, not a seam measurement. It now checks
|
||||||
|
endpoint height and tangent continuity at 0.01 voxel and 0.001 voxel/m; the 3-voxel step and
|
||||||
|
0.25 voxel/m tangent-step controls are both detected. SurfaceWorld owner hashes remain identical
|
||||||
|
to L32 because the route-mouth, expanded column sizing and short-span support changes apply only
|
||||||
|
when SurfaceWorld is the upper strate. No evaluator or recipe version changed.
|
||||||
|
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md)
|
||||||
|
records the four-style audits, captures, determinism, owner hashes, costs and retries.
|
||||||
|
|
||||||
|
### 8.48 Irregular passage-wall relief and grounded tread shoulders — World Assembler M3 L35
|
||||||
|
|
||||||
|
L34's owner-SW profile had a 7.438 m radius repeat with 0.0361 m (3.61 cm) amplitude; an independent
|
||||||
|
mesh-normal scan also found a 4.750 m period at 0.02682 (1.537°). The geometry came from piecewise
|
||||||
|
capsule joins, while broad value-noise bands added a second regular pattern. The shared carve now
|
||||||
|
uses Catmull–Rom centerline interpolation on eligible interior segments, excluding short spans and
|
||||||
|
terminal controls. Passage walls use deterministic, rotated 43/29/19 m relief with reduced terrain
|
||||||
|
band contribution; other `VF_FieldSurfaceRelief` users, including grounded bridge columns, are
|
||||||
|
unchanged. The shared evaluator is version 21.
|
||||||
|
|
||||||
|
The residual radius scan on owner-SW style 0 measures a 7.062 m period at 0.0109 m (1.09 cm),
|
||||||
|
with 0.0044 m (0.44 cm) coherent amplitude. The independent L35 mesh-normal scan passes at 8 m
|
||||||
|
and 0.02496 axial amplitude, below its 0.0255 threshold. The ripple audit samples both side-wall
|
||||||
|
radii every 0.125 m in 32 m windows, scans 0.5–8 m periods, and rejects amplitudes of 0.035 m or
|
||||||
|
more. Its synthetic control uses 2 m radius spheres spaced every 1.5 m; it measures 0.0599 m and
|
||||||
|
must fail.
|
||||||
|
|
||||||
|
At the FI mouth, the flat apron is the shared passage support tread, not `LandingExtensionQ`, a tile
|
||||||
|
edge or a separate support layer. The tread is required by capsule and support probes. L35 gives its
|
||||||
|
outer entry shoulder a shallow taper and seeded grain while preserving the supported walk core and
|
||||||
|
lower-mouth overlap width. The shallow quarter-weight support-height/radius spline keeps the tread
|
||||||
|
continuous without moving its XY footprint.
|
||||||
|
|
||||||
|
Cascade routes also replace identical full-depth cosine lobes with a seeded shallow profile on each
|
||||||
|
turn. Passage, capsule, support, open-air, bridge, binding, strict layout, capture, determinism, ring,
|
||||||
|
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)
|
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||||
|
|
||||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
|||||||
| Term | Meaning |
|
| 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. |
|
| 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. |
|
| 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`). |
|
| Density | Scalar field. `< 0` solid, `> 0` air, `0` = surface (`IsoLevel`). |
|
||||||
| Strate | Vertical layer of the world, stacked downward. Has its own generator + params. |
|
| 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)
|
(RealtimeMeshComponent)
|
||||||
```
|
```
|
||||||
|
|
||||||
`UVoxelStrateManager` is the side oracle: "what strate is at this Z, what params,
|
`UVoxelStrateManager` owns resolved half-open metre intervals (`[bottom, top)`) and is the side
|
||||||
what generator type, and is there a passage/elevator SDF near here?"
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -167,14 +169,16 @@ remains `0`, and lateral regions remain gated off.
|
|||||||
| `../../Standalone/Core/src/CoreNamespacePreamble.h` | First include of every Core TU. Forces FP contraction off per compiler (Clang/MSVC/GCC pragmas; unknown compiler = `#error`) and defines `vfcore::fp_contract::selfTest`, which `World::compile` runs before building: a fused multiply-add (e.g. Clang `-ffp-contract=fast`) refuses world generation. The world depends on contraction (Linux2/Linux3 reports), so never remove either. |
|
| `../../Standalone/Core/src/CoreNamespacePreamble.h` | First include of every Core TU. Forces FP contraction off per compiler (Clang/MSVC/GCC pragmas; unknown compiler = `#error`) and defines `vfcore::fp_contract::selfTest`, which `World::compile` runs before building: a fused multiply-add (e.g. Clang `-ffp-contract=fast`) refuses world generation. The world depends on contraction (Linux2/Linux3 reports), so never remove either. |
|
||||||
| `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. |
|
| `../../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` | 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, transition and dispatch provenance, 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.41. |
|
| `../../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. |
|
| `../../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.
|
| `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`. |
|
| 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). |
|
| `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/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/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. |
|
| `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. |
|
| `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. |
|
| `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.
|
type.
|
||||||
|
|
||||||
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:152).
|
**`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
|
- `FVoxelPassage` (h:46): standing endpoints, radius, type, wandering control points, and
|
||||||
upper/lower local landing descriptors (room, floor, and door; no root connector).
|
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 |
|
| 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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `FindSlotIndexForChunkZ` / `FindSlotIndexForWorldZCentimetres` | 6239 / 6251 | Chunk callers use the cached compatibility projection; world-Z callers search physical half-open micrometre intervals. |
|
||||||
| `GetStrateAt` / `GetStrateIndex` | 6127 / 6139 | World-Z queries. |
|
| `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. |
|
| `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. |
|
| `GetStrateForChunk` | 6298 | Chunk → definition via the compatibility projection. |
|
||||||
| `GetGeneratorTypeForChunk` | 6176 | Chunk → generator type. |
|
| `GetGeneratorTypeForChunk` | 6324 | Chunk → generator type via the compatibility projection. |
|
||||||
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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`
|
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
|
||||||
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
|
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
|
||||||
@@ -1160,7 +1167,7 @@ and [`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Resear
|
|||||||
| Symbol | Canonical file | Role |
|
| Symbol | Canonical file | Role |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PlanResolvedLayoutPassagesV1`, `ResolvedLayoutPassageV1::lowerMouthControlIndex` | `Standalone/Core/src/ResolvedLayoutInternal.h`, public record in `VoxelForgeCore.h` | Plans deterministic fixed-metre passages from adjacent director-resolved strata, binds route departure/arrival mouths, and records the derived split between the monotone descent and route-bound landing tail. |
|
| `PlanResolvedLayoutPassagesV1`, `ResolvedLayoutPassageV1::lowerMouthControlIndex` | `Standalone/Core/src/ResolvedLayoutInternal.h`, public record in `VoxelForgeCore.h` | Plans deterministic fixed-metre passages from adjacent director-resolved strata, binds route departure/arrival mouths, and records the derived split between the monotone descent and route-bound landing tail. |
|
||||||
| `FStandaloneShapeWorld::ApplyResolvedStrateComposition`, `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies boundary bands first, then opens the passage capsule and supported floor along the passage for every native generator. |
|
| `FStandaloneShapeWorld::ApplyResolvedStrateComposition`, `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies resolved boundary bands first, then opens the passage capsule and supported floor along the passage for every native generator. |
|
||||||
| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples passage capsules, floor support, mouth ownership/radius, in-strate route bindings, and a blocked-passage negative control. |
|
| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples passage capsules, floor support, mouth ownership/radius, in-strate route bindings, and a blocked-passage negative control. |
|
||||||
|
|
||||||
The evaluator version is 9. See ARCHITECTURE §8.41 and
|
The evaluator version is 9. See ARCHITECTURE §8.41 and
|
||||||
@@ -1217,6 +1224,105 @@ schema V3 is unchanged. See ARCHITECTURE §8.44 and
|
|||||||
See ARCHITECTURE §8.45 and
|
See ARCHITECTURE §8.45 and
|
||||||
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md).
|
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md).
|
||||||
|
|
||||||
|
### 3.29 Honored bedrock/ceiling transitions — World Assembler L29
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `EvaluateResolvedLayoutTransitionBandsV1`, `AuditResolvedStrateTransitions` | Public Core header / `Standalone/Core/src/ResolvedLayoutInternal.h`; `Standalone/Source/StreamBenchmarkPipeline.h` | Resolve the deterministic mode field from seed, interface and coordinates; audit actual crossing depth, minimum band thickness, passage-excluded holes, and strict negative controls. |
|
||||||
|
| `FStandaloneShapeWorld::ApplyResolvedStrateBoundaryBand` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies Hard, Gradient or Interleaved to both facing bands in the shared Core composition path; tapers locally at passage mouths to preserve landing support. |
|
||||||
|
|
||||||
|
The shared evaluator is version 15. Recipe and resolved-layout schema versions are unchanged.
|
||||||
|
The Unreal adapter now forwards the authored transition type and chunk width; those Unreal edits
|
||||||
|
were not compiled here. See ARCHITECTURE §8.46 and
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux29/REPORT.md).
|
||||||
|
|
||||||
|
### 3.30 SurfaceWorld as an upper strate and floor-profile joins — World Assembler L33
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `BuildFiniteStrateRoute` | `Standalone/Source/WorldAssemblyPrototype.h` | Samples route heights from the composed terrain ground; bounds candidate chords by the arrival/departure placement radii and records per-segment grade failures. |
|
||||||
|
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Starts an upper-SurfaceWorld departure at its terrain mouth, opens a small portal and smoothsteps to the rock tunnel while orienting the ring outward. |
|
||||||
|
| `BuildPassageVoidCrossingPlan`, `PassageBridgeSignedDistance` | `Standalone/Core/src/ShapeWorldInternal.h` | Sizes unsupported ranges from the full control-segment window only for upper SurfaceWorld; other upper generators keep the previous bracketing-sample bounds. |
|
||||||
|
| `--audit-resolved-layout-passages` | `Standalone/Source/StreamBenchmarkPipeline.h` | Uses floor height rather than bore-centre height for naturalness and records failed-only wall probes for column audits. |
|
||||||
|
| `FloorProfileBlend` | `Standalone/Core/src/ShapeWorldInternal.h` | Measures endpoint height/tangent continuity; chord deviation is informational, with step and tangent-step negative controls. |
|
||||||
|
|
||||||
|
The owner SurfaceWorld hashes match L32 and evaluator/recipe versions are unchanged. See
|
||||||
|
ARCHITECTURE §8.47 and
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md).
|
||||||
|
|
||||||
|
### 3.31 Passage-wall ripple and FI tread-shoulder audit — World Assembler M3 L35
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `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 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. The density pipeline (most-edited hot path)
|
||||||
|
|
||||||
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||||
|
|||||||
@@ -4,6 +4,27 @@
|
|||||||
|
|
||||||
namespace VoxelClipmapDesiredTiles
|
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
|
struct FParameters
|
||||||
{
|
{
|
||||||
int32 ClipRadius = 3;
|
int32 ClipRadius = 3;
|
||||||
@@ -13,7 +34,10 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
|
|
||||||
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
|
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)
|
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
|
||||||
@@ -24,7 +48,10 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
|
|
||||||
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
|
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,
|
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
|
||||||
@@ -33,10 +60,12 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
||||||
int32& OutAxisMin, int32& OutAxisMax)
|
int32& OutAxisMin, int32& OutAxisMax)
|
||||||
{
|
{
|
||||||
const int32 BaseMin = CenterAxis - Radius;
|
const int64 SafeRadius = FMath::Max<int64>(0, Radius);
|
||||||
const int32 BaseEnd = CenterAxis + Radius + 1;
|
const int64 SafeAlignment = FMath::Max<int64>(1, Alignment);
|
||||||
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment;
|
const int64 BaseMin = static_cast<int64>(CenterAxis) - SafeRadius;
|
||||||
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1;
|
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.X, OutMin.X, OutMax.X);
|
||||||
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
||||||
@@ -49,8 +78,13 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
{
|
{
|
||||||
return Radius;
|
return Radius;
|
||||||
}
|
}
|
||||||
return FMath::Max(Radius,
|
const int32 SafeLevel = FMath::Clamp(MaxLevel, 0, 8);
|
||||||
(Parameters.RenderDistanceChunks + (1 << MaxLevel) - 1) >> MaxLevel);
|
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,
|
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
|
||||||
@@ -73,12 +107,12 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
OutMinChunkZ = CenterChunkZ - ViewDistanceDown;
|
OutMinChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) - ViewDistanceDown);
|
||||||
OutMaxChunkZ = CenterChunkZ + ViewDistanceUp;
|
OutMaxChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) + ViewDistanceUp);
|
||||||
if (bHasStrate)
|
if (bHasStrate)
|
||||||
{
|
{
|
||||||
OutMinChunkZ = StrateBottomChunkZ - StrateViewMarginChunks;
|
OutMinChunkZ = ClampInt32(static_cast<int64>(StrateBottomChunkZ) - StrateViewMarginChunks);
|
||||||
OutMaxChunkZ = StrateTopChunkZ + StrateViewMarginChunks;
|
OutMaxChunkZ = ClampInt32(static_cast<int64>(StrateTopChunkZ) + StrateViewMarginChunks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +128,7 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
|
|
||||||
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
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 FIntVector CenterLevel = FloorDiv(Center, Pow);
|
||||||
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
||||||
|
|
||||||
@@ -105,24 +139,23 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
FIntVector RingMin, RingMax;
|
FIntVector RingMin, RingMax;
|
||||||
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
||||||
|
|
||||||
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
|
int64 TileMinZ = RingMin.Z;
|
||||||
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
|
int64 TileMaxZ = RingMax.Z;
|
||||||
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
||||||
{
|
{
|
||||||
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
|
TileMinZ = FMath::Max<int64>(TileMinZ, FloorDiv(MinChunkZ, Pow));
|
||||||
DeltaZMax = FMath::Min(DeltaZMax, FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z);
|
TileMaxZ = FMath::Min<int64>(TileMaxZ, FloorDiv(MaxChunkZ, Pow));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
for (int64 Z = TileMinZ; Z <= TileMaxZ; ++Z)
|
||||||
for (int32 DeltaY = RingMin.Y - CenterLevel.Y;
|
for (int64 Y = RingMin.Y; Y <= RingMax.Y; ++Y)
|
||||||
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY)
|
for (int64 X = RingMin.X; X <= RingMax.X; ++X)
|
||||||
for (int32 DeltaX = RingMin.X - CenterLevel.X;
|
|
||||||
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
|
|
||||||
{
|
{
|
||||||
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
|
const FIntVector Tile(static_cast<int32>(X), static_cast<int32>(Y),
|
||||||
const int32 TileMinZ = Tile.Z << Level;
|
static_cast<int32>(Z));
|
||||||
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1;
|
const int64 TileBottomChunkZ = static_cast<int64>(Tile.Z) * Pow;
|
||||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
const int64 TileTopChunkZ = (static_cast<int64>(Tile.Z) + 1) * Pow - 1;
|
||||||
|
if (TileTopChunkZ < MinChunkZ || TileBottomChunkZ > MaxChunkZ)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -130,12 +163,12 @@ namespace VoxelClipmapDesiredTiles
|
|||||||
if (Level > 0)
|
if (Level > 0)
|
||||||
{
|
{
|
||||||
const bool bCovered =
|
const bool bCovered =
|
||||||
(2 * Tile.X >= PreviousRingMin.X)
|
(2 * static_cast<int64>(Tile.X) >= PreviousRingMin.X)
|
||||||
&& (2 * Tile.X + 1 <= PreviousRingMax.X)
|
&& (2 * static_cast<int64>(Tile.X) + 1 <= PreviousRingMax.X)
|
||||||
&& (2 * Tile.Y >= PreviousRingMin.Y)
|
&& (2 * static_cast<int64>(Tile.Y) >= PreviousRingMin.Y)
|
||||||
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y)
|
&& (2 * static_cast<int64>(Tile.Y) + 1 <= PreviousRingMax.Y)
|
||||||
&& (2 * Tile.Z >= PreviousRingMin.Z)
|
&& (2 * static_cast<int64>(Tile.Z) >= PreviousRingMin.Z)
|
||||||
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z);
|
&& (2 * static_cast<int64>(Tile.Z) + 1 <= PreviousRingMax.Z);
|
||||||
if (bCovered)
|
if (bCovered)
|
||||||
{
|
{
|
||||||
continue;
|
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
|
// 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
|
// 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.
|
// 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)
|
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,
|
static FORCEINLINE bool IsRegionDesired(const FIntPoint& Region, const FIntPoint& PlayerCell,
|
||||||
int32 FarR, int32 R)
|
int32 FarR, int32 R)
|
||||||
{
|
{
|
||||||
const int32 MinX = FloorDivPos(PlayerCell.X - FarR, R);
|
const int32 MinX = FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R);
|
||||||
const int32 MaxX = FloorDivPos(PlayerCell.X + FarR, R);
|
const int32 MaxX = FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R);
|
||||||
const int32 MinY = FloorDivPos(PlayerCell.Y - FarR, R);
|
const int32 MinY = FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R);
|
||||||
const int32 MaxY = FloorDivPos(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;
|
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();
|
const int32 R = RegionSize();
|
||||||
|
|
||||||
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
|
// Desired regions = every region whose footprint touches the radius-FarR cell box around the player.
|
||||||
const FIntPoint RMin(FloorDivPos(PlayerCell.X - FarR, R),
|
const FIntPoint RMin(FloorDivPos(static_cast<int64>(PlayerCell.X) - FarR, R),
|
||||||
FloorDivPos(PlayerCell.Y - FarR, R));
|
FloorDivPos(static_cast<int64>(PlayerCell.Y) - FarR, R));
|
||||||
const FIntPoint RMax(FloorDivPos(PlayerCell.X + FarR, R),
|
const FIntPoint RMax(FloorDivPos(static_cast<int64>(PlayerCell.X) + FarR, R),
|
||||||
FloorDivPos(PlayerCell.Y + FarR, R));
|
FloorDivPos(static_cast<int64>(PlayerCell.Y) + FarR, R));
|
||||||
|
|
||||||
TSet<FIntPoint> DesiredRegions;
|
TSet<FIntPoint> DesiredRegions;
|
||||||
DesiredRegions.Reserve((RMax.X - RMin.X + 1) * (RMax.Y - RMin.Y + 1));
|
constexpr int64 MaxRegionReserve = 4096;
|
||||||
for (int32 ry = RMin.Y; ry <= RMax.Y; ++ry)
|
const int64 WidthX = static_cast<int64>(RMax.X) - RMin.X + 1;
|
||||||
for (int32 rx = RMin.X; rx <= RMax.X; ++rx)
|
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).
|
// 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));
|
const uint32 CH = VoxelHash::Mix(H ^ (0x00C0FFEEu + (uint32)ci * 0x9E3779B1u));
|
||||||
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
|
if (VoxelHash::ToFloat01(CH) > Comp.Probability) continue;
|
||||||
|
|
||||||
const int32 Span = FMath::Max(0, Comp.CountMax - Comp.CountMin);
|
const int64 Span = FMath::Max<int64>(0,
|
||||||
const int32 Count = Comp.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(CH ^ 0x1234u)) * (float)(Span + 1));
|
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)
|
for (int32 ii = 0; ii < Count; ++ii)
|
||||||
{
|
{
|
||||||
if (CompBudget <= 0) break;
|
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));
|
const uint32 SH = VoxelHash::Mix(IH ^ (0x0000544Bu + (uint32)si * 0x27D4EB2Fu));
|
||||||
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
|
if (VoxelHash::ToFloat01(SH) > Sub.Probability) continue;
|
||||||
|
|
||||||
const int32 SubSpan = FMath::Max(0, Sub.CountMax - Sub.CountMin);
|
const int64 SubSpan = FMath::Max<int64>(0,
|
||||||
const int32 SubCount = Sub.CountMin + (int32)(VoxelHash::ToFloat01(VoxelHash::Mix(SH ^ 0x1234u)) * (float)(SubSpan + 1));
|
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)
|
for (int32 sj = 0; sj < SubCount; ++sj)
|
||||||
{
|
{
|
||||||
if (CompBudget <= 0) break;
|
if (CompBudget <= 0) break;
|
||||||
|
|||||||
@@ -155,9 +155,11 @@ int32 UVoxelDensityVolume::NumLevels() const
|
|||||||
|
|
||||||
FORCEINLINE int32 UVoxelDensityVolume::FloorDiv(int32 A, int32 B)
|
FORCEINLINE int32 UVoxelDensityVolume::FloorDiv(int32 A, int32 B)
|
||||||
{
|
{
|
||||||
// True floor division (B > 0). FMath::DivideAndRoundDown truncates toward zero for negatives —
|
// True floor division (B > 0). Remainder adjustment handles INT_MIN without negating it.
|
||||||
// a footgun the content manager hit too (see FloorDivPos there). Cells span the origin, so floor.
|
if (B <= 0) { return 0; }
|
||||||
return (A >= 0) ? (A / B) : -(((-A) + B - 1) / B);
|
const int32 Quotient = A / B;
|
||||||
|
const int32 Remainder = A % B;
|
||||||
|
return Remainder < 0 ? Quotient - 1 : Quotient;
|
||||||
}
|
}
|
||||||
|
|
||||||
FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity)
|
FORCEINLINE uint8 UVoxelDensityVolume::Quantize(float MCDensity)
|
||||||
@@ -180,7 +182,8 @@ void UVoxelDensityVolume::EnsureAllocated()
|
|||||||
for (int32 L = 0; L < N; ++L)
|
for (int32 L = 0; L < N; ++L)
|
||||||
{
|
{
|
||||||
FClipLevel& Lv = Levels[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.OriginCells = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||||
Lv.bHasData = false;
|
Lv.bHasData = false;
|
||||||
Lv.Density.SetNumZeroed(Count); // start all-air (0)
|
Lv.Density.SetNumZeroed(Count); // start all-air (0)
|
||||||
@@ -632,7 +635,8 @@ void UVoxelDensityVolume::ProcessOneFill(const FPendingFill& F)
|
|||||||
if (!Gen) return;
|
if (!Gen) return;
|
||||||
if (bShuttingDown.load(std::memory_order_relaxed) || bFillThreadStop.load(std::memory_order_relaxed)) 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;
|
FFillResult R;
|
||||||
R.Level = F.Level;
|
R.Level = F.Level;
|
||||||
R.Epoch = F.Epoch;
|
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);
|
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 WorldAssemblyRoundedSqrt(std::uint64_t Value)
|
||||||
{
|
{
|
||||||
std::uint64_t Root = 0;
|
std::uint64_t Root = 0;
|
||||||
@@ -312,17 +320,18 @@ VoxelForgeCore::WorldAssemblyBiomeProfileV1 BuildSurfaceProfile(
|
|||||||
bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
|
bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
|
||||||
const FStrateSlot& Slot,
|
const FStrateSlot& Slot,
|
||||||
VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||||
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum, FString& OutError)
|
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum,
|
||||||
|
std::uint32_t CoreVoxelMicrometres, FString& OutError)
|
||||||
{
|
{
|
||||||
using namespace VoxelForgeCore;
|
using namespace VoxelForgeCore;
|
||||||
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
|
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
|
||||||
const FSurfaceGenerationParams Surface = StrateManager->GetSurfaceParamsForChunk(Chunk);
|
const FSurfaceGenerationParams Surface = StrateManager->GetSurfaceParamsForChunk(Chunk);
|
||||||
const FBiomeContext BiomeContext = StrateManager->GetBiomeContextForChunk(Chunk);
|
const FBiomeContext BiomeContext = StrateManager->GetBiomeContextForChunk(Chunk);
|
||||||
const UVoxelStrateDefinition* Definition = Slot.Definition;
|
const UVoxelStrateDefinition* Definition = Slot.Definition;
|
||||||
const double LayoutChunkVoxels = static_cast<double>(Recipe.layoutChunkWidthSubvoxels)
|
const double BottomVoxel = static_cast<double>(Slot.BottomWorldMicrometres)
|
||||||
/ WorldAssemblySubvoxelsPerVoxel;
|
/ std::max<std::uint32_t>(1, CoreVoxelMicrometres);
|
||||||
const double BottomVoxel = static_cast<double>(Slot.BottomChunkZ) * LayoutChunkVoxels;
|
const double HeightVoxels = static_cast<double>(Slot.HeightMicrometres)
|
||||||
const double HeightVoxels = static_cast<double>(Slot.TopChunkZ - Slot.BottomChunkZ + 1) * LayoutChunkVoxels;
|
/ std::max<std::uint32_t>(1, CoreVoxelMicrometres);
|
||||||
const FBiomeMapParams& Map = BiomeContext.Map;
|
const FBiomeMapParams& Map = BiomeContext.Map;
|
||||||
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||||
Recipe.biomeCellSizeSubvoxels = WorldAssemblyFixed(Map.CellSize);
|
Recipe.biomeCellSizeSubvoxels = WorldAssemblyFixed(Map.CellSize);
|
||||||
@@ -454,9 +463,12 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Shelf.minimumSubvoxels = {WorldAssemblyFixed(MinX), WorldAssemblyFixed(MinY),
|
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),
|
Shelf.maximumSubvoxels = {WorldAssemblyFixed(MaxX), WorldAssemblyFixed(MaxY),
|
||||||
std::min(WorldAssemblyFixed((Slot.TopChunkZ + 1) * LayoutChunkVoxels),
|
std::min(WorldAssemblyMicrometresToSubvoxels(
|
||||||
|
Slot.TopWorldMicrometres, CoreVoxelMicrometres),
|
||||||
HighestGround + WorldAssemblyFixed(ShelfHeight + 4.0))};
|
HighestGround + WorldAssemblyFixed(ShelfHeight + 4.0))};
|
||||||
Shelf.parametersSubvoxels = {WorldAssemblyFixed(DirX), WorldAssemblyFixed(DirY),
|
Shelf.parametersSubvoxels = {WorldAssemblyFixed(DirX), WorldAssemblyFixed(DirY),
|
||||||
WorldAssemblyFixed(ShelfHeight), WorldAssemblyFixed(Reach)};
|
WorldAssemblyFixed(ShelfHeight), WorldAssemblyFixed(Reach)};
|
||||||
@@ -479,13 +491,14 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
|||||||
using namespace VoxelForgeCore;
|
using namespace VoxelForgeCore;
|
||||||
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
|
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
|
||||||
const FFloatingIslandParams Params = StrateManager->GetFloatingIslandParamsForChunk(Chunk);
|
const FFloatingIslandParams Params = StrateManager->GetFloatingIslandParamsForChunk(Chunk);
|
||||||
const FStratePassageConfig& Passage = Slot.Definition->PassageConfig;
|
const FStratePassageConfig& Passage = Slot.ResolvedPassageConfig;
|
||||||
WorldAssemblyFloatingIslandPlanV1 IslandPlan;
|
WorldAssemblyFloatingIslandPlanV1 IslandPlan;
|
||||||
IslandPlan.strateIndex = Slot.StrateIndex;
|
IslandPlan.strateIndex = Slot.StrateIndex;
|
||||||
IslandPlan.topChunkZ = Slot.TopChunkZ;
|
IslandPlan.topChunkZ = Slot.TopChunkZ;
|
||||||
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
|
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
|
||||||
auto& Recipe = IslandPlan.manifest.recipe;
|
auto& Recipe = IslandPlan.manifest.recipe;
|
||||||
Recipe.geometryVersion = 8;
|
Recipe.geometryVersion = 8;
|
||||||
|
Recipe.terrainFollowingRoute = static_cast<std::uint32_t>(Passage.Style) == 0;
|
||||||
Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels;
|
Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels;
|
||||||
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||||
Recipe.ownerVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
Recipe.ownerVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
||||||
@@ -499,10 +512,10 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
|||||||
}
|
}
|
||||||
Recipe.ownerToCoreScaleNumerator = Recipe.ownerVoxelSizeMicrons / ScaleGcd;
|
Recipe.ownerToCoreScaleNumerator = Recipe.ownerVoxelSizeMicrons / ScaleGcd;
|
||||||
Recipe.ownerToCoreScaleDenominator = Recipe.coreVoxelSizeMicrons / ScaleGcd;
|
Recipe.ownerToCoreScaleDenominator = Recipe.coreVoxelSizeMicrons / ScaleGcd;
|
||||||
Recipe.layoutMinimumZSubvoxels = static_cast<std::int64_t>(Slot.BottomChunkZ)
|
Recipe.layoutMinimumZSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||||
* LayoutChunkWidthSubvoxels;
|
Slot.BottomWorldMicrometres, Recipe.coreVoxelSizeMicrons);
|
||||||
Recipe.layoutMaximumZSubvoxels = static_cast<std::int64_t>(Slot.TopChunkZ + 1)
|
Recipe.layoutMaximumZSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||||
* LayoutChunkWidthSubvoxels;
|
Slot.TopWorldMicrometres, Recipe.coreVoxelSizeMicrons);
|
||||||
Recipe.layoutCenterTopZSubvoxels = (Recipe.layoutMinimumZSubvoxels
|
Recipe.layoutCenterTopZSubvoxels = (Recipe.layoutMinimumZSubvoxels
|
||||||
+ Recipe.layoutMaximumZSubvoxels) / 2;
|
+ Recipe.layoutMaximumZSubvoxels) / 2;
|
||||||
Recipe.islandDensityQ16 = WorldAssemblyQ16(FMath::Clamp(Params.IslandDensity, 0.0f, 1.0f));
|
Recipe.islandDensityQ16 = WorldAssemblyQ16(FMath::Clamp(Params.IslandDensity, 0.0f, 1.0f));
|
||||||
@@ -523,20 +536,35 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
|||||||
*WorldAssemblySubvoxelsPerVoxel/Recipe.coreVoxelSizeMicrons));
|
*WorldAssemblySubvoxelsPerVoxel/Recipe.coreVoxelSizeMicrons));
|
||||||
};
|
};
|
||||||
Recipe.anchorIslandOrdinal=Recipe.primaryIslandCount-1;
|
Recipe.anchorIslandOrdinal=Recipe.primaryIslandCount-1;
|
||||||
Recipe.anchorPlacementRadiusSubvoxels=OwnerVoxelsToCoreQ(Passage.DistanceMax);
|
Recipe.anchorPlacementRadiusSubvoxels=WorldAssemblyMicrometresToSubvoxels(
|
||||||
const std::int64_t spiralRadius=OwnerVoxelsToCoreQ(Passage.SpiralRadius);
|
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.
|
// Reserve at least a 1.5 m mid-bore through the anchor root.
|
||||||
constexpr double AnchorPassageMinimumBoreOwnerVoxels=12.0;
|
constexpr double AnchorPassageMinimumBoreOwnerVoxels=12.0;
|
||||||
const std::int64_t mouthBore=std::max(OwnerVoxelsToCoreQ(Passage.MouthRadius),
|
const std::int64_t OwnerVoxelMicrometres = Recipe.ownerVoxelSizeMicrons;
|
||||||
OwnerVoxelsToCoreQ(12.0));
|
const std::int64_t mouthBore=std::max(
|
||||||
const std::int64_t bore=std::max(OwnerVoxelsToCoreQ(Passage.MidRadius),
|
WorldAssemblyMicrometresToSubvoxels(Slot.PassageMouthRadiusMicrometres,
|
||||||
OwnerVoxelsToCoreQ(AnchorPassageMinimumBoreOwnerVoxels));
|
Recipe.coreVoxelSizeMicrons),
|
||||||
const std::int64_t wallReserve=OwnerVoxelsToCoreQ(12.0);
|
WorldAssemblyMicrometresToSubvoxels(12 * OwnerVoxelMicrometres,
|
||||||
Recipe.anchorMouthOffsetSubvoxels=spiralRadius+mouthBore+OwnerVoxelsToCoreQ(4.0);
|
Recipe.coreVoxelSizeMicrons));
|
||||||
const std::int64_t excursion=std::max(OwnerVoxelsToCoreQ(Passage.Wander),spiralRadius);
|
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.anchorRootRadiusSubvoxels=excursion+bore+wallReserve;
|
||||||
Recipe.anchorIslandMinimumRadiusSubvoxels=std::max(Recipe.minimumRadiusSubvoxels,
|
Recipe.anchorIslandMinimumRadiusSubvoxels=std::max(Recipe.minimumRadiusSubvoxels,
|
||||||
Recipe.anchorMouthOffsetSubvoxels+mouthBore+OwnerVoxelsToCoreQ(4.0));
|
Recipe.anchorMouthOffsetSubvoxels+mouthBore+FourOwnerVoxelsSubvoxels);
|
||||||
Recipe.anchorRootBottomZSubvoxels=Recipe.layoutMinimumZSubvoxels
|
Recipe.anchorRootBottomZSubvoxels=Recipe.layoutMinimumZSubvoxels
|
||||||
+Recipe.boundarySealSubvoxels;
|
+Recipe.boundarySealSubvoxels;
|
||||||
std::string PlanError;
|
std::string PlanError;
|
||||||
@@ -881,7 +909,7 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
|||||||
SlotRecipe.layoutChunkWidthSubvoxels=LayoutChunkWidthSubvoxels;
|
SlotRecipe.layoutChunkWidthSubvoxels=LayoutChunkWidthSubvoxels;
|
||||||
VoxelForgeCore::WorldAssemblyStratumPlanV1 Stratum;
|
VoxelForgeCore::WorldAssemblyStratumPlanV1 Stratum;
|
||||||
if (!BuildSurfaceStratumPlan(StrateManager, Slot,
|
if (!BuildSurfaceStratumPlan(StrateManager, Slot,
|
||||||
SlotRecipe, Stratum, OutError))
|
SlotRecipe, Stratum, CoreVoxelMicrometres, OutError))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1047,41 +1075,47 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
|||||||
return "Unknown";
|
return "Unknown";
|
||||||
};
|
};
|
||||||
const std::int64_t OwnerVoxelMicrons = Layout.ownerVoxelSizeMicrometres;
|
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;
|
std::vector<VoxelForgeCore::ResolvedLayoutPassageConfigV1> PassageConfigs;
|
||||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const FStratePassageConfig& AuthoredPassage = Slot.Definition->PassageConfig;
|
const FStratePassageConfig& ResolvedPassage = Slot.ResolvedPassageConfig;
|
||||||
const auto OwnerVoxelsToCoreQ = [OwnerVoxelMicrons, ExpectedCoreVoxelMicrons](double Value)
|
|
||||||
{
|
|
||||||
return static_cast<std::int64_t>(std::llround(Value * OwnerVoxelMicrons
|
|
||||||
* VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel / ExpectedCoreVoxelMicrons));
|
|
||||||
};
|
|
||||||
VoxelForgeCore::ResolvedLayoutPassageConfigV1 PassageConfig;
|
VoxelForgeCore::ResolvedLayoutPassageConfigV1 PassageConfig;
|
||||||
PassageConfig.upperStrateIndex = Slot.StrateIndex;
|
PassageConfig.upperStrateIndex = Slot.StrateIndex;
|
||||||
PassageConfig.connections = AuthoredPassage.Connections;
|
PassageConfig.connections = ResolvedPassage.Connections;
|
||||||
PassageConfig.style = static_cast<std::uint32_t>(AuthoredPassage.Style);
|
PassageConfig.style = static_cast<std::uint32_t>(ResolvedPassage.Style);
|
||||||
PassageConfig.mouthRadiusSubvoxels = std::max(OwnerVoxelsToCoreQ(AuthoredPassage.MouthRadius),
|
PassageConfig.mouthRadiusSubvoxels = std::max(
|
||||||
OwnerVoxelsToCoreQ(12.0));
|
WorldAssemblyMicrometresToSubvoxels(
|
||||||
PassageConfig.midRadiusSubvoxels = std::max(OwnerVoxelsToCoreQ(AuthoredPassage.MidRadius),
|
Slot.PassageMouthRadiusMicrometres, ExpectedCoreVoxelMicrons),
|
||||||
OwnerVoxelsToCoreQ(12.0));
|
WorldAssemblyMicrometresToSubvoxels(
|
||||||
PassageConfig.reachMinimumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.ReachMin);
|
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||||
PassageConfig.reachMaximumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.ReachMax);
|
PassageConfig.midRadiusSubvoxels = std::max(
|
||||||
PassageConfig.distanceMinimumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.DistanceMin);
|
WorldAssemblyMicrometresToSubvoxels(
|
||||||
PassageConfig.distanceMaximumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.DistanceMax);
|
Slot.PassageMidRadiusMicrometres, ExpectedCoreVoxelMicrons),
|
||||||
PassageConfig.wanderSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.Wander);
|
WorldAssemblyMicrometresToSubvoxels(
|
||||||
PassageConfig.segments = static_cast<std::uint32_t>(FMath::Clamp(AuthoredPassage.Segments,1,48));
|
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||||
PassageConfig.verticalWobbleSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.VerticalWobble);
|
PassageConfig.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||||
PassageConfig.spiralRadiusSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.SpiralRadius);
|
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(
|
PassageConfig.spiralTurnsQ16 = static_cast<std::int64_t>(std::llround(
|
||||||
static_cast<double>(AuthoredPassage.SpiralTurns)*65536.0));
|
static_cast<double>(ResolvedPassage.SpiralTurns) * 65536.0));
|
||||||
PassageConfig.cascadeSteps = static_cast<std::uint32_t>(FMath::Clamp(AuthoredPassage.CascadeSteps,1,16));
|
PassageConfig.cascadeSteps = static_cast<std::uint32_t>(
|
||||||
PassageConfig.cascadeLedgeSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.CascadeLedge);
|
FMath::Clamp(ResolvedPassage.CascadeSteps, 1, 16));
|
||||||
|
PassageConfig.cascadeLedgeSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||||
|
Slot.PassageCascadeLedgeMicrometres, ExpectedCoreVoxelMicrons);
|
||||||
PassageConfigs.push_back(PassageConfig);
|
PassageConfigs.push_back(PassageConfig);
|
||||||
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(
|
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(
|
||||||
FIntVector(0, 0, Slot.TopChunkZ));
|
FIntVector(0, 0, Slot.TopChunkZ));
|
||||||
@@ -1095,24 +1129,29 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
|||||||
: std::string("legacy.") + generatorName(Type);
|
: std::string("legacy.") + generatorName(Type);
|
||||||
Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0;
|
Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0;
|
||||||
Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons;
|
Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons;
|
||||||
Item.interiorBottomMicrometres = static_cast<std::int64_t>(Slot.BottomChunkZ)
|
Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres;
|
||||||
* LayoutChunkSizeMicrometres;
|
Item.interiorTopMicrometres = Slot.TopWorldMicrometres;
|
||||||
Item.interiorTopMicrometres = static_cast<std::int64_t>(Slot.TopChunkZ + 1)
|
|
||||||
* LayoutChunkSizeMicrometres;
|
|
||||||
Item.managerChunkWidthMicrometres = LayoutChunkSizeMicrometres;
|
Item.managerChunkWidthMicrometres = LayoutChunkSizeMicrometres;
|
||||||
Item.dispatchChunkWidthMicrometres = LayoutChunkSizeMicrometres;
|
Item.dispatchChunkWidthMicrometres = LayoutChunkSizeMicrometres;
|
||||||
Item.transitionMode = transitionName(Slot.Definition->TransitionType);
|
Item.transitionMode = transitionName(Slot.ResolvedTransitionType);
|
||||||
Item.transitionBlendChunks = Slot.Definition->TransitionBlendChunks;
|
Item.transitionBlendChunks = Slot.ResolvedTransitionBlendChunks;
|
||||||
Item.transitionWidthMicrometres = static_cast<std::int64_t>(Item.transitionBlendChunks)
|
Item.transitionWidthMicrometres = Slot.TransitionWidthMicrometres;
|
||||||
* Item.managerChunkWidthMicrometres;
|
|
||||||
Item.transitionWarpAmplitudeMicrometres = Item.transitionMode == "Interleaved"
|
Item.transitionWarpAmplitudeMicrometres = Item.transitionMode == "Interleaved"
|
||||||
? 2 * Item.managerChunkWidthMicrometres : 0;
|
? std::min(Item.transitionWidthMicrometres / 2, Item.managerChunkWidthMicrometres) : 0;
|
||||||
Item.transitionInterpenetrationMicrometres = 0;
|
Item.transitionInterpenetrationMicrometres = Item.transitionMode == "Interleaved"
|
||||||
Item.transitionHonouredInCore = Slot.Definition->TransitionType == EVoxelStrateTransition::Hard;
|
? Item.transitionWidthMicrometres
|
||||||
Item.transitionFallsBackToHard = Slot.Definition->TransitionType != EVoxelStrateTransition::Hard;
|
: Item.transitionMode == "Gradient" ? Item.transitionWidthMicrometres / 4 : 0;
|
||||||
|
Item.transitionFallsBackToHard = Item.transitionMode != "Hard"
|
||||||
|
&& Item.transitionWidthMicrometres <= 0;
|
||||||
|
Item.transitionHonouredInCore = Item.transitionMode == "Hard"
|
||||||
|
|| !Item.transitionFallsBackToHard;
|
||||||
Item.transitionDispatchPath = Item.transitionFallsBackToHard
|
Item.transitionDispatchPath = Item.transitionFallsBackToHard
|
||||||
? "declared Gradient/Interleaved recorded; Core uses an explicit Hard transition fallback with zero interpenetration"
|
? "declared zero-width transition uses an explicit Hard fallback"
|
||||||
: "declared Hard transition is applied at this interface";
|
: Item.transitionMode == "Hard"
|
||||||
|
? "declared Hard transition preserves the flat shared bedrock/ceiling interface"
|
||||||
|
: Item.transitionMode == "Interleaved"
|
||||||
|
? "shared composition overlaps both facing bands with deterministic domain-warped fingers inside the declared width"
|
||||||
|
: "shared composition moves both band faces together with the smooth declared-width transition field";
|
||||||
Item.managerLookup = "UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate";
|
Item.managerLookup = "UVoxelStrateManager::GetGeneratorTypeForChunk; transition is controlled by upper strate";
|
||||||
if (Type == ECaveGeneratorType::SurfaceWorld)
|
if (Type == ECaveGeneratorType::SurfaceWorld)
|
||||||
{
|
{
|
||||||
@@ -1152,11 +1191,8 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
|||||||
Item.queryDispatchPath = "NativeFloatingIslandDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3";
|
Item.queryDispatchPath = "NativeFloatingIslandDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3";
|
||||||
const auto Plan = std::find_if(Manifest->floatingIslands.begin(), Manifest->floatingIslands.end(),
|
const auto Plan = std::find_if(Manifest->floatingIslands.begin(), Manifest->floatingIslands.end(),
|
||||||
[&Slot](const auto& Entry) { return Entry.strateIndex == Slot.StrateIndex; });
|
[&Slot](const auto& Entry) { return Entry.strateIndex == Slot.StrateIndex; });
|
||||||
std::int64_t SealMicrometres = 0;
|
|
||||||
if (Plan != Manifest->floatingIslands.end())
|
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;
|
Item.recipeVersion = Plan->manifest.recipe.geometryVersion;
|
||||||
for (std::size_t IslandIndex = 0; IslandIndex < Plan->manifest.plan.islands.size(); ++IslandIndex)
|
for (std::size_t IslandIndex = 0; IslandIndex < Plan->manifest.plan.islands.size(); ++IslandIndex)
|
||||||
{
|
{
|
||||||
@@ -1306,6 +1342,8 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int32 Seed = StrateManager->GetWorldSeed();
|
const int32 Seed = StrateManager->GetWorldSeed();
|
||||||
|
const std::uint32_t LayoutChunkSizeMicrometres =
|
||||||
|
StrateManager->GetStrateLayoutChunkSizeMicrometres();
|
||||||
const float OriginSpineRadius = Settings->GetEffectiveOriginSpineRadius();
|
const float OriginSpineRadius = Settings->GetEffectiveOriginSpineRadius();
|
||||||
const float WorldRadiusVoxels = Settings->GetEffectiveWorldRadiusVoxels();
|
const float WorldRadiusVoxels = Settings->GetEffectiveWorldRadiusVoxels();
|
||||||
const int32 CoreClipRadius = FMath::Clamp(ClipRadius, 1, 64);
|
const int32 CoreClipRadius = FMath::Clamp(ClipRadius, 1, 64);
|
||||||
@@ -1323,7 +1361,7 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
|||||||
SettingsValue->AsObject()->SetNumberField(TEXT("ClipRadius"), CoreClipRadius);
|
SettingsValue->AsObject()->SetNumberField(TEXT("ClipRadius"), CoreClipRadius);
|
||||||
SettingsValue->AsObject()->SetNumberField(TEXT("MaxClipLevel"), CoreMaxClipLevel);
|
SettingsValue->AsObject()->SetNumberField(TEXT("MaxClipLevel"), CoreMaxClipLevel);
|
||||||
SettingsValue->AsObject()->SetNumberField(TEXT("StrateLayoutChunkSizeMeters"),
|
SettingsValue->AsObject()->SetNumberField(TEXT("StrateLayoutChunkSizeMeters"),
|
||||||
Settings->StrateLayoutChunkSizeMeters);
|
static_cast<double>(StrateManager->GetStrateLayoutChunkSizeMicrometres()) / 1000000.0);
|
||||||
|
|
||||||
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||||
Root->SetNumberField(TEXT("schema_version"), 1);
|
Root->SetNumberField(TEXT("schema_version"), 1);
|
||||||
@@ -1334,17 +1372,20 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
|||||||
static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0)));
|
static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0)));
|
||||||
Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm);
|
Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm);
|
||||||
Root->SetNumberField(TEXT("core_voxel_size_microns"), CoreVoxelSizeMicrons);
|
Root->SetNumberField(TEXT("core_voxel_size_microns"), CoreVoxelSizeMicrons);
|
||||||
Root->SetNumberField(TEXT("owner_to_core_length_scale"),
|
|
||||||
OwnerVoxelSizeCm / ResolvedCoreVoxelSizeCm);
|
|
||||||
Root->SetNumberField(TEXT("seed"), Seed);
|
Root->SetNumberField(TEXT("seed"), Seed);
|
||||||
Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry);
|
Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry);
|
||||||
Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius);
|
Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius);
|
||||||
Root->SetNumberField(TEXT("world_radius_voxels"), WorldRadiusVoxels);
|
Root->SetNumberField(TEXT("world_radius_voxels"), WorldRadiusVoxels);
|
||||||
Root->SetNumberField(TEXT("edge_seal_thickness"), Settings->EdgeSealThickness);
|
Root->SetNumberField(TEXT("edge_seal_thickness"), Settings->EdgeSealThickness);
|
||||||
Root->SetNumberField(TEXT("total_strates"), StrateManager->GetNumStrates());
|
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"),
|
Root->SetNumberField(TEXT("strate_layout_chunk_size_um"),
|
||||||
std::llround(Settings->StrateLayoutChunkSizeMeters*1000000.0));
|
LayoutChunkSizeMicrometres);
|
||||||
Root->SetNumberField(TEXT("clip_radius"), CoreClipRadius);
|
Root->SetNumberField(TEXT("clip_radius"), CoreClipRadius);
|
||||||
Root->SetNumberField(TEXT("max_clip_level"), CoreMaxClipLevel);
|
Root->SetNumberField(TEXT("max_clip_level"), CoreMaxClipLevel);
|
||||||
Root->SetNumberField(TEXT("full_res_clip_levels"), Settings->FullResClipLevels);
|
Root->SetNumberField(TEXT("full_res_clip_levels"), Settings->FullResClipLevels);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||||
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
||||||
#include "HAL/IConsoleManager.h"
|
#include "HAL/IConsoleManager.h"
|
||||||
|
#include <cmath> // std::llround
|
||||||
#include "Misc/CommandLine.h"
|
#include "Misc/CommandLine.h"
|
||||||
#include "Misc/Crc.h"
|
#include "Misc/Crc.h"
|
||||||
#include "Misc/Parse.h"
|
#include "Misc/Parse.h"
|
||||||
@@ -24,6 +25,27 @@ namespace
|
|||||||
{
|
{
|
||||||
std::atomic<uint64> GNextStrateManagerLifetimeId { 0 };
|
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-
|
// 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
|
// preserving rewrites in this round are deliberately unconditional; this switch isolates
|
||||||
// the projection reuse without changing any density arithmetic.
|
// 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).
|
// Resolve the stack in physical integer micrometres first. The historical chunk indices are
|
||||||
int32 CurrentTopZ = 0;
|
// 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++)
|
for (int32 i = 0; i < TotalStrates; i++)
|
||||||
{
|
{
|
||||||
@@ -1356,16 +1381,28 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
const int32 LayoutChunkVoxels=FMath::RoundToInt(StrateLayoutChunkWidthVoxels);
|
const int32 LayoutChunkVoxels=FMath::RoundToInt(StrateLayoutChunkWidthVoxels);
|
||||||
Slot.TopChunkZ = SeasonStrate.TopWorldZ / LayoutChunkVoxels - 1;
|
Slot.TopChunkZ = SeasonStrate.TopWorldZ / LayoutChunkVoxels - 1;
|
||||||
Slot.BottomChunkZ = SeasonStrate.BottomWorldZ / LayoutChunkVoxels;
|
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
|
if (SeasonStrate.TopWorldZ % LayoutChunkVoxels != 0
|
||||||
|| SeasonStrate.BottomWorldZ % LayoutChunkVoxels != 0
|
|| SeasonStrate.BottomWorldZ % LayoutChunkVoxels != 0
|
||||||
|| Slot.TopChunkZ != CurrentTopZ
|
|| Slot.TopChunkZ != ExpectedTopChunk
|
||||||
|| Slot.BottomChunkZ != CurrentTopZ - (Slot.HeightInChunks - 1))
|
|| Slot.BottomChunkZ != ExpectedBottomChunk
|
||||||
|
|| Slot.TopWorldMicrometres != CurrentTopWorldMicrometres
|
||||||
|
|| Slot.HeightMicrometres != static_cast<int64>(Slot.HeightInChunks)
|
||||||
|
* StrateLayoutChunkSizeMicrometres)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Error,
|
UE_LOG(LogTemp, Error,
|
||||||
TEXT("[StrateManager] Season slot %d bounds do not describe the declared stacked layout."), i);
|
TEXT("[StrateManager] Season slot %d bounds do not describe the declared stacked layout."), i);
|
||||||
RestorePreviousLayout();
|
RestorePreviousLayout();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
CurrentTopWorldMicrometres = Slot.BottomWorldMicrometres - ResolvedGapMicrometres;
|
||||||
SeasonStrates.Add(SeasonStrate);
|
SeasonStrates.Add(SeasonStrate);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1388,15 +1425,50 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute Z range from definition's height
|
// Resolve the authored chunk height into an exact physical interval.
|
||||||
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
|
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
|
||||||
Slot.TopChunkZ = CurrentTopZ;
|
Slot.TopWorldMicrometres = CurrentTopWorldMicrometres;
|
||||||
Slot.BottomChunkZ = CurrentTopZ - (Slot.HeightInChunks - 1);
|
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
|
Slot.ResolvedTransitionType = Slot.Definition->TransitionType;
|
||||||
// InterStrateGapChunks chunks between this strate and the next.
|
Slot.ResolvedTransitionBlendChunks = Slot.Definition->TransitionBlendChunks;
|
||||||
CurrentTopZ = Slot.BottomChunkZ - 1 - LayoutGapChunks;
|
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);
|
StrateLayout.Add(Slot);
|
||||||
|
|
||||||
@@ -1429,6 +1501,7 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
OriginSpineRadius = bUseSeason
|
OriginSpineRadius = bUseSeason
|
||||||
? SeasonManifest.OriginSpineRadius : Settings->OriginSpineRadius;
|
? SeasonManifest.OriginSpineRadius : Settings->OriginSpineRadius;
|
||||||
InterStrateGapChunks = LayoutGapChunks;
|
InterStrateGapChunks = LayoutGapChunks;
|
||||||
|
ResolvedInterStrateGapMicrometres = ResolvedGapMicrometres;
|
||||||
if (bUseSeason)
|
if (bUseSeason)
|
||||||
{
|
{
|
||||||
ActiveSeasonContentHash = SeasonManifest.ContentHash;
|
ActiveSeasonContentHash = SeasonManifest.ContentHash;
|
||||||
@@ -1537,8 +1610,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
|
|||||||
}
|
}
|
||||||
VF_SetComposerRuntimeBounds(
|
VF_SetComposerRuntimeBounds(
|
||||||
Override->Params,
|
Override->Params,
|
||||||
(float)(TargetSlot->TopChunkZ + 1) * StrateLayoutChunkWidthVoxels,
|
TargetSlot->LegacyTopWorldZVoxels,
|
||||||
(float)TargetSlot->BottomChunkZ * StrateLayoutChunkWidthVoxels);
|
TargetSlot->LegacyBottomWorldZVoxels);
|
||||||
Override->bUseRecipe = bUseRecipe;
|
Override->bUseRecipe = bUseRecipe;
|
||||||
if (Recipe != nullptr)
|
if (Recipe != nullptr)
|
||||||
{
|
{
|
||||||
@@ -1557,8 +1630,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
|
|||||||
Override->bUseRegions = true;
|
Override->bUseRegions = true;
|
||||||
Override->Regions = *InRegions;
|
Override->Regions = *InRegions;
|
||||||
VF_RekeyStrateRegionManifest(Override->Regions, CandidateSeed, StrateIndex);
|
VF_RekeyStrateRegionManifest(Override->Regions, CandidateSeed, StrateIndex);
|
||||||
Override->Regions.StrateTopWorldZ = (float)(TargetSlot->TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Override->Regions.StrateTopWorldZ = TargetSlot->LegacyTopWorldZVoxels;
|
||||||
Override->Regions.StrateBottomWorldZ = (float)TargetSlot->BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Override->Regions.StrateBottomWorldZ = TargetSlot->LegacyBottomWorldZVoxels;
|
||||||
VF_SetStrateArchetypeRuntimeBounds(Override->Params,
|
VF_SetStrateArchetypeRuntimeBounds(Override->Params,
|
||||||
Override->Regions.StrateTopWorldZ,
|
Override->Regions.StrateTopWorldZ,
|
||||||
Override->Regions.StrateBottomWorldZ);
|
Override->Regions.StrateBottomWorldZ);
|
||||||
@@ -1805,14 +1878,36 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
// that opens the next room rather than a second vertical spine.
|
// that opens the next room rather than a second vertical spine.
|
||||||
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
|
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
|
||||||
if (!UpperDef) continue;
|
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).
|
// Upper strate floor and lower strate ceiling (differ when there's a bedrock gap).
|
||||||
const float UpperTopZ = (float)(Upper.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
const float UpperTopZ = Upper.LegacyTopWorldZVoxels;
|
||||||
const float UpperBottomZ = (float)(Upper.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float UpperBottomZ = Upper.LegacyBottomWorldZVoxels;
|
||||||
const float LowerTopZ = (float)(Lower.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
const float LowerTopZ = Lower.LegacyTopWorldZVoxels;
|
||||||
const float UpperMax = (float)Upper.HeightInChunks * StrateLayoutChunkWidthVoxels * 0.9f;
|
const float UpperMax = Upper.LegacyHeightVoxels * 0.9f;
|
||||||
const float LowerMax = (float)Lower.HeightInChunks * StrateLayoutChunkWidthVoxels * 0.9f;
|
const float LowerMax = Lower.LegacyHeightVoxels * 0.9f;
|
||||||
|
|
||||||
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||||
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
|
||||||
@@ -1942,7 +2037,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
LowerDef->FloatingIslandParams,
|
LowerDef->FloatingIslandParams,
|
||||||
LowerQuerySeed,
|
LowerQuerySeed,
|
||||||
LowerTopZ,
|
LowerTopZ,
|
||||||
(float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels,
|
Lower.LegacyBottomWorldZVoxels,
|
||||||
PX,
|
PX,
|
||||||
PY,
|
PY,
|
||||||
MaxLateralSnap,
|
MaxLateralSnap,
|
||||||
@@ -2002,7 +2097,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
if (bAimedLowerAtPlayerFitPoint)
|
if (bAimedLowerAtPlayerFitPoint)
|
||||||
{
|
{
|
||||||
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
|
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
|
||||||
const float LowerBottomZ = (float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float LowerBottomZ = Lower.LegacyBottomWorldZVoxels;
|
||||||
const float InnerBottomZ = LowerBottomZ + LowerSealThickness;
|
const float InnerBottomZ = LowerBottomZ + LowerSealThickness;
|
||||||
const float InnerTopZ = LowerTopZ - LowerSealThickness;
|
const float InnerTopZ = LowerTopZ - LowerSealThickness;
|
||||||
if (VoxelMath::IsFinite(SuggestedLowerPoint.X)
|
if (VoxelMath::IsFinite(SuggestedLowerPoint.X)
|
||||||
@@ -2170,7 +2265,7 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
Cfg.MouthRadius,
|
Cfg.MouthRadius,
|
||||||
LowerDoorDirection,
|
LowerDoorDirection,
|
||||||
LowerTopZ,
|
LowerTopZ,
|
||||||
(float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels,
|
Lower.LegacyBottomWorldZVoxels,
|
||||||
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
|
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
|
||||||
bAimedLowerAtPlayerFitPoint);
|
bAimedLowerAtPlayerFitPoint);
|
||||||
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
|
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
|
||||||
@@ -2595,8 +2690,8 @@ void UVoxelStrateManager::GeneratePassages()
|
|||||||
if (bOpenSurfaceEntry && OriginSpineRadius > 0.0f && StrateLayout.Num() > 0)
|
if (bOpenSurfaceEntry && OriginSpineRadius > 0.0f && StrateLayout.Num() > 0)
|
||||||
{
|
{
|
||||||
const FStrateSlot& Top = StrateLayout[0];
|
const FStrateSlot& Top = StrateLayout[0];
|
||||||
const float TopZ = (float)(Top.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Top.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = (float)Top.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Top.LegacyBottomWorldZVoxels;
|
||||||
const float TopSeal = Top.Definition
|
const float TopSeal = Top.Definition
|
||||||
? BoundarySealThicknessFor(*Top.Definition) : 0.0f;
|
? BoundarySealThicknessFor(*Top.Definition) : 0.0f;
|
||||||
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
|
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
|
||||||
@@ -5921,8 +6016,8 @@ bool UVoxelStrateManager::AnyLandingFloorAtLattice(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||||
@@ -5964,8 +6059,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearBox(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
if (VoxelPassageGeometry::OriginLandingRoomTouchesBox(
|
if (VoxelPassageGeometry::OriginLandingRoomTouchesBox(
|
||||||
VoxelBox, TopZ, BottomZ,
|
VoxelBox, TopZ, BottomZ,
|
||||||
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
||||||
@@ -5985,8 +6080,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearBox(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
|
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
|
||||||
VoxelBox, TopZ, BottomZ,
|
VoxelBox, TopZ, BottomZ,
|
||||||
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
|
||||||
@@ -6010,8 +6105,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearLattice(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
if (VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
|
if (VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
|
||||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius,
|
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius,
|
||||||
@@ -6048,8 +6143,8 @@ bool UVoxelStrateManager::AnyOriginLandingAirNearLattice(
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
|
||||||
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
VoxelPassageGeometry::BuildOriginLandingGeometry(
|
||||||
@@ -6125,8 +6220,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
|
|||||||
for (const FStrateSlot& Slot : StrateLayout)
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
{
|
{
|
||||||
if (Slot.Definition == nullptr) continue;
|
if (Slot.Definition == nullptr) continue;
|
||||||
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
|
||||||
if (VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
|
if (VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
|
||||||
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
|
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
|
||||||
@@ -6143,15 +6238,36 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
|
|||||||
|
|
||||||
int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
||||||
{
|
{
|
||||||
// Linear search through strate layout.
|
// Legacy chunk callers use the projected interval cached during Initialize. The source bounds
|
||||||
// With ~10-20 strates this is fine. If we ever have hundreds,
|
// are the slot's physical micrometre interval; these indices do not define or stack strates.
|
||||||
// switch to binary search (layout is sorted by Z).
|
for (int32 i = 0; i < StrateLayout.Num(); ++i)
|
||||||
for (int32 i = 0; i < StrateLayout.Num(); i++)
|
|
||||||
{
|
{
|
||||||
const FStrateSlot& Slot = StrateLayout[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
|
||||||
|
{
|
||||||
|
const double WorldZMicrometres = WorldZCentimetres * 10000.0;
|
||||||
|
int32 Low = 0;
|
||||||
|
int32 High = StrateLayout.Num() - 1;
|
||||||
|
while (Low <= High)
|
||||||
{
|
{
|
||||||
return i;
|
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;
|
return -1;
|
||||||
@@ -6159,9 +6275,8 @@ int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
|
|||||||
|
|
||||||
UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
||||||
{
|
{
|
||||||
// Convert world Z to chunk Z coordinate
|
if (!FMath::IsFinite(WorldZ)) return nullptr;
|
||||||
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
|
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||||
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
|
||||||
if (SlotIdx >= 0)
|
if (SlotIdx >= 0)
|
||||||
{
|
{
|
||||||
return StrateLayout[SlotIdx].Definition;
|
return StrateLayout[SlotIdx].Definition;
|
||||||
@@ -6171,8 +6286,8 @@ UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
|
|||||||
|
|
||||||
int32 UVoxelStrateManager::GetStrateIndex(float WorldZ) const
|
int32 UVoxelStrateManager::GetStrateIndex(float WorldZ) const
|
||||||
{
|
{
|
||||||
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
|
if (!FMath::IsFinite(WorldZ)) return -1;
|
||||||
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||||
if (SlotIdx >= 0)
|
if (SlotIdx >= 0)
|
||||||
{
|
{
|
||||||
return StrateLayout[SlotIdx].StrateIndex;
|
return StrateLayout[SlotIdx].StrateIndex;
|
||||||
@@ -6285,9 +6400,13 @@ bool UVoxelStrateManager::IsGapChunk(const FIntVector& ChunkCoord) const
|
|||||||
if (StrateLayout.Num() == 0) return false;
|
if (StrateLayout.Num() == 0) return false;
|
||||||
|
|
||||||
// Above the top strate or below the bottom strate = open air, NOT a gap.
|
// Above the top strate or below the bottom strate = open air, NOT a gap.
|
||||||
const int32 StackTop = StrateLayout[0].TopChunkZ;
|
const int64 ChunkCenterMicrometres = static_cast<int64>(ChunkCoord.Z)
|
||||||
const int32 StackBottom = StrateLayout.Last().BottomChunkZ;
|
* StrateLayoutChunkSizeMicrometres + StrateLayoutChunkSizeMicrometres / 2;
|
||||||
if (ChunkCoord.Z > StackTop || ChunkCoord.Z < StackBottom) return false;
|
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.
|
// Inside the stack's Z span but not in any strate slot → it's a bedrock gap.
|
||||||
return FindSlotIndexForChunkZ(ChunkCoord.Z) < 0;
|
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
|
// Fill in the runtime Z bounds (voxel coordinates, same convention as
|
||||||
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
||||||
// TopChunkZ+1 because the top chunk's CEILING is at (TopChunkZ+1)*StrateLayoutChunkWidthVoxels.
|
// TopChunkZ+1 because the top chunk's CEILING is at (TopChunkZ+1)*StrateLayoutChunkWidthVoxels.
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
@@ -6365,8 +6484,8 @@ FMazeGenerationParams UVoxelStrateManager::GetMazeParamsForChunk(const FIntVecto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6392,8 +6511,8 @@ FSurfaceGenerationParams UVoxelStrateManager::GetSurfaceParamsForChunk(const FIn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6419,8 +6538,8 @@ FVerticalShaftParams UVoxelStrateManager::GetVerticalShaftParamsForChunk(const F
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6446,8 +6565,8 @@ FFloatingIslandParams UVoxelStrateManager::GetFloatingIslandParamsForChunk(const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6481,14 +6600,14 @@ FBiomeContext UVoxelStrateManager::GetBiomeContextForChunk(const FIntVector& Chu
|
|||||||
|
|
||||||
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
|
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
|
||||||
{
|
{
|
||||||
const int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
|
if (!FMath::IsFinite(WorldZ)) return false;
|
||||||
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
|
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
|
||||||
if (SlotIdx < 0) return false;
|
if (SlotIdx < 0) return false;
|
||||||
|
|
||||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
// Voxel-space Z bounds → Unreal units. Ceiling = top chunk's upper edge.
|
// Return the resolved physical bounds in Unreal centimeters.
|
||||||
OutTopZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels * VOXEL_SIZE;
|
OutTopZ = static_cast<float>(static_cast<double>(Slot.TopWorldMicrometres) / 10000.0);
|
||||||
OutBottomZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels * VOXEL_SIZE;
|
OutBottomZ = static_cast<float>(static_cast<double>(Slot.BottomWorldMicrometres) / 10000.0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6501,8 +6620,8 @@ FStrateDisturbanceParams UVoxelStrateManager::GetDisturbanceParamsForChunk(const
|
|||||||
}
|
}
|
||||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
FStrateDisturbanceParams Result = Slot.Definition->Disturbances;
|
FStrateDisturbanceParams Result = Slot.Definition->Disturbances;
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6534,8 +6653,8 @@ float UVoxelStrateManager::GetWaterLevelWorldZForChunk(const FIntVector& ChunkCo
|
|||||||
}
|
}
|
||||||
if (Rel <= 0.0f) return -FLT_MAX;
|
if (Rel <= 0.0f) return -FLT_MAX;
|
||||||
|
|
||||||
const float BottomZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
const float TopZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
const float TopZ = Slot.LegacyTopWorldZVoxels;
|
||||||
return FMath::Lerp(BottomZ, TopZ, FMath::Clamp(Rel, 0.0f, 1.0f));
|
return FMath::Lerp(BottomZ, TopZ, FMath::Clamp(Rel, 0.0f, 1.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6579,8 +6698,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
FindComposerOverride(Slot.StrateIndex))
|
FindComposerOverride(Slot.StrateIndex))
|
||||||
{
|
{
|
||||||
FStrateGenerationParams Result = Override->Params.TunnelNetworkParams;
|
FStrateGenerationParams Result = Override->Params.TunnelNetworkParams;
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
// A candidate is a hard replacement of this slot. Do not blend its rolled vector with an
|
// 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.
|
// authored neighbour at a boundary; this is also how the offline fixture measures it.
|
||||||
return ResolveFinalParams(Result);
|
return ResolveFinalParams(Result);
|
||||||
@@ -6590,8 +6709,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (SeasonStrates.IsValidIndex(SlotIdx))
|
if (SeasonStrates.IsValidIndex(SlotIdx))
|
||||||
{
|
{
|
||||||
FStrateGenerationParams Result = SeasonStrates[SlotIdx].Params.TunnelNetworkParams;
|
FStrateGenerationParams Result = SeasonStrates[SlotIdx].Params.TunnelNetworkParams;
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
|
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
// Season vectors are already final, measured slot records. Vertical structural posts own
|
// Season vectors are already final, measured slot records. Vertical structural posts own
|
||||||
// their boundary; blending them with another selected recipe would describe neither one.
|
// their boundary; blending them with another selected recipe would describe neither one.
|
||||||
return ResolveFinalParams(Result);
|
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
|
// 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
|
// BottomChunkZ=-3: bottom of the strate = chunk -3's bottom edge = voxel Z=-3*32 = -96
|
||||||
BaseParams.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
BaseParams.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
|
||||||
BaseParams.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
BaseParams.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
// Core currently dispatches one generator field per resolved strate. Gradient and
|
// Core currently dispatches one generator field per resolved strate. Gradient and
|
||||||
// interleaved metadata therefore use the explicitly declared Hard fallback for every
|
// 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)
|
// 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
|
// When 0, we're right at the boundary. When == TransitionBlendChunks, we're at
|
||||||
// the outer edge of the transition zone.
|
// 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())
|
if (SlotIdx + 1 < StrateLayout.Num())
|
||||||
{
|
{
|
||||||
// The upper strate (this one) controls the transition type at its lower edge
|
// 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.
|
// 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)
|
// Prepare the neighbor's params (only used for Gradient and Interleaved)
|
||||||
const FStrateSlot& BelowSlot = StrateLayout[SlotIdx + 1];
|
const FStrateSlot& BelowSlot = StrateLayout[SlotIdx + 1];
|
||||||
@@ -6691,8 +6815,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (EffectiveBlend > 0 && DistFromBottom < EffectiveBlend)
|
if (EffectiveBlend > 0 && DistFromBottom < EffectiveBlend)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||||
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
|
||||||
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
// Linear alpha: 0 at EffectiveBlend chunks away, 1 at the boundary
|
// Linear alpha: 0 at EffectiveBlend chunks away, 1 at the boundary
|
||||||
float Alpha = 1.0f - ((float)DistFromBottom / (float)EffectiveBlend);
|
float Alpha = 1.0f - ((float)DistFromBottom / (float)EffectiveBlend);
|
||||||
@@ -6729,8 +6853,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (DistFromBottom < CheckRange)
|
if (DistFromBottom < CheckRange)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
|
||||||
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
|
||||||
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
// Sample 3D Perlin noise to warp the boundary position.
|
// Sample 3D Perlin noise to warp the boundary position.
|
||||||
// XY frequency 0.15 gives medium-scale variation (~6-7 chunks per cycle).
|
// 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,
|
// 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].
|
// 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)
|
if (SlotIdx > 0)
|
||||||
{
|
{
|
||||||
// The strate ABOVE controls the transition at its lower edge (= our upper edge)
|
// The strate ABOVE controls the transition at its lower edge (= our upper edge)
|
||||||
const FStrateSlot& AboveSlot = StrateLayout[SlotIdx - 1];
|
const FStrateSlot& AboveSlot = StrateLayout[SlotIdx - 1];
|
||||||
const EVoxelStrateTransition TransType = AboveSlot.Definition->TransitionType;
|
const EVoxelStrateTransition TransType = AboveSlot.ResolvedTransitionType;
|
||||||
const int32 EffectiveBlend = AboveSlot.Definition->TransitionBlendChunks;
|
const int32 EffectiveBlend = static_cast<int32>(VF_FloorDivide64(
|
||||||
|
AboveSlot.TransitionWidthMicrometres, StrateLayoutChunkSizeMicrometres));
|
||||||
|
|
||||||
switch (TransType)
|
switch (TransType)
|
||||||
{
|
{
|
||||||
@@ -6790,8 +6918,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (EffectiveBlend > 0 && DistFromTop < EffectiveBlend)
|
if (EffectiveBlend > 0 && DistFromTop < EffectiveBlend)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||||
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
|
||||||
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
float Alpha = 1.0f - ((float)DistFromTop / (float)EffectiveBlend);
|
float Alpha = 1.0f - ((float)DistFromTop / (float)EffectiveBlend);
|
||||||
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
|
||||||
@@ -6812,8 +6940,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
if (DistFromTop < CheckRange)
|
if (DistFromTop < CheckRange)
|
||||||
{
|
{
|
||||||
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
|
||||||
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
|
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
|
||||||
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
|
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
|
||||||
|
|
||||||
// Same noise function but with a different seed offset to avoid
|
// Same noise function but with a different seed offset to avoid
|
||||||
// symmetry between top and bottom boundaries of adjacent strates
|
// symmetry between top and bottom boundaries of adjacent strates
|
||||||
|
|||||||
@@ -82,6 +82,17 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <unordered_set>
|
#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,
|
static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile,
|
||||||
const FVector& BoundsMin, const FVector& BoundsMax);
|
const FVector& BoundsMin, const FVector& BoundsMax);
|
||||||
static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B);
|
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)
|
const FVoxelTileKey& Tile, int32 AncestorLevel, CandidateArray& OutCandidates)
|
||||||
{
|
{
|
||||||
if (AncestorLevel <= Tile.Level) { return; }
|
if (AncestorLevel <= Tile.Level) { return; }
|
||||||
const int32 Divisor = 1 << (AncestorLevel - Tile.Level);
|
const int32 Divisor = VF_ClipLevelScale(AncestorLevel - Tile.Level);
|
||||||
const auto FloorDiv = [Divisor](int32 Value)
|
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)
|
const auto AxisCandidates = [&FloorDiv, Divisor](int32 Value)
|
||||||
{
|
{
|
||||||
TArray<int32, TInlineAllocator<2>> Candidates;
|
TArray<int32, TInlineAllocator<2>> Candidates;
|
||||||
int32 First = FloorDiv(Value);
|
int32 First = FloorDiv(Value);
|
||||||
if ((Value % Divisor) == 0) { --First; }
|
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); }
|
for (int32 Coord = First; Coord <= Last; ++Coord) { Candidates.Add(Coord); }
|
||||||
return Candidates;
|
return Candidates;
|
||||||
};
|
};
|
||||||
@@ -6338,7 +6351,7 @@ void AVoxelWorld::UpdateVisualStalenessAudit()
|
|||||||
const FVector& WorldPosition, int32 Level, auto& OutCandidates)
|
const FVector& WorldPosition, int32 Level, auto& OutCandidates)
|
||||||
{
|
{
|
||||||
const FVector LocalVoxel = WorldToLocalVoxel(WorldPosition);
|
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>> XCandidates;
|
||||||
TArray<int32, TInlineAllocator<2>> YCandidates;
|
TArray<int32, TInlineAllocator<2>> YCandidates;
|
||||||
TArray<int32, TInlineAllocator<2>> ZCandidates;
|
TArray<int32, TInlineAllocator<2>> ZCandidates;
|
||||||
@@ -7182,7 +7195,7 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
{
|
{
|
||||||
const FIntVector Min = SteadyLevelMins.FindChecked(Level);
|
const FIntVector Min = SteadyLevelMins.FindChecked(Level);
|
||||||
const FIntVector Max = SteadyLevelMaxs.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 TileExtentMetres = static_cast<double>(ExtentVoxels) * VOXEL_SIZE / 100.0;
|
||||||
const double SpanX = static_cast<double>(Max.X - Min.X + 1) * TileExtentMetres;
|
const double SpanX = static_cast<double>(Max.X - Min.X + 1) * TileExtentMetres;
|
||||||
const double SpanY = static_cast<double>(Max.Y - Min.Y + 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)
|
for (const VoxelForgeCore::TileKey& Key : InitialDesired)
|
||||||
{
|
{
|
||||||
const int32 Extent = 32 << Key.lod;
|
const int32 Extent = VF_ClipLevelExtentVoxels(Key.lod);
|
||||||
const int32 OriginX = Key.coordinate.x * Extent;
|
const int64 OriginXWide = static_cast<int64>(Key.coordinate.x) * Extent;
|
||||||
const int32 OriginY = Key.coordinate.y * Extent;
|
const int64 OriginYWide = static_cast<int64>(Key.coordinate.y) * Extent;
|
||||||
const int32 OriginZ = Key.coordinate.z * 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.x, ClipMax.x, OriginX, Extent);
|
||||||
IncludeExtent(ClipMin.y, ClipMax.y, OriginY, Extent);
|
IncludeExtent(ClipMin.y, ClipMax.y, OriginY, Extent);
|
||||||
IncludeExtent(ClipMin.z, ClipMax.z, OriginZ, Extent);
|
IncludeExtent(ClipMin.z, ClipMax.z, OriginZ, Extent);
|
||||||
@@ -7771,6 +7794,12 @@ void AVoxelWorld::FallBackToLegacyAfterCoreInitializationFailure(const FString&
|
|||||||
VoxelForgeCoreWorldAssemblyJson.Reset();
|
VoxelForgeCoreWorldAssemblyJson.Reset();
|
||||||
VoxelForgeCoreWorldAssemblyHash.Reset();
|
VoxelForgeCoreWorldAssemblyHash.Reset();
|
||||||
bUseVoxelForgeCore = false;
|
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;
|
CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
||||||
if (Mesher != nullptr)
|
if (Mesher != nullptr)
|
||||||
{
|
{
|
||||||
@@ -10068,8 +10097,11 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
|||||||
{
|
{
|
||||||
BandChunkLo = MeshBandChunkLo;
|
BandChunkLo = MeshBandChunkLo;
|
||||||
BandChunkHi = MeshBandChunkHi;
|
BandChunkHi = MeshBandChunkHi;
|
||||||
BandVoxLo = MeshBandChunkLo * CHUNK_SIZE;
|
BandVoxLo = static_cast<int32>(FMath::Clamp<int64>(
|
||||||
BandVoxHi = (MeshBandChunkHi + 1) * CHUNK_SIZE - 1;
|
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;
|
FChunkResult Result;
|
||||||
@@ -10232,7 +10264,7 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
|||||||
ReadyToHide.Reserve(TileComponents.Num());
|
ReadyToHide.Reserve(TileComponents.Num());
|
||||||
auto GetAncestorAtLevel = [](const FVoxelTileKey& Tile, int32 AncestorLevel)
|
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);
|
return FVoxelTileKey(VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord, Divisor), AncestorLevel);
|
||||||
};
|
};
|
||||||
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& Pair : TileComponents)
|
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& Pair : TileComponents)
|
||||||
@@ -10355,7 +10387,10 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
|
|||||||
// Integer floor-division (correct for negatives), scalar + vector.
|
// Integer floor-division (correct for negatives), scalar + vector.
|
||||||
static FORCEINLINE int32 VF_FloorDiv(int32 V, int32 D)
|
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)
|
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);
|
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);
|
const FIntVector CMax = VF_FloorDiv(Center, PowMax);
|
||||||
|
|
||||||
FVector CV;
|
FVector CV;
|
||||||
if (bUseVoxelForgeCore)
|
if (bUseVoxelForgeCore)
|
||||||
{
|
{
|
||||||
const double TileExtentVoxels = static_cast<double>(CHUNK_SIZE << Tile.Level);
|
const double TileExtentVoxels = static_cast<double>(VF_ClipLevelExtentVoxels(Tile.Level));
|
||||||
CV = FVector(
|
CV = FVector(
|
||||||
(static_cast<double>(Tile.Coord.X) + 0.5) * TileExtentVoxels,
|
(static_cast<double>(Tile.Coord.X) + 0.5) * TileExtentVoxels,
|
||||||
(static_cast<double>(Tile.Coord.Y) + 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.Y), SizeMax),
|
||||||
VF_FloorDiv(FMath::FloorToInt(CV.Z), SizeMax));
|
VF_FloorDiv(FMath::FloorToInt(CV.Z), SizeMax));
|
||||||
|
|
||||||
return FMath::Abs(TMax.X - CMax.X) <= ROuter
|
return FMath::Abs(static_cast<int64>(TMax.X) - CMax.X) <= ROuter
|
||||||
&& FMath::Abs(TMax.Y - CMax.Y) <= ROuter
|
&& FMath::Abs(static_cast<int64>(TMax.Y) - CMax.Y) <= ROuter
|
||||||
&& FMath::Abs(TMax.Z - CMax.Z) <= ROuter;
|
&& FMath::Abs(static_cast<int64>(TMax.Z) - CMax.Z) <= ROuter;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
||||||
@@ -12052,7 +12087,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
LevelSummaries.Num(), Leavers.Num(), CollisionOnlyTiles.Num()));
|
LevelSummaries.Num(), Leavers.Num(), CollisionOnlyTiles.Num()));
|
||||||
for (const FDesiredLevelSummary& Summary : LevelSummaries)
|
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 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 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;
|
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)
|
for (const FVoxelTileKey& T : LoadedTiles)
|
||||||
{
|
{
|
||||||
if (T.Level < CutMin) continue;
|
if (T.Level < CutMin) continue;
|
||||||
const int32 CLo = T.Coord.Z << T.Level;
|
const int64 LevelScale = VF_ClipLevelScale(T.Level);
|
||||||
const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1;
|
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 bInOld = CLo >= MeshBandChunkLo && CHi <= MeshBandChunkHi;
|
||||||
const bool bInNew = CLo >= NewLo && CHi <= NewHi;
|
const bool bInNew = CLo >= NewLo && CHi <= NewHi;
|
||||||
const bool bOutOld = CHi < MeshBandChunkLo || CLo > MeshBandChunkHi;
|
const bool bOutOld = CHi < MeshBandChunkLo || CLo > MeshBandChunkHi;
|
||||||
@@ -12324,7 +12360,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
}
|
}
|
||||||
Heading = Heading.GetSafeNormal();
|
Heading = Heading.GetSafeNormal();
|
||||||
const FVector PlayerLocalCm = WorldToLocalCm(CenterPosition);
|
const FVector PlayerLocalCm = WorldToLocalCm(CenterPosition);
|
||||||
const double TileSizeCm = static_cast<double>(CHUNK_SIZE << Level)
|
const double TileSizeCm = static_cast<double>(VF_ClipLevelExtentVoxels(Level))
|
||||||
* GetActiveVoxelSizeCm();
|
* GetActiveVoxelSizeCm();
|
||||||
LevelTiles.Sort([&](const FVoxelTileKey& A, const FVoxelTileKey& B)
|
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
|
const int32 FullRes = GVoxelForgeTestFullResClipLevels >= 0
|
||||||
? FMath::Clamp(GVoxelForgeTestFullResClipLevels, 1, 9)
|
? FMath::Clamp(GVoxelForgeTestFullResClipLevels, 1, 9)
|
||||||
: (Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2);
|
: (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)
|
const int32 Cells = (Tile.Level < FullRes)
|
||||||
? CHUNK_SIZE
|
? 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
|
// 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,
|
// ⇒ on n'émet RIEN. Résultat vide via ProcessQueue (bookkeeping normal : PendingTiles,
|
||||||
// LoadedTiles, epoch) ; jamais figé — le changement de bande re-queue via BandRemeshQueue.
|
// 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;
|
FChunkResult Empty;
|
||||||
Empty.Tile = Tile;
|
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.
|
// Convertir les bornes physiques de tuile vers les strates owner, y compris les rapports fractionnaires.
|
||||||
const double OwnerChunksPerCoreTile = static_cast<double>(GetActiveVoxelSizeCm())
|
const double OwnerChunksPerCoreTile = static_cast<double>(GetActiveVoxelSizeCm())
|
||||||
/ VOXEL_SIZE;
|
/ 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(
|
const int32 MinOwnerChunkX = FMath::FloorToInt(
|
||||||
Tile.Coord.X * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE);
|
Tile.Coord.X * TileExtentCoreVoxels * OwnerChunksPerCoreTile / CHUNK_SIZE);
|
||||||
const int32 MinOwnerChunkY = FMath::FloorToInt(
|
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.
|
// BackgroundHigh; the old component remains visible until its post-edit replacement is applied.
|
||||||
auto TileCoversDirtyCoord = [&DirtyCoords](const FVoxelTileKey& Tile) -> bool
|
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 MinChunk = Tile.Coord * Span;
|
||||||
const FIntVector MaxChunk = MinChunk + FIntVector(Span, Span, Span);
|
const FIntVector MaxChunk = MinChunk + FIntVector(Span, Span, Span);
|
||||||
for (const FIntVector& Coord : DirtyCoords)
|
for (const FIntVector& Coord : DirtyCoords)
|
||||||
|
|||||||
@@ -261,8 +261,12 @@ namespace VoxelHash
|
|||||||
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
|
FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed)
|
||||||
{
|
{
|
||||||
uint32 H = Seed;
|
uint32 H = Seed;
|
||||||
H ^= Mix((uint32)(CellX + 0x7FFFFFFF));
|
// Bias in unsigned arithmetic. Adding the signed cell coordinate first overflows for
|
||||||
H ^= Mix((uint32)(CellY + 0x7FFFFFFF) * 2654435761u);
|
// 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);
|
return Mix(H);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,10 +278,14 @@ namespace VoxelHash
|
|||||||
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
|
int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY);
|
||||||
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
|
int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY);
|
||||||
uint32 H = Seed ^ 0xDEADBEEF;
|
uint32 H = Seed ^ 0xDEADBEEF;
|
||||||
H ^= Mix((uint32)(MinX + 0x7FFFFFFF));
|
const uint32 BiasedMinX = static_cast<uint32>(MinX) + 0x7FFFFFFFu;
|
||||||
H ^= Mix((uint32)(MinY + 0x7FFFFFFF) * 2654435761u);
|
const uint32 BiasedMinY = static_cast<uint32>(MinY) + 0x7FFFFFFFu;
|
||||||
H ^= Mix((uint32)(MaxX + 0x7FFFFFFF) * 374761393u);
|
const uint32 BiasedMaxX = static_cast<uint32>(MaxX) + 0x7FFFFFFFu;
|
||||||
H ^= Mix((uint32)(MaxY + 0x7FFFFFFF) * 668265263u);
|
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);
|
return Mix(H);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,10 +485,13 @@ namespace VoxelNoise
|
|||||||
|
|
||||||
// Hash the neighbor cell to get a feature point position [0,1)
|
// Hash the neighbor cell to get a feature point position [0,1)
|
||||||
// Using three different hash mixes for X, Y, Z offsets
|
// 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 H = VoxelHash::Mix(
|
||||||
(uint32)(NX + 0x7FFFFFFF)
|
BiasedX
|
||||||
^ VoxelHash::Mix((uint32)(NY + 0x7FFFFFFF) * 2654435761u)
|
^ VoxelHash::Mix(BiasedY * 2654435761u)
|
||||||
^ VoxelHash::Mix((uint32)(NZ + 0x7FFFFFFF) * 374761393u)
|
^ VoxelHash::Mix(BiasedZ * 374761393u)
|
||||||
);
|
);
|
||||||
|
|
||||||
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
|
float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX;
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ struct FVoxelPassage
|
|||||||
* FStrateSlot — Runtime info for one strate in the layout.
|
* FStrateSlot — Runtime info for one strate in the layout.
|
||||||
*
|
*
|
||||||
* Created during Initialize() and stored in the StrateLayout array.
|
* Created during Initialize() and stored in the StrateLayout array.
|
||||||
* Each slot knows its definition, its Z-range in chunk coordinates,
|
* Each slot owns its resolved half-open physical Z interval. Chunk coordinates and legacy
|
||||||
* and its index in the sequence.
|
* authoring values below are projections kept for existing callers and assets.
|
||||||
*/
|
*/
|
||||||
USTRUCT()
|
USTRUCT()
|
||||||
struct FStrateSlot
|
struct FStrateSlot
|
||||||
@@ -136,6 +136,33 @@ struct FStrateSlot
|
|||||||
// Strate index (0 = topmost, increases downward)
|
// Strate index (0 = topmost, increases downward)
|
||||||
int32 StrateIndex = 0;
|
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.
|
// 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
|
// Example: TopZ = 0, BottomZ = -3 means this strate spans chunks Z=0 to Z=-3
|
||||||
int32 TopChunkZ = 0;
|
int32 TopChunkZ = 0;
|
||||||
@@ -248,7 +275,11 @@ public:
|
|||||||
// This is independent of Core's 32^3 tile width.
|
// This is independent of Core's 32^3 tile width.
|
||||||
float GetStrateLayoutChunkWidthVoxels() const { return StrateLayoutChunkWidthVoxels; }
|
float GetStrateLayoutChunkWidthVoxels() const { return StrateLayoutChunkWidthVoxels; }
|
||||||
uint32 GetStrateLayoutChunkSizeMicrometres() const { return StrateLayoutChunkSizeMicrometres; }
|
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; }
|
{ bHardTransitionFallbackForAssembly = bEnabled; }
|
||||||
|
|
||||||
/** Layout/passage generation counter (bumped by every Initialize → GeneratePassages).
|
/** Layout/passage generation counter (bumped by every Initialize → GeneratePassages).
|
||||||
@@ -528,14 +559,15 @@ protected:
|
|||||||
// Copied from VoxelSettings::OriginSpineRadius.
|
// Copied from VoxelSettings::OriginSpineRadius.
|
||||||
float OriginSpineRadius = 28.0f;
|
float OriginSpineRadius = 28.0f;
|
||||||
|
|
||||||
// Solid-bedrock gap between consecutive strates, in chunks. Copied from
|
// Compatibility gap projection, in layout chunks. Physical authority is the micrometre value
|
||||||
// VoxelSettings::InterStrateGapChunks during Initialize().
|
// resolved during Initialize below.
|
||||||
int32 InterStrateGapChunks = 0;
|
int32 InterStrateGapChunks = 0;
|
||||||
|
int64 ResolvedInterStrateGapMicrometres = 0;
|
||||||
|
|
||||||
// Resolved once from VoxelSettings::StrateLayoutChunkSizeMeters during Initialize().
|
// Resolved once from VoxelSettings::StrateLayoutChunkSizeMeters during Initialize().
|
||||||
float StrateLayoutChunkWidthVoxels = 32.0f;
|
float StrateLayoutChunkWidthVoxels = 32.0f;
|
||||||
uint32 StrateLayoutChunkSizeMicrometres = 8000000;
|
uint32 StrateLayoutChunkSizeMicrometres = 8000000;
|
||||||
bool bHardTransitionFallbackForAssembly = false;
|
mutable bool bHardTransitionFallbackForAssembly = false;
|
||||||
|
|
||||||
// Production keeps the legacy writer available only as a fail-closed fallback for a
|
// 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
|
// 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.
|
// Find which slot a chunk Z coordinate falls into.
|
||||||
// Returns INDEX into StrateLayout, or -1 if not found.
|
// Returns INDEX into StrateLayout, or -1 if not found.
|
||||||
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
|
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
|
||||||
|
int32 FindSlotIndexForWorldZCentimetres(double WorldZCentimetres) const;
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
const FVoxelStrateComposerSlotOverride* FindComposerOverride(int32 StrateIndex) const;
|
const FVoxelStrateComposerSlotOverride* FindComposerOverride(int32 StrateIndex) const;
|
||||||
|
|||||||
@@ -201,15 +201,19 @@ enum class ECaveGeneratorType : uint8
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EVoxelStrateTransition — How two adjacent strates blend at their shared boundary.
|
* EVoxelStrateTransition — How two adjacent strates meet at their shared boundary.
|
||||||
*
|
*
|
||||||
* Each strate boundary can use a different transition style. The transition type
|
* Each strate boundary can use a different transition style. The transition type
|
||||||
* is defined on the UPPER strate's definition and controls the boundary between
|
* is defined on the UPPER strate's definition and controls the boundary between
|
||||||
* itself and the strate below it.
|
* itself and the strate below it.
|
||||||
*
|
*
|
||||||
* Gradient: Smooth linear interpolation of generation params across TransitionBlendChunks.
|
* In the Core composition path, TransitionBlendChunks defines the full permitted
|
||||||
* This is the classic approach — no seam visible, but also no dramatic
|
* crossing depth between the upper bedrock and lower ceiling bands. Each band keeps
|
||||||
* geological feature at the boundary. Best for similar-looking strates.
|
* its authored thickness and the solid bands always overlap or touch. The legacy manager
|
||||||
|
* may also blend generation parameters for Gradient; Core does not blend unrelated fields.
|
||||||
|
*
|
||||||
|
* Gradient: A low-amplitude smooth height field moves the shared interface and both
|
||||||
|
* content-facing band surfaces. Best for gentle geological variation.
|
||||||
*
|
*
|
||||||
* Hard: No blending at all. The strate's params apply right up to the boundary,
|
* Hard: No blending at all. The strate's params apply right up to the boundary,
|
||||||
* then instantly switch to the neighbor's params. The abrupt change in
|
* then instantly switch to the neighbor's params. The abrupt change in
|
||||||
@@ -217,26 +221,21 @@ enum class ECaveGeneratorType : uint8
|
|||||||
* discontinuity at the boundary. Best for strates that should feel like
|
* discontinuity at the boundary. Best for strates that should feel like
|
||||||
* distinct geological layers with a visible dividing line.
|
* distinct geological layers with a visible dividing line.
|
||||||
*
|
*
|
||||||
* Interleaved: The boundary position is warped by a 3D noise field, so "fingers"
|
* Interleaved: A deterministic 3D domain-warped hash field sets the overlap depth per XY
|
||||||
* of one strate reach into the other. Some XY columns transition early,
|
* column from zero to the declared width, creating irregular fingers.
|
||||||
* others late, creating an irregular, interlocking boundary. The warp
|
* Best for strates that should feel grown together.
|
||||||
* amplitude is ~2 chunks, and Z frequency is lower than XY to produce
|
|
||||||
* horizontal finger-like intrusions. Best for strates that should feel
|
|
||||||
* like they grew into each other over geological time.
|
|
||||||
*/
|
*/
|
||||||
UENUM(BlueprintType)
|
UENUM(BlueprintType)
|
||||||
enum class EVoxelStrateTransition : uint8
|
enum class EVoxelStrateTransition : uint8
|
||||||
{
|
{
|
||||||
// Smooth linear interpolation of all generation params across the blend zone.
|
// Smoothly undulates the shared bedrock/ceiling interface in the Core composition path.
|
||||||
// No visible boundary — params change gradually over TransitionBlendChunks distance.
|
|
||||||
Gradient UMETA(DisplayName = "Gradient (smooth blend)"),
|
Gradient UMETA(DisplayName = "Gradient (smooth blend)"),
|
||||||
|
|
||||||
// No blending — params switch instantly at the boundary.
|
// No blending — params switch instantly at the boundary.
|
||||||
// Creates natural cliffs, ledges, or material discontinuities.
|
// Creates natural cliffs, ledges, or material discontinuities.
|
||||||
Hard UMETA(DisplayName = "Hard (sharp boundary with cliff)"),
|
Hard UMETA(DisplayName = "Hard (sharp boundary with cliff)"),
|
||||||
|
|
||||||
// 3D noise warps the boundary position per XY column.
|
// A 3D domain-warped hash field creates finger-like intrusions within the declared width.
|
||||||
// Creates finger-like intrusions of one strate into the other.
|
|
||||||
Interleaved UMETA(DisplayName = "Interleaved (domain-warped fingers)")
|
Interleaved UMETA(DisplayName = "Interleaved (domain-warped fingers)")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -332,16 +332,33 @@ struct FVoxelTileKey
|
|||||||
FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {}
|
FVoxelTileKey(const FIntVector& InCoord, int32 InLevel) : Coord(InCoord), Level(InLevel) {}
|
||||||
|
|
||||||
// Cell size (sampling step) in voxels, and tile extent in voxels per axis.
|
// Cell size (sampling step) in voxels, and tile extent in voxels per axis.
|
||||||
int32 StepVoxels() const { return 1 << Level; }
|
int32 StepVoxels() const
|
||||||
int32 ExtentVoxels() const { return CHUNK_SIZE << Level; }
|
{
|
||||||
|
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).
|
// 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).
|
// Tile centre in world cm (for distance sorting / LOD selection).
|
||||||
FVector CenterCm() const
|
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);
|
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)"
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ROOT="$SCRIPT_DIR"
|
ROOT="$SCRIPT_DIR"
|
||||||
|
PROJECT_ROOT="$(cd -- "$ROOT/.." && pwd)"
|
||||||
|
VERIFY_DIR="$ROOT/Tools/verify"
|
||||||
COMPILER="clang++-19"
|
COMPILER="clang++-19"
|
||||||
VARIANT="clang19"
|
VARIANT="clang19"
|
||||||
FP_CONTRACT="off"
|
FP_CONTRACT="off"
|
||||||
JOBS=12
|
JOBS=12
|
||||||
|
SANITIZERS=""
|
||||||
|
EXCLUDE_UNSIGNED_OVERFLOW=0
|
||||||
|
|
||||||
while (($#)); do
|
while (($#)); do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -18,6 +22,8 @@ while (($#)); do
|
|||||||
--variant=*) VARIANT="${1#*=}" ;;
|
--variant=*) VARIANT="${1#*=}" ;;
|
||||||
--fp-contract=*) FP_CONTRACT="${1#*=}" ;;
|
--fp-contract=*) FP_CONTRACT="${1#*=}" ;;
|
||||||
--jobs=*) JOBS="${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 ;;
|
*) printf 'Unknown build option: %s\n' "$1" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
@@ -41,6 +47,15 @@ BIN_DIR="${VOXELFORGE_BIN_DIR:-$ROOT/bin/linux}"
|
|||||||
BUILD_DIR="$BUILD_ROOT/linux-$VARIANT"
|
BUILD_DIR="$BUILD_ROOT/linux-$VARIANT"
|
||||||
mkdir -p "$BUILD_DIR/wrappers" "$BUILD_DIR/objects" "$BIN_DIR"
|
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=(
|
INCLUDES=(
|
||||||
"-I$ROOT/Core/include"
|
"-I$ROOT/Core/include"
|
||||||
"-I$ROOT/Core/src"
|
"-I$ROOT/Core/src"
|
||||||
@@ -49,10 +64,22 @@ INCLUDES=(
|
|||||||
"-I$ROOT/Source/VoxelForge/Public"
|
"-I$ROOT/Source/VoxelForge/Public"
|
||||||
"-I$ROOT/Source/VoxelForge/Private"
|
"-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=(
|
FLAGS=(
|
||||||
-std=c++20 -O2 -mavx2 -mfma -fno-fast-math
|
-std=c++20 "$OPT_LEVEL" -mavx2 -mfma -fno-fast-math
|
||||||
-fexceptions -pthread "${INCLUDES[@]}"
|
-fexceptions -pthread "${INCLUDES[@]}"
|
||||||
|
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\""
|
||||||
)
|
)
|
||||||
|
FLAGS+=("${LINK_FLAGS[@]}")
|
||||||
if [[ "$FP_CONTRACT" != "default" ]]; then
|
if [[ "$FP_CONTRACT" != "default" ]]; then
|
||||||
FLAGS+=("-ffp-contract=$FP_CONTRACT")
|
FLAGS+=("-ffp-contract=$FP_CONTRACT")
|
||||||
fi
|
fi
|
||||||
@@ -119,11 +146,19 @@ PIDS=()
|
|||||||
CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
|
CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a"
|
||||||
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
|
"$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}"
|
||||||
OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT"
|
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"
|
CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT"
|
||||||
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
|
"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \
|
||||||
|
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\"" -I"$ROOT/Source" \
|
||||||
"$ROOT/Source/LinuxOffscreenCapture.cpp" -o "$CAPTURE_OUTPUT" \
|
"$ROOT/Source/LinuxOffscreenCapture.cpp" -o "$CAPTURE_OUTPUT" \
|
||||||
$(pkg-config --cflags --libs egl glesv2 libpng)
|
$(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 Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT"
|
||||||
printf 'Built EGL offscreen capture: %s\n' "$CAPTURE_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
|
#pragma once
|
||||||
|
|
||||||
|
#include "VoxelForgeCore/CpuCycleCounter.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <chrono>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -12,6 +15,23 @@
|
|||||||
|
|
||||||
namespace VoxelForgeCoreShared
|
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:
|
// 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.
|
// the same recipe/plan column has the same bits at every sampling step.
|
||||||
struct FColumnCacheKey
|
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
|
// 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
|
// 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
|
// 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.
|
// 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.
|
// 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
|
class TSharedColumnValueCache
|
||||||
{
|
{
|
||||||
static_assert(SetCount > 0 && (SetCount & (SetCount - 1)) == 0,
|
static_assert(SetCount > 0 && (SetCount & (SetCount - 1)) == 0,
|
||||||
@@ -49,14 +93,14 @@ namespace VoxelForgeCoreShared
|
|||||||
"shared column values must be trivially copyable");
|
"shared column values must be trivially copyable");
|
||||||
|
|
||||||
static constexpr std::size_t ValueWordCount = (sizeof(TValue) + 7) / 8;
|
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>;
|
using FValueWords = std::array<std::uint64_t, ValueWordCount>;
|
||||||
|
|
||||||
struct FEntry
|
struct FEntry
|
||||||
{
|
{
|
||||||
std::atomic<std::uint64_t> Version{0};
|
std::atomic<std::uint64_t> Version{0};
|
||||||
std::atomic<std::uint64_t> Tag{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{};
|
std::array<std::atomic<std::uint64_t>, ValueWordCount> Value{};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,13 +113,28 @@ namespace VoxelForgeCoreShared
|
|||||||
|
|
||||||
struct FSpinGuard
|
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))
|
while (Flag.test_and_set(std::memory_order_acquire))
|
||||||
std::this_thread::yield();
|
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); }
|
~FSpinGuard() { Flag.clear(std::memory_order_release); }
|
||||||
std::atomic_flag& Flag;
|
std::atomic_flag& Flag;
|
||||||
|
FColumnCacheMetrics* Metrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -93,8 +152,8 @@ namespace VoxelForgeCoreShared
|
|||||||
static TSharedColumnValueCache* threadActive() noexcept { return Active; }
|
static TSharedColumnValueCache* threadActive() noexcept { return Active; }
|
||||||
|
|
||||||
template <typename TFactory>
|
template <typename TFactory>
|
||||||
TValue getOrCompute(const FColumnCacheKey& key, TFactory&& factory,
|
TValue getOrCompute(const TKey& key, TFactory&& factory,
|
||||||
bool& computed)
|
bool& computed, FColumnCacheMetrics* metrics = nullptr)
|
||||||
{
|
{
|
||||||
computed = false;
|
computed = false;
|
||||||
const FKeyWords keyWords = key.words();
|
const FKeyWords keyWords = key.words();
|
||||||
@@ -102,13 +161,47 @@ namespace VoxelForgeCoreShared
|
|||||||
const std::size_t setIndex = static_cast<std::size_t>(tag) & (SetCount - 1);
|
const std::size_t setIndex = static_cast<std::size_t>(tag) & (SetCount - 1);
|
||||||
FSet& set = Sets[setIndex];
|
FSet& set = Sets[setIndex];
|
||||||
TValue value{};
|
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);
|
FSpinGuard lock(set.Writer, metrics,sampleCycles);
|
||||||
if (TryReadSet(set, keyWords, tag, value)) return value;
|
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)();
|
value = std::forward<TFactory>(factory)();
|
||||||
|
if(sampleCycles)
|
||||||
|
{
|
||||||
|
metrics->sampledBuildCycles+=AdjustedCpuCycleDelta(buildStart,ReadCpuCycles());
|
||||||
|
++metrics->sampledBuilds;
|
||||||
|
}
|
||||||
computed = true;
|
computed = true;
|
||||||
|
if (metrics) ++metrics->computes;
|
||||||
FValueWords valueWords{};
|
FValueWords valueWords{};
|
||||||
std::memcpy(valueWords.data(), &value, sizeof(TValue));
|
std::memcpy(valueWords.data(), &value, sizeof(TValue));
|
||||||
|
|
||||||
@@ -118,6 +211,7 @@ namespace VoxelForgeCoreShared
|
|||||||
{ replacement = way; break; }
|
{ replacement = way; break; }
|
||||||
if (replacement == Ways)
|
if (replacement == Ways)
|
||||||
{
|
{
|
||||||
|
if (metrics) ++metrics->conflictReplacements;
|
||||||
replacement = set.NextWay;
|
replacement = set.NextWay;
|
||||||
set.NextWay = static_cast<std::uint8_t>((set.NextWay + 1) % Ways);
|
set.NextWay = static_cast<std::uint8_t>((set.NextWay + 1) % Ways);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ namespace VoxelForgeCore
|
|||||||
Vec3d normal;
|
Vec3d normal;
|
||||||
Vec2f uv;
|
Vec2f uv;
|
||||||
ColorRgba8 color;
|
ColorRgba8 color;
|
||||||
|
std::uint16_t hostMaterialId = 1;
|
||||||
|
std::array<std::uint16_t, 2> resourceMaterialIds{};
|
||||||
|
std::array<std::uint16_t, 2> resourceWeightsQ16{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MeshGroups
|
struct MeshGroups
|
||||||
@@ -136,6 +139,18 @@ namespace VoxelForgeCore
|
|||||||
std::uint64_t columnCacheConflictMisses = 0;
|
std::uint64_t columnCacheConflictMisses = 0;
|
||||||
std::uint64_t sharedColumnCacheHits = 0;
|
std::uint64_t sharedColumnCacheHits = 0;
|
||||||
std::uint64_t columnEvaluations = 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 uniformBlockProofsChecked = 0;
|
||||||
std::uint64_t uniformBlockProofSamplesChecked = 0;
|
std::uint64_t uniformBlockProofSamplesChecked = 0;
|
||||||
std::uint64_t uniformBlockProofFallbackBlocks = 0;
|
std::uint64_t uniformBlockProofFallbackBlocks = 0;
|
||||||
@@ -157,12 +172,30 @@ namespace VoxelForgeCore
|
|||||||
double marchingCubesSkirtMilliseconds = 0.0;
|
double marchingCubesSkirtMilliseconds = 0.0;
|
||||||
double marchingCubesOtherMilliseconds = 0.0;
|
double marchingCubesOtherMilliseconds = 0.0;
|
||||||
std::uint64_t marchingCubesGradientFallbackDensityCalls = 0;
|
std::uint64_t marchingCubesGradientFallbackDensityCalls = 0;
|
||||||
|
std::uint64_t marchingCubesGradientCalls = 0;
|
||||||
std::uint64_t marchingCubesEdgeMidpointDensityCalls = 0;
|
std::uint64_t marchingCubesEdgeMidpointDensityCalls = 0;
|
||||||
|
std::uint64_t marchingCubesEdgeMidpointDensityMemoHits = 0;
|
||||||
std::uint64_t marchingCubesGradientMemoHits = 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 groundRefinementExclusiveMilliseconds = 0.0;
|
||||||
double capRefinementExclusiveMilliseconds = 0.0;
|
double capRefinementExclusiveMilliseconds = 0.0;
|
||||||
double meshAssemblyMilliseconds = 0.0;
|
double meshAssemblyMilliseconds = 0.0;
|
||||||
double meshingMilliseconds = 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 featureListMilliseconds = 0.0;
|
||||||
double farShellMilliseconds = 0.0;
|
double farShellMilliseconds = 0.0;
|
||||||
double farStratumShellMilliseconds = 0.0;
|
double farStratumShellMilliseconds = 0.0;
|
||||||
@@ -216,7 +249,9 @@ namespace VoxelForgeCore
|
|||||||
std::uint64_t densitySampleCount = 0;
|
std::uint64_t densitySampleCount = 0;
|
||||||
std::uint64_t meshPayloadAllocatedBytes = 0;
|
std::uint64_t meshPayloadAllocatedBytes = 0;
|
||||||
std::uint64_t densityHash = 0, meshHash = 0, payloadHash = 0, topologyHash = 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
|
struct WorldConfiguration
|
||||||
@@ -249,8 +284,129 @@ namespace VoxelForgeCore
|
|||||||
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
|
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
|
||||||
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
||||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
||||||
// Version 14 adds shared passage columns for long native open-air crossings.
|
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces;
|
||||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 14;
|
// 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
|
enum class FieldChannel : std::uint32_t
|
||||||
{
|
{
|
||||||
@@ -297,6 +453,59 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t capRidgeWavelengthSubvoxels = 256 * 64;
|
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
|
struct WorldAssemblyRecipeV1
|
||||||
{
|
{
|
||||||
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
|
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
|
||||||
@@ -314,6 +523,7 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t biomeClimateReliefWavelengthSubvoxels = 256 * 3333;
|
std::int64_t biomeClimateReliefWavelengthSubvoxels = 256 * 3333;
|
||||||
std::int64_t biomeClimateMoistureWavelengthSubvoxels = 256 * 2500;
|
std::int64_t biomeClimateMoistureWavelengthSubvoxels = 256 * 2500;
|
||||||
std::int64_t biomeClimateReliefContrastQ16 = 2ll << 16;
|
std::int64_t biomeClimateReliefContrastQ16 = 2ll << 16;
|
||||||
|
WorldAssemblySurfaceProfileV1 surfaceProfile;
|
||||||
std::vector<WorldAssemblyBiomeProfileV1> biomes;
|
std::vector<WorldAssemblyBiomeProfileV1> biomes;
|
||||||
std::uint64_t contentHash = 0;
|
std::uint64_t contentHash = 0;
|
||||||
};
|
};
|
||||||
@@ -373,6 +583,7 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t capRidgeWavelengthSubvoxels = 256 * 64;
|
std::int64_t capRidgeWavelengthSubvoxels = 256 * 64;
|
||||||
std::int64_t waterSurfaceSubvoxels = 0;
|
std::int64_t waterSurfaceSubvoxels = 0;
|
||||||
bool waterEnabled = false;
|
bool waterEnabled = false;
|
||||||
|
WorldAssemblySurfaceProfileV1 surfaceProfile;
|
||||||
std::vector<WorldAssemblyFeatureV1> features;
|
std::vector<WorldAssemblyFeatureV1> features;
|
||||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||||
std::vector<WorldAssemblyRoutePointV1> route;
|
std::vector<WorldAssemblyRoutePointV1> route;
|
||||||
@@ -444,6 +655,8 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t anchorIslandMinimumRadiusSubvoxels = 0;
|
std::int64_t anchorIslandMinimumRadiusSubvoxels = 0;
|
||||||
std::int64_t anchorRootBottomZSubvoxels = 0;
|
std::int64_t anchorRootBottomZSubvoxels = 0;
|
||||||
std::int64_t anchorMouthOffsetSubvoxels = 0;
|
std::int64_t anchorMouthOffsetSubvoxels = 0;
|
||||||
|
// Style-zero mouths follow the sampled island surface into the passage lip.
|
||||||
|
bool terrainFollowingRoute = false;
|
||||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||||
std::uint64_t contentHash = 0;
|
std::uint64_t contentHash = 0;
|
||||||
};
|
};
|
||||||
@@ -510,8 +723,42 @@ namespace VoxelForgeCore
|
|||||||
// Hors du domaine prepare, revenir au parcours complet canonique.
|
// Hors du domaine prepare, revenir au parcours complet canonique.
|
||||||
std::array<std::int64_t, 3> minimumSubvoxels{};
|
std::array<std::int64_t, 3> minimumSubvoxels{};
|
||||||
std::array<std::int64_t, 3> maximumSubvoxels{};
|
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 FloatingIslandTileFeatureListV1 = WorldAssemblyFeatureTileListV1;
|
||||||
using FloatingIslandFeatureGroupBoundsV1 = WorldAssemblyFeatureGroupBoundsV1;
|
using FloatingIslandFeatureGroupBoundsV1 = WorldAssemblyFeatureGroupBoundsV1;
|
||||||
|
|
||||||
@@ -534,10 +781,13 @@ namespace VoxelForgeCore
|
|||||||
ScopedWorldAssemblyFeatureQueriesV1& operator=(const ScopedWorldAssemblyFeatureQueriesV1&) = delete;
|
ScopedWorldAssemblyFeatureQueriesV1& operator=(const ScopedWorldAssemblyFeatureQueriesV1&) = delete;
|
||||||
static const WorldAssemblyFeatureTileListV1* find(const void* recipe, const void* plan,
|
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;
|
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:
|
private:
|
||||||
std::span<const WorldAssemblyFeatureQueryV1> queries_;
|
std::span<const WorldAssemblyFeatureQueryV1> queries_;
|
||||||
bool forceCandidates_ = false;
|
bool forceCandidates_ = false;
|
||||||
const ScopedWorldAssemblyFeatureQueriesV1* previous_ = nullptr;
|
const ScopedWorldAssemblyFeatureQueriesV1* previous_ = nullptr;
|
||||||
|
std::uint64_t densityCacheContextLow_ = 0, densityCacheContextHigh_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VFCORE_API FloatingIslandManifestV1
|
struct VFCORE_API FloatingIslandManifestV1
|
||||||
@@ -584,8 +834,139 @@ namespace VoxelForgeCore
|
|||||||
// Core adapter. Coordinates are half-open micrometre intervals; recipe anchors retain their
|
// Core adapter. Coordinates are half-open micrometre intervals; recipe anchors retain their
|
||||||
// source fixed-point units so this record never rounds the recipe itself.
|
// source fixed-point units so this record never rounds the recipe itself.
|
||||||
inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV3 = 3;
|
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 };
|
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
|
struct ResolvedLayoutAnchorV2
|
||||||
{
|
{
|
||||||
std::string kind;
|
std::string kind;
|
||||||
@@ -608,6 +989,24 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t solidDensityQ16 = 1ll << 16;
|
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
|
struct ResolvedLayoutStratumV2
|
||||||
{
|
{
|
||||||
std::string stableId;
|
std::string stableId;
|
||||||
@@ -637,6 +1036,39 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t managerChunkWidthMicrometres = 0;
|
std::int64_t managerChunkWidthMicrometres = 0;
|
||||||
std::int64_t dispatchChunkWidthMicrometres = 0;
|
std::int64_t dispatchChunkWidthMicrometres = 0;
|
||||||
std::vector<ResolvedLayoutAnchorV2> anchors;
|
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.
|
||||||
|
*/
|
||||||
|
struct ResolvedLayoutTransitionBandsV1
|
||||||
|
{
|
||||||
|
std::int64_t crossingDepthMicrometres = 0;
|
||||||
|
std::int64_t upperBedrockBottomMicrometres = 0;
|
||||||
|
std::int64_t upperBedrockTopMicrometres = 0;
|
||||||
|
std::int64_t lowerCeilingBottomMicrometres = 0;
|
||||||
|
std::int64_t lowerCeilingTopMicrometres = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ResolvedLayoutIssueV2
|
struct ResolvedLayoutIssueV2
|
||||||
@@ -656,6 +1088,8 @@ namespace VoxelForgeCore
|
|||||||
std::string stableId;
|
std::string stableId;
|
||||||
std::int32_t upperStrateIndex = -1;
|
std::int32_t upperStrateIndex = -1;
|
||||||
std::int32_t lowerStrateIndex = -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::int64_t placementRadiusSubvoxels = 0;
|
||||||
std::array<std::int64_t, 3> upperMouthSubvoxels{};
|
std::array<std::int64_t, 3> upperMouthSubvoxels{};
|
||||||
std::array<std::int64_t, 3> lowerMouthSubvoxels{};
|
std::array<std::int64_t, 3> lowerMouthSubvoxels{};
|
||||||
@@ -719,6 +1153,16 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t strateLayoutChunkWidthSubvoxels = 8192;
|
std::int64_t strateLayoutChunkWidthSubvoxels = 8192;
|
||||||
std::vector<ResolvedLayoutStratumV2> strates;
|
std::vector<ResolvedLayoutStratumV2> strates;
|
||||||
std::vector<ResolvedLayoutPassageV1> passages;
|
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;
|
std::string stableIdentity;
|
||||||
|
|
||||||
bool finalize(std::string& error);
|
bool finalize(std::string& error);
|
||||||
@@ -730,6 +1174,17 @@ namespace VoxelForgeCore
|
|||||||
std::string_view json, std::string& error);
|
std::string_view json, std::string& error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the deterministic band faces at one XY column. Gradient shares one smooth
|
||||||
|
* displaced face; Interleaved moves the faces inward by a total crossing no larger than
|
||||||
|
* the declared width. Both bands retain authored thickness and touch or overlap, so no
|
||||||
|
* air gap can form.
|
||||||
|
*/
|
||||||
|
VFCORE_API ResolvedLayoutTransitionBandsV1 EvaluateResolvedLayoutTransitionBandsV1(
|
||||||
|
const ResolvedLayoutStratumV2& upper, const ResolvedLayoutStratumV2& lower,
|
||||||
|
std::int32_t worldSeed, std::int64_t layoutChunkSizeMicrometres,
|
||||||
|
std::int64_t xMicrometres, std::int64_t yMicrometres);
|
||||||
|
|
||||||
struct ResolvedLayoutValidatorAuditV3
|
struct ResolvedLayoutValidatorAuditV3
|
||||||
{
|
{
|
||||||
std::uint32_t controlsRun = 0;
|
std::uint32_t controlsRun = 0;
|
||||||
@@ -743,6 +1198,68 @@ namespace VoxelForgeCore
|
|||||||
const std::vector<ResolvedLayoutPassageConfigV1>& configs,
|
const std::vector<ResolvedLayoutPassageConfigV1>& configs,
|
||||||
std::string& error);
|
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
|
struct FloatingIslandBoundsAuditResult
|
||||||
{
|
{
|
||||||
std::uint64_t boxesTested = 0;
|
std::uint64_t boxesTested = 0;
|
||||||
@@ -867,6 +1384,8 @@ namespace VoxelForgeCore
|
|||||||
std::uint64_t columnConflictMisses = 0;
|
std::uint64_t columnConflictMisses = 0;
|
||||||
std::uint64_t sharedColumnHits = 0;
|
std::uint64_t sharedColumnHits = 0;
|
||||||
std::uint64_t columnEvaluations = 0;
|
std::uint64_t columnEvaluations = 0;
|
||||||
|
std::uint64_t densitySampleCacheHits = 0;
|
||||||
|
std::uint64_t densitySampleCacheComputes = 0;
|
||||||
double columnComputeMilliseconds = 0.0;
|
double columnComputeMilliseconds = 0.0;
|
||||||
bool captureCoordinates = false;
|
bool captureCoordinates = false;
|
||||||
bool coordinateCaptureComplete = true;
|
bool coordinateCaptureComplete = true;
|
||||||
@@ -1021,6 +1540,7 @@ namespace VoxelForgeCore
|
|||||||
const WorldConfiguration& configuration() const;
|
const WorldConfiguration& configuration() const;
|
||||||
const WorldAssemblyManifest* worldAssembly() const noexcept;
|
const WorldAssemblyManifest* worldAssembly() const noexcept;
|
||||||
const ResolvedLayoutSnapshotV3* resolvedLayout() const noexcept;
|
const ResolvedLayoutSnapshotV3* resolvedLayout() const noexcept;
|
||||||
|
ResolvedWorldSnapshot withResolvedLayout(const ResolvedLayoutSnapshotV3& layout) const;
|
||||||
ResolvedWorldSnapshot withoutResolvedLayoutPassages() const;
|
ResolvedWorldSnapshot withoutResolvedLayoutPassages() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -1064,6 +1584,7 @@ namespace VoxelForgeCore
|
|||||||
bool roundTunnels = false;
|
bool roundTunnels = false;
|
||||||
bool enhancedTunnelWalls = true;
|
bool enhancedTunnelWalls = true;
|
||||||
bool profileDetailBand = false;
|
bool profileDetailBand = false;
|
||||||
|
bool parallelSignEdgeRepair = false;
|
||||||
// World-wide walkability is a validation pass, not an input to the compiled field.
|
// 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.
|
// Keep it enabled for benchmarks/audits; the interactive viewer can defer it.
|
||||||
bool validatePassageRoutes = true;
|
bool validatePassageRoutes = true;
|
||||||
@@ -1263,9 +1784,18 @@ namespace VoxelForgeCore
|
|||||||
// Diagnostic seulement; enregistre les colonnes XY evaluees par cette tuile LOD8.
|
// Diagnostic seulement; enregistre les colonnes XY evaluees par cette tuile LOD8.
|
||||||
bool captureColumnCoordinates = false;
|
bool captureColumnCoordinates = false;
|
||||||
bool useSharedColumnCache = true;
|
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.
|
// 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.
|
// Passe froide separee seulement; mesure les etapes internes MC sans changer la sortie.
|
||||||
bool profileMesherStages = false;
|
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 only; fills and checks every lattice sample in blocks certified uniform.
|
||||||
// Diagnostic seulement; evalue et verifie tous les echantillons des blocs uniformes.
|
// Diagnostic seulement; evalue et verifie tous les echantillons des blocs uniformes.
|
||||||
bool auditUniformBlockProofs = false;
|
bool auditUniformBlockProofs = false;
|
||||||
@@ -1282,7 +1812,7 @@ namespace VoxelForgeCore
|
|||||||
// LOD4-8 mesh hashes are salted; density and LOD0-3 keep their direct identities.
|
// LOD4-8 mesh hashes are salted; density and LOD0-3 keep their direct identities.
|
||||||
// Incremente quand les plafonds partages ou l'horizon lointain changent.
|
// 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.
|
// 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.
|
// Linux research controls for reproducing L5 negative controls. Defaults preserve output.
|
||||||
std::uint32_t diagnosticGlobalOctaveDrop = 0;
|
std::uint32_t diagnosticGlobalOctaveDrop = 0;
|
||||||
std::uint32_t diagnosticCellStride = 1;
|
std::uint32_t diagnosticCellStride = 1;
|
||||||
@@ -1336,6 +1866,12 @@ namespace VoxelForgeCore
|
|||||||
ShapeBuildStats buildStats() const;
|
ShapeBuildStats buildStats() const;
|
||||||
|
|
||||||
float densityAt(Vec3d voxelPosition) 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.
|
// Audit-only view of density-field columns and short spans supporting passage voids.
|
||||||
std::vector<PassageVoidCrossingAuditV1> passageVoidCrossings() const;
|
std::vector<PassageVoidCrossingAuditV1> passageVoidCrossings() const;
|
||||||
bool isPassageColumnSample(std::size_t passageIndex, std::size_t segmentIndex,
|
bool isPassageColumnSample(std::size_t passageIndex, std::size_t segmentIndex,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||||
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
||||||
#include "VoxelForgeCore/SharedColumnCache.h"
|
#include "VoxelForgeCore/SharedColumnCache.h"
|
||||||
|
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
|
||||||
|
|
||||||
// Keep Core's expression-level FMA behavior fixed regardless of the toolchain setting.
|
// 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.
|
// Clang's "fast" mode ignores the pragma, so Core's runtime self-test rejects that build.
|
||||||
|
|||||||
@@ -1217,6 +1217,82 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
|||||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill only underside cells that marching cubes missed. Active transitions use the
|
||||||
|
// same displaced face and passage-mouth fade as the signed-distance field.
|
||||||
|
struct FTransitionFaceReplacement
|
||||||
|
{
|
||||||
|
double BaseZVoxels=0.0;
|
||||||
|
double MaximumDisplacementVoxels=0.0;
|
||||||
|
std::vector<uint8> Cells;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline int32 RemoveCoarseTransitionFaceTriangles(FVoxelMeshData& Mesh,
|
||||||
|
const FIntVector& Origin,int32 Step,int32 GridSide,double VoxelSizeCm,
|
||||||
|
const std::vector<FTransitionFaceReplacement>& Replacements,bool bUpward)
|
||||||
|
{
|
||||||
|
if(Step<=0||GridSide<2||VoxelSizeCm<=0.0||Replacements.empty())return 0;
|
||||||
|
const int32 TriangleCount=Mesh.Triangles.Num()/3;
|
||||||
|
const int32 OldCeilingStart=std::max(0,TriangleCount-Mesh.NumCeilingTriangles);
|
||||||
|
TArray<uint8> OldMasks=Mesh.TriangleSkirtFaceMasks;
|
||||||
|
OldMasks.SetNum(TriangleCount);
|
||||||
|
TArray<int32> KeptTriangles;
|
||||||
|
TArray<uint8> KeptMasks;
|
||||||
|
KeptTriangles.Reserve(Mesh.Triangles.Num());
|
||||||
|
KeptMasks.Reserve(TriangleCount);
|
||||||
|
int32 Removed=0,RemovedCeiling=0;
|
||||||
|
const int32 CellSide=GridSide-1;
|
||||||
|
for(int32 Triangle=0;Triangle<TriangleCount;++Triangle)
|
||||||
|
{
|
||||||
|
const int32 IA=Mesh.Triangles[Triangle*3];
|
||||||
|
const int32 IB=Mesh.Triangles[Triangle*3+1];
|
||||||
|
const int32 IC=Mesh.Triangles[Triangle*3+2];
|
||||||
|
bool bRemove=false;
|
||||||
|
if(OldMasks[Triangle]==0&&IA>=0&&IB>=0&&IC>=0
|
||||||
|
&&IA<Mesh.Vertices.Num()&&IB<Mesh.Vertices.Num()&&IC<Mesh.Vertices.Num())
|
||||||
|
{
|
||||||
|
const FVector& A=Mesh.Vertices[IA];
|
||||||
|
const FVector& B=Mesh.Vertices[IB];
|
||||||
|
const FVector& C=Mesh.Vertices[IC];
|
||||||
|
const FVector GeometricNormal=FVector::CrossProduct(B-A,C-A);
|
||||||
|
const double NormalLength=GeometricNormal.Size();
|
||||||
|
const double Facing=NormalLength>1.0e-12?GeometricNormal.Z/NormalLength:0.0;
|
||||||
|
if((bUpward&&Facing>0.35)||(!bUpward&&Facing< -0.35))
|
||||||
|
{
|
||||||
|
const double X=(A.X+B.X+C.X)/(3.0*VoxelSizeCm);
|
||||||
|
const double Y=(A.Y+B.Y+C.Y)/(3.0*VoxelSizeCm);
|
||||||
|
const double Z=(A.Z+B.Z+C.Z)/(3.0*VoxelSizeCm);
|
||||||
|
const int32 CellX=static_cast<int32>(std::floor((X-Origin.X)/Step));
|
||||||
|
const int32 CellY=static_cast<int32>(std::floor((Y-Origin.Y)/Step));
|
||||||
|
if(CellX>=0&&CellX<CellSide&&CellY>=0&&CellY<CellSide)
|
||||||
|
{
|
||||||
|
const std::size_t Cell=static_cast<std::size_t>(CellY)*CellSide+CellX;
|
||||||
|
for(const auto& Replacement:Replacements)
|
||||||
|
{
|
||||||
|
if(Cell>=Replacement.Cells.size()||!Replacement.Cells[Cell])continue;
|
||||||
|
const double Margin=Replacement.MaximumDisplacementVoxels+4.0;
|
||||||
|
if(Z>=Replacement.BaseZVoxels-Margin
|
||||||
|
&&Z<=Replacement.BaseZVoxels+Margin)
|
||||||
|
{bRemove=true;break;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(bRemove)
|
||||||
|
{
|
||||||
|
++Removed;
|
||||||
|
if(Triangle>=OldCeilingStart)++RemovedCeiling;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
KeptTriangles.Add(IA);KeptTriangles.Add(IB);KeptTriangles.Add(IC);
|
||||||
|
KeptMasks.Add(OldMasks[Triangle]);
|
||||||
|
}
|
||||||
|
if(Removed==0)return 0;
|
||||||
|
Mesh.Triangles=std::move(KeptTriangles);
|
||||||
|
Mesh.TriangleSkirtFaceMasks=std::move(KeptMasks);
|
||||||
|
Mesh.NumCeilingTriangles=std::max(0,Mesh.NumCeilingTriangles-RemovedCeiling);
|
||||||
|
return Removed;
|
||||||
|
}
|
||||||
|
|
||||||
inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
||||||
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||||
{
|
{
|
||||||
@@ -1230,9 +1306,14 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
{
|
{
|
||||||
double FaceZ=0.0;
|
double FaceZ=0.0;
|
||||||
double Thickness=0.0;
|
double Thickness=0.0;
|
||||||
|
double MaximumDisplacementVoxels=0.0;
|
||||||
|
std::vector<double> FaceHeights;
|
||||||
std::vector<uint8> SolidAbove;
|
std::vector<uint8> SolidAbove;
|
||||||
|
const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr;
|
||||||
|
bool bTransition=false;
|
||||||
};
|
};
|
||||||
std::vector<FCandidate> Candidates;
|
std::vector<FCandidate> Candidates;
|
||||||
|
std::vector<FTransitionFaceReplacement> Replacements;
|
||||||
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
||||||
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
||||||
{
|
{
|
||||||
@@ -1243,19 +1324,34 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
Candidate.FaceZ=static_cast<double>(Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
Candidate.FaceZ=static_cast<double>(Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
||||||
Candidate.Thickness=static_cast<double>(Stratum->ceiling.topMicrometres
|
Candidate.Thickness=static_cast<double>(Stratum->ceiling.topMicrometres
|
||||||
-Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
-Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
||||||
if(Candidate.FaceZ<Origin.Z-1.0e-6||Candidate.FaceZ>Origin.Z+Extent+1.0e-6)continue;
|
Candidate.Stratum=Stratum;
|
||||||
|
const auto* transitionUpper=Shape.FindResolvedLayoutStratumByOrder(Stratum->order-1);
|
||||||
|
const bool bTransition=transitionUpper
|
||||||
|
&&Shape.IsResolvedLayoutTransitionActive(*transitionUpper);
|
||||||
|
const double displacement=bTransition?static_cast<double>(
|
||||||
|
Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(*transitionUpper,*Stratum))
|
||||||
|
/CoreMicrons:0.0;
|
||||||
|
if(Candidate.FaceZ<Origin.Z-displacement-1.0e-6
|
||||||
|
||Candidate.FaceZ>Origin.Z+Extent+displacement+1.0e-6)continue;
|
||||||
|
Candidate.bTransition=bTransition;
|
||||||
|
Candidate.MaximumDisplacementVoxels=displacement;
|
||||||
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
||||||
if(Epsilon<=0.0)continue;
|
if(Epsilon<=0.0)continue;
|
||||||
Candidate.SolidAbove.resize(static_cast<std::size_t>(GridSide)*GridSide,0);
|
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||||
|
Candidate.FaceHeights.resize(GridCount,Candidate.FaceZ);
|
||||||
|
Candidate.SolidAbove.resize(GridCount,0);
|
||||||
bool AnySolidAbove=false;
|
bool AnySolidAbove=false;
|
||||||
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
||||||
{
|
{
|
||||||
const double WorldX=Origin.X+X*GridStep;
|
const double WorldX=Origin.X+X*GridStep;
|
||||||
const double WorldY=Origin.Y+Y*GridStep;
|
const double WorldY=Origin.Y+Y*GridStep;
|
||||||
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
|
Candidate.bTransition=Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,
|
||||||
|
WorldX,WorldY,Candidate.FaceHeights[GridIndex])||Candidate.bTransition;
|
||||||
const float Above=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
const float Above=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
||||||
static_cast<float>(Candidate.FaceZ+Epsilon));
|
static_cast<float>(Candidate.FaceHeights[GridIndex]+Epsilon));
|
||||||
const bool bSolidAbove=std::isfinite(Above)&&Above<0.0f;
|
const bool bSolidAbove=std::isfinite(Above)&&Above<0.0f;
|
||||||
Candidate.SolidAbove[static_cast<std::size_t>(Y*GridSide+X)]=bSolidAbove?1:0;
|
Candidate.SolidAbove[GridIndex]=bSolidAbove?1:0;
|
||||||
AnySolidAbove=AnySolidAbove||bSolidAbove;
|
AnySolidAbove=AnySolidAbove||bSolidAbove;
|
||||||
}
|
}
|
||||||
if(AnySolidAbove)Candidates.push_back(std::move(Candidate));
|
if(AnySolidAbove)Candidates.push_back(std::move(Candidate));
|
||||||
@@ -1277,8 +1373,19 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
std::vector<int32> NewTriangles;
|
std::vector<int32> NewTriangles;
|
||||||
for(const FCandidate& Candidate:Candidates)
|
for(const FCandidate& Candidate:Candidates)
|
||||||
{
|
{
|
||||||
std::vector<int32> VertexMap(static_cast<std::size_t>(GridSide)*GridSide,INDEX_NONE);
|
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||||
const double CeilingZ=Candidate.FaceZ;
|
std::vector<int32> VertexMap(GridCount,INDEX_NONE);
|
||||||
|
FTransitionFaceReplacement Replacement;
|
||||||
|
Replacement.BaseZVoxels=Candidate.FaceZ;
|
||||||
|
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
|
||||||
|
Replacement.Cells.resize(static_cast<std::size_t>(GridSide-1)*(GridSide-1),0);
|
||||||
|
const auto heightAt=[&](double X,double Y)
|
||||||
|
{
|
||||||
|
if(!Candidate.bTransition)return Candidate.FaceZ;
|
||||||
|
double height=Candidate.FaceZ;
|
||||||
|
Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,X,Y,height);
|
||||||
|
return height;
|
||||||
|
};
|
||||||
const auto AddVertex=[&](int32 X,int32 Y)
|
const auto AddVertex=[&](int32 X,int32 Y)
|
||||||
{
|
{
|
||||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
@@ -1286,8 +1393,20 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
if(Mapped!=INDEX_NONE)return Mapped;
|
if(Mapped!=INDEX_NONE)return Mapped;
|
||||||
Mapped=static_cast<int32>(NewVertices.size());
|
Mapped=static_cast<int32>(NewVertices.size());
|
||||||
const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep;
|
const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep;
|
||||||
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,CeilingZ*VoxelSizeCm);
|
const double FaceZ=Candidate.FaceHeights[GridIndex];
|
||||||
NewNormals.emplace_back(0.0,0.0,-1.0);
|
FVector Normal(0.0,0.0,-1.0);
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
{
|
||||||
|
const double Delta=std::max(1.0,std::min<double>(GridStep,4.0));
|
||||||
|
const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY))
|
||||||
|
/(2.0*Delta);
|
||||||
|
const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta))
|
||||||
|
/(2.0*Delta);
|
||||||
|
Normal=FVector(DX,DY,-1.0);
|
||||||
|
Normal.Normalize();
|
||||||
|
}
|
||||||
|
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,FaceZ*VoxelSizeCm);
|
||||||
|
NewNormals.push_back(Normal);
|
||||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
||||||
if(bHasColors)NewColors.push_back(Color);
|
if(bHasColors)NewColors.push_back(Color);
|
||||||
return Mapped;
|
return Mapped;
|
||||||
@@ -1300,15 +1419,40 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
||!Candidate.SolidAbove[I01]||!Candidate.SolidAbove[I11])continue;
|
||!Candidate.SolidAbove[I01]||!Candidate.SolidAbove[I11])continue;
|
||||||
const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep;
|
const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep;
|
||||||
const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep;
|
const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep;
|
||||||
// Suppress a coincident plane only when it covers the full cell. A hit at just the
|
bool fullyCovered=false;
|
||||||
// center of a curved shell triangle is not enough to leave the rest of this cap cell open.
|
if(!Candidate.bTransition)
|
||||||
if(MeshIndex.HasDownwardCellCoverage(CellX0,CellY0,CellX1,CellY1,CeilingZ,0.001))continue;
|
fullyCovered=MeshIndex.HasDownwardCellCoverage(
|
||||||
|
CellX0,CellY0,CellX1,CellY1,Candidate.FaceZ,0.001);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fullyCovered=true;
|
||||||
|
constexpr double CoverageSamples[5][2]={
|
||||||
|
{0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}};
|
||||||
|
for(const auto& Sample:CoverageSamples)
|
||||||
|
{
|
||||||
|
const double U=Sample[0],V=Sample[1];
|
||||||
|
const double SampleX=CellX0+(CellX1-CellX0)*U;
|
||||||
|
const double SampleY=CellY0+(CellY1-CellY0)*V;
|
||||||
|
const double SampleZ=heightAt(SampleX,SampleY);
|
||||||
|
if(!MeshIndex.HasDownwardHit(SampleX,SampleY,SampleZ,0.25))
|
||||||
|
fullyCovered=false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(fullyCovered)continue;
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition)
|
||||||
|
Replacement.Cells[static_cast<std::size_t>(Y)*(GridSide-1)+X]=1;
|
||||||
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
||||||
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
||||||
NewTriangles.insert(NewTriangles.end(),{A,C,B,A,D,C});
|
NewTriangles.insert(NewTriangles.end(),{A,C,B,A,D,C});
|
||||||
}
|
}
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition
|
||||||
|
&&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(),
|
||||||
|
[](uint8 Value){return Value!=0;}))
|
||||||
|
Replacements.push_back(std::move(Replacement));
|
||||||
}
|
}
|
||||||
if(NewTriangles.empty())return;
|
if(NewTriangles.empty())return;
|
||||||
|
const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,GridStep,
|
||||||
|
GridSide,VoxelSizeCm,Replacements,false);
|
||||||
|
|
||||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
||||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||||
@@ -1347,9 +1491,8 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
Mesh.NumCeilingTriangles+=static_cast<int32>(NewTriangles.size()/3);
|
Mesh.NumCeilingTriangles+=static_cast<int32>(NewTriangles.size()/3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the interior-facing top of each bedrock band. The density composition already
|
// Fill only top cells that marching cubes missed. Active transitions use the same
|
||||||
// makes this band solid and editable; this planar closure keeps its visible floor
|
// displaced face and passage-mouth fade as the signed-distance field.
|
||||||
// continuous when a coarse marching-cubes step would otherwise skip a sub-voxel band.
|
|
||||||
inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
||||||
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||||
{
|
{
|
||||||
@@ -1358,8 +1501,16 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
const int32 Extent=Key.ExtentVoxels();
|
const int32 Extent=Key.ExtentVoxels();
|
||||||
if(Extent<=0||Extent%Step!=0)return;
|
if(Extent<=0||Extent%Step!=0)return;
|
||||||
const int32 GridSide=Extent/Step+1;
|
const int32 GridSide=Extent/Step+1;
|
||||||
struct FCandidate { double FloorZ=0.0; double Thickness=0.0; };
|
struct FCandidate
|
||||||
|
{
|
||||||
|
double FloorZ=0.0;
|
||||||
|
double Thickness=0.0;
|
||||||
|
double MaximumDisplacementVoxels=0.0;
|
||||||
|
const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr;
|
||||||
|
bool bTransition=false;
|
||||||
|
};
|
||||||
std::vector<FCandidate> Candidates;
|
std::vector<FCandidate> Candidates;
|
||||||
|
std::vector<FTransitionFaceReplacement> Replacements;
|
||||||
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
||||||
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
||||||
{
|
{
|
||||||
@@ -1367,9 +1518,17 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
if(!Stratum||!Stratum->bedrock.presentInCurrentField
|
if(!Stratum||!Stratum->bedrock.presentInCurrentField
|
||||||
||Stratum->bedrock.topMicrometres<=Stratum->bedrock.bottomMicrometres)continue;
|
||Stratum->bedrock.topMicrometres<=Stratum->bedrock.bottomMicrometres)continue;
|
||||||
const double FloorZ=static_cast<double>(Stratum->bedrock.topMicrometres)/CoreMicrons;
|
const double FloorZ=static_cast<double>(Stratum->bedrock.topMicrometres)/CoreMicrons;
|
||||||
if(FloorZ<Origin.Z-1.0e-6||FloorZ>Origin.Z+Extent+1.0e-6)continue;
|
const auto* transitionLower=Shape.FindResolvedLayoutStratumByOrder(Stratum->order+1);
|
||||||
|
const bool bTransition=transitionLower
|
||||||
|
&&Shape.IsResolvedLayoutTransitionActive(*Stratum);
|
||||||
|
const double displacement=bTransition
|
||||||
|
?static_cast<double>(Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
||||||
|
*Stratum,*transitionLower))/CoreMicrons
|
||||||
|
:0.0;
|
||||||
|
if(FloorZ<Origin.Z-displacement-1.0e-6
|
||||||
|
||FloorZ>Origin.Z+Extent+displacement+1.0e-6)continue;
|
||||||
Candidates.push_back({FloorZ,static_cast<double>(Stratum->bedrock.topMicrometres
|
Candidates.push_back({FloorZ,static_cast<double>(Stratum->bedrock.topMicrometres
|
||||||
-Stratum->bedrock.bottomMicrometres)/CoreMicrons});
|
-Stratum->bedrock.bottomMicrometres)/CoreMicrons,displacement,Stratum,bTransition});
|
||||||
}
|
}
|
||||||
if(Candidates.empty())return;
|
if(Candidates.empty())return;
|
||||||
|
|
||||||
@@ -1390,16 +1549,34 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
{
|
{
|
||||||
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
||||||
if(Epsilon<=0.0)continue;
|
if(Epsilon<=0.0)continue;
|
||||||
std::vector<uint8> SolidBelow(static_cast<std::size_t>(GridSide)*GridSide,0);
|
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||||
|
std::vector<double> faceHeights(GridCount,Candidate.FloorZ);
|
||||||
|
std::vector<uint8> solidBelow(GridCount,0);
|
||||||
|
FTransitionFaceReplacement Replacement;
|
||||||
|
Replacement.BaseZVoxels=Candidate.FloorZ;
|
||||||
|
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
|
||||||
|
Replacement.Cells.resize(static_cast<std::size_t>(GridSide-1)*(GridSide-1),0);
|
||||||
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
||||||
{
|
{
|
||||||
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
||||||
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
Shape.ResolvedBedrockTopAt(*Candidate.Stratum,WorldX,WorldY,
|
||||||
|
faceHeights[GridIndex]);
|
||||||
const float Density=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
const float Density=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
||||||
static_cast<float>(Candidate.FloorZ-Epsilon));
|
static_cast<float>(faceHeights[GridIndex]-Epsilon));
|
||||||
SolidBelow[static_cast<std::size_t>(Y*GridSide+X)]=
|
solidBelow[GridIndex]=
|
||||||
std::isfinite(Density)&&Density<0.0f?1:0;
|
std::isfinite(Density)&&Density<0.0f?1:0;
|
||||||
}
|
}
|
||||||
std::vector<int32> VertexMap(static_cast<std::size_t>(GridSide)*GridSide,INDEX_NONE);
|
const auto heightAt=[&](double X,double Y)
|
||||||
|
{
|
||||||
|
if(!Candidate.bTransition)return Candidate.FloorZ;
|
||||||
|
double height=Candidate.FloorZ;
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
Shape.ResolvedBedrockTopAt(*Candidate.Stratum,X,Y,height);
|
||||||
|
return height;
|
||||||
|
};
|
||||||
|
std::vector<int32> VertexMap(GridCount,INDEX_NONE);
|
||||||
const auto AddVertex=[&](int32 X,int32 Y)
|
const auto AddVertex=[&](int32 X,int32 Y)
|
||||||
{
|
{
|
||||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
@@ -1407,9 +1584,20 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
if(Mapped!=INDEX_NONE)return Mapped;
|
if(Mapped!=INDEX_NONE)return Mapped;
|
||||||
Mapped=static_cast<int32>(NewVertices.size());
|
Mapped=static_cast<int32>(NewVertices.size());
|
||||||
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
||||||
|
FVector Normal(0.0,0.0,1.0);
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
{
|
||||||
|
const double Delta=std::max(1.0,std::min<double>(Step,4.0));
|
||||||
|
const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY))
|
||||||
|
/(2.0*Delta);
|
||||||
|
const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta))
|
||||||
|
/(2.0*Delta);
|
||||||
|
Normal=FVector(-DX,-DY,1.0);
|
||||||
|
Normal.Normalize();
|
||||||
|
}
|
||||||
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,
|
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,
|
||||||
Candidate.FloorZ*VoxelSizeCm);
|
faceHeights[GridIndex]*VoxelSizeCm);
|
||||||
NewNormals.emplace_back(0.0,0.0,1.0);
|
NewNormals.push_back(Normal);
|
||||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
||||||
if(bHasColors)NewColors.push_back(Color);
|
if(bHasColors)NewColors.push_back(Color);
|
||||||
return Mapped;
|
return Mapped;
|
||||||
@@ -1418,16 +1606,43 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
{
|
{
|
||||||
const std::size_t I00=static_cast<std::size_t>(Y*GridSide+X);
|
const std::size_t I00=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1;
|
const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1;
|
||||||
if(!SolidBelow[I00]||!SolidBelow[I10]||!SolidBelow[I01]||!SolidBelow[I11])continue;
|
if(!solidBelow[I00]||!solidBelow[I10]||!solidBelow[I01]||!solidBelow[I11])continue;
|
||||||
const double X0=Origin.X+X*Step,Y0=Origin.Y+Y*Step;
|
const double X0=Origin.X+X*Step,Y0=Origin.Y+Y*Step;
|
||||||
const double X1=X0+Step,Y1=Y0+Step;
|
const double X1=X0+Step,Y1=Y0+Step;
|
||||||
if(MeshIndex.HasUpwardCellCoverage(X0,Y0,X1,Y1,Candidate.FloorZ,0.001))continue;
|
bool fullyCovered=false;
|
||||||
|
if(!Candidate.bTransition)
|
||||||
|
fullyCovered=MeshIndex.HasUpwardCellCoverage(
|
||||||
|
X0,Y0,X1,Y1,Candidate.FloorZ,0.001);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fullyCovered=true;
|
||||||
|
constexpr double CoverageSamples[5][2]={
|
||||||
|
{0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}};
|
||||||
|
for(const auto& Sample:CoverageSamples)
|
||||||
|
{
|
||||||
|
const double U=Sample[0],V=Sample[1];
|
||||||
|
const double SampleX=X0+(X1-X0)*U;
|
||||||
|
const double SampleY=Y0+(Y1-Y0)*V;
|
||||||
|
const double SampleZ=heightAt(SampleX,SampleY);
|
||||||
|
if(!MeshIndex.HasUpwardHit(SampleX,SampleY,SampleZ,0.25))
|
||||||
|
fullyCovered=false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(fullyCovered)continue;
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition)
|
||||||
|
Replacement.Cells[static_cast<std::size_t>(Y)*(GridSide-1)+X]=1;
|
||||||
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
||||||
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
||||||
NewTriangles.insert(NewTriangles.end(),{A,B,C,A,C,D});
|
NewTriangles.insert(NewTriangles.end(),{A,B,C,A,C,D});
|
||||||
}
|
}
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition
|
||||||
|
&&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(),
|
||||||
|
[](uint8 Value){return Value!=0;}))
|
||||||
|
Replacements.push_back(std::move(Replacement));
|
||||||
}
|
}
|
||||||
if(NewTriangles.empty())return;
|
if(NewTriangles.empty())return;
|
||||||
|
const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,Step,
|
||||||
|
GridSide,VoxelSizeCm,Replacements,true);
|
||||||
|
|
||||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
||||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
||||||
|
#include "VoxelForgeCore/CpuCycleCounter.h"
|
||||||
|
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
|
||||||
|
#include "VoxelForgeCore/SharedColumnCache.h"
|
||||||
#include "Json.h"
|
#include "Json.h"
|
||||||
#include "VoxelForgeCore/VoxelForgeCore.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.boundarySealSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeHalfWidthSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeHalfWidthSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeThicknessSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeThicknessSubvoxels), 8);
|
||||||
|
if(recipe.terrainFollowingRoute)HashWord(hash,1,1);
|
||||||
HashWord(hash, HashRouteConstraints(recipe.routeConstraints), 8);
|
HashWord(hash, HashRouteConstraints(recipe.routeConstraints), 8);
|
||||||
return hash;
|
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;
|
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 =
|
using FSharedIslandColumnCache =
|
||||||
VoxelForgeCoreShared::TSharedColumnValueCache<FIslandColumnCacheEntry,16384,4>;
|
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(
|
inline const FIslandColumnCacheEntry& IslandColumnCache(
|
||||||
const FloatingIslandRecipeV1& recipe,const FloatingIslandPlanV1& plan,
|
const FloatingIslandRecipeV1& recipe,const FloatingIslandPlanV1& plan,
|
||||||
const FloatingIslandAnchorV1& island,double x,double y)
|
const FloatingIslandAnchorV1& island,double x,double y)
|
||||||
{
|
{
|
||||||
|
FScopedIslandAttributionTimer columnTimer(EIslandAttributionBucket::Column);
|
||||||
// Density samples arrive on integer Core lattice columns; reuse the exact column metrics
|
// 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.
|
// 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.
|
// 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<
|
static thread_local VoxelForgeCoreThreadLocal::FLazy<
|
||||||
std::array<FIslandColumnCacheEntry,SetCount*Ways>> Cache;
|
std::array<FIslandColumnCacheEntry,SetCount*Ways>> Cache;
|
||||||
auto& cache=Cache.Get();
|
auto& cache=Cache.Get();
|
||||||
|
FScopedIslandAttributionTimer localProbeTimer(EIslandAttributionBucket::LocalProbe);
|
||||||
const std::int64_t SampleX=static_cast<std::int64_t>(x);
|
const std::int64_t SampleX=static_cast<std::int64_t>(x);
|
||||||
const std::int64_t SampleY=static_cast<std::int64_t>(y);
|
const std::int64_t SampleY=static_cast<std::int64_t>(y);
|
||||||
std::uint64_t IndexKey=island.stableId
|
std::uint64_t IndexKey=island.stableId
|
||||||
@@ -500,6 +561,7 @@ namespace floating_island_detail
|
|||||||
IndexKey^=IndexKey>>33;
|
IndexKey^=IndexKey>>33;
|
||||||
const std::size_t Base=static_cast<std::size_t>(IndexKey&(SetCount-1))*Ways;
|
const std::size_t Base=static_cast<std::size_t>(IndexKey&(SetCount-1))*Ways;
|
||||||
FIslandColumnCacheEntry* Replacement=&cache[Base+((IndexKey>>11)&(Ways-1))];
|
FIslandColumnCacheEntry* Replacement=&cache[Base+((IndexKey>>11)&(Ways-1))];
|
||||||
|
bool bFoundInvalid=false;
|
||||||
for(std::size_t Way=0;Way<Ways;++Way)
|
for(std::size_t Way=0;Way<Ways;++Way)
|
||||||
{
|
{
|
||||||
FIslandColumnCacheEntry& Entry=cache[Base+Way];
|
FIslandColumnCacheEntry& Entry=cache[Base+Way];
|
||||||
@@ -515,8 +577,64 @@ namespace floating_island_detail
|
|||||||
&&Entry.TopHalf==island.topHalfSubvoxels
|
&&Entry.TopHalf==island.topHalfSubvoxels
|
||||||
&&Entry.TopFlattenQ16==recipe.topFlattenQ16
|
&&Entry.TopFlattenQ16==recipe.topFlattenQ16
|
||||||
&&Entry.ContourWarp==recipe.contourWarpSubvoxels;
|
&&Entry.ContourWarp==recipe.contourWarpSubvoxels;
|
||||||
if(bKeyMatches)return Entry;
|
if(bKeyMatches)
|
||||||
if(!Entry.bValid)Replacement=&Entry;
|
{
|
||||||
|
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=[&]()
|
const auto build=[&]()
|
||||||
@@ -543,9 +661,14 @@ namespace floating_island_detail
|
|||||||
Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY);
|
Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY);
|
||||||
Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX);
|
Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX);
|
||||||
Entry.LobePhase=LobePhase(recipe,island);
|
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)
|
const double EffectiveRadius=std::max(1.0,static_cast<double>(island.radiusSubvoxels)
|
||||||
+LobeOffsetWithPhase(recipe,Entry.Angle,static_cast<double>(island.topZSubvoxels),
|
+contourOffset);
|
||||||
Entry.LobePhase));
|
|
||||||
const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0);
|
const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0);
|
||||||
const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
||||||
*static_cast<double>(island.topHalfSubvoxels)*2.0*Edge*Edge;
|
*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)),
|
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
|
||||||
plan.contentHash,SampleX,SampleY,island.stableId};
|
plan.contentHash,SampleX,SampleY,island.stableId};
|
||||||
bool computed=false;
|
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();
|
else Entry=build();
|
||||||
*Replacement=Entry;
|
*Replacement=Entry;
|
||||||
@@ -624,6 +752,7 @@ namespace floating_island_detail
|
|||||||
const FloatingIslandAnchorV1& island,
|
const FloatingIslandAnchorV1& island,
|
||||||
std::int32_t rootIndex, double x, double y, double z)
|
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
|
// Cache derived root constants per worker. The complete immutable input key keeps this
|
||||||
// value-only cache safe across worlds, epochs and worker schedules.
|
// 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.
|
// 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;
|
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,
|
inline double ApplySurfaceDetail(const FloatingIslandRecipeV1& recipe,
|
||||||
std::uint64_t featureId, double structural,
|
std::uint64_t featureId, double structural,
|
||||||
double x, double y, double z, double detailScale = 1.0)
|
double x, double y, double z, double detailScale = 1.0)
|
||||||
@@ -712,6 +873,7 @@ namespace floating_island_detail
|
|||||||
const FloatingIslandAnchorV1& island,
|
const FloatingIslandAnchorV1& island,
|
||||||
double x, double y, double z)
|
double x, double y, double z)
|
||||||
{
|
{
|
||||||
|
FScopedIslandAttributionTimer islandTimer(EIslandAttributionBucket::Island);
|
||||||
if (recipe.geometryVersion < 2)
|
if (recipe.geometryVersion < 2)
|
||||||
{
|
{
|
||||||
const double dx = x - static_cast<double>(island.centerSubvoxels[0]);
|
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 radial=Column.Radial;
|
||||||
const double topZ = static_cast<double>(island.topZSubvoxels);
|
const double topZ = static_cast<double>(island.topZSubvoxels);
|
||||||
const double radius = static_cast<double>(island.radiusSubvoxels);
|
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,
|
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 edge = std::clamp(radial / std::max(effectiveRadius, 1.0), 0.0, 1.0);
|
||||||
const double lowerSurface = static_cast<double>(IslandCrustBottom(recipe, island))
|
const double lowerSurface = static_cast<double>(IslandCrustBottom(recipe, island))
|
||||||
+ radius * 0.16 * edge * edge;
|
+ radius * 0.16 * edge * edge;
|
||||||
@@ -787,6 +954,7 @@ namespace floating_island_detail
|
|||||||
bool* topSurface = nullptr,
|
bool* topSurface = nullptr,
|
||||||
double* alongFraction = nullptr)
|
double* alongFraction = nullptr)
|
||||||
{
|
{
|
||||||
|
FScopedIslandAttributionTimer connectorTimer(EIslandAttributionBucket::Connector);
|
||||||
const double ax = static_cast<double>(edge.startSubvoxels[0]);
|
const double ax = static_cast<double>(edge.startSubvoxels[0]);
|
||||||
const double ay = static_cast<double>(edge.startSubvoxels[1]);
|
const double ay = static_cast<double>(edge.startSubvoxels[1]);
|
||||||
const double az = static_cast<double>(edge.startSubvoxels[2]);
|
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);
|
const double t = std::clamp(along / length, 0.0, 1.0);
|
||||||
if (alongFraction) *alongFraction = t;
|
if (alongFraction) *alongFraction = t;
|
||||||
const double deck = az + (static_cast<double>(edge.endSubvoxels[2]) - az) * 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
|
const double arch = recipe.geometryVersion >= 2
|
||||||
|
&& WorldAssemblyEvaluatorVersion < 172
|
||||||
? 2.0 * static_cast<double>(edge.thicknessSubvoxels) * (4.0 * t * (1.0 - t))
|
? 2.0 * static_cast<double>(edge.thicknessSubvoxels) * (4.0 * t * (1.0 - t))
|
||||||
: 0.0;
|
: 0.0;
|
||||||
const double upper = z - deck;
|
const double upper = z - deck;
|
||||||
@@ -814,6 +1012,227 @@ namespace floating_island_detail
|
|||||||
return std::max({horizontal, upper, lower});
|
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,
|
inline std::int64_t FieldDensity(const FloatingIslandRecipeV1& recipe,
|
||||||
const FloatingIslandPlanV1& plan,
|
const FloatingIslandPlanV1& plan,
|
||||||
std::int64_t x, std::int64_t y, std::int64_t z)
|
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
|
// 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.
|
// supported across the bridge-to-island transition, including bounded surface roughness.
|
||||||
// Les connecteurs recouvrent le plateau pour soutenir toute la capsule au raccord.
|
// 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.
|
// Keep the capsule footprint supported while seating L37 connectors just below the native island tops.
|
||||||
// Garder la passerelle etroite visible dans le vide ; une fraction plus faible l'enfouit dans les plateaux.
|
// 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 landingFraction = recipe.geometryVersion >= 2 ? 0.52 : 0.62;
|
||||||
const double fromReach = static_cast<double>(from.radiusSubvoxels) * landingFraction;
|
const double fromReach = static_cast<double>(from.radiusSubvoxels) * landingFraction;
|
||||||
const double toReach = static_cast<double>(to.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 sy = from.centerSubvoxels[1] + uy * fromReach;
|
||||||
const double ex = to.centerSubvoxels[0] - ux * toReach;
|
const double ex = to.centerSubvoxels[0] - ux * toReach;
|
||||||
const double ey = to.centerSubvoxels[1] - uy * 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 sz = IslandTopSurface(recipe, from, sx, sy) + routeDeckLift;
|
||||||
const double ez = IslandTopSurface(recipe, to, ex, ey) + routeDeckLift;
|
const double ez = IslandTopSurface(recipe, to, ex, ey) + routeDeckLift;
|
||||||
edge.startSubvoxels = {RoundQ(sx), RoundQ(sy), RoundQ(sz)};
|
edge.startSubvoxels = {RoundQ(sx), RoundQ(sy), RoundQ(sz)};
|
||||||
@@ -1336,6 +1767,7 @@ std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
|||||||
manifest.recipe.anchorMouthOffsetSubvoxels))
|
manifest.recipe.anchorMouthOffsetSubvoxels))
|
||||||
{error="floating-island geometry version 7 is missing a passage-rooted anchor field";return std::nullopt;}
|
{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)
|
if (!floating_island_detail::ReadU32(root.Get("schema_version"), schema)
|
||||||
|| !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash)
|
|| !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash)
|
||||||
|| !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion)
|
|| !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_island_minimum_radius_q\":" << recipe.anchorIslandMinimumRadiusSubvoxels
|
||||||
<< ",\"anchor_root_bottom_z_q\":" << recipe.anchorRootBottomZSubvoxels
|
<< ",\"anchor_root_bottom_z_q\":" << recipe.anchorRootBottomZSubvoxels
|
||||||
<< ",\"anchor_mouth_offset_q\":" << recipe.anchorMouthOffsetSubvoxels;
|
<< ",\"anchor_mouth_offset_q\":" << recipe.anchorMouthOffsetSubvoxels;
|
||||||
|
if(recipe.terrainFollowingRoute)out << ",\"terrain_following_route\":true";
|
||||||
out << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels
|
out << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels
|
||||||
<< ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels
|
<< ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels
|
||||||
<< ",\"roughness_q\":" << recipe.roughnessSubvoxels
|
<< ",\"roughness_q\":" << recipe.roughnessSubvoxels
|
||||||
@@ -1746,6 +2179,26 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|||||||
const std::int64_t y = floating_island_detail::RoundQ(
|
const std::int64_t y = floating_island_detail::RoundQ(
|
||||||
static_cast<double>(a.centerSubvoxels[1]) + dy * t);
|
static_cast<double>(a.centerSubvoxels[1]) + dy * t);
|
||||||
std::int64_t routeX=x,routeY=y;
|
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)
|
if(recipe.geometryVersion>=5&&segment+2==count&&piece==pieces)
|
||||||
{
|
{
|
||||||
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
||||||
@@ -1762,8 +2215,11 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|||||||
routeX=floating_island_detail::RoundQ(mouthX);
|
routeX=floating_island_detail::RoundQ(mouthX);
|
||||||
routeY=floating_island_detail::RoundQ(mouthY);
|
routeY=floating_island_detail::RoundQ(mouthY);
|
||||||
}
|
}
|
||||||
const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
const auto surface = WorldAssemblyEvaluatorVersion>=199
|
||||||
recipe, manifest.plan, routeX, routeY);
|
&&recipe.terrainFollowingRoute
|
||||||
|
?floating_island_detail::FindSurfaceZ(recipe,manifest.plan,routeX,routeY)
|
||||||
|
:floating_island_detail::FindCapsuleFootingTopZ(
|
||||||
|
recipe,manifest.plan,routeX,routeY);
|
||||||
if (!surface)
|
if (!surface)
|
||||||
{ error = "island planner could not find a full capsule-width support surface along its connector chain"; return std::nullopt; }
|
{ error = "island planner could not find a full capsule-width support surface along its connector chain"; return std::nullopt; }
|
||||||
WorldAssemblyRoutePointV1 point;
|
WorldAssemblyRoutePointV1 point;
|
||||||
@@ -1830,14 +2286,19 @@ bool FloatingIslandFieldV1::isValid() const noexcept
|
|||||||
std::int64_t FloatingIslandFieldV1::densitySubvoxels(std::int64_t x,
|
std::int64_t FloatingIslandFieldV1::densitySubvoxels(std::int64_t x,
|
||||||
std::int64_t y, std::int64_t z) const noexcept
|
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.
|
// 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.
|
// 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))
|
if (const auto* list = ScopedWorldAssemblyFeatureQueriesV1::find(recipe_, plan_, x, y, z))
|
||||||
return densitySubvoxelsForTile(x, y, z, *list);
|
density=densitySubvoxelsForTile(x, y, z, *list);
|
||||||
if (!isValid() || !floating_island_detail::ValidScalar(x)
|
else if (!isValid() || !floating_island_detail::ValidScalar(x)
|
||||||
|| !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z))
|
|| !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z))
|
||||||
return floating_island_detail::MaxDensity;
|
density=floating_island_detail::MaxDensity;
|
||||||
return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
else density=floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
||||||
|
fieldTimer.Stop();
|
||||||
|
return density;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<WorldAssemblyFeatureBoundsV1> FloatingIslandFieldV1::solidSupportBounds() const
|
std::vector<WorldAssemblyFeatureBoundsV1> FloatingIslandFieldV1::solidSupportBounds() const
|
||||||
@@ -1864,7 +2325,8 @@ FloatingIslandTileFeatureListV1 FloatingIslandFieldV1::buildFeatureTileList(
|
|||||||
result.candidateEvaluationUseful=plan_->islands.size()>4;
|
result.candidateEvaluationUseful=plan_->islands.size()>4;
|
||||||
result.minimumSubvoxels = tileMinimumSubvoxels;
|
result.minimumSubvoxels = tileMinimumSubvoxels;
|
||||||
result.maximumSubvoxels = tileMaximumSubvoxels;
|
result.maximumSubvoxels = tileMaximumSubvoxels;
|
||||||
if (!isValid()) return result;
|
if (!isValid())
|
||||||
|
{ FinalizeWorldAssemblyFeatureTileListDensityContextV1(result); return result; }
|
||||||
const auto features = solidSupportBounds();
|
const auto features = solidSupportBounds();
|
||||||
|
|
||||||
result.featureIndices = BuildWorldAssemblyFeatureTileListV1(features,
|
result.featureIndices = BuildWorldAssemblyFeatureTileListV1(features,
|
||||||
@@ -1886,7 +2348,8 @@ FloatingIslandTileFeatureListV1 FloatingIslandFieldV1::buildFeatureTileList(
|
|||||||
{
|
{
|
||||||
const auto source = findSourceIsland(edge);
|
const auto source = findSourceIsland(edge);
|
||||||
if (source == plan_->islands.end())
|
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())];
|
auto& group = result.groupBounds[static_cast<std::size_t>(source - plan_->islands.begin())];
|
||||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
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;
|
const std::size_t edgeIndex = static_cast<std::size_t>(featureIndex) - islandCount;
|
||||||
if (edgeIndex >= plan_->clusterEdges.size())
|
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]);
|
const auto source = findSourceIsland(plan_->clusterEdges[edgeIndex]);
|
||||||
if (source == plan_->islands.end())
|
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;
|
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)
|
if (plan_->clusterEdges[edgeIndex].fromIslandId == plan_->islands[islandIndex].stableId)
|
||||||
insertSorted(static_cast<std::uint32_t>(islandCount + edgeIndex));
|
insertSorted(static_cast<std::uint32_t>(islandCount + edgeIndex));
|
||||||
}
|
}
|
||||||
|
FinalizeWorldAssemblyFeatureTileListDensityContextV1(result);
|
||||||
return 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(minX) || !floating_island_detail::ValidScalar(minY)
|
||||||
|| !floating_island_detail::ValidScalar(minZ) || !floating_island_detail::ValidScalar(maxX)
|
|| !floating_island_detail::ValidScalar(minZ) || !floating_island_detail::ValidScalar(maxX)
|
||||||
|| !floating_island_detail::ValidScalar(maxY) || !floating_island_detail::ValidScalar(maxZ)) return result;
|
|| !floating_island_detail::ValidScalar(maxY) || !floating_island_detail::ValidScalar(maxZ)) return result;
|
||||||
const std::array<std::int64_t, 3> minimum{minX, minY, minZ};
|
using namespace floating_island_detail;
|
||||||
const std::array<std::int64_t, 3> maximum{maxX, maxY, maxZ};
|
if (plan_->islands.empty())
|
||||||
for (const auto& island : plan_->islands)
|
return {true,MaxDensity,MaxDensity};
|
||||||
if (floating_island_detail::Intersects(minimum, maximum,
|
|
||||||
island.influenceMinimumSubvoxels, island.influenceMaximumSubvoxels)) return result;
|
const std::array<double,3> minimum{static_cast<double>(minX),
|
||||||
for (const auto& edge : plan_->clusterEdges)
|
static_cast<double>(minY),static_cast<double>(minZ)};
|
||||||
if (floating_island_detail::Intersects(minimum, maximum,
|
const std::array<double,3> maximum{static_cast<double>(maxX),
|
||||||
edge.influenceMinimumSubvoxels, edge.influenceMaximumSubvoxels)) return result;
|
static_cast<double>(maxY),static_cast<double>(maxZ)};
|
||||||
// Root is void. A box outside every lobe/taper/blend/connector influence bound proves air.
|
double fieldMinimum=static_cast<double>(MaxDensity);
|
||||||
// La racine est le vide : une boite hors des influences bornees prouve l'air.
|
double fieldMaximum=static_cast<double>(MaxDensity);
|
||||||
result.known = true;
|
for(const auto& island:plan_->islands)
|
||||||
result.minimumDensitySubvoxels = 0;
|
{
|
||||||
result.maximumDensitySubvoxels = floating_island_detail::MaxDensity;
|
FDensityRange group=IslandDensityRange(*recipe_,island,minimum,maximum);
|
||||||
|
for(const auto& edge:plan_->clusterEdges)
|
||||||
|
{
|
||||||
|
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=static_cast<std::int64_t>(minimumRounded);
|
||||||
|
result.maximumDensitySubvoxels=static_cast<std::int64_t>(maximumRounded);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,12 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <map>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <new>
|
#include <new>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
namespace vfcore::detail
|
namespace vfcore::detail
|
||||||
{
|
{
|
||||||
@@ -40,6 +42,34 @@ namespace VoxelForgeCore
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||||
|
void PrintL30CostCounters(const FStandaloneShapeWorld::FClassificationThreadStats& stats)
|
||||||
|
{
|
||||||
|
if(stats.BoundaryBandCalls==0&&stats.ColumnZOverlapCalls==0)return;
|
||||||
|
std::printf("[L30CostTile] boundary_band_calls=%llu transition_window_checks=%llu "
|
||||||
|
"transition_window_hits=%llu transition_evaluations=%llu passage_fade_calls=%llu "
|
||||||
|
"passage_fade_mouth_tests=%llu column_z_overlap_calls=%llu column_guard_calls=%llu "
|
||||||
|
"column_guard_nonpositive_skips=%llu column_guard_radial_rejects=%llu "
|
||||||
|
"boundary_tile_cull_rejects=%llu boundary_point_cull_rejects=%llu "
|
||||||
|
"transition_passage_tile_skips=%llu "
|
||||||
|
"column_sdf_calls=%llu\n",
|
||||||
|
static_cast<unsigned long long>(stats.BoundaryBandCalls),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionWindowChecks),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionWindowHits),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionEvaluatorCalls),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionPassageFadeCalls),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionPassageMouthTests),
|
||||||
|
static_cast<unsigned long long>(stats.ColumnZOverlapCalls),
|
||||||
|
static_cast<unsigned long long>(stats.ColumnGuardCalls),
|
||||||
|
static_cast<unsigned long long>(stats.ColumnGuardNonpositiveSkips),
|
||||||
|
static_cast<unsigned long long>(stats.ColumnGuardRadialRejects),
|
||||||
|
static_cast<unsigned long long>(stats.BoundaryTileCullRejects),
|
||||||
|
static_cast<unsigned long long>(stats.BoundaryPointCullRejects),
|
||||||
|
static_cast<unsigned long long>(stats.TransitionPassageTileSkips),
|
||||||
|
static_cast<unsigned long long>(stats.ColumnSignedDistanceCalls));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
FIntVector ToNative(Vec3i value) { return {value.x,value.y,value.z}; }
|
FIntVector ToNative(Vec3i value) { return {value.x,value.y,value.z}; }
|
||||||
FVector ToNative(Vec3d value) { return {value.x,value.y,value.z}; }
|
FVector ToNative(Vec3d value) { return {value.x,value.y,value.z}; }
|
||||||
Vec3d FromNative(const FVector& value) { return {value.X,value.Y,value.Z}; }
|
Vec3d FromNative(const FVector& value) { return {value.X,value.Y,value.Z}; }
|
||||||
@@ -188,6 +218,19 @@ namespace VoxelForgeCore
|
|||||||
return hash;
|
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 HashBrushTransaction(const EditTransaction& transaction)
|
||||||
{
|
{
|
||||||
std::uint64_t hash=14695981039346656037ull;
|
std::uint64_t hash=14695981039346656037ull;
|
||||||
@@ -961,6 +1004,15 @@ namespace VoxelForgeCore
|
|||||||
const ResolvedLayoutSnapshotV3* ResolvedWorldSnapshot::resolvedLayout() const noexcept
|
const ResolvedLayoutSnapshotV3* ResolvedWorldSnapshot::resolvedLayout() const noexcept
|
||||||
{ return data_?data_->resolvedLayout.get():nullptr; }
|
{ 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
|
ResolvedWorldSnapshot ResolvedWorldSnapshot::withoutResolvedLayoutPassages() const
|
||||||
{
|
{
|
||||||
if(!data_||!data_->resolvedLayout)return {};
|
if(!data_||!data_->resolvedLayout)return {};
|
||||||
@@ -989,6 +1041,8 @@ namespace VoxelForgeCore
|
|||||||
std::atomic<bool> shapeReady{false};
|
std::atomic<bool> shapeReady{false};
|
||||||
std::unique_ptr<world_assembly_detail::FSharedHeightfieldColumnCache>
|
std::unique_ptr<world_assembly_detail::FSharedHeightfieldColumnCache>
|
||||||
sharedHeightfieldColumns;
|
sharedHeightfieldColumns;
|
||||||
|
std::unique_ptr<world_assembly_detail::FSharedDensitySampleCache>
|
||||||
|
sharedDensitySamples;
|
||||||
std::unique_ptr<floating_island_detail::FSharedIslandColumnCache>
|
std::unique_ptr<floating_island_detail::FSharedIslandColumnCache>
|
||||||
sharedIslandColumns;
|
sharedIslandColumns;
|
||||||
ShapeBuildRequest buildRequest;
|
ShapeBuildRequest buildRequest;
|
||||||
@@ -1018,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>())
|
World::World(ResolvedWorldSnapshot snapshot):impl_(std::make_unique<Impl>())
|
||||||
{
|
{
|
||||||
if(snapshot.data_)
|
if(snapshot.data_)
|
||||||
@@ -1026,6 +1092,8 @@ namespace VoxelForgeCore
|
|||||||
impl_->worldAssembly=snapshot.data_->worldAssembly;
|
impl_->worldAssembly=snapshot.data_->worldAssembly;
|
||||||
impl_->resolvedLayout=snapshot.data_->resolvedLayout;
|
impl_->resolvedLayout=snapshot.data_->resolvedLayout;
|
||||||
impl_->configuration=snapshot.data_->configuration;
|
impl_->configuration=snapshot.data_->configuration;
|
||||||
|
impl_->sharedDensitySamples=
|
||||||
|
std::make_unique<world_assembly_detail::FSharedDensitySampleCache>();
|
||||||
if(impl_->worldAssembly)
|
if(impl_->worldAssembly)
|
||||||
{
|
{
|
||||||
if(!impl_->worldAssembly->plan.strates.empty())
|
if(!impl_->worldAssembly->plan.strates.empty())
|
||||||
@@ -1179,6 +1247,7 @@ namespace VoxelForgeCore
|
|||||||
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
||||||
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
||||||
for(const auto& passage:layout.passages)
|
for(const auto& passage:layout.passages)
|
||||||
|
{
|
||||||
for(std::size_t segment=1;segment<passage.controlPoints.size();++segment)
|
for(std::size_t segment=1;segment<passage.controlPoints.size();++segment)
|
||||||
{
|
{
|
||||||
const auto& a=passage.controlPoints[segment-1];
|
const auto& a=passage.controlPoints[segment-1];
|
||||||
@@ -1200,6 +1269,128 @@ namespace VoxelForgeCore
|
|||||||
overlap&=high[axis]>=tileMinimum[axis]&&low[axis]<=tileMaximum[axis];
|
overlap&=high[axis]>=tileMinimum[axis]&&low[axis]<=tileMaximum[axis];
|
||||||
if(overlap)return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1220,7 +1411,8 @@ namespace VoxelForgeCore
|
|||||||
static_cast<int32>(std::clamp<std::uint32_t>(request.workerLimit,1,16)),ToNative(request.detail),
|
static_cast<int32>(std::clamp<std::uint32_t>(request.workerLimit,1,16)),ToNative(request.detail),
|
||||||
request.includeFarWorld,error,&scheduler,request.roundTunnels,request.profileDetailBand,
|
request.includeFarWorld,error,&scheduler,request.roundTunnels,request.profileDetailBand,
|
||||||
request.validatePassageRoutes,request.gpuVisualDetailTiles,
|
request.validatePassageRoutes,request.gpuVisualDetailTiles,
|
||||||
request.cpuVisualDetailTiles,impl_->worldAssembly.get(),impl_->resolvedLayout.get());
|
request.cpuVisualDetailTiles,impl_->worldAssembly.get(),impl_->resolvedLayout.get(),
|
||||||
|
request.parallelSignEdgeRepair);
|
||||||
if(ok)
|
if(ok)
|
||||||
{
|
{
|
||||||
impl_->shape=std::move(shape);
|
impl_->shape=std::move(shape);
|
||||||
@@ -1247,13 +1439,31 @@ namespace VoxelForgeCore
|
|||||||
const int32 firstSurfaceChunk=FloorDiv(boundsMin.Z,LayoutChunkWidth);
|
const int32 firstSurfaceChunk=FloorDiv(boundsMin.Z,LayoutChunkWidth);
|
||||||
const int32 lastSurfaceChunk=FloorDiv(boundsMax.Z,LayoutChunkWidth);
|
const int32 lastSurfaceChunk=FloorDiv(boundsMax.Z,LayoutChunkWidth);
|
||||||
const WorldAssemblyStratumPlanV1* selectedPlan=nullptr;
|
const WorldAssemblyStratumPlanV1* selectedPlan=nullptr;
|
||||||
|
const ResolvedLayoutStratumV2* selectedGraphStratum=nullptr;
|
||||||
for(int32 chunkZ=firstSurfaceChunk;chunkZ<=lastSurfaceChunk;++chunkZ)
|
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(
|
const int32 strateIndex=impl_->owner->Manager->GetStrateIndex(
|
||||||
static_cast<float>((static_cast<double>(chunkZ)+0.5)
|
static_cast<float>((static_cast<double>(chunkZ)+0.5)
|
||||||
*impl_->resolvedLayout->strateLayoutChunkSizeMicrometres/10000.0));
|
*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(),
|
const auto found=std::find_if(impl_->worldAssembly->plan.strates.begin(),
|
||||||
impl_->worldAssembly->plan.strates.end(),[strateIndex,chunkZ](const auto& plan)
|
impl_->worldAssembly->plan.strates.end(),[strateIndex,chunkZ](const auto& plan)
|
||||||
{return plan.strateIndex==strateIndex&&chunkZ<=plan.topChunkZ
|
{return plan.strateIndex==strateIndex&&chunkZ<=plan.topChunkZ
|
||||||
@@ -1262,7 +1472,9 @@ namespace VoxelForgeCore
|
|||||||
||(selectedPlan&&selectedPlan!=&*found))return false;
|
||(selectedPlan&&selectedPlan!=&*found))return false;
|
||||||
selectedPlan=&*found;
|
selectedPlan=&*found;
|
||||||
}
|
}
|
||||||
if(!selectedPlan||!Heightfield2D(impl_->worldAssembly->recipe,*selectedPlan).isValid())
|
if((!selectedGraphStratum&&(!selectedPlan
|
||||||
|
||!Heightfield2D(impl_->worldAssembly->recipe,*selectedPlan).isValid()))
|
||||||
|
||(selectedGraphStratum&&selectedPlan))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const double coreVoxelsPerOwnerChunk=static_cast<double>(
|
const double coreVoxelsPerOwnerChunk=static_cast<double>(
|
||||||
@@ -1280,10 +1492,17 @@ namespace VoxelForgeCore
|
|||||||
tileMinimum[2]*scale};
|
tileMinimum[2]*scale};
|
||||||
const std::int64_t subMaximum[3]={tileMaximum[0]*scale,tileMaximum[1]*scale,
|
const std::int64_t subMaximum[3]={tileMaximum[0]*scale,tileMaximum[1]*scale,
|
||||||
tileMaximum[2]*scale};
|
tileMaximum[2]*scale};
|
||||||
|
if(selectedPlan)
|
||||||
|
{
|
||||||
for(const WorldAssemblyFeatureV1& feature:selectedPlan->features)
|
for(const WorldAssemblyFeatureV1& feature:selectedPlan->features)
|
||||||
if(IntersectsBox(feature.minimumSubvoxels,feature.maximumSubvoxels,
|
if(IntersectsBox(feature.minimumSubvoxels,feature.maximumSubvoxels,
|
||||||
subMinimum,subMaximum))return false;
|
subMinimum,subMaximum))return false;
|
||||||
if(RouteTouchesBox(*selectedPlan,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,
|
if(ResolvedPassageTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||||
impl_->owner->CoreVoxelSizeMicrons))return false;
|
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||||
if(OwnerPassageTouchesBox(*impl_->owner,boundsMin,boundsMax))return false;
|
if(OwnerPassageTouchesBox(*impl_->owner,boundsMin,boundsMax))return false;
|
||||||
@@ -1542,6 +1761,62 @@ namespace VoxelForgeCore
|
|||||||
|
|
||||||
float World::densityAt(Vec3d position) const
|
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; }
|
{ 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> World::passageVoidCrossings() const
|
||||||
{
|
{
|
||||||
std::vector<PassageVoidCrossingAuditV1> result;
|
std::vector<PassageVoidCrossingAuditV1> result;
|
||||||
@@ -1742,6 +2017,10 @@ namespace VoxelForgeCore
|
|||||||
FScopedSharedColumnCaches sharedColumnCaches(options.useSharedColumnCache
|
FScopedSharedColumnCaches sharedColumnCaches(options.useSharedColumnCache
|
||||||
?impl_->sharedHeightfieldColumns.get():nullptr,
|
?impl_->sharedHeightfieldColumns.get():nullptr,
|
||||||
options.useSharedColumnCache?impl_->sharedIslandColumns.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;
|
const auto& world=*impl_->owner;
|
||||||
FStandaloneShapeWorld nativeOnlyShape;
|
FStandaloneShapeWorld nativeOnlyShape;
|
||||||
FIntVector nativeOnlyMinimum,nativeOnlyMaximum;
|
FIntVector nativeOnlyMinimum,nativeOnlyMaximum;
|
||||||
@@ -1771,7 +2050,12 @@ namespace VoxelForgeCore
|
|||||||
mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors;
|
mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors;
|
||||||
mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups;
|
mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups;
|
||||||
mesher.bStandaloneEnableGradientMemoization=options.memoizeMcGradients;
|
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.bGenerateSkirts=world.bGenerateSkirts;
|
||||||
|
mesher.SetVisibleSkirtFaceMask(options.visibleSkirtFaceMask);
|
||||||
mesher.SkirtCells=static_cast<float>(world.SkirtCells)
|
mesher.SkirtCells=static_cast<float>(world.SkirtCells)
|
||||||
*static_cast<float>(world.OwnerVoxelSizeMicrons)
|
*static_cast<float>(world.OwnerVoxelSizeMicrons)
|
||||||
/static_cast<float>(world.CoreVoxelSizeMicrons);
|
/static_cast<float>(world.CoreVoxelSizeMicrons);
|
||||||
@@ -1878,9 +2162,129 @@ namespace VoxelForgeCore
|
|||||||
MeshCells=FMath::Max(1,cells/Stride);
|
MeshCells=FMath::Max(1,cells/Stride);
|
||||||
MeshStep=FMath::Max(1,extent/MeshCells);
|
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;
|
int64 sampleCount=0;std::vector<float> densityValues;
|
||||||
FVoxelMarchingCubesStageTimings McStageTimings;
|
FVoxelMarchingCubesStageTimings McStageTimings;
|
||||||
TileMesh out;out.key=key;
|
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;
|
out.diagnostics.featureRefinedResolution=bFeatureRefinedResolution;
|
||||||
if(options.profileMesherStages)
|
if(options.profileMesherStages)
|
||||||
{
|
{
|
||||||
@@ -1917,14 +2321,43 @@ namespace VoxelForgeCore
|
|||||||
bHasNativeSurfaceHorizonPlan=bHasNativeSurfaceHorizonPlan
|
bHasNativeSurfaceHorizonPlan=bHasNativeSurfaceHorizonPlan
|
||||||
||(TileMinimumZ<=PlanMaximumZ&&TileMaximumZ>=PlanMinimumZ);
|
||(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)
|
for(const auto& Entry:impl_->worldAssembly->floatingIslands)
|
||||||
{
|
{
|
||||||
const int64 PlanMinimumZ=static_cast<int64>(std::min(Entry.bottomChunkZ,Entry.topChunkZ))
|
const int64 PlanMinimumZ=static_cast<int64>(std::min(Entry.bottomChunkZ,Entry.topChunkZ))
|
||||||
*impl_->resolvedLayout->strateLayoutChunkWidthSubvoxels/WorldAssemblySubvoxelsPerVoxel;
|
*impl_->resolvedLayout->strateLayoutChunkWidthSubvoxels/WorldAssemblySubvoxelsPerVoxel;
|
||||||
const int64 PlanMaximumZ=(static_cast<int64>(std::max(Entry.bottomChunkZ,Entry.topChunkZ)+1))
|
const int64 PlanMaximumZ=(static_cast<int64>(std::max(Entry.bottomChunkZ,Entry.topChunkZ)+1))
|
||||||
*impl_->resolvedLayout->strateLayoutChunkWidthSubvoxels/WorldAssemblySubvoxelsPerVoxel;
|
*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
|
bHasIslandPlanInTile=bHasIslandPlanInTile
|
||||||
||(TileMinimumZ<=PlanMaximumZ&&TileMaximumZ>=PlanMinimumZ);
|
||bIslandFeatureOverlap||bBoundarySealOverlap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const bool bUseNativeSurfaceHorizon=key.lod>=5&&!bOverlapsPlayerEdit
|
const bool bUseNativeSurfaceHorizon=key.lod>=5&&!bOverlapsPlayerEdit
|
||||||
@@ -1995,6 +2428,9 @@ namespace VoxelForgeCore
|
|||||||
out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0,
|
out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0,
|
||||||
Counts.Milliseconds-classificationColumnComputeMilliseconds);
|
Counts.Milliseconds-classificationColumnComputeMilliseconds);
|
||||||
out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds;
|
out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds;
|
||||||
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||||
|
PrintL30CostCounters(Counts);
|
||||||
|
#endif
|
||||||
if(options.captureColumnCoordinates)
|
if(options.captureColumnCoordinates)
|
||||||
{
|
{
|
||||||
out.diagnostics.columnEvaluationWorkerHash=static_cast<std::uint64_t>(
|
out.diagnostics.columnEvaluationWorkerHash=static_cast<std::uint64_t>(
|
||||||
@@ -2002,6 +2438,7 @@ namespace VoxelForgeCore
|
|||||||
out.diagnostics.columnCoordinateCaptureComplete=Columns.coordinateCaptureComplete;
|
out.diagnostics.columnCoordinateCaptureComplete=Columns.coordinateCaptureComplete;
|
||||||
out.diagnostics.evaluatedColumnCoordinates=Columns.evaluatedCoordinates;
|
out.diagnostics.evaluatedColumnCoordinates=Columns.evaluatedCoordinates;
|
||||||
}
|
}
|
||||||
|
CopyIslandCacheMetrics();
|
||||||
out.densitySampleCount=0;
|
out.densitySampleCount=0;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -2094,6 +2531,88 @@ namespace VoxelForgeCore
|
|||||||
RelevantMinimum=std::min(RelevantMinimum,FeatureMinimum);
|
RelevantMinimum=std::min(RelevantMinimum,FeatureMinimum);
|
||||||
RelevantMaximum=std::max(RelevantMaximum,FeatureMaximum);
|
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))
|
if(std::isfinite(RelevantMinimum)&&std::isfinite(RelevantMaximum))
|
||||||
{
|
{
|
||||||
const int32 NativeBandMinimum=FMath::FloorToInt(RelevantMinimum)-step;
|
const int32 NativeBandMinimum=FMath::FloorToInt(RelevantMinimum)-step;
|
||||||
@@ -2161,12 +2680,19 @@ namespace VoxelForgeCore
|
|||||||
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
||||||
const auto MarchingCubesStart=std::chrono::steady_clock::now();
|
const auto MarchingCubesStart=std::chrono::steady_clock::now();
|
||||||
FVoxelUniformBlockProofAuditStats BlockProofAudit;
|
FVoxelUniformBlockProofAuditStats BlockProofAudit;
|
||||||
|
uint64 SkippedSkirtTriangleSeeds=0;
|
||||||
FVoxelMeshData mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr,
|
FVoxelMeshData mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr,
|
||||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr,
|
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr,
|
||||||
options.profileMesherStages?&McStageTimings:nullptr,
|
options.profileMesherStages?&McStageTimings:nullptr,
|
||||||
options.auditUniformBlockProofs?&BlockProofAudit: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)
|
if(options.auditUniformBlockProofs)
|
||||||
{
|
{
|
||||||
out.diagnostics.uniformBlockProofsChecked=BlockProofAudit.CheckedBlocks;
|
out.diagnostics.uniformBlockProofsChecked=BlockProofAudit.CheckedBlocks;
|
||||||
@@ -2211,9 +2737,29 @@ namespace VoxelForgeCore
|
|||||||
out.diagnostics.marchingCubesOtherMilliseconds=McStageTimings.OtherMilliseconds;
|
out.diagnostics.marchingCubesOtherMilliseconds=McStageTimings.OtherMilliseconds;
|
||||||
out.diagnostics.marchingCubesGradientFallbackDensityCalls=
|
out.diagnostics.marchingCubesGradientFallbackDensityCalls=
|
||||||
McStageTimings.GradientFallbackDensityCalls;
|
McStageTimings.GradientFallbackDensityCalls;
|
||||||
|
out.diagnostics.marchingCubesGradientCalls=McStageTimings.GradientCalls;
|
||||||
out.diagnostics.marchingCubesEdgeMidpointDensityCalls=
|
out.diagnostics.marchingCubesEdgeMidpointDensityCalls=
|
||||||
McStageTimings.EdgeMidpointDensityCalls;
|
McStageTimings.EdgeMidpointDensityCalls;
|
||||||
|
out.diagnostics.marchingCubesEdgeMidpointDensityMemoHits=
|
||||||
|
McStageTimings.EdgeMidpointDensityMemoHits;
|
||||||
out.diagnostics.marchingCubesGradientMemoHits=McStageTimings.GradientMemoHits;
|
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>(
|
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
|
||||||
std::chrono::steady_clock::now()-MeshingStart).count();
|
std::chrono::steady_clock::now()-MeshingStart).count();
|
||||||
@@ -2356,6 +2902,9 @@ namespace VoxelForgeCore
|
|||||||
if(bHasWorldAssembly)
|
if(bHasWorldAssembly)
|
||||||
{
|
{
|
||||||
const auto Counts=FStandaloneShapeWorld::GetThreadClassificationStats();
|
const auto Counts=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||||
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||||
|
PrintL30CostCounters(Counts);
|
||||||
|
#endif
|
||||||
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
|
const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats();
|
||||||
out.diagnostics.fieldCalls=Columns.pointCalls;
|
out.diagnostics.fieldCalls=Columns.pointCalls;
|
||||||
out.diagnostics.floatFieldCalls=Counts.NativeFloatFieldCalls;
|
out.diagnostics.floatFieldCalls=Counts.NativeFloatFieldCalls;
|
||||||
@@ -2408,6 +2957,21 @@ namespace VoxelForgeCore
|
|||||||
out.skirtFaceMasks.assign(mesh.TriangleSkirtFaceMasks.begin(),
|
out.skirtFaceMasks.assign(mesh.TriangleSkirtFaceMasks.begin(),
|
||||||
mesh.TriangleSkirtFaceMasks.end());
|
mesh.TriangleSkirtFaceMasks.end());
|
||||||
out.skirtFaceMasks.resize(mesh.Triangles.Num()/3,0);
|
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()));
|
out.vertices.reserve(static_cast<std::size_t>(mesh.Vertices.Num()));
|
||||||
for(int32 i=0;i<mesh.Vertices.Num();++i)
|
for(int32 i=0;i<mesh.Vertices.Num();++i)
|
||||||
{
|
{
|
||||||
@@ -2415,9 +2979,25 @@ namespace VoxelForgeCore
|
|||||||
const FVector normal=i<mesh.Normals.Num()?mesh.Normals[i]:FVector::ZeroVector;
|
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 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);
|
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},
|
MaterialSampleV1 material;
|
||||||
{color.R,color.G,color.B,color.A}});
|
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()));
|
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));
|
for(const int32 index:mesh.Triangles)out.indices.push_back(static_cast<std::uint32_t>(index));
|
||||||
if(options.captureDensitySamples)out.debugDensitySamples=std::move(densityValues);
|
if(options.captureDensitySamples)out.debugDensitySamples=std::move(densityValues);
|
||||||
@@ -2429,6 +3009,7 @@ namespace VoxelForgeCore
|
|||||||
{ densityHash^=std::bit_cast<std::uint32_t>(value);densityHash*=1099511628211ull; }
|
{ densityHash^=std::bit_cast<std::uint32_t>(value);densityHash*=1099511628211ull; }
|
||||||
out.densityHash=densityHash;out.meshHash=HashViewerMesh(mesh);
|
out.densityHash=densityHash;out.meshHash=HashViewerMesh(mesh);
|
||||||
out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh);
|
out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh);
|
||||||
|
out.materialHash=HashMaterialSamples(out.vertices);
|
||||||
if(key.lod>=4)
|
if(key.lod>=4)
|
||||||
{
|
{
|
||||||
out.meshHash=SaltFarHorizonHash(out.meshHash,options.farHorizonVersion);
|
out.meshHash=SaltFarHorizonHash(out.meshHash,options.farHorizonVersion);
|
||||||
@@ -2436,9 +3017,11 @@ namespace VoxelForgeCore
|
|||||||
out.topologyHash=SaltFarHorizonHash(out.topologyHash,options.farHorizonVersion);
|
out.topologyHash=SaltFarHorizonHash(out.topologyHash,options.farHorizonVersion);
|
||||||
}
|
}
|
||||||
out.hasDensityHash=out.hasMeshHash=out.hasPayloadHash=out.hasTopologyHash=true;
|
out.hasDensityHash=out.hasMeshHash=out.hasPayloadHash=out.hasTopologyHash=true;
|
||||||
|
out.hasMaterialHash=true;
|
||||||
}
|
}
|
||||||
out.diagnostics.meshAssemblyMilliseconds=std::chrono::duration<double,std::milli>(
|
out.diagnostics.meshAssemblyMilliseconds=std::chrono::duration<double,std::milli>(
|
||||||
std::chrono::steady_clock::now()-MeshAssemblyStart).count();
|
std::chrono::steady_clock::now()-MeshAssemblyStart).count();
|
||||||
|
CopyIslandCacheMetrics();
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -331,6 +331,28 @@ VF_FIELD_INLINE float VF_FieldPerlin3D(float x,float y,float z)
|
|||||||
return VF_FieldPerlinLerp(y0,y1,sw);
|
return VF_FieldPerlinLerp(y0,y1,sw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VF_FIELD_INLINE float VF_FieldPassageWallRelief(float x,float y,float z,VF_FieldU32 seed)
|
||||||
|
{
|
||||||
|
// Use long, non-commensurate rotated scales. The shared passage wall keeps broad,
|
||||||
|
// uneven rock relief without a short wavelength that can read as repeated rings.
|
||||||
|
const float broad=VF_FieldValueNoise(x+17.0f,y-23.0f,z+11.0f,43,
|
||||||
|
seed^0xB5297A4Du);
|
||||||
|
const float mid=VF_FieldValueNoise(y-7.0f,z+19.0f,x-31.0f,29,
|
||||||
|
seed^0x63D83595u);
|
||||||
|
const float fine=VF_FieldValueNoise(z+5.0f,x+29.0f,y-13.0f,19,
|
||||||
|
seed^0xA511E9B3u);
|
||||||
|
return 0.50f*broad+0.35f*mid+0.15f*fine;
|
||||||
|
}
|
||||||
|
|
||||||
|
VF_FIELD_INLINE float VF_FieldPassageGroundGrain(float x,float y,VF_FieldU32 seed)
|
||||||
|
{
|
||||||
|
const float broad=VF_FieldValueNoise(x+3.25f,y-7.5f,0.0f,8,
|
||||||
|
seed^0xC2B2AE35u);
|
||||||
|
const float fine=VF_FieldValueNoise(y+11.75f,x-5.0f,0.0f,5,
|
||||||
|
seed^0x27D4EB2Fu);
|
||||||
|
return 0.65f*broad+0.35f*fine;
|
||||||
|
}
|
||||||
|
|
||||||
VF_FIELD_INLINE float VF_FieldOwnerFractal(float x,float y,float z,VF_FieldI32 octaves,
|
VF_FIELD_INLINE float VF_FieldOwnerFractal(float x,float y,float z,VF_FieldI32 octaves,
|
||||||
VF_FieldI32 mode)
|
VF_FieldI32 mode)
|
||||||
{
|
{
|
||||||
@@ -372,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_FieldI32 nx=cellX+dx,ny=cellY+dy,nz=cellZ+dz;
|
||||||
const VF_FieldU32 h=VF_FieldVoxelHashMix(
|
const VF_FieldU32 h=VF_FieldVoxelHashMix(
|
||||||
VF_FIELD_CAST_U32(nx+0x7FFFFFFF)
|
(VF_FIELD_CAST_U32(nx)+0x7FFFFFFFu)
|
||||||
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(ny+0x7FFFFFFF)*2654435761u)
|
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(ny)+0x7FFFFFFFu)*2654435761u)
|
||||||
^VF_FieldVoxelHashMix(VF_FIELD_CAST_U32(nz+0x7FFFFFFF)*374761393u));
|
^VF_FieldVoxelHashMix((VF_FIELD_CAST_U32(nz)+0x7FFFFFFFu)*374761393u));
|
||||||
const float fpx=VF_FIELD_CAST_F32(dx)
|
const float fpx=VF_FIELD_CAST_F32(dx)
|
||||||
+VF_FIELD_CAST_F32(h&0xFFFFFFu)*(1.0f/16777216.0f)-fracX;
|
+VF_FIELD_CAST_F32(h&0xFFFFFFu)*(1.0f/16777216.0f)-fracX;
|
||||||
const VF_FieldU32 hy=VF_FieldVoxelHashMix(h^0x12345678u);
|
const VF_FieldU32 hy=VF_FieldVoxelHashMix(h^0x12345678u);
|
||||||
|
|||||||
@@ -32,6 +32,34 @@ ScopedWorldAssemblyFeatureQueriesV1::ScopedWorldAssemblyFeatureQueriesV1(
|
|||||||
for (const auto& query : queries_)
|
for (const auto& query : queries_)
|
||||||
if (forceCandidates_ || query.features.candidateEvaluationUseful)
|
if (forceCandidates_ || query.features.candidateEvaluationUseful)
|
||||||
{ ActiveAssemblyQueries = this; break; }
|
{ 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()
|
ScopedWorldAssemblyFeatureQueriesV1::~ScopedWorldAssemblyFeatureQueriesV1()
|
||||||
@@ -56,6 +84,15 @@ const WorldAssemblyFeatureTileListV1* ScopedWorldAssemblyFeatureQueriesV1::find(
|
|||||||
return nullptr;
|
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::vector<std::uint32_t> BuildWorldAssemblyFeatureTileListV1(
|
||||||
std::span<const WorldAssemblyFeatureBoundsV1> features,
|
std::span<const WorldAssemblyFeatureBoundsV1> features,
|
||||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||||
@@ -124,6 +161,8 @@ namespace world_assembly_detail
|
|||||||
{
|
{
|
||||||
using FSharedHeightfieldColumnCache =
|
using FSharedHeightfieldColumnCache =
|
||||||
VoxelForgeCoreShared::TSharedColumnValueCache<HeightfieldPointSample,131072,4>;
|
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.
|
// 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.
|
// 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 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 MinimumCliffShelfThicknessSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel;
|
||||||
constexpr std::int64_t MaxSerializedMagnitude = 1000000000000ll;
|
constexpr std::int64_t MaxSerializedMagnitude = 1000000000000ll;
|
||||||
constexpr std::size_t MaxBiomeCount = 8;
|
constexpr std::size_t MaxBiomeCount = 8;
|
||||||
@@ -228,6 +273,114 @@ namespace world_assembly_detail
|
|||||||
inline bool OutsideRange(std::int64_t value)
|
inline bool OutsideRange(std::int64_t value)
|
||||||
{ return value < -MaxSerializedMagnitude || value > MaxSerializedMagnitude; }
|
{ 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)
|
inline bool ValidateRecipe(const WorldAssemblyRecipeV1& recipe, std::string& error)
|
||||||
{
|
{
|
||||||
if (recipe.schemaVersion != WorldAssemblySchemaVersion
|
if (recipe.schemaVersion != WorldAssemblySchemaVersion
|
||||||
@@ -296,16 +449,57 @@ namespace world_assembly_detail
|
|||||||
|| recipe.biomeClimateMoistureWavelengthSubvoxels <= 0
|
|| recipe.biomeClimateMoistureWavelengthSubvoxels <= 0
|
||||||
|| recipe.biomeClimateReliefContrastQ16 < 0))
|
|| recipe.biomeClimateReliefContrastQ16 < 0))
|
||||||
{ error = "world assembly climate parameters are outside the supported fixed-scale range"; return false; }
|
{ 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool ValidateStratum(const WorldAssemblyStratumPlanV1& stratum,
|
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
|
if (stratum.strateIndex < 0 || stratum.topChunkZ < stratum.bottomChunkZ
|
||||||
|| stratum.capWavelengthSubvoxels <= 0 || stratum.capUndulationSubvoxels < 0
|
|| stratum.capWavelengthSubvoxels <= 0 || stratum.capUndulationSubvoxels < 0
|
||||||
|| stratum.features.size() > MaxFeatureCount || stratum.route.size() > MaxRoutePointCount)
|
|| stratum.features.size() > MaxFeatureCount || stratum.route.size() > MaxRoutePointCount)
|
||||||
{ error = "world assembly stratum plan has an invalid extent or exceeds its bounded payload"; return false; }
|
{ 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)
|
if (!ValidScalar(stratum.capHeightSubvoxels) || !ValidScalar(stratum.capUndulationSubvoxels)
|
||||||
|| !ValidScalar(stratum.capWavelengthSubvoxels)
|
|| !ValidScalar(stratum.capWavelengthSubvoxels)
|
||||||
|| !ValidScalar(stratum.capDetailAmplitudeSubvoxels)
|
|| !ValidScalar(stratum.capDetailAmplitudeSubvoxels)
|
||||||
@@ -466,6 +660,8 @@ namespace world_assembly_detail
|
|||||||
HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (recipe.surfaceProfile.version != 0)
|
||||||
|
HashSurfaceProfile(hash, recipe.surfaceProfile);
|
||||||
return hash;
|
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);
|
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);
|
HashWord(hash, static_cast<std::uint64_t>(point.radiusSubvoxels), 8);
|
||||||
}
|
}
|
||||||
|
if (stratum.surfaceProfile.version != 0)
|
||||||
|
HashSurfaceProfile(hash, stratum.surfaceProfile);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,6 +825,59 @@ namespace world_assembly_detail
|
|||||||
return out.str();
|
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,
|
inline bool ParseRecipe(const VFJson::Value& value, WorldAssemblyRecipeV1& recipe,
|
||||||
std::string& error)
|
std::string& error)
|
||||||
{
|
{
|
||||||
@@ -647,6 +898,10 @@ namespace world_assembly_detail
|
|||||||
|| !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels)
|
|| !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels)
|
||||||
|| !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16)))
|
|| !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16)))
|
||||||
{ error = "world assembly recipe is missing evaluator-2 climate parameters"; return false; }
|
{ 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())
|
for (const auto& item : value.Get("biomes").AsArray())
|
||||||
{
|
{
|
||||||
WorldAssemblyBiomeProfileV1 biome;
|
WorldAssemblyBiomeProfileV1 biome;
|
||||||
@@ -709,6 +964,8 @@ namespace world_assembly_detail
|
|||||||
out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels
|
out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels
|
||||||
<< ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels
|
<< ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels
|
||||||
<< ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16;
|
<< ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16;
|
||||||
|
if (recipe.surfaceProfile.version != 0)
|
||||||
|
out << ",\"surface_profile\":" << SurfaceProfileJson(recipe.surfaceProfile);
|
||||||
out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":[";
|
out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":[";
|
||||||
for (std::size_t i = 0; i < recipe.biomes.size(); ++i)
|
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; }
|
{ error = "world assembly stratum has malformed optional evaluator-2 fields"; return false; }
|
||||||
if(hasWaterBoolean)stratum.waterEnabled=waterEnabled.Boolean(false);
|
if(hasWaterBoolean)stratum.waterEnabled=waterEnabled.Boolean(false);
|
||||||
if(hasWaterNumber)stratum.waterEnabled=waterEnabled.Number()!=0.0;
|
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");
|
const VFJson::Value& routeConstraintsValue = item.Get("route_constraints");
|
||||||
if (!std::holds_alternative<std::nullptr_t>(routeConstraintsValue.Data))
|
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)));
|
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
|
struct HeightfieldColumnCacheEntry
|
||||||
{
|
{
|
||||||
const WorldAssemblyRecipeV1* recipe = nullptr;
|
const WorldAssemblyRecipeV1* recipe = nullptr;
|
||||||
@@ -1415,6 +1866,61 @@ namespace world_assembly_detail
|
|||||||
HeightfieldPointSample out;
|
HeightfieldPointSample out;
|
||||||
if(recipe.evaluatorVersion>=2)
|
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 BiomeSelectionV2 selection=SelectBiomeV2(recipe,x,y);
|
||||||
const auto& primary=recipe.biomes[selection.primary];
|
const auto& primary=recipe.biomes[selection.primary];
|
||||||
const auto& neighbor=recipe.biomes[selection.neighbor];
|
const auto& neighbor=recipe.biomes[selection.neighbor];
|
||||||
@@ -1742,6 +2248,16 @@ std::optional<WorldAssemblyManifest> WorldAssemblyManifest::FromJson(
|
|||||||
|
|
||||||
bool WorldAssemblyManifest::finalize(std::string& error)
|
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
|
if (schemaVersion != WorldAssemblySchemaVersion || plan.schemaVersion != WorldAssemblySchemaVersion
|
||||||
|| !world_assembly_detail::ValidateRecipe(recipe, error))
|
|| !world_assembly_detail::ValidateRecipe(recipe, error))
|
||||||
{ if (error.empty()) error = "world assembly manifest or plan version is unsupported"; return false; }
|
{ 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; }
|
{ error = "SurfaceWorld stratum recipe seed does not match the world assembly season seed"; return false; }
|
||||||
stratum.recipeOverride.contentHash = world_assembly_detail::HashRecipe(stratum.recipeOverride);
|
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 (recipe.evaluatorVersion >= 4)
|
||||||
{
|
{
|
||||||
if (stratum.routeConstraints.version != 1 || stratum.route.size() < 2)
|
if (stratum.routeConstraints.version != 1 || stratum.route.size() < 2)
|
||||||
@@ -1882,6 +2398,9 @@ std::string WorldAssemblyManifest::ToJson() const
|
|||||||
if (stratum.hasRecipeOverride)
|
if (stratum.hasRecipeOverride)
|
||||||
out << ",\"recipe_override\":"
|
out << ",\"recipe_override\":"
|
||||||
<< world_assembly_detail::RecipeJson(stratum.recipeOverride);
|
<< world_assembly_detail::RecipeJson(stratum.recipeOverride);
|
||||||
|
if (stratum.surfaceProfile.version != 0)
|
||||||
|
out << ",\"surface_profile\":"
|
||||||
|
<< world_assembly_detail::SurfaceProfileJson(stratum.surfaceProfile);
|
||||||
out
|
out
|
||||||
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash)
|
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash)
|
||||||
<< "\",\"features\":[";
|
<< "\",\"features\":[";
|
||||||
@@ -1957,6 +2476,8 @@ bool Heightfield2D::isValid() const noexcept
|
|||||||
|| recipe_->biomes.empty() || recipe_->biomes.size() > world_assembly_detail::MaxBiomeCount
|
|| recipe_->biomes.empty() || recipe_->biomes.size() > world_assembly_detail::MaxBiomeCount
|
||||||
|| recipe_->biomeCellSizeSubvoxels <= 0 || recipe_->biomeWarpWavelengthSubvoxels <= 0
|
|| recipe_->biomeCellSizeSubvoxels <= 0 || recipe_->biomeWarpWavelengthSubvoxels <= 0
|
||||||
|| recipe_->biomeWarpSubvoxels < 0 || recipe_->biomeBorderBlendSubvoxels < 0
|
|| recipe_->biomeWarpSubvoxels < 0 || recipe_->biomeBorderBlendSubvoxels < 0
|
||||||
|
|| recipe_->surfaceProfile != plan_->surfaceProfile
|
||||||
|
|| recipe_->surfaceProfile.version > 1
|
||||||
|| plan_->strateIndex < 0 || plan_->topChunkZ < plan_->bottomChunkZ
|
|| plan_->strateIndex < 0 || plan_->topChunkZ < plan_->bottomChunkZ
|
||||||
|| plan_->capWavelengthSubvoxels <= 0 || plan_->capUndulationSubvoxels < 0)
|
|| plan_->capWavelengthSubvoxels <= 0 || plan_->capUndulationSubvoxels < 0)
|
||||||
return false;
|
return false;
|
||||||
@@ -1973,6 +2494,32 @@ bool Heightfield2D::isValid() const noexcept
|
|||||||
std::array<std::int64_t,2> Heightfield2D::capHeightBounds() const noexcept
|
std::array<std::int64_t,2> Heightfield2D::capHeightBounds() const noexcept
|
||||||
{
|
{
|
||||||
if(!isValid())return {0,0};
|
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 Minimum=std::numeric_limits<long double>::infinity();
|
||||||
long double Maximum=-std::numeric_limits<long double>::infinity();
|
long double Maximum=-std::numeric_limits<long double>::infinity();
|
||||||
const auto IncludeBiome=[&](const WorldAssemblyBiomeProfileV1& Biome)
|
const auto IncludeBiome=[&](const WorldAssemblyBiomeProfileV1& Biome)
|
||||||
@@ -2062,6 +2609,7 @@ WorldAssemblyFeatureTileListV1 Heightfield2D::buildFeatureTileList(
|
|||||||
bounds.push_back(bound);
|
bounds.push_back(bound);
|
||||||
}
|
}
|
||||||
result.featureIndices=BuildWorldAssemblyFeatureTileListV1(bounds,minimum,maximum);
|
result.featureIndices=BuildWorldAssemblyFeatureTileListV1(bounds,minimum,maximum);
|
||||||
|
FinalizeWorldAssemblyFeatureTileListDensityContextV1(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2126,6 +2674,8 @@ void Heightfield2D::resetThreadColumnCacheStats(bool captureCoordinates, bool cl
|
|||||||
Stats.columnConflictMisses=0;
|
Stats.columnConflictMisses=0;
|
||||||
Stats.sharedColumnHits=0;
|
Stats.sharedColumnHits=0;
|
||||||
Stats.columnEvaluations=0;
|
Stats.columnEvaluations=0;
|
||||||
|
Stats.densitySampleCacheHits=0;
|
||||||
|
Stats.densitySampleCacheComputes=0;
|
||||||
Stats.columnComputeMilliseconds=0.0;
|
Stats.columnComputeMilliseconds=0.0;
|
||||||
Stats.captureCoordinates=captureCoordinates;
|
Stats.captureCoordinates=captureCoordinates;
|
||||||
Stats.coordinateCaptureComplete=true;
|
Stats.coordinateCaptureComplete=true;
|
||||||
@@ -2178,6 +2728,15 @@ HeightfieldBoxClass Heightfield2D::classifyLatticeBox(std::int64_t minimumX,
|
|||||||
&&feature.minimumSubvoxels[2]<=maximumZ&&feature.maximumSubvoxels[2]>=minimumZ)
|
&&feature.minimumSubvoxels[2]<=maximumZ&&feature.maximumSubvoxels[2]>=minimumZ)
|
||||||
return HeightfieldBoxClass::mixed;
|
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)
|
const std::int64_t stride=static_cast<std::int64_t>(stepVoxels)
|
||||||
*WorldAssemblySubvoxelsPerVoxel;
|
*WorldAssemblySubvoxelsPerVoxel;
|
||||||
if((maximumX-minimumX)%stride!=0||(maximumY-minimumY)%stride!=0
|
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 groundMax = std::numeric_limits<std::int64_t>::min();
|
||||||
std::int64_t capMin=plan_->capHeightSubvoxels-plan_->capUndulationSubvoxels;
|
std::int64_t capMin=plan_->capHeightSubvoxels-plan_->capUndulationSubvoxels;
|
||||||
std::int64_t capMax=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)
|
for(const auto& biome:recipe_->biomes)
|
||||||
{
|
{
|
||||||
@@ -2313,6 +2894,24 @@ FieldDetailEnvelope Heightfield2D::detailEnvelope() const noexcept
|
|||||||
{
|
{
|
||||||
FieldDetailEnvelope out;
|
FieldDetailEnvelope out;
|
||||||
if (!isValid()) return 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
|
std::int64_t maximum = plan_->capUndulationSubvoxels*5/2
|
||||||
+plan_->capDetailAmplitudeSubvoxels*5/2+plan_->capRidgeAmplitudeSubvoxels;
|
+plan_->capDetailAmplitudeSubvoxels*5/2+plan_->capRidgeAmplitudeSubvoxels;
|
||||||
std::int64_t minimumFeature = std::numeric_limits<std::int64_t>::max();
|
std::int64_t minimumFeature = std::numeric_limits<std::int64_t>::max();
|
||||||
@@ -2448,6 +3047,7 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block
|
|||||||
&result.capBoxes,false,"cap");
|
&result.capBoxes,false,"cap");
|
||||||
}
|
}
|
||||||
bool hasCliffProfile=false;
|
bool hasCliffProfile=false;
|
||||||
|
if(recipe->surfaceProfile.version==0)
|
||||||
for(const auto& biome:recipe->biomes)hasCliffProfile|=biome.cliffStrengthQ16>0;
|
for(const auto& biome:recipe->biomes)hasCliffProfile|=biome.cliffStrengthQ16>0;
|
||||||
if(hasCliffProfile)
|
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.
|
// Controle negatif : cette plage unilaterale exclut un echantillon connu; l'audit doit rejeter cette borne mensongere.
|
||||||
const FieldInterval deliberatelyWrong{true, controlPoint + 1, controlPoint + 2};
|
const FieldInterval deliberatelyWrong{true, controlPoint + 1, controlPoint + 2};
|
||||||
result.negativeControlDetected = !world_assembly_detail::InInterval(deliberatelyWrong, controlPoint);
|
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;
|
||result.biomeBorderBoxes>0;
|
||||||
const bool cliffCoverage=!hasCliffProfile||result.cliffBoxes>0;
|
const bool cliffCoverage=!hasCliffProfile||result.cliffBoxes>0;
|
||||||
const bool exceptionCoverage=std::none_of(plan->features.begin(),plan->features.end(),
|
const bool exceptionCoverage=std::none_of(plan->features.begin(),plan->features.end(),
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ Costs below are design estimates, not timings. Low means bounded analytic featur
|
|||||||
|
|
||||||
A passage planner must produce a traversable route or pair a non-walkable visual passage with another guaranteed route. A narrow crack or drop cannot be assumed walkable because its centerline is air.
|
A passage planner must produce a traversable route or pair a non-walkable visual passage with another guaranteed route. A narrow crack or drop cannot be assumed walkable because its centerline is air.
|
||||||
|
|
||||||
|
For a SurfaceWorld strate, the supported arrival-to-departure route follows the sampled terrain
|
||||||
|
ground and stays within the mouth placement bounds. The inter-strate departure passage owns the
|
||||||
|
descent from that ground mouth through rock and bedrock. Treating the bottom of a tall SurfaceWorld
|
||||||
|
strate as the route's departure height would ask the walk planner to cross open terrain air; it is
|
||||||
|
the wrong endpoint for the intra-strate route.
|
||||||
|
|
||||||
| Passage type | Planner output | Bound and likely cost |
|
| Passage type | Planner output | Bound and likely cost |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| SlopedTunnel | Straight-style path with switchbacks as needed, capsule clearance, supported floor, maximum grade and landing geometry. | Swept capsule bounds plus floor envelope; low–medium. |
|
| SlopedTunnel | Straight-style path with switchbacks as needed, capsule clearance, supported floor, maximum grade and landing geometry. | Swept capsule bounds plus floor envelope; low–medium. |
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# World Assembler L29 — bedrock/ceiling transitions
|
||||||
|
|
||||||
|
**Core now honors the upper strate's `EVoxelStrateTransition` at the shared interface.** The owner
|
||||||
|
world's upper two-chunk Gradient resolves to 16 m at the 8 m layout chunk size. SurfaceWorld's
|
||||||
|
one-chunk setting remains in the resolved strate record. Core does not blend unrelated generator
|
||||||
|
parameters. `WorldAssemblyEvaluatorVersion` is 15; recipe versions and resolved-layout schema V3
|
||||||
|
are unchanged.
|
||||||
|
|
||||||
|
## Appearance and safety
|
||||||
|
|
||||||
|
| Mode | Result |
|
||||||
|
|---|---|
|
||||||
|
| Hard | The original flat, touching boundary. No displacement or overlap. |
|
||||||
|
| Gradient | Both faces follow one smooth 2D hash surface. On the 16 m interface, the measured peak-to-peak undulation is 3.988 m. |
|
||||||
|
| Interleaved | A deterministic 3D domain-warped hash field moves both faces inward, producing fingered overlap. Measured peak crossing was 6.487 m in FloatingIslands and 6.952 m in SurfaceWorld, within the declared 16 m. |
|
||||||
|
|
||||||
|
Each boundary band keeps at least **two Core voxels** of solid thickness: 0.5 m at the default
|
||||||
|
25 cm voxel size. The owner bands measured 0.5–1.0 m on the upper side and 6.25 m on the lower
|
||||||
|
side. Interleaved is capped by both the declared width and the bands' combined thickness, so the
|
||||||
|
solid intervals still touch or overlap.
|
||||||
|
|
||||||
|
The strict interface audit covers a 512 m square: 4,221 columns per interface, with four passage
|
||||||
|
columns excluded from the hole sweep. Hard, Gradient and Interleaved each report zero hole columns
|
||||||
|
and zero air samples. Both negative controls reject as expected: crossing one micrometre wider than
|
||||||
|
declared, and either band one micrometre thinner than the minimum. All six recipe/mode runs pass.
|
||||||
|
|
||||||
|
Transition faces return to the nominal seam near passage mouths: the protected radius is the mouth
|
||||||
|
radius plus 1 m, followed by a 2 m smooth fade. This kept the style-2 FI landing supported; its
|
||||||
|
final capsule/support audit passes.
|
||||||
|
|
||||||
|
## Passage and parity audits
|
||||||
|
|
||||||
|
The final matrix has 4 styles × 2 recipes × 2 compilers (16 runs). Capsule and walkability,
|
||||||
|
open-air, naturalness, no-long-bridge, route bindings, strict-validator controls, L13 route parity,
|
||||||
|
and L9 proof audits pass. Open-air reports zero violations; validator controls reject 10/10;
|
||||||
|
route-parity and proof audits report zero mismatches/violations. The L23/L24/L26 negative controls
|
||||||
|
are detected.
|
||||||
|
|
||||||
|
The L28 `FloorProfileBlend` diagnostic still exceeds its 2-voxel threshold in styles 1 and 3:
|
||||||
|
2.544 and 3.645 voxels. L28 records the same values on both compilers, so this is inherited and
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
## Hashes
|
||||||
|
|
||||||
|
The L25 W1/W8/W12 hash matrix is identical across Clang and GCC for both recipes: full-detail,
|
||||||
|
whole-view, all LOD rows and exact-query hashes match. Hash order below is density / mesh / payload /
|
||||||
|
topology.
|
||||||
|
|
||||||
|
| Recipe | Full detail (960 tiles) | Whole view (2,370 tiles) | Exact-query density / detail / collision |
|
||||||
|
|---|---|---|---|
|
||||||
|
| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `0e8c5af01df96b14 / 70099e89b77811ed / dbce0fdcf6764126 / ddaf81a1228f9b4b` | `c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849` |
|
||||||
|
| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `a492eefa4f534975 / 141f00f3e411a0df / 93f50c569526c5cb / d83b53c8e53df81c` | `7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf` |
|
||||||
|
|
||||||
|
The 24 m moving crossing processed 314 entering tiles per recipe; Clang/GCC crossing hashes match.
|
||||||
|
SurfaceWorld's away-from-interface probe at z = −400 m also matches L28: all 960 full-detail
|
||||||
|
tiles and the exact-query density/detail/collision hashes are identical. The broader whole-view
|
||||||
|
hash differs because its far-LOD coverage also includes the changed interface.
|
||||||
|
|
||||||
|
## Cost
|
||||||
|
|
||||||
|
Five clean, alternating W12 pairs against
|
||||||
|
`Saved/Linux/bin/VoxelForgeCoreHeadless-linux28-clang-r28`, hashing off:
|
||||||
|
|
||||||
|
| Recipe | L28 whole-view ready | L29 whole-view ready | Change |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| SurfaceWorld | 1.581 s | 1.644 s | +3.97% |
|
||||||
|
| FloatingIslands | 16.605 s | 16.814 s | +1.26% |
|
||||||
|
|
||||||
|
FloatingIslands is within the ~3% target. SurfaceWorld is about one percentage point above it.
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
|
||||||
|
All are 1280×720 JPEGs, each under 21 KB (well below 300 KB):
|
||||||
|
|
||||||
|
- `captures/fi-floor-gradient-above.jpg` — FI floor from above, inside FI.
|
||||||
|
- `captures/sw-ceiling-gradient-below.jpg` — SW ceiling from below, inside SW.
|
||||||
|
- `captures/interface-hard-side-cutaway.jpg`
|
||||||
|
- `captures/interface-gradient-side-cutaway.jpg`
|
||||||
|
- `captures/interface-interleaved-side-cutaway.jpg`
|
||||||
|
|
||||||
|
## Changes and run history
|
||||||
|
|
||||||
|
Core transition evaluation/validation and the shared composition path changed, along with the
|
||||||
|
headless transition audit, architecture/codemap, and the two Unreal files
|
||||||
|
`Source/VoxelForge/Public/VoxelStrateTypes.h` and
|
||||||
|
`Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`. Unreal was unavailable here, so those edits
|
||||||
|
were not compiled. No commit, push or stash was made.
|
||||||
|
|
||||||
|
Failed or retried runs:
|
||||||
|
|
||||||
|
- An early Clang build used a nonexistent dispatch helper; corrected to the current query method and rebuilt with Clang and GCC.
|
||||||
|
- The first Interleaved overlap used the full sum of band thicknesses and opened gaps (1,085 columns / 20,313 air samples). Capping overlap to `min(declared width, combined thickness)` removed the gaps.
|
||||||
|
- An initial hole sweep sampled a half-open band edge and reported 752 boundary-rounding holes. Sampling half a voxel inside each band removed those edge artifacts; final sweeps report zero holes.
|
||||||
|
- The first FI style-2 passage audit had 11 unsupported probes after Gradient moved the mouth face. The local mouth taper fixed it; all 16 final passage runs pass.
|
||||||
|
- A resolved-layout-only command initially omitted required pipeline flags and exited 2; rerun with the required flags passed. Early cutaway camera/clip settings produced blank framing; the final orientation and clip plane produced the captures above.
|
||||||
|
- One cost folder contained a duplicated timing sequence and was discarded. The five-pair clean run above was sequential. A cache experiment did not improve cost and was removed. A cost-extraction `sed` typo was rerun with Python; benchmark logs were unaffected.
|
||||||
|
|
||||||
|
No build or audit exceeded 30 minutes, and no step remained stuck for 15 minutes. All builds and runs
|
||||||
|
used CPUs 0–11, at most 12 workers, normal priority, and the 6 GiB address-space cap.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged: transitions honoured safely (0 holes over 4,221 columns per interface, negative controls reject, all
|
||||||
|
passage audits pass, deterministic).
|
||||||
|
- **The look is not verified by the captures:** `interface-interleaved-side-cutaway.jpg` shows flat faces
|
||||||
|
(black = uncapped solid between faces), with no visible fingers; `sw-ceiling-gradient-below.jpg` is a flat
|
||||||
|
plane; `fi-floor-gradient-above.jpg` shows **concentric dashed rings** on the FI floor, which looks like an
|
||||||
|
artifact (possibly the passage-mouth protected-radius fade, or quantised undulation), not a smooth Gradient.
|
||||||
|
- Cumulative SW cost vs L24 is now ~+8% (L28 +3.7% plus L29 +4%). L30 covers the ring artifact, readable
|
||||||
|
transition captures, and cost recovery.
|
||||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,239 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rasterize upward mesh triangles into a top-down field height map."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
Q16 = 1 << 16
|
||||||
|
MASK64 = (1 << 64) - 1
|
||||||
|
|
||||||
|
|
||||||
|
def trunc_div(numerator, denominator):
|
||||||
|
return numerator // denominator if numerator >= 0 else -((-numerator) // denominator)
|
||||||
|
|
||||||
|
|
||||||
|
def transition_hash(seed, interface, x, y, z, salt):
|
||||||
|
value = 14695981039346656037
|
||||||
|
for word in (seed & 0xffffffff, interface & 0xffffffff,
|
||||||
|
x & MASK64, y & MASK64, z & MASK64, salt):
|
||||||
|
for byte in range(8):
|
||||||
|
value ^= (word >> (byte * 8)) & 0xff
|
||||||
|
value = (value * 1099511628211) & MASK64
|
||||||
|
value ^= value >> 30
|
||||||
|
value = (value * 0xbf58476d1ce4e5b9) & MASK64
|
||||||
|
value ^= value >> 27
|
||||||
|
value = (value * 0x94d049bb133111eb) & MASK64
|
||||||
|
value ^= value >> 31
|
||||||
|
return ((value >> 32) >> 15) - Q16
|
||||||
|
|
||||||
|
|
||||||
|
def transition_fade(value):
|
||||||
|
value = max(0, min(Q16, value))
|
||||||
|
square = value * value // Q16
|
||||||
|
return square * (3 * Q16 - 2 * value) // Q16
|
||||||
|
|
||||||
|
|
||||||
|
def gradient_height_um(layout, x_um, y_um):
|
||||||
|
upper = next(item for item in layout["strates"] if item["order"] == 0)
|
||||||
|
lower = next(item for item in layout["strates"] if item["order"] == 1)
|
||||||
|
width = upper["transition_width_um"]
|
||||||
|
cell = max(1_000_000, layout["strate_layout_chunk_size_um"] * 4)
|
||||||
|
ix = x_um // cell
|
||||||
|
iy = y_um // cell
|
||||||
|
tx = transition_fade(((x_um - ix * cell) * Q16) // cell)
|
||||||
|
ty = transition_fade(((y_um - iy * cell) * Q16) // cell)
|
||||||
|
|
||||||
|
def at(dx, dy):
|
||||||
|
noise = transition_hash(0, upper["strate_index"], ix + dx, iy + dy, 0,
|
||||||
|
0x47524144)
|
||||||
|
return noise
|
||||||
|
|
||||||
|
def lerp(a, b, t):
|
||||||
|
return a + trunc_div((b - a) * t, Q16)
|
||||||
|
|
||||||
|
low = lerp(lerp(at(0, 0), at(1, 0), tx),
|
||||||
|
lerp(at(0, 1), at(1, 1), tx), ty)
|
||||||
|
displacement = trunc_div(low * (width // 8), Q16)
|
||||||
|
seam = upper["bedrock"]["bottom_um"]
|
||||||
|
thickness = upper["bedrock"]["top_um"] - seam
|
||||||
|
return seam + displacement + thickness
|
||||||
|
|
||||||
|
|
||||||
|
def read_scene(path):
|
||||||
|
triangles = []
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid scene header")
|
||||||
|
version, voxel_um, count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.read(40)
|
||||||
|
if version != 1 or count > 100000:
|
||||||
|
raise SystemExit("unsupported scene")
|
||||||
|
for _ in range(count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
for index in range(0, index_count, 3):
|
||||||
|
if masks and masks[index // 3]:
|
||||||
|
continue
|
||||||
|
a, b, c = indices[index:index + 3]
|
||||||
|
if max(a, b, c) >= surface_vertices:
|
||||||
|
continue
|
||||||
|
nz = (normals[a * 3 + 2] + normals[b * 3 + 2]
|
||||||
|
+ normals[c * 3 + 2]) / 3.0
|
||||||
|
if nz < 0.35:
|
||||||
|
continue
|
||||||
|
vertices = tuple((positions[v * 3], positions[v * 3 + 1],
|
||||||
|
positions[v * 3 + 2]) for v in (a, b, c))
|
||||||
|
triangles.append(vertices)
|
||||||
|
return voxel_um, triangles
|
||||||
|
|
||||||
|
|
||||||
|
def rasterize(triangles, bounds, width, height):
|
||||||
|
x0, x1, y0, y1 = bounds
|
||||||
|
values = [None] * (width * height)
|
||||||
|
sx, sy = width / (x1 - x0), height / (y1 - y0)
|
||||||
|
for tri in triangles:
|
||||||
|
(ax, ay, az), (bx, by, bz), (cx, cy, cz) = tri
|
||||||
|
den = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
|
||||||
|
if abs(den) < 1.0e-12:
|
||||||
|
continue
|
||||||
|
left = max(0, int(math.floor((min(ax, bx, cx) - x0) * sx)))
|
||||||
|
right = min(width - 1, int(math.ceil((max(ax, bx, cx) - x0) * sx)))
|
||||||
|
top = max(0, int(math.floor((y1 - max(ay, by, cy)) * sy)))
|
||||||
|
bottom = min(height - 1, int(math.ceil((y1 - min(ay, by, cy)) * sy)))
|
||||||
|
for py in range(top, bottom + 1):
|
||||||
|
y = y1 - (py + 0.5) / sy
|
||||||
|
row = py * width
|
||||||
|
for px in range(left, right + 1):
|
||||||
|
x = x0 + (px + 0.5) / sx
|
||||||
|
u = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / den
|
||||||
|
v = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / den
|
||||||
|
w = 1.0 - u - v
|
||||||
|
if min(u, v, w) < -1.0e-6:
|
||||||
|
continue
|
||||||
|
z = u * az + v * bz + w * cz
|
||||||
|
idx = row + px
|
||||||
|
if values[idx] is None or z > values[idx]:
|
||||||
|
values[idx] = z
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("output_ppm")
|
||||||
|
parser.add_argument("--bounds", nargs=4, type=float, required=True,
|
||||||
|
metavar=("XMIN", "XMAX", "YMIN", "YMAX"))
|
||||||
|
parser.add_argument("--size", nargs=2, type=int, default=(640, 640))
|
||||||
|
parser.add_argument("--range", nargs=2, type=float, default=(-122.0, -118.0),
|
||||||
|
metavar=("ZMIN", "ZMAX"))
|
||||||
|
parser.add_argument("--ring-center", nargs=2, type=float,
|
||||||
|
metavar=("X", "Y"))
|
||||||
|
parser.add_argument("--layout")
|
||||||
|
args = parser.parse_args()
|
||||||
|
voxel_um, triangles = read_scene(args.scene)
|
||||||
|
width, height = args.size
|
||||||
|
sx, sy = width / (args.bounds[1] - args.bounds[0]), height / (args.bounds[3] - args.bounds[2])
|
||||||
|
values = rasterize(triangles, args.bounds, width, height)
|
||||||
|
finite = sorted(value for value in values if value is not None)
|
||||||
|
if not finite:
|
||||||
|
raise SystemExit("no upward-facing field samples in bounds")
|
||||||
|
low, high = args.range
|
||||||
|
if high <= low:
|
||||||
|
raise SystemExit("height range must increase")
|
||||||
|
stops = ((0.00, (31, 52, 92)), (0.24, (41, 119, 153)),
|
||||||
|
(0.50, (85, 177, 143)), (0.73, (226, 207, 105)),
|
||||||
|
(1.00, (181, 71, 49)))
|
||||||
|
with open(args.output_ppm, "wb") as output:
|
||||||
|
output.write(f"P6\n{width} {height}\n255\n".encode())
|
||||||
|
for value in values:
|
||||||
|
if value is None:
|
||||||
|
output.write(bytes((18, 22, 30)))
|
||||||
|
continue
|
||||||
|
t = max(0.0, min(1.0, (value - low) / (high - low)))
|
||||||
|
for index in range(len(stops) - 1):
|
||||||
|
if t <= stops[index + 1][0]:
|
||||||
|
ta, ca = stops[index]
|
||||||
|
tb, cb = stops[index + 1]
|
||||||
|
blend = (t - ta) / (tb - ta)
|
||||||
|
rgb = tuple(round(ca[c] + blend * (cb[c] - ca[c]))
|
||||||
|
for c in range(3))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rgb = stops[-1][1]
|
||||||
|
output.write(bytes(rgb))
|
||||||
|
def quantile(p):
|
||||||
|
return finite[round((len(finite) - 1) * p)]
|
||||||
|
print(f"[RingFieldRaster] voxel_um={voxel_um} triangles={len(triangles)} "
|
||||||
|
f"pixels={len(finite)}/{len(values)} height_min_m={finite[0]:.6f} "
|
||||||
|
f"height_p05_m={quantile(.05):.6f} height_median_m={quantile(.5):.6f} "
|
||||||
|
f"height_p95_m={quantile(.95):.6f} height_max_m={finite[-1]:.6f} "
|
||||||
|
f"height_range_m={finite[-1]-finite[0]:.6f} "
|
||||||
|
f"map={args.output_ppm}")
|
||||||
|
if args.ring_center:
|
||||||
|
cx, cy = args.ring_center
|
||||||
|
def at_world(x, y):
|
||||||
|
fx = (x - args.bounds[0]) * sx - 0.5
|
||||||
|
fy = (args.bounds[3] - y) * sy - 0.5
|
||||||
|
ix, iy = math.floor(fx), math.floor(fy)
|
||||||
|
tx, ty = fx - ix, fy - iy
|
||||||
|
samples = []
|
||||||
|
for px, py in ((ix, iy), (ix + 1, iy), (ix, iy + 1), (ix + 1, iy + 1)):
|
||||||
|
if not (0 <= px < width and 0 <= py < height):
|
||||||
|
return None
|
||||||
|
value = values[py * width + px]
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
samples.append(value)
|
||||||
|
return ((samples[0] * (1 - tx) + samples[1] * tx) * (1 - ty)
|
||||||
|
+ (samples[2] * (1 - tx) + samples[3] * tx) * ty)
|
||||||
|
print(f"[RingRadialProbe] center_m=({cx:.3f},{cy:.3f}) "
|
||||||
|
"radius_m mean_m angular_sd_m min_m max_m samples")
|
||||||
|
for radius in range(1, 17):
|
||||||
|
row = [at_world(cx + radius * math.cos(a * math.tau / 32),
|
||||||
|
cy + radius * math.sin(a * math.tau / 32))
|
||||||
|
for a in range(32)]
|
||||||
|
row = [value for value in row if value is not None]
|
||||||
|
if row:
|
||||||
|
mean = sum(row) / len(row)
|
||||||
|
sd = math.sqrt(sum((value - mean) ** 2 for value in row) / len(row))
|
||||||
|
print(f" {radius:>2} {mean:.6f} {sd:.6f} {min(row):.6f} "
|
||||||
|
f"{max(row):.6f} {len(row)}")
|
||||||
|
if args.layout:
|
||||||
|
layout = json.load(open(args.layout, encoding="utf-8"))
|
||||||
|
if next(item for item in layout["strates"] if item["order"] == 0)["transition_mode"] != "Gradient":
|
||||||
|
raise SystemExit("--layout must describe a Gradient owner interface")
|
||||||
|
errors = []
|
||||||
|
for index, actual in enumerate(values):
|
||||||
|
if actual is None:
|
||||||
|
continue
|
||||||
|
px, py = index % width, index // width
|
||||||
|
x = args.bounds[0] + (px + 0.5) / sx
|
||||||
|
y = args.bounds[3] - (py + 0.5) / sy
|
||||||
|
expected = gradient_height_um(layout, round(x * 1_000_000),
|
||||||
|
round(y * 1_000_000)) / 1_000_000.0
|
||||||
|
errors.append(actual - expected)
|
||||||
|
absolute = sorted(abs(error) for error in errors)
|
||||||
|
rms = math.sqrt(sum(error * error for error in errors) / len(errors))
|
||||||
|
print(f"[RingGradientMatch] samples={len(errors)} rms_m={rms:.6f} "
|
||||||
|
f"p95_abs_m={absolute[round((len(absolute)-1)*.95)]:.6f} "
|
||||||
|
f"max_abs_m={absolute[-1]:.6f} "
|
||||||
|
"expected=owner-resolved-2d-hash-value-noise")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#include "CoreNamespacePreamble.h"
|
||||||
|
#include "ResolvedLayoutInternal.h"
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
VoxelForgeCore::ResolvedLayoutStratumV2 upper, lower;
|
||||||
|
upper.strateIndex=0;upper.order=0;upper.transitionMode="Gradient";
|
||||||
|
upper.transitionWidthMicrometres=16000000;
|
||||||
|
upper.bedrock.bottomMicrometres=-120000000;
|
||||||
|
upper.bedrock.topMicrometres=-119500000;
|
||||||
|
upper.bedrock.presentInCurrentField=true;
|
||||||
|
lower.strateIndex=1;lower.order=1;
|
||||||
|
lower.ceiling.bottomMicrometres=-126250000;
|
||||||
|
lower.ceiling.topMicrometres=-120000000;
|
||||||
|
lower.ceiling.presentInCurrentField=true;
|
||||||
|
for(const auto& point:std::array<std::array<std::int64_t,2>,5>{
|
||||||
|
std::array<std::int64_t,2>{64000000,-32000000},
|
||||||
|
{65000000,-32000000},{64000000,-31000000},{70000000,-32000000},
|
||||||
|
{80000000,-35000000}})
|
||||||
|
{
|
||||||
|
const auto bands=VoxelForgeCore::EvaluateResolvedLayoutTransitionBandsV1(
|
||||||
|
upper,lower,0,8000000,point[0],point[1]);
|
||||||
|
std::printf("x_m=%.3f y_m=%.3f top_m=%.6f bottom_m=%.6f\n",
|
||||||
|
point[0]/1000000.0,point[1]/1000000.0,
|
||||||
|
bands.upperBedrockTopMicrometres/1000000.0,
|
||||||
|
bands.upperBedrockBottomMicrometres/1000000.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# VoxelForge L30
|
||||||
|
|
||||||
|
Work is on `assembler/world`. The speed and look patches are saved separately under
|
||||||
|
`Saved/Linux/l30/`; evaluator version is now 16 for the field change. No Unreal files were edited.
|
||||||
|
Nothing was committed, pushed, or stashed.
|
||||||
|
|
||||||
|
## Ring measurement and fix
|
||||||
|
|
||||||
|
I rasterized upward-facing triangles from the L29 mesh (skirt triangles excluded) over the same
|
||||||
|
409,600 pixels as the after field. At `(64, -32)`, the L29 field stayed almost flat at `-120.063 m` from
|
||||||
|
radii 1–11 m, then broke at 12 m. The expected Gradient at radius 1 m is `-121.360 m`. This is a
|
||||||
|
field defect, not shading: the probe is about 67 m from the passage mouth, and the raster has no
|
||||||
|
skirts or readable-shading tint. The passage column flare is at that distant mouth as well.
|
||||||
|
|
||||||
|
The upper FloatingIslands generator could leave native solid density in the air-side gap above a
|
||||||
|
negatively displaced Gradient face. The constant band densities also snapped the face to Core
|
||||||
|
voxel levels. The owner Gradient path now clears that gap with positive signed distance and writes
|
||||||
|
negative signed distance below the face, allowing the mesh to interpolate its height. The after
|
||||||
|
profile follows the resolved hash surface at every sampled radius. Match error fell from RMS
|
||||||
|
`0.361765 m`, p95 `1.016178 m`, max `1.304169 m` to RMS `0.110357 m`, p95 `0.218531 m`, max
|
||||||
|
`0.248818 m` (one Core voxel is `0.25 m`).
|
||||||
|
|
||||||
|
Before/after captures use the same color ramp and bounds:
|
||||||
|
|
||||||
|
- [L29 height map](captures/fi-floor-gradient-before-heightmap.jpg), [fixed height map](captures/fi-floor-gradient-after-heightmap.jpg)
|
||||||
|
- [L29 readable render](captures/fi-floor-gradient-before.jpg), [fixed readable render](captures/fi-floor-gradient-after.jpg)
|
||||||
|
|
||||||
|
## Transition captures
|
||||||
|
|
||||||
|
The maps cover the full 640×640 surface and are all below 300 KB. Gradient is the owner world
|
||||||
|
without a mode override. Hard and Interleaved use fixture overrides; the resolved layouts record
|
||||||
|
those modes.
|
||||||
|
|
||||||
|
- [Hard](captures/transition-hard-heightmap.jpg) — flat interface.
|
||||||
|
- [Gradient](captures/transition-gradient-heightmap.jpg) — owner field.
|
||||||
|
- [Interleaved](captures/transition-interleaved-heightmap.jpg) — fixture override.
|
||||||
|
|
||||||
|
## Cost
|
||||||
|
|
||||||
|
Five alternating W12 pairs, hashing off and `cache_hits=0`. First table is the separable speed-only
|
||||||
|
build, whose hashes remain L29-identical. Second table is the final version-16 build.
|
||||||
|
|
||||||
|
| Recipe | L24 | L29 | L30 speed-only | vs L24 | L30 final | vs L24 |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| SurfaceWorld | 1.566 s | 1.653 s | 1.606 s | +2.55% | 1.530 s | +2.68% |
|
||||||
|
| FloatingIslands | 16.625 s | 16.779 s | 16.470 s | −0.93% | 16.543 s | +0.21% |
|
||||||
|
|
||||||
|
Counter-enabled runs on the same plain W12 workload locate the saved work:
|
||||||
|
|
||||||
|
| Recipe | Boundary calls | Window checks → evaluations | Fade calls | Boundary tile / point culls | Column Z tests → guards → radial rejects | SDF calls |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| SurfaceWorld | 19.95 M | 3.54 M → 739 k | 8.2 k | 8.99 M / 8.96 M | 1.87 M → 1.85 M → 1.58 M | 18.4 k |
|
||||||
|
| FloatingIslands | 66.33 M | 8.00 M → 2.68 M | 8.6 k | 38.25 M / 23.63 M | 8.17 M → 8.14 M → 6.82 M | 83.4 k |
|
||||||
|
|
||||||
|
The fade-tile cull skips 731 k SurfaceWorld and 2.67 M FloatingIslands fade evaluations; the
|
||||||
|
column guard exits on nonpositive field values 272 k and 1.27 M times. Timing comes from
|
||||||
|
uninstrumented binaries; the counts above use counter logging.
|
||||||
|
|
||||||
|
## Hashes and audits
|
||||||
|
|
||||||
|
The speed-only v15 build matches all 12 L29 hash records for each recipe/compiler/worker matrix
|
||||||
|
(W1/W8/W12), and all 12 records for each 24 m crossing matrix. That is 24 exact comparisons;
|
||||||
|
all pass. The final v16 build is deterministic across both compilers and all worker counts, in the
|
||||||
|
normal and 24 m crossing runs. Representative W12 hashes (`density / mesh / payload / topology`):
|
||||||
|
|
||||||
|
| Recipe | Full detail | Whole view |
|
||||||
|
|---|---|---|
|
||||||
|
| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `0e8c5af01df96b14 / 70099e89b77811ed / dbce0fdcf6764126 / ddaf81a1228f9b4b` |
|
||||||
|
| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `4ee19ea6b6406ca6 / be648f94c899b47e / 310b525f31604f98 / 10366eb6dfce2384` |
|
||||||
|
|
||||||
|
Exact-query hashes also match across v16 runs: SurfaceWorld
|
||||||
|
`c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849`; FloatingIslands
|
||||||
|
`7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf`.
|
||||||
|
|
||||||
|
- Interface audit: all 12 compiler/recipe/mode combinations pass; 4,221 columns each, zero holes
|
||||||
|
and air samples, both negative controls reject.
|
||||||
|
- The 16 L29 fixture-matrix replays pass passage safety/open-air, validator controls (10/10), L13
|
||||||
|
route parity, L9 proof checks (zero violations), and walkability. `FloorProfileBlend` still
|
||||||
|
reports 2.544 vox in style 1 and 3.645 vox in style 3, unchanged from L29.
|
||||||
|
- Audit scope caveat: the L29 files named `floating-islands` were run with
|
||||||
|
`--recipe=surface-world`. With the actual `--recipe=floating-islands` option, the rock-column
|
||||||
|
audit fails on all four styles on both L29 and L30: connected ends are 1/2, 3/4, 1/2, 3/4.
|
||||||
|
The open-air audit passes. This is inherited, but means the L29 report's “two recipes” matrix did
|
||||||
|
not exercise the FloatingIslands CLI recipe.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- Broad air-gap experiments first lost a SurfaceWorld style-2 support sample. Route/radius masks
|
||||||
|
and support restoration then caused open-air violations. Widening the fade restored that case,
|
||||||
|
but measurement placed the ring far outside the fade, so those variants were discarded. The
|
||||||
|
final owner-only gate and signed-distance face pass the targeted support audit.
|
||||||
|
- Early speed variants missed the target: tile-only `+4.04%`, Z-point `+3.11%`, scalar path
|
||||||
|
`+4.33%` vs L24. The final band and fade-tile culls meet the target.
|
||||||
|
- The first counter-instrumentation compile failed; the counter hook was corrected and rebuilt
|
||||||
|
with Clang and GCC. One duplicated timing sequence was discarded; only the clean five-pair runs
|
||||||
|
are reported.
|
||||||
|
- The first 24 m crossing comparison supplied an extra camera override and mismatched. Rerunning
|
||||||
|
with the L29 camera setup and only `--camera-step=24,0,0` matches all hashes.
|
||||||
|
- An early Hard export hit `std::bad_alloc`; retry passed. The first EGL render used an unwritable
|
||||||
|
cache; retry with `XDG_CACHE_HOME` under `Saved/Linux/l30` passed. The first map conversion also
|
||||||
|
stopped before the Hard/Interleaved maps; both were rerun individually and have full raster
|
||||||
|
coverage.
|
||||||
|
- The first passage-matrix summary script assumed a different marker order and reported failure;
|
||||||
|
checking each audit marker by name confirms the core matrix passes. A `check=True` true-recipe
|
||||||
|
control runner stopped on the expected L29 exit code 10; rerunning while recording return codes
|
||||||
|
confirmed the same four inherited rock-column failures on L29 and L30.
|
||||||
|
|
||||||
|
No build or run exceeded 30 minutes or remained stuck for 15 minutes. Builds and runs used CPUs
|
||||||
|
0–11, at most 12 workers, normal priority, and the 6 GiB address-space cap. No Unreal build was
|
||||||
|
attempted.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged: the ring was a real field defect (air-side gap left solid, face snapped to voxel levels), now fixed
|
||||||
|
(RMS 0.36 → 0.11 m). Cost back to SW +2.7% / FI +0.2% vs L24. The speed part is bit-identical; final v16 is
|
||||||
|
deterministic.
|
||||||
|
- Still visible: `fi-floor-gradient-after.jpg` shows faint concentric lines. Unknown whether that is residual
|
||||||
|
geometry (≤0.25 m steps) or the readable shader's height-tint banding; L31 measures it.
|
||||||
|
- **Important finding (thanks to Luna's caveat):** L29's "both recipes" passage matrix ran SurfaceWorld twice. On
|
||||||
|
the real `--recipe=floating-islands`, the rock-column connected-ends audit fails in all 4 styles (1/2 or 3/4).
|
||||||
|
Since L27, FI column results were never really tested. L31 priority.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Interleave five W12 hash-off whole-view cost triplets for L24/L29/L30."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l30"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"l24": ROOT / "Saved/Linux/l24-source/bin/linux/VoxelForgeCoreHeadless-linux24-clang19",
|
||||||
|
"l29": ROOT / "Saved/Linux/bin/VoxelForgeCoreHeadless-linux29-clang",
|
||||||
|
"l30": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade",
|
||||||
|
}
|
||||||
|
ORDERS = (
|
||||||
|
("l24", "l29", "l30"),
|
||||||
|
("l29", "l30", "l24"),
|
||||||
|
("l30", "l24", "l29"),
|
||||||
|
("l24", "l30", "l29"),
|
||||||
|
("l29", "l24", "l30"),
|
||||||
|
)
|
||||||
|
WHOLE_READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for pair, order in enumerate(ORDERS, 1):
|
||||||
|
for build in order:
|
||||||
|
name = f"cost-speedfade-{recipe}-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"prlimit", "--as=6442450944", "--", 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()
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
check=True, timeout=1800)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = WHOLE_READY.findall(text)
|
||||||
|
if len(matches) != 1 or "hash=off" not in text or "cache_hits=0" not in text:
|
||||||
|
raise SystemExit(f"invalid cost run: {name}; see {logfile}")
|
||||||
|
ready = float(matches[0])
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.3f}s", flush=True)
|
||||||
|
|
||||||
|
summary = OUT / "cost-runs-speedfade.csv"
|
||||||
|
summary.write_text("recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.6f}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed in rows), encoding="utf-8")
|
||||||
|
print(f"wrote {summary}", flush=True)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify speed-only L30 hashes on the 24 m crossing against L29."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l30"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"clang": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade",
|
||||||
|
"gcc": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade-gcc",
|
||||||
|
}
|
||||||
|
PATTERN = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
passed = True
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
reference_path = ROOT / f"Saved/Linux/linux29-final-crossing24-{compiler}-{recipe}.log"
|
||||||
|
reference = PATTERN.findall(reference_path.read_text(encoding="utf-8"))
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"crossing-speedonly-default-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"prlimit", "--as=6442450944", "--", str(binary),
|
||||||
|
str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", f"--workers={workers}",
|
||||||
|
"--camera-step=24,0,0",
|
||||||
|
"--region-crossing", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
check=True, timeout=1800)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
rows = PATTERN.findall(logfile.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
ok = len(reference) == 12 and rows == reference
|
||||||
|
passed &= ok
|
||||||
|
print(f"{compiler} {recipe} W{workers} elapsed={elapsed:.3f}s "
|
||||||
|
f"hash_rows={len(rows)} {'MATCH' if ok else 'MISMATCH'}", flush=True)
|
||||||
|
print(f"[CrossingSpeedOnly] runs=12 status={'PASS' if passed else 'FAIL'}", flush=True)
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Combine the initial and repeat L31 interleaved cost batches."""
|
||||||
|
import csv
|
||||||
|
import statistics
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
sources = (
|
||||||
|
("first", 0, OUT / "cost-first/cost-runs.csv"),
|
||||||
|
("repeat", 5, OUT / "cost-runs.csv"),
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
for batch, offset, path in sources:
|
||||||
|
with path.open(newline="", encoding="utf-8") as stream:
|
||||||
|
for row in csv.DictReader(stream):
|
||||||
|
row["pair"] = str(int(row["pair"]) + offset)
|
||||||
|
row["source_batch"] = batch
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
with (OUT / "cost-combined.csv").open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.DictWriter(stream, fieldnames=(
|
||||||
|
"recipe", "pair", "build", "whole_view_ready_seconds", "process_elapsed_seconds",
|
||||||
|
"exit_code", "status", "source_batch"))
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
print("recipe pairs median_l30_s median_l31_s median_change_pct paired_median_pct range_pct")
|
||||||
|
failed = False
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
by_build = {
|
||||||
|
build: [float(row["whole_view_ready_seconds"]) for row in rows
|
||||||
|
if row["recipe"] == recipe and row["build"] == build]
|
||||||
|
for build in ("l30", "l31")
|
||||||
|
}
|
||||||
|
paired = []
|
||||||
|
for pair in range(1, 11):
|
||||||
|
times = {
|
||||||
|
row["build"]: float(row["whole_view_ready_seconds"])
|
||||||
|
for row in rows if row["recipe"] == recipe and int(row["pair"]) == pair
|
||||||
|
}
|
||||||
|
paired.append((times["l31"] / times["l30"] - 1.0) * 100.0)
|
||||||
|
median_l30 = statistics.median(by_build["l30"])
|
||||||
|
median_l31 = statistics.median(by_build["l31"])
|
||||||
|
median_change = (median_l31 / median_l30 - 1.0) * 100.0
|
||||||
|
print(f"{recipe} 10 {median_l30:.3f} {median_l31:.3f} {median_change:+.2f} "
|
||||||
|
f"{statistics.median(paired):+.2f} {min(paired):+.2f}..{max(paired):+.2f}")
|
||||||
|
failed |= len(by_build["l30"]) != 10 or len(by_build["l31"]) != 10
|
||||||
|
print(f"[L31CostCombined] pairs_per_recipe=10 runs={len(rows)} "
|
||||||
|
f"status={'FAIL' if failed else 'PASS'} summary={OUT / 'cost-combined.csv'}")
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 40 else 0)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare L25 and 24 m crossing hash records for matching recipe/compiler/workers."""
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
pattern = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
lines = []
|
||||||
|
failed = False
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
left = pattern.findall((OUT / f"l25-{compiler}-{recipe}-w{workers}.log").read_text())
|
||||||
|
right = pattern.findall((OUT / f"crossing24-{compiler}-{recipe}-w{workers}.log").read_text())
|
||||||
|
same = bool(left) and len(left) == 12 and left == right
|
||||||
|
failed |= not same
|
||||||
|
lines.append(f"{recipe},{compiler},W{workers},{'MATCH' if same else 'MISMATCH'}")
|
||||||
|
|
||||||
|
path = OUT / "determinism-crossing-compare.log"
|
||||||
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
print("\n".join(lines))
|
||||||
|
print(f"[L31CrossingHashCompare] comparisons={len(lines)} "
|
||||||
|
f"pass={sum(line.endswith('MATCH') for line in lines)} fail={sum(line.endswith('MISMATCH') for line in lines)}")
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Encode the six requested L31 screenshots as small, pinned JPEGs."""
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
SOURCE = ROOT / "Saved/Linux/l31"
|
||||||
|
DEST = Path(__file__).resolve().parent / "captures"
|
||||||
|
DEST.mkdir(parents=True, exist_ok=True)
|
||||||
|
names = (
|
||||||
|
"fi-column-top-close-v19",
|
||||||
|
"fi-column-top-wide-v19",
|
||||||
|
"fi-column-base-close-v19",
|
||||||
|
"fi-column-base-wide-v19",
|
||||||
|
"fi-floor-v19-tint-off",
|
||||||
|
"fi-floor-v19-tint-on",
|
||||||
|
)
|
||||||
|
failed = False
|
||||||
|
for name in names:
|
||||||
|
source = SOURCE / f"{name}.png"
|
||||||
|
destination = DEST / f"{name.removesuffix('-v19').replace('-v19-', '-')}.jpg"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "ffmpeg",
|
||||||
|
"-y", "-hide_banner", "-loglevel", "error", "-i", str(source),
|
||||||
|
"-frames:v", "1", "-q:v", "4", str(destination),
|
||||||
|
]
|
||||||
|
result = subprocess.run(command, check=False)
|
||||||
|
size = destination.stat().st_size if destination.exists() else 0
|
||||||
|
ok = result.returncode == 0 and size <= 300_000
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L31CaptureJPEG] source={source.name} output={destination.name} "
|
||||||
|
f"bytes={size} status={'PASS' if ok else 'FAIL'}")
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Measure surface heights and normals on an eastward FI floor radial line."""
|
||||||
|
import array
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def read_surface_vertices(path):
|
||||||
|
vertices = []
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid scene header")
|
||||||
|
version, voxel_um, count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.read(40)
|
||||||
|
if version != 1 or count > 100000:
|
||||||
|
raise SystemExit("unsupported scene")
|
||||||
|
for _ in range(count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
used = set()
|
||||||
|
for tri in range(0, index_count, 3):
|
||||||
|
if masks and masks[tri // 3]:
|
||||||
|
continue
|
||||||
|
for index in indices[tri:tri + 3]:
|
||||||
|
if index < surface_count:
|
||||||
|
used.add(index)
|
||||||
|
for index in used:
|
||||||
|
offset = index * 3
|
||||||
|
vertices.append((positions[offset], positions[offset + 1],
|
||||||
|
positions[offset + 2], normals[offset],
|
||||||
|
normals[offset + 1], normals[offset + 2]))
|
||||||
|
return voxel_um, vertices
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
scene = sys.argv[1]
|
||||||
|
center_x, center_y = 64.0, -32.0
|
||||||
|
voxel_um, vertices = read_surface_vertices(scene)
|
||||||
|
floor = [v for v in vertices if abs(v[2] + 119.5) < 0.05
|
||||||
|
and math.hypot(v[0] - center_x, v[1] - center_y) <= 16.5]
|
||||||
|
if not floor:
|
||||||
|
raise SystemExit("no floor surface vertices near expected FI bedrock top")
|
||||||
|
mean = tuple(sum(v[index] for v in floor) / len(floor) for index in (3, 4, 5))
|
||||||
|
normal_rms = math.sqrt(sum(sum((v[index] - mean[axis]) ** 2
|
||||||
|
for axis, index in enumerate((3, 4, 5)))
|
||||||
|
for v in floor) / len(floor))
|
||||||
|
min_z = min(v[2] for v in floor)
|
||||||
|
max_z = max(v[2] for v in floor)
|
||||||
|
print(f"[RingNormalField] voxel_um={voxel_um} center_m=({center_x:.3f},{center_y:.3f}) "
|
||||||
|
f"vertices={len(floor)} height_min_m={min_z:.6f} height_max_m={max_z:.6f} "
|
||||||
|
f"mean_normal=({mean[0]:.8f},{mean[1]:.8f},{mean[2]:.8f}) "
|
||||||
|
f"normal_rms={normal_rms:.9f}")
|
||||||
|
print("[RingNormalRadialLine] radius_m sampled_radius_m height_m nx ny nz")
|
||||||
|
for radius in range(1, 17):
|
||||||
|
x, y = center_x + radius, center_y
|
||||||
|
vertex = min(floor, key=lambda v: (v[0] - x) ** 2 + (v[1] - y) ** 2)
|
||||||
|
sampled_radius = math.hypot(vertex[0] - center_x, vertex[1] - center_y)
|
||||||
|
if math.hypot(vertex[0] - x, vertex[1] - y) > 0.5:
|
||||||
|
raise SystemExit(f"missing near vertex at radius {radius} m")
|
||||||
|
print(f"{radius:2d} {sampled_radius:.6f} {vertex[2]:.6f} "
|
||||||
|
f"{vertex[3]:.8f} {vertex[4]:.8f} {vertex[5]:.8f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rasterize upward mesh triangles into a top-down field height map."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
Q16 = 1 << 16
|
||||||
|
MASK64 = (1 << 64) - 1
|
||||||
|
|
||||||
|
|
||||||
|
def trunc_div(numerator, denominator):
|
||||||
|
return numerator // denominator if numerator >= 0 else -((-numerator) // denominator)
|
||||||
|
|
||||||
|
|
||||||
|
def transition_hash(seed, interface, x, y, z, salt):
|
||||||
|
value = 14695981039346656037
|
||||||
|
for word in (seed & 0xffffffff, interface & 0xffffffff,
|
||||||
|
x & MASK64, y & MASK64, z & MASK64, salt):
|
||||||
|
for byte in range(8):
|
||||||
|
value ^= (word >> (byte * 8)) & 0xff
|
||||||
|
value = (value * 1099511628211) & MASK64
|
||||||
|
value ^= value >> 30
|
||||||
|
value = (value * 0xbf58476d1ce4e5b9) & MASK64
|
||||||
|
value ^= value >> 27
|
||||||
|
value = (value * 0x94d049bb133111eb) & MASK64
|
||||||
|
value ^= value >> 31
|
||||||
|
return ((value >> 32) >> 15) - Q16
|
||||||
|
|
||||||
|
|
||||||
|
def transition_fade(value):
|
||||||
|
value = max(0, min(Q16, value))
|
||||||
|
square = value * value // Q16
|
||||||
|
return square * (3 * Q16 - 2 * value) // Q16
|
||||||
|
|
||||||
|
|
||||||
|
def gradient_height_um(layout, x_um, y_um):
|
||||||
|
upper = next(item for item in layout["strates"] if item["order"] == 0)
|
||||||
|
lower = next(item for item in layout["strates"] if item["order"] == 1)
|
||||||
|
width = upper["transition_width_um"]
|
||||||
|
cell = max(1_000_000, layout["strate_layout_chunk_size_um"] * 4)
|
||||||
|
ix = x_um // cell
|
||||||
|
iy = y_um // cell
|
||||||
|
tx = transition_fade(((x_um - ix * cell) * Q16) // cell)
|
||||||
|
ty = transition_fade(((y_um - iy * cell) * Q16) // cell)
|
||||||
|
|
||||||
|
def at(dx, dy):
|
||||||
|
noise = transition_hash(0, upper["strate_index"], ix + dx, iy + dy, 0,
|
||||||
|
0x47524144)
|
||||||
|
return noise
|
||||||
|
|
||||||
|
def lerp(a, b, t):
|
||||||
|
return a + trunc_div((b - a) * t, Q16)
|
||||||
|
|
||||||
|
low = lerp(lerp(at(0, 0), at(1, 0), tx),
|
||||||
|
lerp(at(0, 1), at(1, 1), tx), ty)
|
||||||
|
amplitude_divisor = 256 if upper.get("generator_kind") == "FloatingIslands" else 8
|
||||||
|
displacement = trunc_div(low * (width // amplitude_divisor), Q16)
|
||||||
|
seam = upper["bedrock"]["bottom_um"]
|
||||||
|
thickness = upper["bedrock"]["top_um"] - seam
|
||||||
|
return seam + displacement + thickness
|
||||||
|
|
||||||
|
|
||||||
|
def read_scene(path):
|
||||||
|
triangles = []
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid scene header")
|
||||||
|
version, voxel_um, count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.read(40)
|
||||||
|
if version != 1 or count > 100000:
|
||||||
|
raise SystemExit("unsupported scene")
|
||||||
|
for _ in range(count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
for index in range(0, index_count, 3):
|
||||||
|
if masks and masks[index // 3]:
|
||||||
|
continue
|
||||||
|
a, b, c = indices[index:index + 3]
|
||||||
|
if max(a, b, c) >= surface_vertices:
|
||||||
|
continue
|
||||||
|
nz = (normals[a * 3 + 2] + normals[b * 3 + 2]
|
||||||
|
+ normals[c * 3 + 2]) / 3.0
|
||||||
|
if nz < 0.35:
|
||||||
|
continue
|
||||||
|
vertices = tuple((positions[v * 3], positions[v * 3 + 1],
|
||||||
|
positions[v * 3 + 2]) for v in (a, b, c))
|
||||||
|
triangles.append(vertices)
|
||||||
|
return voxel_um, triangles
|
||||||
|
|
||||||
|
|
||||||
|
def rasterize(triangles, bounds, width, height):
|
||||||
|
x0, x1, y0, y1 = bounds
|
||||||
|
values = [None] * (width * height)
|
||||||
|
sx, sy = width / (x1 - x0), height / (y1 - y0)
|
||||||
|
for tri in triangles:
|
||||||
|
(ax, ay, az), (bx, by, bz), (cx, cy, cz) = tri
|
||||||
|
den = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
|
||||||
|
if abs(den) < 1.0e-12:
|
||||||
|
continue
|
||||||
|
left = max(0, int(math.floor((min(ax, bx, cx) - x0) * sx)))
|
||||||
|
right = min(width - 1, int(math.ceil((max(ax, bx, cx) - x0) * sx)))
|
||||||
|
top = max(0, int(math.floor((y1 - max(ay, by, cy)) * sy)))
|
||||||
|
bottom = min(height - 1, int(math.ceil((y1 - min(ay, by, cy)) * sy)))
|
||||||
|
for py in range(top, bottom + 1):
|
||||||
|
y = y1 - (py + 0.5) / sy
|
||||||
|
row = py * width
|
||||||
|
for px in range(left, right + 1):
|
||||||
|
x = x0 + (px + 0.5) / sx
|
||||||
|
u = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / den
|
||||||
|
v = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / den
|
||||||
|
w = 1.0 - u - v
|
||||||
|
if min(u, v, w) < -1.0e-6:
|
||||||
|
continue
|
||||||
|
z = u * az + v * bz + w * cz
|
||||||
|
idx = row + px
|
||||||
|
if values[idx] is None or z > values[idx]:
|
||||||
|
values[idx] = z
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("output_ppm")
|
||||||
|
parser.add_argument("--bounds", nargs=4, type=float, required=True,
|
||||||
|
metavar=("XMIN", "XMAX", "YMIN", "YMAX"))
|
||||||
|
parser.add_argument("--size", nargs=2, type=int, default=(640, 640))
|
||||||
|
parser.add_argument("--range", nargs=2, type=float, default=(-122.0, -118.0),
|
||||||
|
metavar=("ZMIN", "ZMAX"))
|
||||||
|
parser.add_argument("--ring-center", nargs=2, type=float,
|
||||||
|
metavar=("X", "Y"))
|
||||||
|
parser.add_argument("--probe-max-radius", type=int, default=16)
|
||||||
|
parser.add_argument("--layout")
|
||||||
|
args = parser.parse_args()
|
||||||
|
voxel_um, triangles = read_scene(args.scene)
|
||||||
|
width, height = args.size
|
||||||
|
sx, sy = width / (args.bounds[1] - args.bounds[0]), height / (args.bounds[3] - args.bounds[2])
|
||||||
|
values = rasterize(triangles, args.bounds, width, height)
|
||||||
|
finite = sorted(value for value in values if value is not None)
|
||||||
|
if not finite:
|
||||||
|
raise SystemExit("no upward-facing field samples in bounds")
|
||||||
|
low, high = args.range
|
||||||
|
if high <= low:
|
||||||
|
raise SystemExit("height range must increase")
|
||||||
|
stops = ((0.00, (31, 52, 92)), (0.24, (41, 119, 153)),
|
||||||
|
(0.50, (85, 177, 143)), (0.73, (226, 207, 105)),
|
||||||
|
(1.00, (181, 71, 49)))
|
||||||
|
with open(args.output_ppm, "wb") as output:
|
||||||
|
output.write(f"P6\n{width} {height}\n255\n".encode())
|
||||||
|
for value in values:
|
||||||
|
if value is None:
|
||||||
|
output.write(bytes((18, 22, 30)))
|
||||||
|
continue
|
||||||
|
t = max(0.0, min(1.0, (value - low) / (high - low)))
|
||||||
|
for index in range(len(stops) - 1):
|
||||||
|
if t <= stops[index + 1][0]:
|
||||||
|
ta, ca = stops[index]
|
||||||
|
tb, cb = stops[index + 1]
|
||||||
|
blend = (t - ta) / (tb - ta)
|
||||||
|
rgb = tuple(round(ca[c] + blend * (cb[c] - ca[c]))
|
||||||
|
for c in range(3))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rgb = stops[-1][1]
|
||||||
|
output.write(bytes(rgb))
|
||||||
|
def quantile(p):
|
||||||
|
return finite[round((len(finite) - 1) * p)]
|
||||||
|
print(f"[RingFieldRaster] voxel_um={voxel_um} triangles={len(triangles)} "
|
||||||
|
f"pixels={len(finite)}/{len(values)} height_min_m={finite[0]:.6f} "
|
||||||
|
f"height_p05_m={quantile(.05):.6f} height_median_m={quantile(.5):.6f} "
|
||||||
|
f"height_p95_m={quantile(.95):.6f} height_max_m={finite[-1]:.6f} "
|
||||||
|
f"height_range_m={finite[-1]-finite[0]:.6f} "
|
||||||
|
f"map={args.output_ppm}")
|
||||||
|
if args.ring_center:
|
||||||
|
cx, cy = args.ring_center
|
||||||
|
def at_world(x, y):
|
||||||
|
fx = (x - args.bounds[0]) * sx - 0.5
|
||||||
|
fy = (args.bounds[3] - y) * sy - 0.5
|
||||||
|
ix, iy = math.floor(fx), math.floor(fy)
|
||||||
|
tx, ty = fx - ix, fy - iy
|
||||||
|
samples = []
|
||||||
|
for px, py in ((ix, iy), (ix + 1, iy), (ix, iy + 1), (ix + 1, iy + 1)):
|
||||||
|
if not (0 <= px < width and 0 <= py < height):
|
||||||
|
return None
|
||||||
|
value = values[py * width + px]
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
samples.append(value)
|
||||||
|
return ((samples[0] * (1 - tx) + samples[1] * tx) * (1 - ty)
|
||||||
|
+ (samples[2] * (1 - tx) + samples[3] * tx) * ty)
|
||||||
|
print(f"[RingRadialProbe] center_m=({cx:.3f},{cy:.3f}) "
|
||||||
|
"radius_m mean_m angular_sd_m min_m max_m samples")
|
||||||
|
for radius in range(1, args.probe_max_radius + 1):
|
||||||
|
row = [at_world(cx + radius * math.cos(a * math.tau / 32),
|
||||||
|
cy + radius * math.sin(a * math.tau / 32))
|
||||||
|
for a in range(32)]
|
||||||
|
row = [value for value in row if value is not None]
|
||||||
|
if row:
|
||||||
|
mean = sum(row) / len(row)
|
||||||
|
sd = math.sqrt(sum((value - mean) ** 2 for value in row) / len(row))
|
||||||
|
print(f" {radius:>2} {mean:.6f} {sd:.6f} {min(row):.6f} "
|
||||||
|
f"{max(row):.6f} {len(row)}")
|
||||||
|
if args.layout:
|
||||||
|
layout = json.load(open(args.layout, encoding="utf-8"))
|
||||||
|
if next(item for item in layout["strates"] if item["order"] == 0)["transition_mode"] != "Gradient":
|
||||||
|
raise SystemExit("--layout must describe a Gradient owner interface")
|
||||||
|
errors = []
|
||||||
|
for index, actual in enumerate(values):
|
||||||
|
if actual is None:
|
||||||
|
continue
|
||||||
|
px, py = index % width, index // width
|
||||||
|
x = args.bounds[0] + (px + 0.5) / sx
|
||||||
|
y = args.bounds[3] - (py + 0.5) / sy
|
||||||
|
expected = gradient_height_um(layout, round(x * 1_000_000),
|
||||||
|
round(y * 1_000_000)) / 1_000_000.0
|
||||||
|
errors.append(actual - expected)
|
||||||
|
absolute = sorted(abs(error) for error in errors)
|
||||||
|
rms = math.sqrt(sum(error * error for error in errors) / len(errors))
|
||||||
|
print(f"[RingGradientMatch] samples={len(errors)} rms_m={rms:.6f} "
|
||||||
|
f"p95_abs_m={absolute[round((len(absolute)-1)*.95)]:.6f} "
|
||||||
|
f"max_abs_m={absolute[-1]:.6f} "
|
||||||
|
"expected=owner-resolved-2d-hash-value-noise")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Linux31 report
|
||||||
|
|
||||||
|
## FI column finding
|
||||||
|
|
||||||
|
The earlier L29/L30 passage table ran SurfaceWorld twice. On the real FloatingIslands recipe, the old column check under-counted connected ends because it treated support at the route foot as the column cap. The route foot is near z = −116 m; the actual lower cap is near z = −122 m, inside native bedrock whose top is z = −119.5 m. The upper caps sit inside anchor-island rock near z = −62 to −69 m. There is no floating column end or unintended gap. The open passage mouth is intentional.
|
||||||
|
|
||||||
|
The audit now samples each planned cap against the native density field. Negative density is solid rock. Every main column cap overlaps native rock within 0.25 m. The style 3 short lower cap overlaps within 0.50 m.
|
||||||
|
|
||||||
|
| FI style | Cap | Z (m) | Native overlap / samples | Minimum native density |
|
||||||
|
|---:|---|---:|---:|---:|
|
||||||
|
| 0 | Main upper / anchor island | −65.846 | 5,051 / 10,005 | −14.761719 |
|
||||||
|
| 0 | Main lower / bedrock | −122.390 | 6,149 / 12,588 | −8.000000 |
|
||||||
|
| 1 | Short upper | −61.574 | 9,725 / 11,033 | −23.464844 |
|
||||||
|
| 1 | Short lower | −75.637 | 3,915 / 11,500 | −15.554688 |
|
||||||
|
| 1 | Main upper / anchor island | −66.485 | 4,497 / 11,118 | −15.613281 |
|
||||||
|
| 1 | Main lower / bedrock | −122.416 | 6,673 / 13,735 | −8.000000 |
|
||||||
|
| 2 | Main upper / anchor island | −68.891 | 3,986 / 8,523 | −15.546875 |
|
||||||
|
| 2 | Main lower / bedrock | −122.382 | 5,396 / 10,885 | −8.000000 |
|
||||||
|
| 3 | Short upper | −65.805 | 5,095 / 10,064 | −14.671875 |
|
||||||
|
| 3 | Short lower | −80.054 | 2,498 / 10,413 | −11.910156 |
|
||||||
|
| 3 | Main upper / anchor island | −69.478 | 3,902 / 9,985 | −15.597656 |
|
||||||
|
| 3 | Main lower / bedrock | −122.436 | 6,130 / 12,453 | −8.000000 |
|
||||||
|
|
||||||
|
The negative control moves the style 0 lower cap into the air gap at z = −96.390 m: 0/3,120 native overlaps, minimum density +22.273438, and `detected=true`. The audit still catches a genuinely disconnected end.
|
||||||
|
|
||||||
|
## Recipe-stamped passage matrix
|
||||||
|
|
||||||
|
Each row is checked against the requested and resolved recipe stamp. The columns are capsule blocked/unsupported counts and mouth checks, open-air violations, naturalness, longest uncolumned span / 12.5 m limit, connected column ends, route bindings / failures, and walkability samples.
|
||||||
|
|
||||||
|
For all eight FI rows, `FloatingIslandRouteAudit` also passes: 78,064 capsule samples, 0 blocked, 0 unsupported, and 0 grade failures. The runner now asserts both capsule audits.
|
||||||
|
|
||||||
|
| Requested → actual | Style | Compiler | Capsule | Open air | Natural | Long bridge | Column ends | Bindings | Walkability |
|
||||||
|
|---|---:|---|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| surface-world → surface-world | 0 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 5642/5642 |
|
||||||
|
| surface-world → surface-world | 0 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 5642/5642 |
|
||||||
|
| surface-world → surface-world | 1 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1490/1490 |
|
||||||
|
| surface-world → surface-world | 1 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1490/1490 |
|
||||||
|
| surface-world → surface-world | 2 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1545/1545 |
|
||||||
|
| surface-world → surface-world | 2 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1545/1545 |
|
||||||
|
| surface-world → surface-world | 3 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1495/1495 |
|
||||||
|
| surface-world → surface-world | 3 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1495/1495 |
|
||||||
|
| floating-islands → floating-islands | 0 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.464/12.500 m | 2/2 | 2 (0 failures) | 4000/4000 |
|
||||||
|
| floating-islands → floating-islands | 0 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.464/12.500 m | 2/2 | 2 (0 failures) | 4000/4000 |
|
||||||
|
| floating-islands → floating-islands | 1 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 3.851/12.500 m | 4/4 | 2 (0 failures) | 1047/1047 |
|
||||||
|
| floating-islands → floating-islands | 1 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 3.851/12.500 m | 4/4 | 2 (0 failures) | 1047/1047 |
|
||||||
|
| floating-islands → floating-islands | 2 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 2 (0 failures) | 1135/1135 |
|
||||||
|
| floating-islands → floating-islands | 2 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 2 (0 failures) | 1135/1135 |
|
||||||
|
| floating-islands → floating-islands | 3 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.243/12.500 m | 4/4 | 2 (0 failures) | 1061/1061 |
|
||||||
|
| floating-islands → floating-islands | 3 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.243/12.500 m | 4/4 | 2 (0 failures) | 1061/1061 |
|
||||||
|
|
||||||
|
All 16 rows pass. The runner rejects a recipe mismatch. Its negative control requests SurfaceWorld while the resolved layout is FloatingIslands; the log records `requested=surface-world actual=floating-islands status=FAIL` and the process exits 2. Logs and the machine-readable matrix are under `Saved/Linux/l31/`.
|
||||||
|
|
||||||
|
## FI floor lines
|
||||||
|
|
||||||
|
The L30 rings persisted with height tint off, so they were geometry. Evaluator v19 reduces FloatingIslands Gradient displacement to `transition_width / 256` (one quarter voxel at the 16 m transition width), and the layout validator uses the same bound. The v18 one-voxel cap still left a 0.251819 m radial height range and a visible contour.
|
||||||
|
|
||||||
|
On the east radial line from (64, −32), samples at radii 1–16 m all have height −119.500000 m and normal (0, 0, 1). The extended mesh raster has a 0.000479 m total height range. The readable shader now applies a cubic smooth tint (`t²(3−2t)`). Tint-off and tint-on captures show a flat floor without contour bands.
|
||||||
|
|
||||||
|
## Captures and hashes
|
||||||
|
|
||||||
|
All six JPEGs are recipe-stamped from FloatingIslands and under 300 KB.
|
||||||
|
|
||||||
|
| Capture | Size | SHA-256 |
|
||||||
|
|---|---:|---|
|
||||||
|
| [FI column top, close](captures/fi-column-top-close.jpg) | 85,696 B | `6b651655fc2127bb5b3502a1a99c024167db2edc21bfe7241f6335a858bb8575` |
|
||||||
|
| [FI column top, wide](captures/fi-column-top-wide.jpg) | 67,043 B | `926193affd463492095b97310c62416910e12fc94e75d8548a01ad1f9601b6ec` |
|
||||||
|
| [FI column bedrock base, close](captures/fi-column-base-close.jpg) | 60,491 B | `3e524e0f6c506413b4b5b29d8835792f8b5240f95787c6a08d12ebe6e46aadd5` |
|
||||||
|
| [FI column bedrock base, wide](captures/fi-column-base-wide.jpg) | 61,543 B | `27ef1a72874c7f8e257969c5e69d614f336edce84693ee80b98783b9f610a4b2` |
|
||||||
|
| [FI floor, tint off](captures/fi-floor-tint-off.jpg) | 43,405 B | `41b9180c9696c7025e23009e6efdb2e86ace5900b08c293c92371797f9a8596d` |
|
||||||
|
| [FI floor, tint on](captures/fi-floor-tint-on.jpg) | 43,405 B | `0672e2ddc93fb6a4fe4f6adfdf4cdee896bdbc90dddfb7cf65050d978f86f32b` |
|
||||||
|
|
||||||
|
Evaluator version is 19. Binary SHA-256:
|
||||||
|
|
||||||
|
- Clang: `a796d433b7e31fa05abba970dbd486db1a904d13222d9c02b9054c394375e4f7`
|
||||||
|
- GCC: `c2ed12ddf4db5ce97fcd2d9c8f32e4515a4fba6d36d4d8a4b5fbb07a8ab8ae73`
|
||||||
|
|
||||||
|
## Determinism and cost
|
||||||
|
|
||||||
|
The L25 command and 24 m crossing each passed 12 cases: Clang and GCC at W1/W8/W12 for both recipes. All 12 hash records per log were stable across compilers and worker counts. SW matches the L30 baseline; FI changes as expected with the v19 geometry. L25 and crossing hashes match for all 12 recipe/compiler/worker combinations.
|
||||||
|
|
||||||
|
| Recipe | Full-detail density / mesh / payload / topology | Whole-view density / mesh / payload / topology |
|
||||||
|
|---|---|---|
|
||||||
|
| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `0e8c5af01df96b14 / 70099e89b77811ed / dbce0fdcf6764126 / ddaf81a1228f9b4b` |
|
||||||
|
| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `52e0b9f95da4291e / 7c6e9e18346278a4 / 59160fe3df02a0ae / ef7f17088fe3253e` |
|
||||||
|
|
||||||
|
Cost compared the Clang L31 binary with the L30 `linux30-speed-fade` binary: 10 interleaved pairs per recipe, 40 runs total, with zero cache hits. Combined median whole-view time was 1.772 s → 1.784 s for SW (+0.71%) and 17.468 s → 17.482 s for FI (+0.08%). The second five-pair batch was +1.20% SW and +0.01% FI; the larger early SW deltas settled as runs warmed. The paired medians across all 10 were +1.86% SW and +0.55% FI, within the observed run variation.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- Five low-clip floor exports (`export-fi-floor.log`, `export-fi-floor-probe.log`, `export-fi-floor-without-mesh.log`, `export-fi-floor-probe-retry.log`, `fi-floor-no-camera-probe.log`) stopped with `std::bad_alloc`. Export succeeded with `--max-clip-level=8 --max-level=0`.
|
||||||
|
- A first radial analyzer invocation omitted the second `--range` value and was rejected by argparse; it was rerun with both bounds.
|
||||||
|
- A proof/parity probe used custom camera positions and failed SW parity because the sampled tile set differed from the established default-camera audit. That probe was stopped; the default-camera proof and transition suite passed 16/16.
|
||||||
|
- The first determinism launch ended before completing the matrix. The full 24-case run passed on retry. A first cost batch had an early SW timing outlier, so five additional interleaved pairs per recipe were run and retained in the combined summary.
|
||||||
|
- Early capture framing probes cropped the FI axis or placed the camera inside rock. A v19 wide floor view exposed only the export skirt edge; the final centered column and square top-down floor views were used.
|
||||||
|
- The first GCC build attempt was interrupted at 19 objects and retried successfully. One v18 diagnostic build used the default Standalone build path; those artifacts were removed, and final v19 build outputs are under `Saved/Linux/l31/`.
|
||||||
|
- Two one-line post-processing commands had quoting/pair-index errors; the checked-in `CompareDeterminismL31.py` and `AggregateCostL31.py` helpers produced the final passing summaries.
|
||||||
|
|
||||||
|
No run exceeded 30 minutes or remained stuck for 15 minutes. No Unreal files were edited. Nothing was committed, pushed, or stashed.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged as an intermediate step: correct FI column-cap audit (with negative control), a recipe-stamped runner that
|
||||||
|
rejects mismatches, all 16 recipe/style/compiler rows passing, smooth shader tint, determinism.
|
||||||
|
- **Not accepted as the final floor fix:** v19 caps the FI Gradient displacement at `width/256` (≈6 cm), which
|
||||||
|
removes the owner's Gradient transition rather than making it smooth; the floor is now flat (radial
|
||||||
|
height range 0.0005 m). The real fix is a smooth, non-terraced undulation at the L29 amplitude (≈4 m
|
||||||
|
peak-to-peak). L32 restores it.
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the recipe-stamped L31 passage audit matrix and reject mislabeled runs."""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc",
|
||||||
|
}
|
||||||
|
MARKERS = (
|
||||||
|
"[ResolvedPassageAuditSummary]",
|
||||||
|
"[PassageOpenAirAuditSummary]",
|
||||||
|
"[PassageNaturalnessSummary]",
|
||||||
|
"[PassageNoLongBridgeAudit]",
|
||||||
|
"[PassageRockColumnAuditSummary]",
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for style in range(4):
|
||||||
|
data = ROOT / f"Saved/Linux/linux25-owner-{recipe}-style{style}.json"
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"passage-{recipe}-style{style}-{compiler}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
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, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, 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)
|
||||||
|
recipe_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
markers_ok = all(re.search(re.escape(marker) + r".*status=PASS", text)
|
||||||
|
for marker in MARKERS)
|
||||||
|
capsule_ok = bool(re.search(
|
||||||
|
r"^\[ResolvedPassageAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 mouth_content=2/2 mouth_radius=2/2 "
|
||||||
|
r"walkability=PASS$", text, re.M))
|
||||||
|
island_route_ok = (recipe != "floating-islands" or bool(re.search(
|
||||||
|
r"^\[FloatingIslandRouteAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 status=PASS$", text, re.M)))
|
||||||
|
binding_ok = bool(re.search(
|
||||||
|
r"\[ResolvedPassageAuditSummary\].*route_bindings=[1-9][0-9]* "
|
||||||
|
r"route_binding_failures=0 .*status=PASS", text))
|
||||||
|
walk_ok = "[Walkability] PASS" in text
|
||||||
|
if recipe == "floating-islands":
|
||||||
|
control_ok = "[PassageColumnDisconnectedEndControl]" in text and bool(
|
||||||
|
re.search(r"\[PassageColumnDisconnectedEndControl\].*detected=true", text))
|
||||||
|
else:
|
||||||
|
control_ok = True
|
||||||
|
ok = (result is not None and result.returncode == 0 and recipe_ok
|
||||||
|
and markers_ok and capsule_ok and island_route_ok and binding_ok
|
||||||
|
and walk_ok and control_ok)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "MISSING"
|
||||||
|
rows.append((recipe, actual_recipe, style, compiler, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s exit={result.returncode if result else 'TIMEOUT'} "
|
||||||
|
f"recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"capsule={'PASS' if capsule_ok and island_route_ok else 'FAIL'} "
|
||||||
|
f"column_control={'PASS' if control_ok else 'FAIL'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
if not ok:
|
||||||
|
failed = True
|
||||||
|
|
||||||
|
summary = OUT / "passage-audit-matrix.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"requested_recipe,actual_recipe,style,compiler,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{requested},{actual},{style},{compiler},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for requested, actual, style, compiler, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
control_log = OUT / "recipe-mismatch-control.log"
|
||||||
|
control_owner = OUT / "recipe-mismatch-control-owner.json"
|
||||||
|
control_owner_data = json.loads(
|
||||||
|
(ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json").read_text(encoding="utf-8"))
|
||||||
|
top_owner_stratum = max(control_owner_data["strates"], key=lambda stratum: stratum["top_chunk_z"])
|
||||||
|
top_owner_stratum["asset"]["GeneratorType"] = 6 # ECaveGeneratorType::FloatingIslands
|
||||||
|
control_owner.write_text(json.dumps(control_owner_data, separators=(",", ":")), encoding="utf-8")
|
||||||
|
control_command = [
|
||||||
|
str(BINARIES["clang"]),
|
||||||
|
str(control_owner),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", "--recipe=surface-world",
|
||||||
|
"--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
with control_log.open("w", encoding="utf-8") as stream:
|
||||||
|
control = subprocess.run(control_command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
control_text = control_log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
control_ok = (control.returncode == 2 and
|
||||||
|
"[WorldAssemblerRecipeAudit] requested=surface-world actual=floating-islands "
|
||||||
|
"top_generator=FloatingIslands " in control_text and
|
||||||
|
"floating_islands_stratum=present status=FAIL" in control_text)
|
||||||
|
print(f"[L31RecipeMismatchControl] requested=surface-world actual=floating-islands "
|
||||||
|
f"exit={control.returncode} detected={'true' if control_ok else 'false'} "
|
||||||
|
f"{'PASS' if control_ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L31PassageMatrix] 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 not control_ok or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run five interleaved W12 whole-view pairs against the final L30 binary."""
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"l30": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade",
|
||||||
|
"l31": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
}
|
||||||
|
ORDERS = (("l30", "l31"), ("l31", "l30"), ("l30", "l31"),
|
||||||
|
("l31", "l30"), ("l30", "l31"))
|
||||||
|
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 = [
|
||||||
|
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, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
if build == "l31":
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "missing"
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
else:
|
||||||
|
configured = re.findall(
|
||||||
|
r"^\[HeadlessConfig\].*\brecipe=([^\s]+)", text, re.M)
|
||||||
|
actual_recipe = configured[0] if len(configured) == 1 else "missing"
|
||||||
|
floating_plan = "[FloatingIslandWorldAssembly]" in text
|
||||||
|
surface_plan = "[WorldAssemblerPrototype]" in text
|
||||||
|
plan_matches = ((actual_recipe == "floating-islands" and floating_plan)
|
||||||
|
or (actual_recipe == "surface-world" and not floating_plan
|
||||||
|
and surface_plan))
|
||||||
|
marker = (f"[L31BaselineRecipeAudit] requested={recipe} actual={actual_recipe} "
|
||||||
|
f"headless_recipe={configured[0] if len(configured) == 1 else 'missing'} "
|
||||||
|
f"floating_island_plan={'present' if floating_plan else 'absent'} "
|
||||||
|
f"surface_world_plan={'present' if surface_plan else 'absent'} "
|
||||||
|
f"status={'PASS' if actual_recipe == recipe and plan_matches else 'FAIL'}\n")
|
||||||
|
with logfile.open("a", encoding="utf-8") as stream:
|
||||||
|
stream.write(marker)
|
||||||
|
text += marker
|
||||||
|
stamp_ok = (actual_recipe == recipe and plan_matches
|
||||||
|
and len(configured) == 1)
|
||||||
|
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
and "hash=off" in text and "cache_hits=0" in text)
|
||||||
|
if build == "l31":
|
||||||
|
valid = valid and stamp_ok and actual_recipe == recipe
|
||||||
|
else:
|
||||||
|
valid = valid and stamp_ok and "status=PASS" in marker
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
summary = OUT / "cost-runs.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
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}
|
||||||
|
l30 = statistics.median(by_build["l30"])
|
||||||
|
l31 = statistics.median(by_build["l31"])
|
||||||
|
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["l31"] - values["l30"]) / values["l30"] * 100.0)
|
||||||
|
print(f"[L31CostSummary] recipe={recipe} pairs=5 median_l30_s={l30:.3f} "
|
||||||
|
f"median_l31_s={l31:.3f} median_change_pct={(l31/l30-1)*100.0:+.2f} "
|
||||||
|
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L31Cost] 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,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Re-run the L25 hash command and 24 m crossing on the L31 compiler matrix."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc",
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
candidate_reference = {}
|
||||||
|
fi_hash_changes = {"l25": False, "crossing24": False}
|
||||||
|
|
||||||
|
|
||||||
|
def hashes(path):
|
||||||
|
return HASH_PATTERN.findall(path.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
|
||||||
|
|
||||||
|
def run(kind, compiler, recipe, workers):
|
||||||
|
global failed
|
||||||
|
name = f"{kind}-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
str(BINARIES[compiler]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--far-horizon-lod8-step=0", f"--workers={workers}", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
if kind == "crossing24":
|
||||||
|
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, 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)
|
||||||
|
current = hashes(logfile)
|
||||||
|
# The normal L30 log stem is ringfix-hash; crossing uses ringfix-crossing24.
|
||||||
|
baseline = ROOT / "Saved/Linux/l30" / (
|
||||||
|
f"ringfix-crossing24-{compiler}-{recipe}-w{workers}.log" if kind == "crossing24"
|
||||||
|
else f"ringfix-hash-{compiler}-{recipe}-w{workers}.log")
|
||||||
|
reference = hashes(baseline) if baseline.is_file() else []
|
||||||
|
key = (kind, recipe)
|
||||||
|
prior = candidate_reference.get(key)
|
||||||
|
stable = prior is None or current == prior
|
||||||
|
if prior is None and current:
|
||||||
|
candidate_reference[key] = current
|
||||||
|
baseline_match = bool(reference) and current == reference
|
||||||
|
baseline_changed = bool(reference) and current != reference
|
||||||
|
if recipe == "surface-world":
|
||||||
|
baseline_ok = baseline_match
|
||||||
|
comparison = "MATCH" if baseline_match else "MISMATCH"
|
||||||
|
else:
|
||||||
|
baseline_ok = baseline_changed
|
||||||
|
comparison = "EXPECTED_CHANGE" if baseline_changed else "UNCHANGED_OR_MISSING"
|
||||||
|
fi_hash_changes[kind] |= baseline_changed
|
||||||
|
ok = (result is not None and result.returncode == 0
|
||||||
|
and stamps == [(recipe, recipe, "PASS")]
|
||||||
|
and "hash=on" in text and len(current) == 12 and stable and baseline_ok)
|
||||||
|
rows.append((kind, recipe, compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", len(current), ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"hashes={len(current)} "
|
||||||
|
f"L30={comparison} candidate_stable={'yes' if stable else 'no'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
if not ok:
|
||||||
|
failed = True
|
||||||
|
|
||||||
|
|
||||||
|
for kind in ("l25", "crossing24"):
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
run("crossing24" if kind == "crossing24" else "l25", compiler, recipe, workers)
|
||||||
|
|
||||||
|
summary = OUT / "determinism.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"kind,recipe,compiler,workers,elapsed_seconds,exit_code,hash_rows,status\n" +
|
||||||
|
"".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code},{count},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for kind, recipe, compiler, workers, elapsed, code, count, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
for kind, changed in fi_hash_changes.items():
|
||||||
|
if not changed:
|
||||||
|
failed = True
|
||||||
|
print(f"[L31ExpectedGeometryHashChange] kind={kind} detected=false FAIL", flush=True)
|
||||||
|
else:
|
||||||
|
print(f"[L31ExpectedGeometryHashChange] kind={kind} detected=true PASS", flush=True)
|
||||||
|
print(f"[L31Determinism] 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,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run L31 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/l31"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc",
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, binary, data, recipe, extra, checks):
|
||||||
|
global failed
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
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=1800, 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"[L31InvariantAudits] 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,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract the validated recipe-stamped passage matrix into a compact table."""
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
rows = []
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for style in range(4):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
name = f"passage-{recipe}-style{style}-{compiler}"
|
||||||
|
path = OUT / f"{name}.log"
|
||||||
|
text = path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
def match(pattern):
|
||||||
|
found = re.search(pattern, text, re.M)
|
||||||
|
if not found:
|
||||||
|
raise SystemExit(f"{path}: missing metric {pattern}")
|
||||||
|
return found
|
||||||
|
|
||||||
|
stamp = match(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=(PASS|FAIL)$")
|
||||||
|
capsule = match(
|
||||||
|
r"^\[ResolvedPassageAudit\].*capsule_samples=(\d+) blocked=(\d+) "
|
||||||
|
r"support_samples=(\d+) unsupported=(\d+) grade_failures=(\d+) "
|
||||||
|
r"mouth_content=([^ ]+) mouth_radius=([^ ]+) .*$")
|
||||||
|
air = match(r"^\[PassageOpenAirAuditSummary\].*violations=(\d+).*status=(PASS|FAIL)$")
|
||||||
|
natural = match(r"^\[PassageNaturalnessSummary\] passed=(\d+)/(\d+).*$")
|
||||||
|
bridge = match(
|
||||||
|
r"^\[PassageNoLongBridgeAudit\].*longest_uncolumned_m=([0-9.]+) "
|
||||||
|
r"threshold_m=([0-9.]+).*$")
|
||||||
|
column = match(
|
||||||
|
r"^\[PassageRockColumnAuditSummary\] columns=(\d+) connected_ends=([^ ]+) "
|
||||||
|
r"short_spans=(\d+) wall_or_support_failures=(\d+).*status=(PASS|FAIL)$")
|
||||||
|
binding = match(
|
||||||
|
r"^\[ResolvedPassageAuditSummary\].*route_bindings=(\d+) "
|
||||||
|
r"route_binding_failures=(\d+).*status=(PASS|FAIL)$")
|
||||||
|
walk = match(r"^\[Walkability\] PASS (\d+)/(\d+) passage centerline samples")
|
||||||
|
rows.append({
|
||||||
|
"requested": stamp.group(1), "actual": stamp.group(2), "stamp": stamp.group(3),
|
||||||
|
"style": style, "compiler": compiler,
|
||||||
|
"capsule_samples": int(capsule.group(1)), "blocked": int(capsule.group(2)),
|
||||||
|
"support_samples": int(capsule.group(3)),
|
||||||
|
"unsupported": int(capsule.group(4)), "grade_failures": int(capsule.group(5)),
|
||||||
|
"mouth_content": capsule.group(6), "mouth_radius": capsule.group(7),
|
||||||
|
"air_violations": int(air.group(1)), "air_status": air.group(2),
|
||||||
|
"natural_passed": natural.group(1),
|
||||||
|
"natural_total": natural.group(2),
|
||||||
|
"longest_uncolumned_m": bridge.group(1), "bridge_threshold_m": bridge.group(2),
|
||||||
|
"column_count": int(column.group(1)), "connected_ends": column.group(2),
|
||||||
|
"short_spans": int(column.group(3)), "column_failures": int(column.group(4)),
|
||||||
|
"route_bindings": int(binding.group(1)),
|
||||||
|
"binding_failures": int(binding.group(2)),
|
||||||
|
"walk_samples": int(walk.group(1)), "walk_total": int(walk.group(2)),
|
||||||
|
"log": path.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
with (OUT / "passage-audit-metrics.csv").open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"| Requested → actual | Style | Compiler | Capsule | Open air | Natural | Long bridge | Column ends | Bindings | Walkability |",
|
||||||
|
"|---|---:|---|---|---:|---:|---:|---:|---:|---:|",
|
||||||
|
]
|
||||||
|
for row in rows:
|
||||||
|
ok = (row["requested"] == row["actual"] and row["stamp"] == "PASS"
|
||||||
|
and row["blocked"] == row["unsupported"] == row["grade_failures"] == 0
|
||||||
|
and row["mouth_content"] == row["mouth_radius"] == "2/2"
|
||||||
|
and row["air_violations"] == 0 and row["air_status"] == "PASS"
|
||||||
|
and row["natural_passed"] == row["natural_total"] == "1"
|
||||||
|
and float(row["longest_uncolumned_m"]) <= float(row["bridge_threshold_m"])
|
||||||
|
and row["connected_ends"] == f"{2 * row['column_count']}/{2 * row['column_count']}"
|
||||||
|
and row["column_failures"] == 0 and row["binding_failures"] == 0
|
||||||
|
and row["route_bindings"] > 0 and row["walk_samples"] == row["walk_total"])
|
||||||
|
if not ok:
|
||||||
|
raise SystemExit(f"{row['log']}: metric audit failed")
|
||||||
|
cells = (
|
||||||
|
f"{row['blocked']}/{row['unsupported']} blocked/unsupported; mouth 2/2",
|
||||||
|
f"{row['air_violations']} violations",
|
||||||
|
f"{row['natural_passed']}/{row['natural_total']}",
|
||||||
|
f"{float(row['longest_uncolumned_m']):.3f}/{row['bridge_threshold_m']} m",
|
||||||
|
row["connected_ends"],
|
||||||
|
f"{row['route_bindings']} (0 failures)",
|
||||||
|
f"{row['walk_samples']}/{row['walk_total']}",
|
||||||
|
)
|
||||||
|
lines.append(f"| {row['requested']} → {row['actual']} | {row['style']} | {row['compiler']} | "
|
||||||
|
+ " | ".join(cells) + " |")
|
||||||
|
|
||||||
|
table = OUT / "passage-audit-table.md"
|
||||||
|
table.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
print(f"[L31PassageAuditTable] rows={len(rows)} pass={len(rows)} "
|
||||||
|
f"csv={OUT / 'passage-audit-metrics.csv'} markdown={table}")
|
||||||
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Audit the displaced floor across the passage-mouth fade edge."""
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import resource
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import AuditRingTerraceL32 as ring
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, required=True)
|
||||||
|
parser.add_argument("--lod", type=int, default=0)
|
||||||
|
parser.add_argument("--inner-radius-m", type=float, default=5.5)
|
||||||
|
parser.add_argument("--outer-radius-m", type=float, default=9.5)
|
||||||
|
parser.add_argument("--step-m", type=float, default=0.125)
|
||||||
|
parser.add_argument("--height-min-m", type=float, default=-122.5)
|
||||||
|
parser.add_argument("--height-max-m", type=float, default=-117.5)
|
||||||
|
parser.add_argument("--preferred-height-m", type=float, default=-119.5)
|
||||||
|
parser.add_argument("--highest-surface", action="store_true",
|
||||||
|
help="choose the topmost upward face inside the height window")
|
||||||
|
parser.add_argument("--plateau-min-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--step-threshold-m", type=float, default=0.1)
|
||||||
|
parser.add_argument("--normal-jump-max-deg", type=float, default=20.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if (args.inner_radius_m < 0.0 or args.outer_radius_m <= args.inner_radius_m
|
||||||
|
or args.step_m <= 0.0):
|
||||||
|
parser.error("invalid radial interval or sample spacing")
|
||||||
|
|
||||||
|
cx, cy = args.center
|
||||||
|
preferred_height = None if args.highest_surface else args.preferred_height_m
|
||||||
|
bounds = (cx - args.outer_radius_m, cx + args.outer_radius_m,
|
||||||
|
cy - args.outer_radius_m, cy + args.outer_radius_m)
|
||||||
|
voxel_um, triangles, selected_tiles = ring.read_scene(
|
||||||
|
args.scene, args.lod, args.height_min_m, args.height_max_m, bounds)
|
||||||
|
bucket_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << args.lod))
|
||||||
|
buckets = ring.build_buckets(triangles, True, bounds, bucket_size)
|
||||||
|
|
||||||
|
count = round((args.outer_radius_m - args.inner_radius_m) / args.step_m)
|
||||||
|
lines = []
|
||||||
|
for angle_degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||||
|
angle = math.radians(angle_degrees)
|
||||||
|
points = [(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||||
|
for radius in (args.inner_radius_m + i * args.step_m
|
||||||
|
for i in range(count + 1))]
|
||||||
|
lines.append((f"radial-{angle_degrees:.0f}deg", points))
|
||||||
|
|
||||||
|
# Tangential sections stay outside the open mouth while crossing the outer
|
||||||
|
# fade edge on both sides of the 6.5 m center radius.
|
||||||
|
tangent_count = round(8.0 / args.step_m)
|
||||||
|
tangent_values = [-4.0 + i * args.step_m for i in range(tangent_count + 1)]
|
||||||
|
for angle_degrees in (225.0, 315.0):
|
||||||
|
angle = math.radians(angle_degrees)
|
||||||
|
normal = (math.cos(angle), math.sin(angle))
|
||||||
|
tangent = (-normal[1], normal[0])
|
||||||
|
points = [(cx + 6.5 * normal[0] + t * tangent[0],
|
||||||
|
cy + 6.5 * normal[1] + t * tangent[1])
|
||||||
|
for t in tangent_values]
|
||||||
|
lines.append((f"straight-tangent-{angle_degrees:.0f}deg", points))
|
||||||
|
|
||||||
|
failed = False
|
||||||
|
max_plateau = 0.0
|
||||||
|
max_step = 0.0
|
||||||
|
max_normal = 0.0
|
||||||
|
min_coverage = 1.0
|
||||||
|
for name, points in lines:
|
||||||
|
profile = ring.audit_profile(points, buckets, True, args.plateau_min_m,
|
||||||
|
0.0005, args.step_threshold_m, bucket_size, preferred_height)
|
||||||
|
samples = [ring.sample_surface(buckets, x, y, True, bucket_size,
|
||||||
|
preferred_height)
|
||||||
|
for x, y in points]
|
||||||
|
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 > args.normal_jump_max_deg:
|
||||||
|
px, py = points[index - 1]
|
||||||
|
x, y = points[index]
|
||||||
|
print(f"[PassageFadeNormalJump] line={name} "
|
||||||
|
f"radius_m={math.hypot(x-cx,y-cy):.3f} "
|
||||||
|
f"from_m=({px:.3f},{py:.3f},{prior[0]:.3f}) "
|
||||||
|
f"to_m=({x:.3f},{y:.3f},{current[0]:.3f}) "
|
||||||
|
f"normal_delta_deg={delta:.3f}")
|
||||||
|
coverage = profile["coverage"]
|
||||||
|
min_coverage = min(min_coverage, coverage)
|
||||||
|
plateau = max((run[0] for run in profile["plateaus"]), default=0.0)
|
||||||
|
step = max((run[1] for run in profile["plateaus"]), default=0.0)
|
||||||
|
angle = profile["max_angle"]
|
||||||
|
max_plateau = max(max_plateau, plateau)
|
||||||
|
max_step = max(max_step, step)
|
||||||
|
max_normal = max(max_normal, angle)
|
||||||
|
ok = (profile["valid"] and plateau < args.plateau_min_m
|
||||||
|
and angle <= args.normal_jump_max_deg)
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[PassageFadeProfile] line={name} coverage={coverage:.3f} "
|
||||||
|
f"height_range_m={profile.get('height_range', 0.0):.6f} "
|
||||||
|
f"flat_step_runs={len(profile['plateaus'])} "
|
||||||
|
f"max_normal_delta_deg={angle:.6f} status={'PASS' if ok else 'FAIL'}")
|
||||||
|
|
||||||
|
passed = not failed and min_coverage >= 0.95
|
||||||
|
print(f"[PassageFadeRingAudit] scene={args.scene} lod={args.lod} "
|
||||||
|
f"center_m=({cx:.6f},{cy:.6f}) mouth_radius_m=4.500 "
|
||||||
|
f"fade_m=5.500..7.500 radial_m={args.inner_radius_m:.3f}.."
|
||||||
|
f"{args.outer_radius_m:.3f} lines={len(lines)} selected_tiles={selected_tiles} "
|
||||||
|
f"coverage_min={min_coverage:.3f} longest_flat_run_m={max_plateau:.6f} "
|
||||||
|
f"following_step_m={max_step:.6f} max_normal_delta_deg={max_normal:.6f} "
|
||||||
|
f"thresholds=(coverage>=0.950,plateau<0.500,step>=0.100,normal<=20.000) "
|
||||||
|
f"status={'PASS' if passed else 'FAIL'}")
|
||||||
|
print(f"[PassageFadeRSS] peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Audit several bounded face windows and combine their height range."""
|
||||||
|
import argparse
|
||||||
|
import gc
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import resource
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, action="append", required=True,
|
||||||
|
help="repeat for each bounded local ring")
|
||||||
|
parser.add_argument("--radius", type=float, default=8.0)
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
parser.add_argument("--preferred-height-m", type=float,
|
||||||
|
help="lock overlapping-face selection near this expected surface height")
|
||||||
|
parser.add_argument("--step", type=float, default=0.125)
|
||||||
|
parser.add_argument("--plateau-min-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--plateau-epsilon-m", type=float, default=0.0005)
|
||||||
|
parser.add_argument("--step-threshold-m", type=float, default=0.1)
|
||||||
|
parser.add_argument("--normal-jump-max-deg", type=float, default=20.0)
|
||||||
|
parser.add_argument("--amplitude-min-m", type=float, default=3.0)
|
||||||
|
parser.add_argument("--min-radial-lines-per-center", type=int, default=4)
|
||||||
|
parser.add_argument("--min-straight-lines-per-center", type=int, default=2)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
audit_path = Path(__file__).with_name("AuditRingTerraceL32.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_audit_l32_set", audit_path)
|
||||||
|
audit = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(audit)
|
||||||
|
|
||||||
|
upward = args.surface == "floor"
|
||||||
|
preferred_height = args.preferred_height_m
|
||||||
|
if preferred_height is None:
|
||||||
|
preferred_height = args.height_max_m if upward else args.height_min_m
|
||||||
|
all_ranges = []
|
||||||
|
overall_max_normal = 0.0
|
||||||
|
overall_max_plateau = 0.0
|
||||||
|
overall_max_step = 0.0
|
||||||
|
overall_min_coverage = 1.0
|
||||||
|
total_valid_lines = 0
|
||||||
|
total_lines = 0
|
||||||
|
failed = False
|
||||||
|
for center in args.center:
|
||||||
|
bounds = (center[0] - args.radius, center[0] + args.radius,
|
||||||
|
center[1] - args.radius, center[1] + args.radius)
|
||||||
|
voxel_um, triangles, selected_tiles = audit.read_scene(
|
||||||
|
args.scene, args.lod, args.height_min_m, args.height_max_m, bounds)
|
||||||
|
max_lod = max((triangle[0] for triangle in triangles), default=0)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << max_lod))
|
||||||
|
buckets = audit.build_buckets(triangles, upward, bounds, cell_size)
|
||||||
|
results = []
|
||||||
|
for name, points in audit.make_lines(*center, args.radius, args.step):
|
||||||
|
result = audit.audit_profile(points, buckets, upward,
|
||||||
|
args.plateau_min_m, args.plateau_epsilon_m,
|
||||||
|
args.step_threshold_m, cell_size, preferred_height)
|
||||||
|
results.append((name, points, result))
|
||||||
|
|
||||||
|
radial = [row for row in results if row[0].startswith("radial-") and row[2]["valid"]]
|
||||||
|
straight = [row for row in results if row[0].startswith("straight-") and row[2]["valid"]]
|
||||||
|
valid = [row for row in results if row[2]["valid"]]
|
||||||
|
local_min = min((row[2]["height_min"] for row in valid), default=float("inf"))
|
||||||
|
local_max = max((row[2]["height_max"] for row in valid), default=float("-inf"))
|
||||||
|
local_amplitude = local_max - local_min
|
||||||
|
local_plateau = max((run[0] for row in valid for run in row[2]["plateaus"]), default=0.0)
|
||||||
|
local_step = max((run[1] for row in valid for run in row[2]["plateaus"]), default=0.0)
|
||||||
|
local_normal = max((row[2]["max_angle"] for row in valid), default=float("inf"))
|
||||||
|
local_coverage = min((row[2]["coverage"] for row in results), default=0.0)
|
||||||
|
local_ok = (len(valid) == len(results)
|
||||||
|
and len(radial) >= args.min_radial_lines_per_center
|
||||||
|
and len(straight) >= args.min_straight_lines_per_center
|
||||||
|
and local_plateau < args.plateau_min_m
|
||||||
|
and local_normal <= args.normal_jump_max_deg)
|
||||||
|
all_ranges.extend((row[2]["height_min"], row[2]["height_max"])
|
||||||
|
for row in valid)
|
||||||
|
overall_max_normal = max(overall_max_normal, local_normal)
|
||||||
|
overall_max_plateau = max(overall_max_plateau, local_plateau)
|
||||||
|
overall_max_step = max(overall_max_step, local_step)
|
||||||
|
overall_min_coverage = min(overall_min_coverage, local_coverage)
|
||||||
|
total_valid_lines += len(valid)
|
||||||
|
total_lines += len(results)
|
||||||
|
print(f"[RingSetWindow] center_m=({center[0]:.3f},{center[1]:.3f}) "
|
||||||
|
f"radius_m={args.radius:.3f} tiles={selected_tiles} lines={len(valid)}/"
|
||||||
|
f"{len(results)} radial={len(radial)} straight={len(straight)} "
|
||||||
|
f"coverage_min={local_coverage:.3f} height_m={local_min:.6f}.."
|
||||||
|
f"{local_max:.6f} amplitude_m={local_amplitude:.6f} "
|
||||||
|
f"longest_flat_m={local_plateau:.6f} following_step_m={local_step:.6f} "
|
||||||
|
f"max_normal_delta_deg={local_normal:.6f} "
|
||||||
|
f"status={'PASS' if local_ok else 'FAIL'}")
|
||||||
|
for name, _, result in results:
|
||||||
|
print(f"[RingSetProfile] center_m=({center[0]:.3f},{center[1]:.3f}) "
|
||||||
|
f"line={name} coverage={result['coverage']:.3f} "
|
||||||
|
f"range_m={result.get('height_range', 0.0):.6f} "
|
||||||
|
f"flat_step_runs={len(result['plateaus'])} "
|
||||||
|
f"max_normal_delta_deg={result['max_angle']:.6f}")
|
||||||
|
failed |= not local_ok
|
||||||
|
del triangles, buckets, results, radial, straight, valid
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
if all_ranges:
|
||||||
|
sampled_min = min(value[0] for value in all_ranges)
|
||||||
|
sampled_max = max(value[1] for value in all_ranges)
|
||||||
|
amplitude = sampled_max - sampled_min
|
||||||
|
else:
|
||||||
|
sampled_min = sampled_max = amplitude = 0.0
|
||||||
|
passed = not failed and amplitude >= args.amplitude_min_m
|
||||||
|
print(f"[RingSetAudit] scene={args.scene} surface={args.surface} lod={args.lod} "
|
||||||
|
f"windows={len(args.center)} radius_m={args.radius:.3f} "
|
||||||
|
f"height_window_m={args.height_min_m:.3f}..{args.height_max_m:.3f} "
|
||||||
|
f"sampled_height_m={sampled_min:.6f}..{sampled_max:.6f} "
|
||||||
|
f"amplitude_range_m={amplitude:.6f} lines={total_valid_lines}/{total_lines} "
|
||||||
|
f"coverage_min={overall_min_coverage:.3f} "
|
||||||
|
f"longest_flat_run_m={overall_max_plateau:.6f} "
|
||||||
|
f"max_step_m={overall_max_step:.6f} "
|
||||||
|
f"max_normal_delta_deg={overall_max_normal:.6f} "
|
||||||
|
f"thresholds=(amplitude>={args.amplitude_min_m:.3f},"
|
||||||
|
f"plateau<{args.plateau_min_m:.3f},"
|
||||||
|
f"normal<={args.normal_jump_max_deg:.3f}) "
|
||||||
|
f"status={'PASS' if passed else 'FAIL'}")
|
||||||
|
print(f"[RingSetRSS] peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail on contour terraces, stepped profiles, or insufficient Gradient amplitude."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import math
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
def read_scene(path, wanted_lod, height_min, height_max, bounds):
|
||||||
|
triangles = []
|
||||||
|
selected_tiles = 0
|
||||||
|
x_min, x_max, y_min, y_max = bounds
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid mesh scene header")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.read(40)
|
||||||
|
if version != 1 or tile_count > 100000:
|
||||||
|
raise SystemExit(f"{path}: unsupported mesh scene")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
tile_extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
tile_x0, tile_y0 = key[0] * tile_extent, key[1] * tile_extent
|
||||||
|
tile_z0 = key[2] * tile_extent
|
||||||
|
if (wanted_lod is not None and key[3] != wanted_lod) or \
|
||||||
|
tile_x0 > x_max or tile_x0 + tile_extent < x_min or \
|
||||||
|
tile_y0 > y_max or tile_y0 + tile_extent < y_min or \
|
||||||
|
tile_z0 > height_max or tile_z0 + tile_extent < height_min:
|
||||||
|
stream.seek(vertex_count * 24 + index_count * 4 + mask_count, 1)
|
||||||
|
continue
|
||||||
|
selected_tiles += 1
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
for offset in range(0, index_count, 3):
|
||||||
|
if masks and masks[offset // 3]:
|
||||||
|
continue
|
||||||
|
ids = indices[offset:offset + 3]
|
||||||
|
if max(ids) >= surface_count:
|
||||||
|
continue
|
||||||
|
verts = []
|
||||||
|
for index in ids:
|
||||||
|
p, n = index * 3, index * 3
|
||||||
|
verts.append(((positions[p], positions[p + 1], positions[p + 2]),
|
||||||
|
(normals[n], normals[n + 1], normals[n + 2])))
|
||||||
|
average_nz = sum(vertex[1][2] for vertex in verts) / 3.0
|
||||||
|
z_values = [vertex[0][2] for vertex in verts]
|
||||||
|
# Select the requested surface branch by its face height. An
|
||||||
|
# overlap or centroid test admits a giant coarse triangle that
|
||||||
|
# merely clips this window and can belong to the neighboring face.
|
||||||
|
mean_z = sum(z_values) / 3.0
|
||||||
|
vertex_margin = voxel_um / 1_000_000.0 * 0.5
|
||||||
|
if (mean_z < height_min or mean_z > height_max or
|
||||||
|
min(z_values) < height_min - vertex_margin or
|
||||||
|
max(z_values) > height_max + vertex_margin):
|
||||||
|
continue
|
||||||
|
if (max(vertex[0][0] for vertex in verts) < x_min or
|
||||||
|
min(vertex[0][0] for vertex in verts) > x_max or
|
||||||
|
max(vertex[0][1] for vertex in verts) < y_min or
|
||||||
|
min(vertex[0][1] for vertex in verts) > y_max):
|
||||||
|
continue
|
||||||
|
triangles.append((key[3], average_nz, tuple(verts)))
|
||||||
|
return voxel_um, triangles, selected_tiles
|
||||||
|
|
||||||
|
|
||||||
|
def build_buckets(triangles, upward, bounds, cell_size=2.0):
|
||||||
|
buckets = defaultdict(list)
|
||||||
|
x_min, x_max, y_min, y_max = bounds
|
||||||
|
for triangle in triangles:
|
||||||
|
_, average_nz, verts = triangle
|
||||||
|
if (upward and average_nz < 0.35) or (not upward and average_nz > -0.35):
|
||||||
|
continue
|
||||||
|
xs = [v[0][0] for v in verts]
|
||||||
|
ys = [v[0][1] for v in verts]
|
||||||
|
x0 = max(math.floor(min(xs) / cell_size), math.floor(x_min / cell_size))
|
||||||
|
x1 = min(math.floor(max(xs) / cell_size), math.floor(x_max / cell_size))
|
||||||
|
y0 = max(math.floor(min(ys) / cell_size), math.floor(y_min / cell_size))
|
||||||
|
y1 = min(math.floor(max(ys) / cell_size), math.floor(y_max / cell_size))
|
||||||
|
for iy in range(y0, y1 + 1):
|
||||||
|
for ix in range(x0, x1 + 1):
|
||||||
|
buckets[(ix, iy)].append(verts)
|
||||||
|
return buckets
|
||||||
|
|
||||||
|
|
||||||
|
def sample_surface(buckets, x, y, upward, cell_size, preferred_height=None):
|
||||||
|
found = None
|
||||||
|
for verts in buckets.get((math.floor(x / cell_size), math.floor(y / cell_size)), ()):
|
||||||
|
(a, na), (b, nb), (c, nc) = verts
|
||||||
|
denominator = (b[1] - c[1]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[1] - c[1])
|
||||||
|
if abs(denominator) < 1.0e-12:
|
||||||
|
continue
|
||||||
|
u = ((b[1] - c[1]) * (x - c[0]) + (c[0] - b[0]) * (y - c[1])) / denominator
|
||||||
|
v = ((c[1] - a[1]) * (x - c[0]) + (a[0] - c[0]) * (y - c[1])) / denominator
|
||||||
|
w = 1.0 - u - v
|
||||||
|
if min(u, v, w) < -1.0e-6:
|
||||||
|
continue
|
||||||
|
z = u * a[2] + v * b[2] + w * c[2]
|
||||||
|
normal = tuple(u * na[axis] + v * nb[axis] + w * nc[axis] for axis in range(3))
|
||||||
|
length = math.sqrt(sum(value * value for value in normal))
|
||||||
|
if length < 1.0e-9:
|
||||||
|
continue
|
||||||
|
normal = tuple(value / length for value in normal)
|
||||||
|
if (normal[2] > 0) != upward:
|
||||||
|
continue
|
||||||
|
if found is None or (abs(z - preferred_height) < abs(found[0] - preferred_height)
|
||||||
|
if preferred_height is not None else z > found[0]):
|
||||||
|
found = (z, normal)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def make_lines(cx, cy, radius, step):
|
||||||
|
lines = []
|
||||||
|
for index in range(8):
|
||||||
|
angle = index * math.tau / 8.0
|
||||||
|
count = round(radius / step)
|
||||||
|
points = [(cx + i * step * math.cos(angle), cy + i * step * math.sin(angle))
|
||||||
|
for i in range(count + 1)]
|
||||||
|
lines.append((f"radial-{index}", points))
|
||||||
|
count = round(2.0 * radius / step)
|
||||||
|
t_values = [-radius + i * step for i in range(count + 1)]
|
||||||
|
for name, points in (
|
||||||
|
("straight-x", [(cx + t, cy) for t in t_values]),
|
||||||
|
("straight-y", [(cx, cy + t) for t in t_values]),
|
||||||
|
("straight-diagonal", [(cx + t / math.sqrt(2.0), cy + t / math.sqrt(2.0))
|
||||||
|
for t in t_values]),
|
||||||
|
):
|
||||||
|
lines.append((name, points))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def audit_profile(points, buckets, upward, plateau_min_m, plateau_epsilon_m,
|
||||||
|
step_threshold_m, cell_size, preferred_height=None):
|
||||||
|
samples = [sample_surface(buckets, x, y, upward, cell_size, preferred_height)
|
||||||
|
for x, y in points]
|
||||||
|
valid = [(index, value) for index, value in enumerate(samples) if value is not None]
|
||||||
|
if len(valid) < max(8, len(points) * 0.75):
|
||||||
|
return {"coverage": len(valid) / len(points), "plateaus": [], "max_angle": math.inf,
|
||||||
|
"height_range": 0.0, "valid": False}
|
||||||
|
heights = [value[0] for _, value in valid]
|
||||||
|
plateau_runs = []
|
||||||
|
run_start = valid[0][0]
|
||||||
|
run_start_height = valid[0][1][0]
|
||||||
|
sample_spacing = math.hypot(points[1][0] - points[0][0],
|
||||||
|
points[1][1] - points[0][1])
|
||||||
|
for cursor in range(1, len(valid)):
|
||||||
|
prior_index, prior = valid[cursor - 1]
|
||||||
|
index, current = valid[cursor]
|
||||||
|
if index != prior_index + 1:
|
||||||
|
run_start = index
|
||||||
|
run_start_height = current[0]
|
||||||
|
continue
|
||||||
|
if abs(current[0] - run_start_height) <= plateau_epsilon_m:
|
||||||
|
continue
|
||||||
|
run_length = (prior_index - run_start + 1) * sample_spacing
|
||||||
|
if run_length >= plateau_min_m and abs(current[0] - prior[0]) >= step_threshold_m:
|
||||||
|
plateau_runs.append((run_length, abs(current[0] - prior[0]), run_start,
|
||||||
|
prior_index, index, prior[0], current[0]))
|
||||||
|
run_start = index
|
||||||
|
run_start_height = current[0]
|
||||||
|
max_angle = 0.0
|
||||||
|
for cursor in range(1, len(valid)):
|
||||||
|
prior_index, prior = valid[cursor - 1]
|
||||||
|
index, current = valid[cursor]
|
||||||
|
if index != prior_index + 1:
|
||||||
|
continue
|
||||||
|
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1]))))
|
||||||
|
max_angle = max(max_angle, math.degrees(math.acos(dot)))
|
||||||
|
return {"coverage": len(valid) / len(points), "plateaus": plateau_runs,
|
||||||
|
"max_angle": max_angle, "height_range": max(heights) - min(heights),
|
||||||
|
"height_min": min(heights), "height_max": max(heights),
|
||||||
|
"valid": True}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), default="floor")
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, default=(64.0, -32.0))
|
||||||
|
parser.add_argument("--radius", type=float, default=16.0)
|
||||||
|
parser.add_argument("--lod", required=True,
|
||||||
|
help="audit one LOD (0..8) or the streamed composite (`all`)")
|
||||||
|
parser.add_argument("--step", type=float, default=0.125)
|
||||||
|
parser.add_argument("--height-min-m", type=float, default=-130.0)
|
||||||
|
parser.add_argument("--height-max-m", type=float, default=-110.0)
|
||||||
|
parser.add_argument("--plateau-min-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--plateau-epsilon-m", type=float, default=0.0005)
|
||||||
|
parser.add_argument("--step-threshold-m", type=float, default=0.1)
|
||||||
|
parser.add_argument("--normal-jump-max-deg", type=float, default=20.0)
|
||||||
|
parser.add_argument("--amplitude-min-m", type=float, default=3.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
wanted_lod = None if args.lod == "all" else int(args.lod)
|
||||||
|
except ValueError:
|
||||||
|
parser.error("--lod must be 0..8 or all")
|
||||||
|
if wanted_lod is not None and not 0 <= wanted_lod <= 8:
|
||||||
|
parser.error("--lod must be 0..8 or all")
|
||||||
|
bounds = (args.center[0] - args.radius, args.center[0] + args.radius,
|
||||||
|
args.center[1] - args.radius, args.center[1] + args.radius)
|
||||||
|
voxel_um, triangles, selected_tiles = read_scene(args.scene, wanted_lod,
|
||||||
|
args.height_min_m, args.height_max_m, bounds)
|
||||||
|
upward = args.surface == "floor"
|
||||||
|
max_lod = max((triangle[0] for triangle in triangles), default=0)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << max_lod))
|
||||||
|
buckets = build_buckets(triangles, upward, bounds, cell_size)
|
||||||
|
results = []
|
||||||
|
# The requested face is the outer boundary of its authored band: highest Z
|
||||||
|
# for a floor, lowest Z for a ceiling. Selecting the band midpoint can jump
|
||||||
|
# between two overlapping downward/upward sheets at a cave edge.
|
||||||
|
preferred_height = args.height_max_m if upward else args.height_min_m
|
||||||
|
for name, points in make_lines(*args.center, args.radius, args.step):
|
||||||
|
results.append((name, points, audit_profile(points, buckets, upward,
|
||||||
|
args.plateau_min_m, args.plateau_epsilon_m,
|
||||||
|
args.step_threshold_m, cell_size, preferred_height)))
|
||||||
|
valid = [result for _, _, result in results if result["valid"]]
|
||||||
|
if not valid:
|
||||||
|
raise SystemExit("no sampled surface profiles; check scene, center, orientation and LOD")
|
||||||
|
amplitude = max(result["height_max"] for result in valid) - min(
|
||||||
|
result["height_min"] for result in valid)
|
||||||
|
sampled_min = min(result["height_min"] for result in valid)
|
||||||
|
sampled_max = max(result["height_max"] for result in valid)
|
||||||
|
longest_plateau = max((run[0] for result in valid for run in result["plateaus"]),
|
||||||
|
default=0.0)
|
||||||
|
largest_step = max((run[1] for result in valid for run in result["plateaus"]),
|
||||||
|
default=0.0)
|
||||||
|
max_normal_jump = max(result["max_angle"] for result in valid)
|
||||||
|
min_coverage = min(result["coverage"] for _, _, result in results)
|
||||||
|
passed = (amplitude >= args.amplitude_min_m and longest_plateau < args.plateau_min_m
|
||||||
|
and max_normal_jump <= args.normal_jump_max_deg and min_coverage >= 0.75)
|
||||||
|
print(f"[RingTerraceAudit] scene={args.scene} surface={args.surface} lod={args.lod} "
|
||||||
|
f"max_lod={max_lod} "
|
||||||
|
f"height_window_m={args.height_min_m:.3f}..{args.height_max_m:.3f} "
|
||||||
|
f"voxel_um={voxel_um} triangles={len(triangles)} lines={len(results)} "
|
||||||
|
f"selected_tiles={selected_tiles} bucket_cell_m={cell_size:.3f} "
|
||||||
|
f"coverage_min={min_coverage:.3f} sampled_height_m={sampled_min:.6f}.."
|
||||||
|
f"{sampled_max:.6f} amplitude_range_m={amplitude:.6f} "
|
||||||
|
f"longest_flat_run_m={longest_plateau:.6f} following_step_m={largest_step:.6f} "
|
||||||
|
f"max_normal_delta_deg={max_normal_jump:.6f} status={'PASS' if passed else 'FAIL'}")
|
||||||
|
for name, points, result in results:
|
||||||
|
print(f"[RingTerraceProfile] line={name} coverage={result['coverage']:.3f} "
|
||||||
|
f"range_m={result['height_range']:.6f} flat_step_runs={len(result['plateaus'])} "
|
||||||
|
f"max_normal_delta_deg={result['max_angle']:.6f}")
|
||||||
|
for run in result["plateaus"]:
|
||||||
|
print(f"[RingTerraceViolation] line={name} flat_start_m=({points[run[2]][0]:.3f},"
|
||||||
|
f"{points[run[2]][1]:.3f}) flat_run_m={run[0]:.6f} "
|
||||||
|
f"step_m={run[1]:.6f} from_height_m={run[5]:.6f} "
|
||||||
|
f"to_height_m={run[6]:.6f}")
|
||||||
|
print(f"[RingTerraceRSS] peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Encode the near/far tint-on/off L32 captures as compact JPEGs."""
|
||||||
|
import resource
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
SOURCE = ROOT / "Saved/Linux/l32"
|
||||||
|
DEST = Path(__file__).resolve().parent / "captures"
|
||||||
|
DEST.mkdir(parents=True, exist_ok=True)
|
||||||
|
captures = {
|
||||||
|
"fi-floor-near-tint-on": "cap-final-fi-near-on",
|
||||||
|
"fi-floor-near-tint-off": "cap-final-fi-near-off",
|
||||||
|
"fi-floor-far-tint-on": "cap-final-fi-far-on",
|
||||||
|
"fi-floor-far-tint-off": "cap-final-fi-far-off",
|
||||||
|
"sw-ceiling-near-tint-on": "cap-final-sw-near-on",
|
||||||
|
"sw-ceiling-near-tint-off": "cap-final-sw-near-off",
|
||||||
|
"sw-ceiling-far-tint-on": "cap-final-sw-far-on",
|
||||||
|
"sw-ceiling-far-tint-off": "cap-final-sw-far-off",
|
||||||
|
}
|
||||||
|
failed = False
|
||||||
|
for output_name, input_name in captures.items():
|
||||||
|
source = SOURCE / f"{input_name}.png"
|
||||||
|
destination = DEST / f"{output_name}.jpg"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "ffmpeg",
|
||||||
|
"-y", "-hide_banner", "-loglevel", "error", "-i", str(source),
|
||||||
|
"-frames:v", "1", "-q:v", "4", str(destination),
|
||||||
|
]
|
||||||
|
result = subprocess.run(command, check=False)
|
||||||
|
size = destination.stat().st_size if destination.exists() else 0
|
||||||
|
ok = source.exists() and result.returncode == 0 and size <= 300_000
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L32CaptureJPEG] source={source.name} output={destination.name} "
|
||||||
|
f"bytes={size} status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
print(f"[L32CaptureJPEG] peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}", flush=True)
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Copy only tiles for one LOD and spatial window from a VFMSH scene."""
|
||||||
|
import argparse
|
||||||
|
import resource
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("source")
|
||||||
|
parser.add_argument("destination")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--bounds", nargs=6, type=float, required=True,
|
||||||
|
metavar=("XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not 0 <= args.lod <= 8:
|
||||||
|
parser.error("--lod must be between 0 and 8")
|
||||||
|
x_min, x_max, y_min, y_max, z_min, z_max = args.bounds
|
||||||
|
if x_max <= x_min or y_max <= y_min or z_max <= z_min:
|
||||||
|
parser.error("bounds must have positive extents")
|
||||||
|
|
||||||
|
selected = []
|
||||||
|
with open(args.source, "rb") as src:
|
||||||
|
prefix = src.read(60)
|
||||||
|
if len(prefix) != 60 or prefix[:8] != b"VFMSH001":
|
||||||
|
raise SystemExit("source has an invalid VFMSH001 header")
|
||||||
|
version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8)
|
||||||
|
if version != 1 or voxel_um == 0 or tile_count > 1_000_000:
|
||||||
|
raise SystemExit("source has an unsupported scene header")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
header = src.read(36)
|
||||||
|
if len(header) != 36:
|
||||||
|
raise SystemExit("truncated tile header")
|
||||||
|
key = struct.unpack_from("<4i", header)
|
||||||
|
_, _, vertex_count, index_count, mask_count = struct.unpack_from("<5I", header, 16)
|
||||||
|
payload_offset = src.tell()
|
||||||
|
payload_size = vertex_count * 24 + index_count * 4 + mask_count
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
tx0, ty0, tz0 = key[0] * extent, key[1] * extent, key[2] * extent
|
||||||
|
intersects = (key[3] == args.lod and tx0 <= x_max and tx0 + extent >= x_min
|
||||||
|
and ty0 <= y_max and ty0 + extent >= y_min
|
||||||
|
and tz0 <= z_max and tz0 + extent >= z_min)
|
||||||
|
if intersects:
|
||||||
|
selected.append((header, payload_offset, payload_size))
|
||||||
|
src.seek(payload_size, 1)
|
||||||
|
if src.read(1):
|
||||||
|
raise SystemExit("source scene has trailing bytes")
|
||||||
|
|
||||||
|
if not selected:
|
||||||
|
raise SystemExit("no tiles matched the requested LOD and bounds")
|
||||||
|
with open(args.source, "rb") as src, open(args.destination, "wb") as dst:
|
||||||
|
prefix = bytearray(src.read(60))
|
||||||
|
struct.pack_into("<I", prefix, 16, len(selected))
|
||||||
|
dst.write(prefix)
|
||||||
|
for header, payload_offset, payload_size in selected:
|
||||||
|
dst.write(header)
|
||||||
|
src.seek(payload_offset)
|
||||||
|
remaining = payload_size
|
||||||
|
while remaining:
|
||||||
|
block = src.read(min(1 << 20, remaining))
|
||||||
|
if not block:
|
||||||
|
raise SystemExit("truncated tile payload")
|
||||||
|
dst.write(block)
|
||||||
|
remaining -= len(block)
|
||||||
|
print(f"[VFMeshLodWindow] source={args.source} lod={args.lod} "
|
||||||
|
f"selected_tiles={len(selected)} output={args.destination} "
|
||||||
|
f"output_bytes={__import__('os').path.getsize(args.destination)} "
|
||||||
|
f"peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss} status=PASS")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stream VFMSH tiles and retain a focused, near-horizontal surface patch."""
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("source")
|
||||||
|
parser.add_argument("destination")
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, required=True, metavar=("X", "Y"))
|
||||||
|
parser.add_argument("--radius-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-m", nargs=2, type=float, required=True, metavar=("MIN", "MAX"))
|
||||||
|
parser.add_argument("--min-abs-normal-z", type=float, default=0.7)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.radius_m <= 0 or args.height_m[1] <= args.height_m[0]:
|
||||||
|
parser.error("radius and height interval must be positive")
|
||||||
|
if not 0.0 <= args.min_abs_normal_z <= 1.0:
|
||||||
|
parser.error("--min-abs-normal-z must be in [0,1]")
|
||||||
|
|
||||||
|
cx, cy = args.center
|
||||||
|
r2 = args.radius_m * args.radius_m
|
||||||
|
zmin, zmax = args.height_m
|
||||||
|
input_tiles = kept_tiles = kept_triangles = 0
|
||||||
|
with open(args.source, "rb") as src, open(args.destination, "wb+") as dst:
|
||||||
|
prefix = bytearray(src.read(60))
|
||||||
|
if len(prefix) != 60 or prefix[:8] != b"VFMSH001":
|
||||||
|
raise SystemExit("source has an invalid VFMSH001 header")
|
||||||
|
version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8)
|
||||||
|
if version != 1 or voxel_um == 0 or tile_count > 1_000_000:
|
||||||
|
raise SystemExit("source has an unsupported scene header")
|
||||||
|
struct.pack_into("<I", prefix, 16, 0)
|
||||||
|
dst.write(prefix)
|
||||||
|
for _ in range(tile_count):
|
||||||
|
header = src.read(36)
|
||||||
|
if len(header) != 36:
|
||||||
|
raise SystemExit("truncated tile header")
|
||||||
|
key = struct.unpack_from("<4i", header)
|
||||||
|
surface_vertices, skirt_count, vertex_count, index_count, mask_count = \
|
||||||
|
struct.unpack_from("<5I", header, 16)
|
||||||
|
if vertex_count > 2_000_000 or index_count > 12_000_000:
|
||||||
|
raise SystemExit("tile exceeds the focused patch memory limit")
|
||||||
|
vertex_bytes = src.read(vertex_count * 24)
|
||||||
|
index_bytes = src.read(index_count * 4)
|
||||||
|
masks = src.read(mask_count)
|
||||||
|
if (len(vertex_bytes) != vertex_count * 24
|
||||||
|
or len(index_bytes) != index_count * 4 or len(masks) != mask_count):
|
||||||
|
raise SystemExit("truncated tile payload")
|
||||||
|
input_tiles += 1
|
||||||
|
positions = list(struct.iter_unpack("<3f", vertex_bytes[:vertex_count * 12]))
|
||||||
|
indices = struct.unpack(f"<{index_count}I", index_bytes) if index_count else ()
|
||||||
|
face_masks = masks if mask_count else bytes(index_count // 12)
|
||||||
|
filtered = []
|
||||||
|
for tri in range(index_count // 3):
|
||||||
|
i0, i1, i2 = indices[tri * 3:tri * 3 + 3]
|
||||||
|
if i0 >= vertex_count or i1 >= vertex_count or i2 >= vertex_count:
|
||||||
|
raise SystemExit("tile index points outside its vertex array")
|
||||||
|
if face_masks[tri] != 0:
|
||||||
|
continue
|
||||||
|
p0, p1, p2 = positions[i0], positions[i1], positions[i2]
|
||||||
|
mx = (p0[0] + p1[0] + p2[0]) / 3.0
|
||||||
|
my = (p0[1] + p1[1] + p2[1]) / 3.0
|
||||||
|
mz = (p0[2] + p1[2] + p2[2]) / 3.0
|
||||||
|
if (mx - cx) ** 2 + (my - cy) ** 2 > r2 or not zmin <= mz <= zmax:
|
||||||
|
continue
|
||||||
|
ab = (p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2])
|
||||||
|
ac = (p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2])
|
||||||
|
nz = ab[0] * ac[1] - ab[1] * ac[0]
|
||||||
|
length = math.sqrt((ab[1] * ac[2] - ab[2] * ac[1]) ** 2
|
||||||
|
+ (ab[2] * ac[0] - ab[0] * ac[2]) ** 2 + nz * nz)
|
||||||
|
if length == 0 or abs(nz) / length < args.min_abs_normal_z:
|
||||||
|
continue
|
||||||
|
filtered.extend((i0, i1, i2))
|
||||||
|
if not filtered:
|
||||||
|
continue
|
||||||
|
tile_header = struct.pack("<4i5I", *key, surface_vertices, 0,
|
||||||
|
vertex_count, len(filtered), 0)
|
||||||
|
dst.write(tile_header)
|
||||||
|
dst.write(vertex_bytes)
|
||||||
|
dst.write(struct.pack(f"<{len(filtered)}I", *filtered))
|
||||||
|
kept_tiles += 1
|
||||||
|
kept_triangles += len(filtered) // 3
|
||||||
|
dst.seek(16)
|
||||||
|
dst.write(struct.pack("<I", kept_tiles))
|
||||||
|
|
||||||
|
if kept_tiles == 0:
|
||||||
|
raise SystemExit("no surface triangles matched the requested patch")
|
||||||
|
print(f"[VFMeshSurfacePatch] source={args.source} input_tiles={input_tiles} "
|
||||||
|
f"tiles={kept_tiles} triangles={kept_triangles} center=({cx:.3f},{cy:.3f}) "
|
||||||
|
f"radius_m={args.radius_m:.3f} height_m={zmin:.3f}..{zmax:.3f} "
|
||||||
|
f"peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss} status=PASS")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Find VFMSH tiles intersecting a small region without reading mesh payloads."""
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("directory")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--bounds", nargs=6, type=float, required=True,
|
||||||
|
metavar=("XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
xmin, xmax, ymin, ymax, zmin, zmax = args.bounds
|
||||||
|
matches = 0
|
||||||
|
for path in sorted(glob.glob(os.path.join(args.directory, "*.vfmesh"))):
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
continue
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.seek(40, 1)
|
||||||
|
if version != 1:
|
||||||
|
continue
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
x0, y0, z0 = (key[axis] * extent for axis in range(3))
|
||||||
|
intersects = (key[3] == args.lod and index_count > 0
|
||||||
|
and x0 <= xmax and x0 + extent >= xmin
|
||||||
|
and y0 <= ymax and y0 + extent >= ymin
|
||||||
|
and z0 <= zmax and z0 + extent >= zmin)
|
||||||
|
if intersects:
|
||||||
|
matches += 1
|
||||||
|
print(f"[VFMeshTileWindow] scene={os.path.basename(path)} key={key} "
|
||||||
|
f"vertices={vertex_count} indices={index_count} "
|
||||||
|
f"extent_m={extent:.3f}")
|
||||||
|
stream.seek(vertex_count * 24 + index_count * 4 + mask_count, 1)
|
||||||
|
print(f"[VFMeshTileWindowSummary] matching_tiles={matches} "
|
||||||
|
f"peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stream selected-LOD tiles and find face windows that pass the ring audit."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
parser.add_argument("--preferred-height-m", type=float,
|
||||||
|
help="lock overlapping-face selection near this expected surface height")
|
||||||
|
parser.add_argument("--step-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--tile-key", nargs=3, type=int,
|
||||||
|
help="audit only this tile's x/y/z key (LOD is supplied separately)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
audit_path = Path(__file__).with_name("AuditRingTerraceL32.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_audit_l32", audit_path)
|
||||||
|
audit = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(audit)
|
||||||
|
want_up = args.surface == "floor"
|
||||||
|
candidates = []
|
||||||
|
with open(args.scene, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit("invalid VFMSH001 scene")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.seek(40, 1)
|
||||||
|
if version != 1:
|
||||||
|
raise SystemExit("unsupported mesh scene")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
header = stream.read(36)
|
||||||
|
key = struct.unpack("<4i", header[:16])
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", header[16:])
|
||||||
|
payload = vertex_count * 24 + index_count * 4 + mask_count
|
||||||
|
if key[3] != args.lod or (args.tile_key and tuple(key[:3]) != tuple(args.tile_key)):
|
||||||
|
stream.seek(payload, 1)
|
||||||
|
continue
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
triangles = []
|
||||||
|
vertex_margin = voxel_um / 1_000_000.0 * 0.5
|
||||||
|
for offset in range(0, index_count, 3):
|
||||||
|
if masks and masks[offset // 3]:
|
||||||
|
continue
|
||||||
|
ids = indices[offset:offset + 3]
|
||||||
|
if max(ids) >= surface_count:
|
||||||
|
continue
|
||||||
|
verts = []
|
||||||
|
for index in ids:
|
||||||
|
p = index * 3
|
||||||
|
verts.append(((positions[p], positions[p + 1], positions[p + 2]),
|
||||||
|
(normals[p], normals[p + 1], normals[p + 2])))
|
||||||
|
average_nz = sum(vertex[1][2] for vertex in verts) / 3.0
|
||||||
|
if (want_up and average_nz < 0.35) or (not want_up and average_nz > -0.35):
|
||||||
|
continue
|
||||||
|
z_values = [vertex[0][2] for vertex in verts]
|
||||||
|
mean_z = sum(z_values) / 3.0
|
||||||
|
if (mean_z < args.height_min_m or mean_z > args.height_max_m or
|
||||||
|
min(z_values) < args.height_min_m - vertex_margin or
|
||||||
|
max(z_values) > args.height_max_m + vertex_margin):
|
||||||
|
continue
|
||||||
|
triangles.append((key[3], average_nz, tuple(verts)))
|
||||||
|
if not triangles:
|
||||||
|
continue
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
cx, cy = (key[0] + 0.5) * extent, (key[1] + 0.5) * extent
|
||||||
|
radius = extent * 0.5
|
||||||
|
bounds = (cx - radius, cx + radius, cy - radius, cy + radius)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << key[3]))
|
||||||
|
buckets = audit.build_buckets(triangles, want_up, bounds, cell_size)
|
||||||
|
profiles = []
|
||||||
|
preferred_height = args.preferred_height_m
|
||||||
|
if preferred_height is None:
|
||||||
|
preferred_height = args.height_max_m if want_up else args.height_min_m
|
||||||
|
for _, points in audit.make_lines(cx, cy, radius, args.step_m):
|
||||||
|
profiles.append(audit.audit_profile(points, buckets, want_up,
|
||||||
|
0.5, 0.0005, 0.1, cell_size, preferred_height))
|
||||||
|
valid = [profile for profile in profiles if profile["valid"]]
|
||||||
|
coverage = min(profile["coverage"] for profile in profiles)
|
||||||
|
amplitude = max((profile["height_max"] for profile in valid), default=0.0) - \
|
||||||
|
min((profile["height_min"] for profile in valid), default=0.0)
|
||||||
|
longest = max((run[0] for profile in valid for run in profile["plateaus"]),
|
||||||
|
default=0.0)
|
||||||
|
max_normal = max((profile["max_angle"] for profile in valid), default=float("inf"))
|
||||||
|
passed = (len(valid) == len(profiles) and coverage >= 0.75 and amplitude >= 3.0
|
||||||
|
and longest < 0.5 and max_normal <= 20.0)
|
||||||
|
candidates.append((passed, amplitude, -max_normal, key, cx, cy,
|
||||||
|
len(triangles), coverage, longest, max_normal))
|
||||||
|
stream.seek(payload - (vertex_count * 24 + index_count * 4 + mask_count), 1)
|
||||||
|
candidates.sort(reverse=True, key=lambda item: (item[0], item[1], item[2]))
|
||||||
|
print(f"[RingWindowSearch] lod={args.lod} surface={args.surface} tiles={len(candidates)} "
|
||||||
|
f"height_window_m={args.height_min_m:.3f}..{args.height_max_m:.3f} "
|
||||||
|
f"step_m={args.step_m:.3f}")
|
||||||
|
for passed, amplitude, neg_normal, key, cx, cy, count, coverage, longest, normal in candidates[:40]:
|
||||||
|
print(f"[RingWindowCandidate] status={'PASS' if passed else 'FAIL'} key={key} "
|
||||||
|
f"center_m=({cx:.3f},{cy:.3f}) triangles={count} coverage={coverage:.3f} "
|
||||||
|
f"amplitude_m={amplitude:.6f} longest_flat_m={longest:.6f} "
|
||||||
|
f"normal_jump_deg={normal:.6f}")
|
||||||
|
print(f"[RingWindowRSS] peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Find open SurfaceWorld columns below the resolved transition ceiling."""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("probe")
|
||||||
|
parser.add_argument("owner")
|
||||||
|
parser.add_argument("output")
|
||||||
|
parser.add_argument("--z", type=float, default=-130.0)
|
||||||
|
parser.add_argument("--minimum", type=float, default=-96.0)
|
||||||
|
parser.add_argument("--maximum", type=float, default=96.0)
|
||||||
|
parser.add_argument("--spacing", type=float, default=16.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
probe = str(Path(args.probe).resolve())
|
||||||
|
owner = str(Path(args.owner).resolve())
|
||||||
|
output = Path(args.output)
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
rows = []
|
||||||
|
count = round((args.maximum - args.minimum) / args.spacing)
|
||||||
|
for iy in range(count + 1):
|
||||||
|
y = args.minimum + iy * args.spacing
|
||||||
|
for ix in range(count + 1):
|
||||||
|
x = args.minimum + ix * args.spacing
|
||||||
|
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
probe, owner, "surface-world", str(x), str(y),
|
||||||
|
str(args.z - 2.0), str(args.z + 2.0), "0.25"]
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=1800,
|
||||||
|
check=False)
|
||||||
|
samples = [line for line in result.stdout.splitlines()
|
||||||
|
if line.startswith(f"{args.z:.6f},")]
|
||||||
|
density = float(samples[0].split(",")[1]) if result.returncode == 0 and samples else float("nan")
|
||||||
|
rows.append((x, y, args.z, density, result.returncode))
|
||||||
|
print(f"[SurfaceWorldAirScan] x_m={x:.1f} y_m={y:.1f} z_m={args.z:.2f} "
|
||||||
|
f"density_vox={density:.6f} exit={result.returncode}", flush=True)
|
||||||
|
with output.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.writer(stream)
|
||||||
|
writer.writerow(("x_m", "y_m", "z_m", "density_vox", "exit_code"))
|
||||||
|
writer.writerows(rows)
|
||||||
|
open_samples = [row for row in rows if row[3] > 0.0]
|
||||||
|
print(f"[SurfaceWorldAirScanSummary] samples={len(rows)} open={len(open_samples)} "
|
||||||
|
f"output={output} status={'PASS' if open_samples else 'FAIL'}")
|
||||||
|
return 0 if open_samples else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#include "Json.h"
|
||||||
|
#include "OwnerWorldLoader.h"
|
||||||
|
#include "StandaloneCoreScheduler.h"
|
||||||
|
#include "WorldAssemblyPrototype.h"
|
||||||
|
#include "StandaloneSnapshotAdapter.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
int main(int argc,char** argv)
|
||||||
|
{
|
||||||
|
if(argc!=8)
|
||||||
|
{
|
||||||
|
std::fprintf(stderr,"usage: %s owner.json surface-world|floating-islands x_m y_m zmin_m zmax_m step_m\n",argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const std::string recipe=argv[2];
|
||||||
|
if(recipe!="surface-world"&&recipe!="floating-islands")return 2;
|
||||||
|
const double x=std::strtod(argv[3],nullptr),y=std::strtod(argv[4],nullptr);
|
||||||
|
const double zmin=std::strtod(argv[5],nullptr),zmax=std::strtod(argv[6],nullptr);
|
||||||
|
const double step=std::strtod(argv[7],nullptr);
|
||||||
|
if(!std::isfinite(x)||!std::isfinite(y)||!std::isfinite(zmin)||!std::isfinite(zmax)
|
||||||
|
||!std::isfinite(step)||zmax<=zmin||step<=0.0)return 2;
|
||||||
|
|
||||||
|
VFJson::Value root;
|
||||||
|
std::string error;
|
||||||
|
{
|
||||||
|
std::ifstream input(argv[1],std::ios::binary);
|
||||||
|
if(!input){std::fprintf(stderr,"owner JSON open failed\n");return 2;}
|
||||||
|
std::string text{std::istreambuf_iterator<char>(input),std::istreambuf_iterator<char>{}};
|
||||||
|
if(!VFJson::Parser(std::move(text)).Parse(root,error))
|
||||||
|
{std::fprintf(stderr,"owner JSON parse failed: %s\n",error.c_str());return 2;}
|
||||||
|
}
|
||||||
|
if(recipe=="floating-islands"
|
||||||
|
&&!StandaloneWorldAssemblyPrototype::SelectTopStrateAsFloatingIslands(root,error))
|
||||||
|
{std::fprintf(stderr,"FI selection failed: %s\n",error.c_str());return 2;}
|
||||||
|
auto owner=std::make_shared<FStandaloneOwnerWorld>();
|
||||||
|
if(!StandaloneOwnerLoader::Resolve(root,*owner,error))
|
||||||
|
{std::fprintf(stderr,"owner resolve failed: %s\n",error.c_str());return 2;}
|
||||||
|
VoxelForgeCore::WorldAssemblyManifest assembly;
|
||||||
|
if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
||||||
|
*owner,root,error,{},&assembly))
|
||||||
|
{std::fprintf(stderr,"assembly setup failed: %s\n",error.c_str());return 2;}
|
||||||
|
if(recipe=="floating-islands"
|
||||||
|
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
||||||
|
*owner,root,assembly,error))
|
||||||
|
{std::fprintf(stderr,"FI setup failed: %s\n",error.c_str());return 2;}
|
||||||
|
VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout;
|
||||||
|
if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
||||||
|
*owner,assembly,recipe,resolvedLayout,error))
|
||||||
|
{std::fprintf(stderr,"resolved layout build failed: %s\n",error.c_str());return 2;}
|
||||||
|
auto* rootObject=std::get_if<VFJson::Value::Object>(&root.Data);
|
||||||
|
if(!rootObject)return 2;
|
||||||
|
VFJson::Value layoutJson;
|
||||||
|
layoutJson.Data=resolvedLayout.ToJson();
|
||||||
|
(*rootObject)["resolved_layout_json"]=std::move(layoutJson);
|
||||||
|
auto snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(
|
||||||
|
root,owner->Seed,3,8,false,false,error);
|
||||||
|
if(!snapshot)
|
||||||
|
{std::fprintf(stderr,"snapshot resolve failed: %s\n",error.c_str());return 2;}
|
||||||
|
const auto* layout=snapshot.resolvedLayout();
|
||||||
|
if(!layout){std::fprintf(stderr,"resolved layout missing\n");return 2;}
|
||||||
|
for(const auto& stratum:layout->strates)
|
||||||
|
std::printf("[DensityStratum] order=%d strate=%d generator=%s mode=%s width_um=%lld bedrock_um=%lld..%lld ceiling_um=%lld..%lld\n",
|
||||||
|
stratum.order,stratum.strateIndex,stratum.generatorKind.c_str(),
|
||||||
|
stratum.transitionMode.c_str(),static_cast<long long>(stratum.transitionWidthMicrometres),
|
||||||
|
static_cast<long long>(stratum.bedrock.bottomMicrometres),
|
||||||
|
static_cast<long long>(stratum.bedrock.topMicrometres),
|
||||||
|
static_cast<long long>(stratum.ceiling.bottomMicrometres),
|
||||||
|
static_cast<long long>(stratum.ceiling.topMicrometres));
|
||||||
|
const auto interface=std::find_if(layout->strates.begin(),layout->strates.end(),
|
||||||
|
[](const auto& stratum){return stratum.order==0;});
|
||||||
|
if(interface==layout->strates.end())return 2;
|
||||||
|
|
||||||
|
VoxelForgeCore::World world(std::move(snapshot));
|
||||||
|
VoxelForgeCore::ShapeBuildRequest request;
|
||||||
|
request.minVoxel={static_cast<std::int32_t>(std::floor((x-32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::floor((y-32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::floor((zmin-4.0)*4.0))};
|
||||||
|
request.maxVoxel={static_cast<std::int32_t>(std::ceil((x+32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::ceil((y+32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::ceil((zmax+4.0)*4.0))};
|
||||||
|
request.clipMinVoxel=request.minVoxel;
|
||||||
|
request.clipMaxVoxel=request.maxVoxel;
|
||||||
|
request.workerLimit=12;
|
||||||
|
request.includeFarWorld=false;
|
||||||
|
FStandaloneCoreScheduler scheduler;
|
||||||
|
if(!world.compile(request,scheduler,error))
|
||||||
|
{std::fprintf(stderr,"shape compile failed: %s\n",error.c_str());return 2;}
|
||||||
|
|
||||||
|
std::printf("[DensityLine] recipe=%s mode=%s width_um=%lld seam_um=%lld band_top_um=%lld band_bottom_um=%lld xy_m=%.6f,%.6f core_voxel_m=0.25 status=PASS\n",
|
||||||
|
recipe.c_str(),interface->transitionMode.c_str(),
|
||||||
|
static_cast<long long>(interface->transitionWidthMicrometres),
|
||||||
|
static_cast<long long>(interface->bedrock.bottomMicrometres),
|
||||||
|
static_cast<long long>(interface->bedrock.topMicrometres),
|
||||||
|
static_cast<long long>(interface->bedrock.bottomMicrometres),x,y);
|
||||||
|
std::printf("z_m,density_vox,lattice_vox,unclamped_vox,dispatch\n");
|
||||||
|
const auto count=static_cast<std::size_t>(std::floor((zmax-zmin)/step+1.0e-9));
|
||||||
|
for(std::size_t i=0;i<=count;++i)
|
||||||
|
{
|
||||||
|
const double z=zmin+static_cast<double>(i)*step;
|
||||||
|
const VoxelForgeCore::Vec3d p{x*4.0,y*4.0,z*4.0};
|
||||||
|
const VoxelForgeCore::Vec3i lattice{
|
||||||
|
static_cast<std::int32_t>(std::llround(x*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::llround(y*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::llround(z*4.0))};
|
||||||
|
std::printf("%.6f,%.9f,%.9f,%.9f,%s\n",z,world.densityAt(p),
|
||||||
|
world.densityAtLattice(lattice),world.unclampedDensityAt(p),
|
||||||
|
world.densityDispatchPathAt(p).c_str());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Dump a bounded surface line from a VFMSH scene using the ring-audit sampler."""
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import resource
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, required=True)
|
||||||
|
parser.add_argument("--radius", type=float, required=True)
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
parser.add_argument("--line", choices=("x", "y", "diagonal"), default="x")
|
||||||
|
parser.add_argument("--stride-m", type=float, default=8.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
audit_path = Path(__file__).with_name("AuditRingTerraceL32.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_audit_l32", audit_path)
|
||||||
|
audit = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(audit)
|
||||||
|
cx, cy = args.center
|
||||||
|
bounds = (cx - args.radius, cx + args.radius,
|
||||||
|
cy - args.radius, cy + args.radius)
|
||||||
|
voxel_um, triangles, tiles = audit.read_scene(args.scene, args.lod,
|
||||||
|
args.height_min_m, args.height_max_m, bounds)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << args.lod))
|
||||||
|
buckets = audit.build_buckets(triangles, args.surface == "floor", bounds, cell_size)
|
||||||
|
count = round(2.0 * args.radius / args.stride_m)
|
||||||
|
print(f"[SurfaceProfile] lod={args.lod} surface={args.surface} tiles={tiles} "
|
||||||
|
f"triangles={len(triangles)} line={args.line} stride_m={args.stride_m}")
|
||||||
|
print("index,x_m,y_m,z_m,nx,ny,nz")
|
||||||
|
for index in range(count + 1):
|
||||||
|
t = -args.radius + index * args.stride_m
|
||||||
|
if args.line == "x":
|
||||||
|
x, y = cx + t, cy
|
||||||
|
elif args.line == "y":
|
||||||
|
x, y = cx, cy + t
|
||||||
|
else:
|
||||||
|
x, y = cx + t / 2.0**0.5, cy + t / 2.0**0.5
|
||||||
|
preferred_height = args.height_max_m if args.surface == "floor" else args.height_min_m
|
||||||
|
hit = audit.sample_surface(buckets, x, y, args.surface == "floor", cell_size,
|
||||||
|
preferred_height)
|
||||||
|
if hit is None:
|
||||||
|
print(f"{index},{x:.3f},{y:.3f},nan,nan,nan,nan")
|
||||||
|
else:
|
||||||
|
z, normal = hit
|
||||||
|
print(f"{index},{x:.3f},{y:.3f},{z:.6f}," +
|
||||||
|
",".join(f"{value:.7f}" for value in normal))
|
||||||
|
print(f"[SurfaceProfileRSS] peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare the v20 transition-column cache with the uncached v20 binary."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
BINARIES = {
|
||||||
|
"base": OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20",
|
||||||
|
"cache": OUT / "bin/transition-cache/VoxelForgeCoreHeadless-l32-transition-cache",
|
||||||
|
}
|
||||||
|
ORDER = (("base", "cache"), ("cache", "base"), ("base", "cache"),
|
||||||
|
("cache", "base"), ("base", "cache"))
|
||||||
|
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for pair, builds in enumerate(ORDER, 1):
|
||||||
|
for build in builds:
|
||||||
|
name = f"cache-compare-sw-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
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", "--recipe=surface-world",
|
||||||
|
]
|
||||||
|
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")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
valid = result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
csv = OUT / "transition-cache-compare.csv"
|
||||||
|
csv.write_text(
|
||||||
|
"pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
for build in BINARIES:
|
||||||
|
values = [row[2] for row in rows if row[1] == build]
|
||||||
|
values.sort()
|
||||||
|
median = values[len(values) // 2]
|
||||||
|
print(f"[TransitionCacheCost] build={build} median_whole_view_ready_s={median:.3f}")
|
||||||
|
print(f"[TransitionCacheCost] runs={len(rows)} status={'FAIL' if failed else 'PASS'}")
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 10 else 0)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# VoxelForge L32 report
|
||||||
|
|
||||||
|
## Cause and fix
|
||||||
|
|
||||||
|
The old Gradient transition wrote constant solid-density values per band. Those values are not signed distance to the displaced boundary, so Marching Cubes crossed band levels as contour-like terraces. The hash surface quantization was not the cause: sampling across the final FI floor gives density -0.53345 at z=-121.50 m and +0.46655 at -121.25 m; the SW ceiling gives +0.01514 at -126.50 m and -0.98486 at -126.25 m. Both cross linearly at about 4 density units/m.
|
||||||
|
|
||||||
|
L32 restores the width/8 Gradient displacement (2 m each side, about 4 m peak-to-peak at 16 m owner width). Density is now signed distance to the union of the displaced solid intervals, continuous across band boundaries. Cap samples follow the same displaced faces. Passage-mouth fades use cubic smoothstep across a 2 m fade from the protected mouth radius; this preserves the face while blending the column flare. Version/evaluator is v20.
|
||||||
|
|
||||||
|
## Surface and interface audits
|
||||||
|
|
||||||
|
Branch-locked radial and straight profiles pass on both faces at LOD0 through LOD8. Every tested LOD has at least 3.15 m height range; no repeated-height plateau or following step was found. LOD0/1 profiles use 0.125 m samples (SW LOD1 also checked at 0.0625 m); LOD2–8 use 0.5 m samples. The largest adjacent-normal change was 17.50° on the fine SW LOD1 scan, below the 20° audit limit. Peak RSS for the final Python ring scans was 373,596 KiB (FI LOD0); the passage-fade scans peaked at 52,128 KiB. Mesh scans use tile/chunk processing.
|
||||||
|
|
||||||
|
Interleaved also passes 11 radial/straight profiles at each LOD0–8: all selected windows have full coverage, no flats or steps, and at most 0.367° adjacent-normal change. Their local height ranges are 0.032–3.174 m; the 3 m minimum is the Gradient amplitude check, not an Interleaved requirement. Peak RSS for this tile-streamed sweep was 24,468 KiB.
|
||||||
|
|
||||||
|
Both ring negative controls fail as intended: L30 has 0.742 m range, a 1.875 m flat run followed by a 0.241 m step, and a 27.34° normal jump; L31's flat fix has only 0.000046 m range. The passage-mouth audit passes on both overlapping faces: upper max normal change 15.06°, lower 7.96°, with full coverage and no flats or steps.
|
||||||
|
|
||||||
|
Hole/interface and minimum-band audits pass for Hard, Gradient and Interleaved, for both recipes and compilers: zero hole columns and zero air samples. Wider-than-declared and thinner-than-minimum controls are rejected. Minimum bands are 0.5 m FI, 1.0 m SW and 6.25 m lower band; maximum Interleaved crossing is 6.487 m against the 16 m bound. Passage audit passes all 16 recipe-stamped rows; recipe-mismatch and disconnected-end controls are detected.
|
||||||
|
|
||||||
|
Determinism passes 24/24: the L25 command and 24 m crossing, both recipes, Clang and GCC, workers 1/8/12.
|
||||||
|
|
||||||
|
## Captures and hashes
|
||||||
|
|
||||||
|
Tint-on and tint-off JPEGs are in `captures/`; all are under 30 KB (limit 300 KB):
|
||||||
|
|
||||||
|
- `fi-floor-near-tint-on.jpg`, `fi-floor-near-tint-off.jpg`
|
||||||
|
- `fi-floor-far-tint-on.jpg`, `fi-floor-far-tint-off.jpg`
|
||||||
|
- `sw-ceiling-near-tint-on.jpg`, `sw-ceiling-near-tint-off.jpg`
|
||||||
|
- `sw-ceiling-far-tint-on.jpg`, `sw-ceiling-far-tint-off.jpg`
|
||||||
|
|
||||||
|
They show the FI floor and SW ceiling from inside each stratum, with grazing light, at LOD0 and LOD8. Capture SHA256 values:
|
||||||
|
|
||||||
|
| Capture | SHA256 |
|
||||||
|
|---|---|
|
||||||
|
| fi-floor-far-tint-off.jpg | `239b6be70c177e267d2db95f206fdcdc8834779a7948fdaaddd136b4163d1f34` |
|
||||||
|
| fi-floor-far-tint-on.jpg | `3ecefd555275036a7045a541f02bb1873e22302495009dbd80b4ac58ce716b58` |
|
||||||
|
| fi-floor-near-tint-off.jpg | `847f10acfb420c2bbded61bdee7f578cde19469777b2ef4c5a0eba08b406611c` |
|
||||||
|
| fi-floor-near-tint-on.jpg | `4a5d3e3f8e8254639fa1840a2fb4497f2ab19f64bb4734e13abe3c3cfcfb4008` |
|
||||||
|
| sw-ceiling-far-tint-off.jpg | `f5ec099076c507bed5a703302d89b665e139804a8f8895a68c1d5705aabf9f61` |
|
||||||
|
| sw-ceiling-far-tint-on.jpg | `b82618814c8320be2de1ac53ba10e9ab4469299a8b53b776be32c6ca9a659988` |
|
||||||
|
| sw-ceiling-near-tint-off.jpg | `5789ce59a7748aa278c1b95edae45aec87721721260c1a3aefe6c4cc1e47cd7a` |
|
||||||
|
| sw-ceiling-near-tint-on.jpg | `aa4351c5effc5e93fc0879257fe056b3f4625e7842f5118f28588fb2c5c1afe2` |
|
||||||
|
|
||||||
|
Final Core binary SHA256:
|
||||||
|
|
||||||
|
- Clang: `052694a569c92ef407c25bd9a534c7123ef800f46733e5c428e965c32d905fdc`
|
||||||
|
- GCC: `c8656901ce18861f12168849165785937a2495233604314df9e005ed1e92afdd`
|
||||||
|
|
||||||
|
## Cost and edits
|
||||||
|
|
||||||
|
Five interleaved pairs per recipe against L31. Median whole-view-ready time was SW 1.723 s → 1.756 s (+1.92%) and FI 16.963 s → 17.221 s (+1.52%); paired results were mixed and within observed run noise. No Unreal files changed. The five edited files are the Core public header, `FarShellsInternal.h`, `ResolvedLayoutInternal.h`, `ShapeWorldInternal.h` and `LinuxOffscreenCapture.cpp`; Unreal compilation was not available here.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- The initial multi-LOD Python audit was stopped by system RAM pressure (reported peak unavailable). Subsequent mesh audits ran with CPU/address-space caps and streamed or selected tiles; peak RSS is recorded above. The first whole-mesh FI LOD8 candidate scan was interrupted and replaced by a targeted tile scan.
|
||||||
|
- Early ring-window searches and narrow-height audits selected the wrong one of two overlapping faces. The FI LOD3/4 branch probes measured 1.423 m amplitude / a 1.0 m flat run; the SW LOD1 low-face probe reached 20.19° normal change. Face-locked, broad-window scans replaced them and pass. The exploratory `ring-*`, `ringset-*`, `search-*` and `window-search-*` logs preserve rejected window candidates. The L30/L31 failures above are intentional negative controls.
|
||||||
|
- The first Interleaved layout/export probe omitted the runner's required streaming flags and exited with usage; the retry produced the LOD0–8 export. Its broad 0.5 m candidate scan applied the Gradient 3 m window heuristic and rejected LOD0–4 (one LOD1 candidate switched overlapping faces at 62.92°); selected branch-locked profiles at those LODs and passing LOD5–8 windows show no plateaus or steps. An unsupported `--help` probe also returned usage with exit 2.
|
||||||
|
- The exact-source passage audit at midpoint preference switched faces and reported a 47.79° jump; upper- and lower-face locked reruns pass. An early wrong-camera export selected zero tiles; the exact camera export was rerun. Initial multi-LOD capture meshes mixed neighboring shells, so the final captures use focused LOD0/LOD8 surface patches.
|
||||||
|
- One fine-normal probe had a tuple-index error and was corrected. A SW far-mesh filter had an argument/layout parsing error and was corrected. The density CSV was overwritten by verbose output; the probe was rebuilt from final Core objects and rerun.
|
||||||
|
- The transition-cache build failed on a nested `Shape` initializer; the retry built, but the cache showed no gain and was removed. `/usr/bin/time` was unavailable for one capture attempt; the capped renderer was rerun directly. The first determinism run was interrupted and resumed; the final matrix passes 24/24. One log-search command used unavailable `rg`; a capped streaming-Python scan replaced it.
|
||||||
|
- Performance experiments were rejected or superseded: initial transition path +36.11% SW/+7.18% FI, planar query +14.89%/+2.28%, 3x3 coverage +6.06%/+1.65%, 5-point coverage +4.04%/+2.03%, single-transition fast path +3.52%/+1.39%; cache trial was slower (2.188 s vs 2.171 s). The retained hash-prefix version is the five-pair result above. Logs and exports are under `Saved/Linux/l32/`.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged. Captures checked by me: the FI floor (near, tint off) is a smooth rolling surface with no rings or terraces;
|
||||||
|
the SW ceiling and far views are smooth. Gradient is restored at width/8 (about 4 m peak-to-peak) with signed-distance
|
||||||
|
density; the ring audit rejects both L30 (rings) and L31 (flat). The memory-pressure kill mid-round was resumed;
|
||||||
|
audits now run capped and tile-streamed.
|
||||||
|
- Cumulative SW cost vs L24 is roughly +5% after L28–L32 (features: columns, transitions, smooth faces). FI is ≈ +2%.
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the recipe-stamped L31 passage audit matrix and reject mislabeled runs."""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l32-gcc-v20",
|
||||||
|
}
|
||||||
|
MARKERS = (
|
||||||
|
"[ResolvedPassageAuditSummary]",
|
||||||
|
"[PassageOpenAirAuditSummary]",
|
||||||
|
"[PassageNaturalnessSummary]",
|
||||||
|
"[PassageNoLongBridgeAudit]",
|
||||||
|
"[PassageRockColumnAuditSummary]",
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for style in range(4):
|
||||||
|
data = ROOT / f"Saved/Linux/linux25-owner-{recipe}-style{style}.json"
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"passage-{recipe}-style{style}-{compiler}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", 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, 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)
|
||||||
|
recipe_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
markers_ok = all(re.search(re.escape(marker) + r".*status=PASS", text)
|
||||||
|
for marker in MARKERS)
|
||||||
|
capsule_ok = bool(re.search(
|
||||||
|
r"^\[ResolvedPassageAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 mouth_content=2/2 mouth_radius=2/2 "
|
||||||
|
r"walkability=PASS$", text, re.M))
|
||||||
|
island_route_ok = (recipe != "floating-islands" or bool(re.search(
|
||||||
|
r"^\[FloatingIslandRouteAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 status=PASS$", text, re.M)))
|
||||||
|
binding_ok = bool(re.search(
|
||||||
|
r"\[ResolvedPassageAuditSummary\].*route_bindings=[1-9][0-9]* "
|
||||||
|
r"route_binding_failures=0 .*status=PASS", text))
|
||||||
|
walk_ok = "[Walkability] PASS" in text
|
||||||
|
if recipe == "floating-islands":
|
||||||
|
control_ok = "[PassageColumnDisconnectedEndControl]" in text and bool(
|
||||||
|
re.search(r"\[PassageColumnDisconnectedEndControl\].*detected=true", text))
|
||||||
|
else:
|
||||||
|
control_ok = True
|
||||||
|
ok = (result is not None and result.returncode == 0 and recipe_ok
|
||||||
|
and markers_ok and capsule_ok and island_route_ok and binding_ok
|
||||||
|
and walk_ok and control_ok)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "MISSING"
|
||||||
|
rows.append((recipe, actual_recipe, style, compiler, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s exit={result.returncode if result else 'TIMEOUT'} "
|
||||||
|
f"recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"capsule={'PASS' if capsule_ok and island_route_ok else 'FAIL'} "
|
||||||
|
f"column_control={'PASS' if control_ok else 'FAIL'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
if not ok:
|
||||||
|
failed = True
|
||||||
|
|
||||||
|
summary = OUT / "passage-audit-matrix.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"requested_recipe,actual_recipe,style,compiler,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{requested},{actual},{style},{compiler},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for requested, actual, style, compiler, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
control_log = OUT / "recipe-mismatch-control.log"
|
||||||
|
control_owner = OUT / "recipe-mismatch-control-owner.json"
|
||||||
|
control_owner_data = json.loads(
|
||||||
|
(ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json").read_text(encoding="utf-8"))
|
||||||
|
top_owner_stratum = max(control_owner_data["strates"], key=lambda stratum: stratum["top_chunk_z"])
|
||||||
|
top_owner_stratum["asset"]["GeneratorType"] = 6 # ECaveGeneratorType::FloatingIslands
|
||||||
|
control_owner.write_text(json.dumps(control_owner_data, separators=(",", ":")), encoding="utf-8")
|
||||||
|
control_command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", str(BINARIES["clang"]),
|
||||||
|
str(control_owner),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", "--recipe=surface-world",
|
||||||
|
"--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
with control_log.open("w", encoding="utf-8") as stream:
|
||||||
|
control = subprocess.run(control_command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
control_text = control_log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
control_ok = (control.returncode == 2 and
|
||||||
|
"[WorldAssemblerRecipeAudit] requested=surface-world actual=floating-islands "
|
||||||
|
"top_generator=FloatingIslands " in control_text and
|
||||||
|
"floating_islands_stratum=present status=FAIL" in control_text)
|
||||||
|
print(f"[L32RecipeMismatchControl] requested=surface-world actual=floating-islands "
|
||||||
|
f"exit={control.returncode} detected={'true' if control_ok else 'false'} "
|
||||||
|
f"{'PASS' if control_ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L32PassageMatrix] 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 not control_ok or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run five interleaved whole-view pairs against the L31 binary."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
RUN_TAG = os.environ.get("VOXELFORGE_COST_TAG", "").strip()
|
||||||
|
BINARIES = {
|
||||||
|
"l31": ROOT / "Saved/Linux/l31/bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"l32": Path(os.environ.get("VOXELFORGE_L32_COST_BINARY",
|
||||||
|
str(OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20"))),
|
||||||
|
}
|
||||||
|
ORDERS = (("l31", "l32"), ("l32", "l31"), ("l31", "l32"),
|
||||||
|
("l32", "l31"), ("l31", "l32"))
|
||||||
|
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:
|
||||||
|
tag = f"-{RUN_TAG}" if RUN_TAG else ""
|
||||||
|
name = f"cost{tag}-{recipe}-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
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, 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")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
if build == "l32":
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "missing"
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
else:
|
||||||
|
configured = re.findall(r"^\[HeadlessConfig\].*\brecipe=([^\s]+)", text, re.M)
|
||||||
|
actual_recipe = configured[0] if len(configured) == 1 else "missing"
|
||||||
|
floating_plan = "[FloatingIslandWorldAssembly]" in text
|
||||||
|
surface_plan = "[WorldAssemblerPrototype]" in text
|
||||||
|
plan_matches = ((actual_recipe == "floating-islands" and floating_plan)
|
||||||
|
or (actual_recipe == "surface-world" and not floating_plan
|
||||||
|
and surface_plan))
|
||||||
|
marker = (f"[L32BaselineRecipeAudit] requested={recipe} actual={actual_recipe} "
|
||||||
|
f"headless_recipe={configured[0] if len(configured) == 1 else 'missing'} "
|
||||||
|
f"floating_island_plan={'present' if floating_plan else 'absent'} "
|
||||||
|
f"surface_world_plan={'present' if surface_plan else 'absent'} "
|
||||||
|
f"status={'PASS' if actual_recipe == recipe and plan_matches else 'FAIL'}\n")
|
||||||
|
with logfile.open("a", encoding="utf-8") as stream:
|
||||||
|
stream.write(marker)
|
||||||
|
text += marker
|
||||||
|
stamp_ok = actual_recipe == recipe and plan_matches and len(configured) == 1
|
||||||
|
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
and "hash=off" in text and "cache_hits=0" in text and stamp_ok)
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
summary = OUT / (f"cost-runs-{RUN_TAG}.csv" if RUN_TAG else "cost-runs.csv")
|
||||||
|
summary.write_text(
|
||||||
|
"recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
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}
|
||||||
|
l31 = statistics.median(by_build["l31"])
|
||||||
|
l32 = statistics.median(by_build["l32"])
|
||||||
|
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["l32"] - values["l31"]) / values["l31"] * 100.0)
|
||||||
|
print(f"[L32CostSummary] recipe={recipe} pairs=5 median_l31_s={l31:.3f} "
|
||||||
|
f"median_l32_s={l32:.3f} median_change_pct={(l32/l31-1)*100.0:+.2f} "
|
||||||
|
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}")
|
||||||
|
|
||||||
|
print(f"[L32Cost] 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,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check the L25 hash command and 24 m crossing across compilers and workers."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
RUN_TAG = os.environ.get("VOXELFORGE_DETERMINISM_TAG", "").strip()
|
||||||
|
RUN_RESUME = os.environ.get("VOXELFORGE_DETERMINISM_RESUME", "0") == "1"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": Path(os.environ.get("VOXELFORGE_L32_CLANG_BINARY",
|
||||||
|
str(OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20"))),
|
||||||
|
"gcc": Path(os.environ.get("VOXELFORGE_L32_GCC_BINARY",
|
||||||
|
str(OUT / "bin/VoxelForgeCoreHeadless-l32-gcc-v20"))),
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
candidate_reference = {}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
prior_elapsed = {}
|
||||||
|
if RUN_RESUME and RUN_TAG:
|
||||||
|
prior_log = OUT / f"determinism-{RUN_TAG}-runner.log"
|
||||||
|
if prior_log.exists():
|
||||||
|
for line in prior_log.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||||
|
match = re.match(r"^(\S+) elapsed=([0-9.]+)s .* PASS$", line)
|
||||||
|
if match:
|
||||||
|
prior_elapsed[match.group(1)] = float(match.group(2))
|
||||||
|
|
||||||
|
|
||||||
|
def run(kind, compiler, recipe, workers):
|
||||||
|
global failed
|
||||||
|
tag = f"-{RUN_TAG}" if RUN_TAG else ""
|
||||||
|
name = f"{kind}{tag}-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(BINARIES[compiler]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--far-horizon-lod8-step=0", f"--workers={workers}", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
if kind == "crossing24":
|
||||||
|
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||||
|
if RUN_RESUME and name in prior_elapsed and logfile.exists():
|
||||||
|
result = subprocess.CompletedProcess(command, 0)
|
||||||
|
elapsed = prior_elapsed[name]
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
hashes = HASH_PATTERN.findall(text)
|
||||||
|
key = (kind, recipe)
|
||||||
|
reference = candidate_reference.get(key)
|
||||||
|
stable = reference is None or hashes == reference
|
||||||
|
if reference is None and hashes:
|
||||||
|
candidate_reference[key] = hashes
|
||||||
|
ok = (result is not None and result.returncode == 0
|
||||||
|
and stamps == [(recipe, recipe, "PASS")]
|
||||||
|
and "hash=on" in text and len(hashes) == 12 and stable)
|
||||||
|
rows.append((kind, recipe, compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", len(hashes), ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"hashes={len(hashes)} stable_across_matrix={'yes' if stable else 'no'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
for kind in ("l25", "crossing24"):
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
run(kind, compiler, recipe, workers)
|
||||||
|
|
||||||
|
summary = OUT / (f"determinism-{RUN_TAG}.csv" if RUN_TAG else "determinism.csv")
|
||||||
|
summary.write_text(
|
||||||
|
"kind,recipe,compiler,workers,elapsed_seconds,exit_code,hash_rows,status\n" +
|
||||||
|
"".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code},{count},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for kind, recipe, compiler, workers, elapsed, code, count, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L32Determinism] 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,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run L31 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/l32"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l32-gcc-v20",
|
||||||
|
}
|
||||||
|
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", "--", 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"[L32InvariantAudits] 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,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stream one VFMSH tile at a time to locate LOD/interface patches."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
candidates = []
|
||||||
|
scanned = 0
|
||||||
|
with open(args.scene, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit("invalid VFMSH001 scene")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.seek(40, 1)
|
||||||
|
if version != 1:
|
||||||
|
raise SystemExit("unsupported mesh scene")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
payload = vertex_count * 24 + index_count * 4 + mask_count
|
||||||
|
if key[3] != args.lod:
|
||||||
|
stream.seek(payload, 1)
|
||||||
|
continue
|
||||||
|
scanned += 1
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
hits = 0
|
||||||
|
sum_x = sum_y = sum_z = 0.0
|
||||||
|
min_z, max_z = float("inf"), float("-inf")
|
||||||
|
want_up = args.surface == "floor"
|
||||||
|
for offset in range(0, index_count, 3):
|
||||||
|
if masks and masks[offset // 3]:
|
||||||
|
continue
|
||||||
|
a, b, c = indices[offset:offset + 3]
|
||||||
|
if max(a, b, c) >= surface_count:
|
||||||
|
continue
|
||||||
|
nz = (normals[a * 3 + 2] + normals[b * 3 + 2] + normals[c * 3 + 2]) / 3.0
|
||||||
|
if (nz > 0) != want_up:
|
||||||
|
continue
|
||||||
|
za, zb, zc = positions[a * 3 + 2], positions[b * 3 + 2], positions[c * 3 + 2]
|
||||||
|
if ((za + zb + zc) / 3.0 < args.height_min_m
|
||||||
|
or (za + zb + zc) / 3.0 > args.height_max_m):
|
||||||
|
continue
|
||||||
|
hits += 1
|
||||||
|
sum_x += (positions[a * 3] + positions[b * 3] + positions[c * 3]) / 3.0
|
||||||
|
sum_y += (positions[a * 3 + 1] + positions[b * 3 + 1] + positions[c * 3 + 1]) / 3.0
|
||||||
|
z = (za + zb + zc) / 3.0
|
||||||
|
sum_z += z
|
||||||
|
min_z, max_z = min(min_z, za, zb, zc), max(max_z, za, zb, zc)
|
||||||
|
if hits:
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
candidates.append((hits, key, sum_x / hits, sum_y / hits,
|
||||||
|
sum_z / hits, min_z, max_z, extent))
|
||||||
|
candidates.sort(reverse=True, key=lambda item: item[0])
|
||||||
|
print(f"[VFMeshTileScan] lod={args.lod} surface={args.surface} scanned_tiles={scanned} "
|
||||||
|
f"candidate_tiles={len(candidates)} peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
for hits, key, x, y, z, low, high, extent in candidates[:40]:
|
||||||
|
print(f"[VFMeshTile] key={key} triangles={hits} centroid_m=({x:.3f},{y:.3f},{z:.3f}) "
|
||||||
|
f"z_m={low:.3f}..{high:.3f} extent_m={extent:.3f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build owner-like FloatingIslands -> SurfaceWorld -> TunnelNetwork fixtures."""
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
SOURCE = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
OUT = ROOT / "Saved/Linux/l33/fixtures"
|
||||||
|
|
||||||
|
|
||||||
|
def make_fixture(style: int) -> Path:
|
||||||
|
world = json.loads(SOURCE.read_text(encoding="utf-8"))
|
||||||
|
source_tunnel, source_surface = world["strates"]
|
||||||
|
|
||||||
|
floating = copy.deepcopy(source_tunnel)
|
||||||
|
floating.update(index=0, top_chunk_z=0, bottom_chunk_z=-15, height_chunks=16)
|
||||||
|
floating["asset"]["StrateName"] = "Linux33 Upper FloatingIslands"
|
||||||
|
floating["asset"]["StrateHeightInChunks"] = 16
|
||||||
|
floating["asset"]["GeneratorType"] = 6
|
||||||
|
|
||||||
|
surface = copy.deepcopy(source_surface)
|
||||||
|
surface.update(index=1, top_chunk_z=-16, bottom_chunk_z=-79, height_chunks=64)
|
||||||
|
surface["asset"]["StrateName"] = "Linux33 Middle SurfaceWorld"
|
||||||
|
surface["asset"]["StrateHeightInChunks"] = 64
|
||||||
|
surface["asset"]["PassageConfig"]["Style"] = style
|
||||||
|
surface["passage_config"]["Style"] = style
|
||||||
|
|
||||||
|
tunnel = copy.deepcopy(source_tunnel)
|
||||||
|
tunnel.update(index=2, top_chunk_z=-80, bottom_chunk_z=-143, height_chunks=64)
|
||||||
|
tunnel["asset"]["StrateName"] = "Linux33 Lower TunnelNetwork"
|
||||||
|
tunnel["asset"]["StrateHeightInChunks"] = 64
|
||||||
|
|
||||||
|
world["strates"] = [floating, surface, tunnel]
|
||||||
|
world["total_strates"] = 3
|
||||||
|
world["settings_asset"]["TotalStrates"] = 3
|
||||||
|
world["settings_asset"]["StratePool"] = [
|
||||||
|
"/Standalone/Linux33/UpperFloatingIslands",
|
||||||
|
"/Standalone/Linux33/MiddleSurfaceWorld",
|
||||||
|
"/Standalone/Linux33/LowerTunnelNetwork",
|
||||||
|
]
|
||||||
|
world["passages"] = [] # The L23 legacy coordinates are not part of this resolved stack.
|
||||||
|
world["source"] = "VoxelForge Linux33 owner-like SurfaceWorld-upper fixture"
|
||||||
|
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = OUT / f"sw-upper-tunnel-network-style{style}.json"
|
||||||
|
path.write_text(json.dumps(world, separators=(",", ":")), encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for style in range(4):
|
||||||
|
path = make_fixture(style)
|
||||||
|
print(f"style={style} fixture={path}")
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export and render two bounded SurfaceWorld departure views."""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l33"
|
||||||
|
FIXTURE = OUT / "fixtures/sw-upper-tunnel-network-style0.json"
|
||||||
|
HEADLESS = OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final"
|
||||||
|
CAPTURE = OUT / "bin/VoxelForgeOffscreenCapture-clang-l33-final"
|
||||||
|
PNG_DIR = OUT / "capture-png"
|
||||||
|
DEST = Path(__file__).resolve().parent / "captures"
|
||||||
|
PNG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
DEST.mkdir(parents=True, exist_ok=True)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["LP_NUM_THREADS"] = "12"
|
||||||
|
env["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||||
|
|
||||||
|
views = (
|
||||||
|
{
|
||||||
|
"name": "sw-departure-mouth",
|
||||||
|
"export_camera": "18.75,-12,-499,0,-22",
|
||||||
|
"render_camera": "18.75,0,-490",
|
||||||
|
"yaw": "0",
|
||||||
|
"pitch": "-90",
|
||||||
|
"fov": "38",
|
||||||
|
"height_center": "-503.166",
|
||||||
|
"height_range": "4",
|
||||||
|
"light": "0,-1,0.2",
|
||||||
|
"headlamp": "off",
|
||||||
|
"clip": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sw-departure-rock-entry",
|
||||||
|
"export_camera": "26,-16,-499,64,-16",
|
||||||
|
"render_camera": "14,-4,-500",
|
||||||
|
"yaw": "78",
|
||||||
|
"pitch": "-16",
|
||||||
|
"fov": "32",
|
||||||
|
"height_center": "-503.166",
|
||||||
|
"height_range": "4",
|
||||||
|
"light": "0,-1,0.2",
|
||||||
|
"headlamp": "off",
|
||||||
|
"clip": "1,0,0,-18.75",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, command):
|
||||||
|
logfile = OUT / f"capture-{name}.log"
|
||||||
|
wrapped = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", *command,
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(wrapped, cwd=ROOT, env=env, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||||
|
print(f"[L33CaptureRun] step={name} elapsed={time.monotonic()-started:.1f}s "
|
||||||
|
f"exit={result.returncode} log={logfile}", flush=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise SystemExit(f"{name} failed; see {logfile}")
|
||||||
|
|
||||||
|
|
||||||
|
for view in views:
|
||||||
|
name = view["name"]
|
||||||
|
mesh = OUT / f"{name}.vfmesh"
|
||||||
|
png = PNG_DIR / f"{name}.png"
|
||||||
|
jpg = DEST / f"{name}.jpg"
|
||||||
|
run(f"{name}-export", [
|
||||||
|
str(HEADLESS), str(FIXTURE), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||||
|
"--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0",
|
||||||
|
"--recipe=floating-islands",
|
||||||
|
f"--camera={view['export_camera']}", f"--mesh-export={mesh}",
|
||||||
|
])
|
||||||
|
command = [
|
||||||
|
str(CAPTURE), str(mesh), str(png), "--size=1600x900", f"--fov-deg={view['fov']}",
|
||||||
|
f"--camera-m={view['render_camera']}", f"--yaw-deg={view['yaw']}",
|
||||||
|
f"--pitch-deg={view['pitch']}", "--shade=readable",
|
||||||
|
f"--headlamp={view['headlamp']}", f"--light-direction={view['light']}",
|
||||||
|
"--height-tint=on", f"--height-center-m={view['height_center']}",
|
||||||
|
f"--height-range-m={view['height_range']}", "--tile-edges=off",
|
||||||
|
]
|
||||||
|
if view["clip"]:
|
||||||
|
command.append(f"--clip-plane={view['clip']}")
|
||||||
|
run(f"{name}-render", command)
|
||||||
|
run(f"{name}-jpeg", [
|
||||||
|
"ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y",
|
||||||
|
"-i", str(png), "-frames:v", "1", "-q:v", "4", str(jpg),
|
||||||
|
])
|
||||||
|
size = jpg.stat().st_size if jpg.exists() else 0
|
||||||
|
ok = 0 < size <= 300_000
|
||||||
|
print(f"[L33CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||||
|
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
if not ok:
|
||||||
|
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check representative owner hashes against L32 before the full matrix."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l33"
|
||||||
|
L32 = ROOT / "Saved/Linux/l32"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-gcc-l33-final",
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
name = f"owner-hash-preflight-{compiler}-{recipe}-w12"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--far-horizon-lod8-step=0", "--workers=12", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
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
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
hashes = HASH_PATTERN.findall(text)
|
||||||
|
baseline_path = L32 / f"l25-settled-final-{compiler}-{recipe}-w12.log"
|
||||||
|
baseline = HASH_PATTERN.findall(baseline_path.read_text(
|
||||||
|
encoding="utf-8", errors="replace")) if baseline_path.exists() else []
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
same = len(hashes) == 12 and hashes == baseline
|
||||||
|
ok = (result is not None and result.returncode == 0 and same
|
||||||
|
and stamps == [(recipe, recipe, "PASS")])
|
||||||
|
failed |= not ok
|
||||||
|
print(f"{name} elapsed={time.monotonic()-started:.1f}s hashes={len(hashes)}/12 "
|
||||||
|
f"same_as_l32={'yes' if same else 'no'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Linux33 report
|
||||||
|
|
||||||
|
## Upper SurfaceWorld route
|
||||||
|
|
||||||
|
The old fixed candidate set used 64 m chords, 2 m waypoint spacing and 16 m centre spacing. On the refreshed upper-stack fixture, the legacy replay measured 366/648 candidates above the 15° grade limit; its worst segment was 79.971° at XY (−84,−48) to (−82,−48). These were long paths over steep terrain relief near the outer search area. L23’s earlier fixture reported 648/648 grade rejects; the refreshed fixture gives a different count. The route Z samples are terrain-ground heights (about −503 m at the chosen mouth), not a target at the bottom of the 499 m strate. There was no unit or bedrock-reference error.
|
||||||
|
|
||||||
|
The planner now caps chords at 58.5 m for the 24 m arrival and 37.5 m departure radii, searches six lengths and places route centres on an 8 m grid, with a proximity cost favouring the origin. It found a 21.5 m walk from (−2.75,0) to departure (18.75,0), average grade about 4.2°. The SW passage starts at the ground floor (−503.166 m), with a 1.5 m portal blending to a 3.5 m tunnel over 9 m, then descends through SW rock. The first tunnel point is at (32.5,0), floor −505.637 m for style 0.
|
||||||
|
|
||||||
|
## Upper-stack fixture audits
|
||||||
|
|
||||||
|
Fixture: FloatingIslands → SurfaceWorld → TunnelNetwork; recipe stamp `floating-islands`; layout identity `resolved-layout-v3-fnv64:fc4e73619037f88c`. Clang and GCC passed every row for every style.
|
||||||
|
|
||||||
|
| Passage style | Floor chord deviation (voxels) | Passage and route audits | Open air | Naturalness | Bridge / columns | Bindings / walkability |
|
||||||
|
|---:|---:|---|---|---|---|---|
|
||||||
|
| 0 | 2.081 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass |
|
||||||
|
| 1 | 10.901 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass |
|
||||||
|
| 2 | 14.822 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass |
|
||||||
|
| 3 | 16.374 | 2/2 each pass | 0 violations | 2/2 pass | pass / 0 failures | 0 failures / pass |
|
||||||
|
|
||||||
|
All rows are recipe-stamped and passed on both compilers. Capsule blocking, unsupported samples and grade failures were zero; mouth-content and mouth-radius checks were 2/2. This includes open-air, naturalness, no-long-bridge, rock-column wall/support, route binding and walkability audits.
|
||||||
|
|
||||||
|
## FloorProfileBlend
|
||||||
|
|
||||||
|
The old 2-voxel check measured maximum curve-to-straight-chord deviation, which grows with valid curvature and does not measure a seam. The check now compares endpoint height and tangent residuals. All styles have 0.00000000 voxel height residual and 0.00000000 voxel/m tangent residual (limits 0.010 and 0.001). Negative controls detected a 3-voxel step and a 0.25 voxel/m tangent step. Verdict: check corrected; no floor geometry change was needed.
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
|
||||||
|
Both 1600×900 JPEGs are under 300 KB. The first shows the SW ground mouth; the second is a cutaway through the rock entry.
|
||||||
|
|
||||||
|
Sizes and SHA-256: mouth 26,939 B, `561d3a1e334ea3a3115fb781a1130ec850d8777b74c1be3704c70b865878640a`; rock entry 37,572 B, `f85d150bf44cd2cdae1ee49de023ad9e4c657b8fa7e8d8f8b93abc86586ad6e5`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Hashes, determinism and cost
|
||||||
|
|
||||||
|
Owner-world hashes match L32 exactly: all 12 hash lines per run across Clang/GCC and both recipes passed the owner preflight; the L25 and 24 m crossing matrix also passed at W1/W8/W12 (24/24 owner rows). W12 whole-view hashes: SurfaceWorld density `953eb1df1705f277`, mesh `d8006cb43f44aa5b`, payload `9217bff766970de5`, topology `83e3aa6f3a063e60`; FloatingIslands density `806537cdd98f8afb`, mesh `a4b06dd4d260943d`, payload `763a99aa04a73eae`, topology `baa37d80d3620d87`. The upper fixture was stable across W1/W8/W12 on both compilers. Total determinism: 30/30. Invariant audits: 16/16.
|
||||||
|
|
||||||
|
Five interleaved W12 cost pairs per recipe against L32: median whole-view ready time changed +0.99% for SurfaceWorld and +0.41% for FloatingIslands; paired medians were +0.57% and +0.41%, within run noise. Recipe/evaluator versions remain 2/20. No Unreal files changed; no Unreal build was run. No version bump was needed because the owner hashes are unchanged.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- CLI setup rejected `--compiler clang`; the accepted form was `--compiler=clang++-19`. One shell quoting attempt ended at EOF. A FloatingIslands fixture initially had a `surface-world` recipe stamp and was regenerated with `floating-islands`.
|
||||||
|
- A translated ring centre failed the strict placement disk and was reverted. Fixture audit iterations: r1 had style 1–3 failures (TN capsule pinch, route undercut, naturalness); r2 had 141 open-air violations plus route/naturalness failures; r3 had strict ring-placement and route-support failures; r4 had four edge open-air samples. The broad column-window fix passed passage audits but changed owner hashes. Its first scoped retry dropped supported bracket samples and failed SurfaceWorld hash preflight on both compilers; restoring the original non-SW bracket bounds fixed it.
|
||||||
|
- An intermediate Clang build failed on `coreVoxelSizeMetres` and `gradeSegments`; both names were corrected. Final Clang 19 and GCC 14 `--fp-contract=off`, `-j12` builds succeeded. An early determinism run on stale binaries was stopped after 21 rows and showed the interim hash mismatch; the final binaries passed all 30 rows.
|
||||||
|
- The first capture export included the far ring and grew to 2.49 GB; it was stopped before rendering. The LOD0 streamed export reduced the scene to about 2.2 MB. Capture framings r5–r8 rendered successfully but obscured the mouth; r9 is the inspected final framing.
|
||||||
|
|
||||||
|
No run exceeded 30 minutes and no step remained stuck for 15 minutes. Logs and exports are in `Saved/Linux/l33/`; no commit, push or stash was made.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged: the owner world is unchanged (hashes equal L32); SW-as-upper now plans its route and passes the full audit set
|
||||||
|
on the FI→SW→TN fixture. The FloorProfileBlend check now measures seams (endpoint height/tangent residuals) with
|
||||||
|
step/tangent negative controls. Accepted: chord deviation measured curvature, not a defect.
|
||||||
|
- Captures: `sw-departure-mouth.jpg` is out of focus and unreadable; `sw-departure-rock-entry.jpg` shows a natural opening
|
||||||
|
but **horizontal step-like ledges inside**, reminiscent of the L23 terraces. L34 checks passage mouths and entries for
|
||||||
|
stepping across styles and fixes the blurry capture setup.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run five interleaved W12 whole-view pairs against the L32 binary."""
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l33"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"l32": ROOT / "Saved/Linux/l32/bin/VoxelForgeCoreHeadless-l32-clang-v20",
|
||||||
|
"l33": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final",
|
||||||
|
}
|
||||||
|
ORDERS = (("l32", "l33"), ("l33", "l32"), ("l32", "l33"),
|
||||||
|
("l33", "l32"), ("l32", "l33"))
|
||||||
|
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", "--",
|
||||||
|
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, 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")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, 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 text and "cache_hits=0" in text and stamp_ok)
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
summary = OUT / "cost-runs.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
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["l32"])
|
||||||
|
current = statistics.median(by_build["l33"])
|
||||||
|
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["l33"] - values["l32"]) / values["l32"] * 100.0)
|
||||||
|
print(f"[L33CostSummary] recipe={recipe} pairs=5 median_l32_s={baseline:.3f} "
|
||||||
|
f"median_l33_s={current:.3f} median_change_pct={(current/baseline-1)*100.0:+.2f} "
|
||||||
|
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L33Cost] 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,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run owner L25 hashes, 24 m crossings, and upper-SurfaceWorld fixture determinism."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l33"
|
||||||
|
L32 = ROOT / "Saved/Linux/l32"
|
||||||
|
FIXTURE = OUT / "fixtures/sw-upper-tunnel-network-style0.json"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-gcc-l33-final",
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
LAYOUT_PATTERN = re.compile(
|
||||||
|
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||||
|
re.M)
|
||||||
|
PLAN_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||||
|
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||||
|
ROUTE_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||||
|
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
owner_reference = {}
|
||||||
|
fixture_reference = None
|
||||||
|
|
||||||
|
|
||||||
|
def run(command, logfile):
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
return result, time.monotonic() - started, logfile.read_text(
|
||||||
|
encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
for kind in ("l25", "crossing24"):
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"{kind}-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"determinism-{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--far-horizon-lod8-step=0",
|
||||||
|
f"--workers={workers}", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
if kind == "crossing24":
|
||||||
|
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||||
|
result, elapsed, text = run(command, logfile)
|
||||||
|
hashes = HASH_PATTERN.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
ref = L32 / f"{kind}-settled-final-{compiler}-{recipe}-w{workers}.log"
|
||||||
|
reference_hashes = HASH_PATTERN.findall(ref.read_text(
|
||||||
|
encoding="utf-8", errors="replace")) if ref.exists() else []
|
||||||
|
stable_key = (kind, recipe)
|
||||||
|
stable = stable_key not in owner_reference or hashes == owner_reference[stable_key]
|
||||||
|
if stable_key not in owner_reference and hashes:
|
||||||
|
owner_reference[stable_key] = hashes
|
||||||
|
equal_l32 = len(hashes) == 12 and hashes == reference_hashes
|
||||||
|
ok = (result is not None and result.returncode == 0
|
||||||
|
and stamps == [(recipe, recipe, "PASS")]
|
||||||
|
and "hash=on" in text and len(hashes) == 12 and stable and equal_l32)
|
||||||
|
rows.append((kind, recipe, compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s hashes={len(hashes)}/12 "
|
||||||
|
f"same_across_matrix={'yes' if stable else 'no'} "
|
||||||
|
f"same_as_l32={'yes' if equal_l32 else 'no'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"upper-fixture-{compiler}-w{workers}"
|
||||||
|
logfile = OUT / f"determinism-{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(binary), str(FIXTURE), "--workers=" + str(workers),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--max-level=-1", "--recipe=floating-islands", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
result, elapsed, text = run(command, logfile)
|
||||||
|
layouts = LAYOUT_PATTERN.findall(text)
|
||||||
|
plans = PLAN_PATTERN.findall(text)
|
||||||
|
routes = ROUTE_PATTERN.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
passage_summary = re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", text, re.M)
|
||||||
|
signature = (tuple(layouts), tuple(plans), tuple(routes))
|
||||||
|
stable = fixture_reference is None or signature == fixture_reference
|
||||||
|
if fixture_reference is None and layouts and plans and routes:
|
||||||
|
fixture_reference = signature
|
||||||
|
ok = (result is not None and result.returncode == 0 and len(layouts) == 1
|
||||||
|
and len(plans) == 1 and len(routes) == 1
|
||||||
|
and stamps == [("floating-islands", "floating-islands", "PASS")]
|
||||||
|
and passage_summary is not None and stable)
|
||||||
|
rows.append(("upper-fixture", "floating-islands", compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s layout={layouts[0] if layouts else 'MISSING'} "
|
||||||
|
f"stable_across_matrix={'yes' if stable else 'no'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
summary = OUT / "determinism-l33.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"kind,recipe,compiler,workers,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for kind, recipe, compiler, workers, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L33Determinism] 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,81 @@
|
|||||||
|
#!/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/l33"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-clang-l33-final",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-gcc-l33-final",
|
||||||
|
}
|
||||||
|
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", "--",
|
||||||
|
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,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Recipe-stamped resolved-passage audits for the L33 upper-SurfaceWorld stack."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l33"
|
||||||
|
TAG = os.environ.get("VOXELFORGE_L33_AUDIT_TAG", "clang").strip() or "clang"
|
||||||
|
BINARY = Path(os.environ.get("VOXELFORGE_L33_AUDIT_BINARY",
|
||||||
|
str(OUT / f"bin/VoxelForgeCoreHeadless-{TAG}-l33-final")))
|
||||||
|
ROWS = []
|
||||||
|
FAILED = False
|
||||||
|
|
||||||
|
for style in range(4):
|
||||||
|
name = f"upper-sw-three-strata-{TAG}-style{style}"
|
||||||
|
fixture = OUT / f"fixtures/sw-upper-tunnel-network-style{style}.json"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(BINARY), str(fixture), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||||
|
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
"--recipe=floating-islands", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamp_ok = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M) == [("floating-islands", "floating-islands", "PASS")]
|
||||||
|
summary = re.search(r"^\[ResolvedPassageAuditSummary\].*$", text, re.M)
|
||||||
|
summary_ok = bool(summary and "interfaces=2 passed=2 natural=2/2" in summary.group()
|
||||||
|
and "route_walkable=2" in summary.group()
|
||||||
|
and "route_binding_failures=0" in summary.group()
|
||||||
|
and "status=PASS" in summary.group())
|
||||||
|
markers = (
|
||||||
|
r"^\[PassageOpenAirAuditSummary\].*violations=0.*status=PASS$",
|
||||||
|
r"^\[PassageNoLongBridgeAudit\].*status=PASS$",
|
||||||
|
r"^\[PassageRockColumnAuditSummary\].*wall_or_support_failures=0.*status=PASS$",
|
||||||
|
r"^\[PassageNaturalnessSummary\].*status=PASS$",
|
||||||
|
)
|
||||||
|
markers_ok = all(re.search(pattern, text, re.M) for pattern in markers)
|
||||||
|
passage_rows = re.findall(
|
||||||
|
r"^\[ResolvedPassageAudit\].*blocked=0 support_samples=\d+ unsupported=0 "
|
||||||
|
r"grade_failures=0 mouth_content=2/2 mouth_radius=2/2 walkability=PASS$",
|
||||||
|
text, re.M)
|
||||||
|
floor_rows = re.findall(r"^\[FloorProfileBlend\].*status=PASS$", text, re.M)
|
||||||
|
route_rows = re.findall(
|
||||||
|
r"^\[ResolvedStrateRouteAudit\].*blocked=0 support_samples=\d+ unsupported=0 "
|
||||||
|
r"grade_failures=0 mouth_binding=PASS status=PASS$", text, re.M)
|
||||||
|
ok = (result is not None and result.returncode == 0 and stamp_ok and summary_ok
|
||||||
|
and markers_ok and len(passage_rows) == 2 and len(route_rows) == 2
|
||||||
|
and len(floor_rows) >= 1 and "[Walkability] PASS" in text)
|
||||||
|
FAILED |= not ok
|
||||||
|
ROWS.append((style, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s exit={result.returncode if result else 'TIMEOUT'} "
|
||||||
|
f"recipe={'PASS' if stamp_ok else 'FAIL'} passages={len(passage_rows)}/2 "
|
||||||
|
f"routes={len(route_rows)}/2 floor_blend_rows={len(floor_rows)} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
summary_path = OUT / f"upper-sw-three-strata-{TAG}-audits.csv"
|
||||||
|
summary_path.write_text(
|
||||||
|
"style,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{style},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for style, elapsed, code, ok in ROWS), encoding="utf-8")
|
||||||
|
print(f"[L33UpperSurfaceFixtureAudit] compiler={TAG} runs={len(ROWS)} pass={sum(row[-1] for row in ROWS)} "
|
||||||
|
f"fail={sum(not row[-1] for row in ROWS)} summary={summary_path}", flush=True)
|
||||||
|
raise SystemExit(1 if FAILED or len(ROWS) != 4 else 0)
|
||||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,251 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Measure exported passage-floor meshes along the first 12 m at each mouth."""
|
||||||
|
import csv
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
RESEARCH = Path(__file__).resolve().parent
|
||||||
|
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
|
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||||
|
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(PROFILE)
|
||||||
|
Q_TO_M = 1.0 / 256.0
|
||||||
|
SPACING = 0.125
|
||||||
|
PLATEAU = 0.50
|
||||||
|
EPSILON = 0.02
|
||||||
|
STEP = 0.10
|
||||||
|
SCENES = (
|
||||||
|
("owner-fi", "layout-owner-fi.json", "FloatingIslands", False),
|
||||||
|
("owner-sw", "layout-owner-sw.json", "SurfaceWorld", True),
|
||||||
|
("upper-sw-fixture", "layout-upper-sw-fixture.json", "SurfaceWorld", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dist(a, b):
|
||||||
|
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate(path, distance):
|
||||||
|
remaining = max(0.0, distance)
|
||||||
|
for a, b in zip(path, path[1:]):
|
||||||
|
length = dist(a[0], b[0])
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
continue
|
||||||
|
if remaining <= length:
|
||||||
|
t = remaining / length
|
||||||
|
p = tuple(a[0][axis] + (b[0][axis] - a[0][axis]) * t
|
||||||
|
for axis in range(3))
|
||||||
|
return p, a[1] + (b[1] - a[1]) * t
|
||||||
|
remaining -= length
|
||||||
|
return path[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def make_inward_path(layout, passage, reverse):
|
||||||
|
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||||
|
controls = [(tuple(value * scale for value in point["position_q"]),
|
||||||
|
point["radius_q"] * scale) for point in passage["control_points"]]
|
||||||
|
lower = tuple(value * scale for value in passage["lower_mouth_q"])
|
||||||
|
lower_index = min(range(len(controls)), key=lambda i: dist(controls[i][0], lower))
|
||||||
|
path = controls[:lower_index + 1]
|
||||||
|
if reverse:
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def select_passage(layout, generator, prefer_lower):
|
||||||
|
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||||
|
preferred_side = "lower" if prefer_lower else "upper"
|
||||||
|
preferred_candidates = []
|
||||||
|
fallback_candidates = []
|
||||||
|
for passage in layout["passages"]:
|
||||||
|
upper = strata[int(passage["upper_strate_index"])]["generator_kind"]
|
||||||
|
lower = strata[int(passage["lower_strate_index"])]["generator_kind"]
|
||||||
|
sides = []
|
||||||
|
if upper == generator:
|
||||||
|
sides.append("upper")
|
||||||
|
if lower == generator:
|
||||||
|
sides.append("lower")
|
||||||
|
if sides:
|
||||||
|
if preferred_side in sides:
|
||||||
|
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||||
|
else:
|
||||||
|
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||||
|
candidates = preferred_candidates or fallback_candidates
|
||||||
|
if not candidates:
|
||||||
|
raise RuntimeError(f"no route for {generator}")
|
||||||
|
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||||
|
|
||||||
|
|
||||||
|
def surface_candidates(buckets, x, y, cell_size, preferred_height):
|
||||||
|
hits = []
|
||||||
|
for verts in buckets.get((math.floor(x / cell_size), math.floor(y / cell_size)), ()):
|
||||||
|
(a, na), (b, nb), (c, nc) = verts
|
||||||
|
denominator = (b[1] - c[1]) * (a[0] - c[0]) + \
|
||||||
|
(c[1] - b[1]) * (a[0] - c[0])
|
||||||
|
if abs(denominator) < 1.0e-12:
|
||||||
|
continue
|
||||||
|
u = ((b[1] - c[1]) * (x - c[0]) + (c[0] - b[0]) * (y - c[1])) / denominator
|
||||||
|
v = ((c[1] - a[1]) * (x - c[0]) + (a[0] - c[0]) * (y - c[1])) / denominator
|
||||||
|
w = 1.0 - u - v
|
||||||
|
if min(u, v, w) < -1.0e-6:
|
||||||
|
continue
|
||||||
|
normal_z = u * na[2] + v * nb[2] + w * nc[2]
|
||||||
|
if normal_z <= 0.0:
|
||||||
|
continue
|
||||||
|
z = u * a[2] + v * b[2] + w * c[2]
|
||||||
|
if abs(z - preferred_height) <= 1.0:
|
||||||
|
hits.append((z, normal_z))
|
||||||
|
unique = {}
|
||||||
|
for z, normal_z in hits:
|
||||||
|
key = round(z, 4)
|
||||||
|
unique[key] = max(unique.get(key, 0.0), normal_z)
|
||||||
|
return [(z, round(normal_z, 3)) for z, normal_z in sorted(unique.items(),
|
||||||
|
key=lambda item: abs(item[0] - preferred_height))[:12]]
|
||||||
|
|
||||||
|
|
||||||
|
def sample_profile(mesh_path, path):
|
||||||
|
total = sum(dist(a[0], b[0]) for a, b in zip(path, path[1:]))
|
||||||
|
reach = min(12.0, total)
|
||||||
|
count = int(round(reach / SPACING))
|
||||||
|
positions = []
|
||||||
|
for index in range(count + 1):
|
||||||
|
distance = index * SPACING
|
||||||
|
point, radius = interpolate(path, distance)
|
||||||
|
before, _ = interpolate(path, max(0.0, distance - 0.25))
|
||||||
|
after, _ = interpolate(path, min(total, distance + 0.25))
|
||||||
|
tangent = tuple(after[axis] - before[axis] for axis in range(3))
|
||||||
|
length = math.hypot(tangent[0], tangent[1])
|
||||||
|
if length <= 1.0e-8:
|
||||||
|
positions.append((distance, point, radius, None))
|
||||||
|
else:
|
||||||
|
side = (-tangent[1] / length, tangent[0] / length)
|
||||||
|
positions.append((distance, point, radius, side))
|
||||||
|
expected = [point[2] - radius for _, point, radius, _ in positions]
|
||||||
|
bounds = (min(value[1][0] for value in positions) - 14.0,
|
||||||
|
max(value[1][0] for value in positions) + 14.0,
|
||||||
|
min(value[1][1] for value in positions) - 14.0,
|
||||||
|
max(value[1][1] for value in positions) + 14.0)
|
||||||
|
voxel_um, triangles, selected = PROFILE.read_scene(
|
||||||
|
str(mesh_path), None, min(expected) - 2.0, max(expected) + 2.0, bounds)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||||
|
buckets = PROFILE.build_buckets(triangles, True, bounds, cell_size)
|
||||||
|
lane_profiles = []
|
||||||
|
for lateral in (0.0, 0.75, -0.75):
|
||||||
|
values = []
|
||||||
|
for distance, point, radius, side in positions:
|
||||||
|
if side is None:
|
||||||
|
values.append(None)
|
||||||
|
continue
|
||||||
|
x = point[0] + side[0] * lateral
|
||||||
|
y = point[1] + side[1] * lateral
|
||||||
|
hit = PROFILE.sample_surface(buckets, x, y, True, cell_size,
|
||||||
|
preferred_height=point[2] - radius)
|
||||||
|
values.append((distance, hit[0]) if hit else None)
|
||||||
|
lane_profiles.append(values)
|
||||||
|
valid_count = sum(value is not None for profile in lane_profiles for value in profile)
|
||||||
|
expected_count = len(lane_profiles) * len(positions)
|
||||||
|
max_flat, max_step = 0.0, 0.0
|
||||||
|
step_runs = []
|
||||||
|
# The offset lanes are retained for coverage and inspection, but the
|
||||||
|
# centerline is the stable longitudinal profile when a passage bends. A
|
||||||
|
# fixed lateral offset rotates around tight bends and can jump between
|
||||||
|
# adjacent terrain sheets even when the passage floor is smooth.
|
||||||
|
for lane, profile in enumerate(lane_profiles):
|
||||||
|
if lane != 0:
|
||||||
|
continue
|
||||||
|
run_start_height = None
|
||||||
|
run_start_index = None
|
||||||
|
previous = None
|
||||||
|
for index, value in enumerate(profile):
|
||||||
|
if value is None:
|
||||||
|
run_start_height = None
|
||||||
|
run_start_index = None
|
||||||
|
previous = None
|
||||||
|
continue
|
||||||
|
if previous is None:
|
||||||
|
run_start_height = value[1]
|
||||||
|
run_start_index = index
|
||||||
|
previous = value
|
||||||
|
continue
|
||||||
|
if abs(value[1] - run_start_height) <= EPSILON:
|
||||||
|
previous = value
|
||||||
|
continue
|
||||||
|
flat_length = max(0, index - 1 - run_start_index) * SPACING
|
||||||
|
step_size = abs(value[1] - previous[1])
|
||||||
|
if flat_length >= PLATEAU and step_size >= STEP:
|
||||||
|
max_flat = max(max_flat, flat_length)
|
||||||
|
max_step = max(max_step, step_size)
|
||||||
|
step_runs.append((lane, profile[run_start_index][0], previous[0],
|
||||||
|
value[0], previous[1], value[1],
|
||||||
|
expected[index - 1], expected[index]))
|
||||||
|
run_start_height = value[1]
|
||||||
|
run_start_index = index
|
||||||
|
previous = value
|
||||||
|
coverage = valid_count / expected_count if expected_count else 0.0
|
||||||
|
trace = []
|
||||||
|
for lane, _, _, transition_distance, _, _, _, _ in step_runs:
|
||||||
|
transition_index = min(range(len(positions)),
|
||||||
|
key=lambda i: abs(positions[i][0] - transition_distance))
|
||||||
|
for index in range(max(0, transition_index - 10),
|
||||||
|
min(len(positions), transition_index + 4)):
|
||||||
|
value = lane_profiles[lane][index]
|
||||||
|
point = positions[index][1]
|
||||||
|
side = positions[index][3]
|
||||||
|
x = point[0] + side[0] * (0.75 if lane == 1 else -0.75 if lane == 2 else 0.0)
|
||||||
|
y = point[1] + side[1] * (0.75 if lane == 1 else -0.75 if lane == 2 else 0.0)
|
||||||
|
trace.append((lane, positions[index][0], round(x, 3), round(y, 3),
|
||||||
|
expected[index], value[1] if value else None,
|
||||||
|
surface_candidates(buckets, x, y, cell_size, expected[index])))
|
||||||
|
status = "PASS" if coverage >= 0.75 and max_flat == 0.0 else \
|
||||||
|
"NO_COVERAGE" if coverage < 0.75 else "FAIL"
|
||||||
|
return {
|
||||||
|
"mesh": mesh_path.name,
|
||||||
|
"tiles": selected,
|
||||||
|
"floor_samples": valid_count,
|
||||||
|
"floor_expected": expected_count,
|
||||||
|
"coverage": coverage,
|
||||||
|
"max_flat_run_m": max_flat,
|
||||||
|
"following_step_m": max_step,
|
||||||
|
"step_runs": step_runs,
|
||||||
|
"trace": trace,
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for scene, layout_file, generator, prefer_lower in SCENES:
|
||||||
|
layout = json.loads((OUT / layout_file).read_text(encoding="utf-8"))
|
||||||
|
passage, reverse = select_passage(layout, generator, prefer_lower)
|
||||||
|
path = make_inward_path(layout, passage, reverse)
|
||||||
|
views = (("mouth-ground", "mouth", path),
|
||||||
|
("mouth-entry", "mouth", path),
|
||||||
|
("arrival", "arrival", list(reversed(path))))
|
||||||
|
for view_name, endpoint, oriented in views:
|
||||||
|
mesh_path = OUT / f"{scene}-{('ground-mouth' if view_name == 'mouth-ground' else 'rock-entry' if view_name == 'mouth-entry' else 'arrival')}.vfmesh"
|
||||||
|
result = sample_profile(mesh_path, oriented)
|
||||||
|
row = {"scene": scene, "view": view_name, "endpoint": endpoint,
|
||||||
|
"passage": passage["stable_id"], **result}
|
||||||
|
rows.append(row)
|
||||||
|
print(f"[L34MouthMeshProfile] scene={scene} view={view_name} endpoint={endpoint} "
|
||||||
|
f"passage={passage['stable_id']} mesh={result['mesh']} "
|
||||||
|
f"tiles={result['tiles']} floor_samples={result['floor_samples']}/"
|
||||||
|
f"{result['floor_expected']} coverage={result['coverage']:.3f} "
|
||||||
|
f"flat_run_m={result['max_flat_run_m']:.3f} "
|
||||||
|
f"following_step_m={result['following_step_m']:.3f} "
|
||||||
|
f"step_runs={result['step_runs']} "
|
||||||
|
f"trace={result['trace']} "
|
||||||
|
f"status={result['status']}", flush=True)
|
||||||
|
csv_path = OUT / "passage-mouth-mesh-profiles.csv"
|
||||||
|
with csv_path.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
print(f"[L34MouthMeshProfileSummary] profiles={len(rows)} "
|
||||||
|
f"pass={sum(row['status'] == 'PASS' for row in rows)} "
|
||||||
|
f"no_coverage={sum(row['status'] == 'NO_COVERAGE' for row in rows)} "
|
||||||
|
f"fail={sum(row['status'] == 'FAIL' for row in rows)} file={csv_path}", flush=True)
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export and render the standard four-view passage set for L34."""
|
||||||
|
import json
|
||||||
|
import importlib.util
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
BIN = OUT / "bin"
|
||||||
|
CAPTURES = Path(__file__).resolve().parent / "captures"
|
||||||
|
HEADLESS = BIN / "VoxelForgeCoreHeadless-l34-clang"
|
||||||
|
RENDERER = BIN / "VoxelForgeOffscreenCapture-l34-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"[L34CaptureRun] 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 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"[L34StandardCaptureSet] 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"]
|
||||||
|
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 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"[L34CaptureCamera] 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"[L34CaptureJPEG] 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,40 @@
|
|||||||
|
# VoxelForge L34 report
|
||||||
|
|
||||||
|
## Verdict and fix
|
||||||
|
|
||||||
|
The 0.125 m floor and wall profiles found no 0.5 m flat run followed by a step of at least 0.1 m in any of the 24 style, recipe, fixture, and compiler runs. The L23-style negative-control profile (2.0 m flat run, then a 0.25 m step) was detected. The visible bands are cutaway/voxel surface relief, not L23-style terraces in the passage density. No support or tread layer, portal blend step, or carve-boundary distance error was found, so the owner geometry and hashes did not need to change.
|
||||||
|
|
||||||
|
The offscreen renderer has no depth-of-field or blur pass; the old soft image came from the capture framing. The reusable capture script uses 80° FOV, 1.6 m eye height, outside mouth cameras 5.15–6.27 m from the route mouth, LOD0 meshes, and tile skirts off. Inside views use clip planes with cap triangles. Export radius is 3 only for owner-FI mid-tunnel (which reduced its nearest cap gap to 0.058 m); other views use radius 2.
|
||||||
|
|
||||||
|
## Audits and captures
|
||||||
|
|
||||||
|
- Clang and GCC builds passed. Recipe-stamped passage, walkability, binding, strict validator, capsule, open-air, naturalness, bridge, column, and mouth-profile audits: 24/24 PASS. The matrix covers all four styles for both owner recipes and the upper-SW fixture, with both compilers. L24 and L26 negative controls also triggered.
|
||||||
|
- Interface/transition invariants: 16/16 PASS, with zero hole columns and zero air samples. L13/L9: 4/4 PASS. Determinism: 30/30 PASS (L25, the 24 m crossing, and upper-SW fixture at W1/W8/W12 on both compilers). Validator controls rejected 10/10 invalid cases.
|
||||||
|
- Standard set: 12 sharp 1600×900 JPEGs, all 23.5–45.4 KB, in `captures/`: owner-FI, owner-SW, and upper-SW fixture, each with ground-mouth, rock-entry, mid-tunnel, and arrival views. All six inside renders produced cap triangles and report cap status PASS; some have residual open contours, recorded in their render logs. SHA-256 values are in `captures/SHA256SUMS.txt`.
|
||||||
|
- Owner W12 hashes match L33. Recipe/evaluator versions remain 2/20; no Unreal files changed.
|
||||||
|
|
||||||
|
| Recipe | Density | Mesh | Payload | Topology |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| SurfaceWorld | `953eb1df1705f277` | `d8006cb43f44aa5b` | `9217bff766970de5` | `83e3aa6f3a063e60` |
|
||||||
|
| FloatingIslands | `806537cdd98f8afb` | `a4b06dd4d260943d` | `763a99aa04a73eae` | `baa37d80d3620d87` |
|
||||||
|
|
||||||
|
- Cost: five interleaved L33/L34 pairs per recipe. Median whole-view-ready delta was +0.06% for SurfaceWorld and +0.52% for FloatingIslands, within run noise. All 20 paired runs passed.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- The first fixture determinism runner used a nonexistent L34 fixture path: its 24 owner rows passed, six fixture rows failed; correcting the path to the L33 fixture directory made all six pass.
|
||||||
|
- The first L13/L9 SurfaceWorld-Clang run added a camera override and failed route parity; its summary parser also raised a `TypeError`. Removing the override and fixing the parser produced the final 4/4 pass.
|
||||||
|
- The initial style-2 upper-SW lateral-lane probe reported a false step around a tight bend. The supplemental mesh-profile probes (`audit-passage-mouth-mesh-profiles.log`, r2–r7) also had incomplete coverage on several capture exports. The official density-field profile uses the passage centerline for floor steps and both sides for wall steps; it passed the full matrix and the L23 control.
|
||||||
|
- Capture renderer exit-3 retries were recorded in `run-captures.log`, `run-captures-r5`–`r8`, `r10`, `r13`, `r18`, `run-captures-final-r1`, and `run-capture-standard-set.log`. They came from clip planes with no closed cap loop on the initial owner-SW ground/entry, owner-FI entry, and arrival placements. A three-value entry clip was rejected; a four-value outward clip had a 5.1 m open gap, and an inside entry clip obscured the passage. Exterior views now use no clip. Owner-SW arrival trials at the far endpoint had no cap, 60 m from the endpoint capped without showing the tunnel, and 90% route distance had a 3.884 m cap gap; the final view uses the exact route camera at 80%. The upper-SW fixture arrival at 60% failed with a 40.786 m gap; 45% passes. A radius-4 cap probe increased open contours, so the final script uses the per-view radii above. The final full capture run passed all 12 views.
|
||||||
|
- Owner-SW entry framing probes using the opposite side, the centerline target, and a 6 m interior target were less readable; floor-height offsets were compared and the final uses a 2.5 m portal target with no clip or focus crop. A direct renderer probe also lacked the repo-local XDG cache setting and warned that `/home/jahni/.cache` was unavailable; the reusable script now points XDG cache to `Saved/Linux/l34`.
|
||||||
|
- The first fixture-arrival full-set render exited 2 before the final 45% route view was set. The later 60% cap probe failed as noted above; the final 45% render passes. No final run failed.
|
||||||
|
- No run exceeded 30 minutes or remained stuck for 15 minutes. All builds and audits used the requested CPU, memory, and job limits. Nothing was committed, pushed, or stashed.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged as tooling: the standard 12-view capture set is sharp and comparable, and the mouth-profile audit with its
|
||||||
|
L23 control is a good addition. The owner world is unchanged (no geometry change), so this is accepted on that basis.
|
||||||
|
- Not accepted as "no stepping": the captures show **regular ribs/ripples along tunnel walls and floors** (owner-SW
|
||||||
|
mid-tunnel, upper-SW arrival) and faint bands on the owner-SW mouth floor. They are periodic and smaller than the
|
||||||
|
0.5 m-flat + 0.1 m-step detector, so the detector can't see them. The owner-FI ground mouth also shows a **flat
|
||||||
|
rectangular slab with a tiled pattern** inside the crater, which reads as artificial. Both go to L35.
|
||||||