wip(standalone): speed round -- interrupted by a PC hard reset at 10:16 (Kernel-Power 41)
Verified before the reset (density + full mesh hashes equal at 1/8/16 workers, routes 5642/5642, look audits green): - merged field lookups, parallel mask merge, compact inline spatial bins, line-local relief hash cache, lattice-specific mesher entry, fused sign+magnitude evaluation, far connectors as one capsule each (181,836 -> 16,491 far primitives). - crater edit (sphere+box+capsule, one transaction across LOD0/1): 15-27 ms; player 3x3x3 neighbourhood meshes ready ~18 ms after streaming starts. NOT yet verified (in the tree, unhashed): standalone-only skips of UV/colour arrays and of the ceiling/ground material grouping (best cold whole view 0.958 s @16 workers) and a far-bin reserve tweak. Those skips drop data the plugin's materials may need: not a game-path win as-is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
No files matched your search
@@ -0,0 +1,141 @@
|
|||||||
|
# Shape world speed round
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
The one-second whole-view goal does **not** pass. On the final rebuilt source, cold process-start to all 6,498 tiles through LOD6 took 1.958 s at 8 workers and 1.629 s at 16. The measured shape build plus uncached mesh generation took 1.805 s / 1.454 s. LOD6 remains the largest single streaming stage. The full-detail 960-tile area took 0.896 s / 0.703 s for shape build plus mesh generation; process-start to completion was 1.057 s / 0.825 s. Collision cooking and GPU upload are not measured by this CPU harness.
|
||||||
|
|
||||||
|
Implementation: [shape compiler and field](../../../Source/StandaloneShapeWorld.h), [streaming/edit benchmark](../../../Source/StreamingWorld.h), [viewer measurements](../../../Source/StandaloneViewer.cpp), and [machine runner](RunMachine.ps1).
|
||||||
|
|
||||||
|
| Cold job | Workers | Shape build | Uncached mesh | Shape + mesh | Process start → completion | Cache hits |
|
||||||
|
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| 64 m full-detail area, 960 tiles | 8 | 0.725 s | 0.171 s | **0.896 s** | **1.057 s** | 0 |
|
||||||
|
| 64 m full-detail area, 960 tiles | 16 | 0.549 s | 0.154 s | **0.703 s** | **0.825 s** | 0 |
|
||||||
|
| Whole view, 6,498 tiles through LOD6 | 8 | 0.844 s | 0.961 s | **1.805 s** | **1.958 s** | 0 |
|
||||||
|
| Whole view, 6,498 tiles through LOD6 | 16 | 0.672 s | 0.782 s | **1.454 s** | **1.629 s** | 0 |
|
||||||
|
|
||||||
|
The first four stage columns use separate cold, no-cache processes. Process-start timing begins immediately before `Start-Process`, includes executable launch and world setup, and ends after the final tile is meshed; it still excludes interactive GPU upload and collision cooking. The worker runner is Normal priority on affinity `0xFFFF` and polls at 50 ms. Peak private memory was 0.41 / 0.47 GiB for whole-view 8 / 16-worker runs.
|
||||||
|
|
||||||
|
The 3×3×3 LOD0 neighborhood completed in 18 ms / 19 ms after streaming started. The whole 960-tile full-detail run took 1.057 s / 0.825 s from process start, so at 16 workers even the full-detail area was ready inside one second. The standalone harness does not cook collision, so the required collidable-ready time remains unverified.
|
||||||
|
|
||||||
|
The edit probe submits a 10 m sphere crater, a rounded-box cut, and a capsule rim as one field transaction, centered 24 m from the player across the LOD0/LOD1 seam. One remesh batch completed all 14 intersecting tiles: 12 at LOD0 and 2 at LOD1. Remesh wall time was 12 ms / 13 ms and edit-request-to-ready was 15 ms / 18 ms at 8 / 16 workers, with zero cache hits. This CPU harness does not update an interactive GPU scene or cook collision.
|
||||||
|
|
||||||
|
## Profile and before/after timings
|
||||||
|
|
||||||
|
The baseline is the first cold, profiled source at the start of this speed round (`profile_whole_view_w8/w16`). “After” uses the final selected source (`restored_final_whole_w8/w16`). Timings vary by a few milliseconds between cold runs; the table uses one no-hash run at each worker count.
|
||||||
|
|
||||||
|
| Whole-view stage | Before, 8 | After, 8 | Before, 16 | After, 16 |
|
||||||
|
| --- | ---: | ---: | ---: | ---: |
|
||||||
|
| Local shapes and line bins | 57.6 ms | 54.6 ms | 57.7 ms | 54.2 ms |
|
||||||
|
| Rock intervals, packed signs, candidate axes and mask merge | 132.6 ms | 123.8 ms | 126.7 ms | 114.0 ms |
|
||||||
|
| Surface detail band | 539.0 ms | 537.3 ms | 395.1 ms | 375.2 ms |
|
||||||
|
| Far parent shapes and spatial bins | 194.7 ms | 120.4 ms | 191.6 ms | 121.1 ms |
|
||||||
|
| **Shape build** | **932.3 ms** | **843.5 ms** | **779.7 ms** | **672.1 ms** |
|
||||||
|
| **All LOD mesh generation** | **1,064 ms** | **961 ms** | **850 ms** | **782 ms** |
|
||||||
|
| **Shape build + mesh** | **1.996 s** | **1.805 s** | **1.630 s** | **1.454 s** |
|
||||||
|
|
||||||
|
At 16 workers the final interval subprofile was: local primitives and line bins 54 ms, rock interval/sign lines 20 ms, candidate-axis lines 55 ms, candidate-mask merge 25 ms (about 4 ms serial remainder), surface detail 375 ms, far-parent generation 76 ms, and far spatial-bin construction 45 ms. The 138,188,413-node field used 36.71 MiB of packed words, 55.06 MiB of candidate-axis words, a 131.79 MiB surface proxy, and 72.78 MiB of primitive storage. The far spatial index has 328,944 bins and 1,428,924 primitive references. Its compact bin lists hold up to four IDs inline in 181,237 bins; 147,707 larger bins spill to a small heap array. Whole-view processes peaked at 0.41 / 0.48 GiB private memory at 8 / 16 workers.
|
||||||
|
|
||||||
|
Per-LOD completion time is measured from stream start and is cumulative, not a sum of LOD durations:
|
||||||
|
|
||||||
|
| LOD | Tiles | Density samples | Before complete, 8 / 16 | After complete, 8 / 16 |
|
||||||
|
| ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| 0 | 512 | 21.952 M | 0.107 / 0.092 s | 0.130 / 0.102 s |
|
||||||
|
| 1 | 448 | 19.208 M | 0.190 / 0.153 s | 0.210 / 0.161 s |
|
||||||
|
| 2 | 320 | 2.195 M | 0.221 / 0.178 s | 0.233 / 0.178 s |
|
||||||
|
| 3 | 208 | 1.427 M | 0.244 / 0.196 s | 0.251 / 0.194 s |
|
||||||
|
| 4 | 144 | 0.988 M | 0.255 / 0.205 s | 0.261 / 0.200 s |
|
||||||
|
| 5 | 96 | 0.658 M | 0.264 / 0.211 s | 0.268 / 0.206 s |
|
||||||
|
| 6 | 4,770 | 32.717 M | 1.061 / 0.836 s | 0.948 / 0.778 s |
|
||||||
|
|
||||||
|
LOD6 emits 1,795,216 vertices, 10,047,528 indices, and 79.42 MiB of CPU mesh data. It starts at about 0.25 / 0.21 s and finishes at 0.948 / 0.778 s at 8 / 16 workers; while active it keeps the worker pool busy. Whole-stream worker-idle estimates were 1.7% / 1.4%. It is the main mesh bottleneck.
|
||||||
|
|
||||||
|
A sequential profile of 32 nearby LOD6 tiles measured 33.1 ms total: density-grid evaluation 14.3 ms, cell classification 2.6 ms, gradients 0.3 ms, edge interpolation 0.3 ms, triangle/vertex emission 12.9 ms, and other mesher work 0.5 ms. This sample diagnoses the per-tile stages; it is not a replacement for the full parallel whole-view timing. Density evaluation and output emission remain similarly expensive.
|
||||||
|
|
||||||
|
## Changes and research decisions
|
||||||
|
|
||||||
|
- `EffectiveRockDistance` now uses one spatial-bin lookup and evaluates cave and solid-overlay distances in the same primitive pass. Lattice semantics and floating-point result ordering were kept. The two full-clipmap hashes stayed bit-identical.
|
||||||
|
- Candidate masks from the three interval axes are merged in parallel by line, using atomic OR on disjoint output words. The candidate set did not change; this reduced the interval/sign stage by about 13 ms at 8 workers and 10 ms at 16 in these runs.
|
||||||
|
- The post-interval far build no longer rebuilds axis-line indices that only the already-finished interval solver uses. Reserving the far spatial hash map from the measured primitive count reduced the original far-bin stage from about 112 ms to 97 ms. Replacing per-bin vectors with a 24-byte four-ID inline/spill list then reduced the measured 16-worker far-bin construction to 45 ms. Primitive IDs are still appended in ascending order, which preserves each bin's field-query order.
|
||||||
|
- The inline-list build had 328,944 far bins: 181,237 fit up to four IDs inline and 147,707 spill to heap storage. Across the full stream, density and CPU mesh hashes remained unchanged. The mesher now also reserves its existing per-worker vertex map from `2 × cells²`; the 32-tile LOD6 attribution sample moved from 33.513 ms to 33.145 ms. The whole-view difference is small and within run variation, so this reserve is retained as a low-cost allocation hint rather than claimed as a major speedup.
|
||||||
|
- Research: [Abseil's Swiss-table design notes](https://abseil.io/about/design/swisstables) and Meta's [F14 hash-table write-up](https://engineering.fb.com/2019/04/25/developer-tools/f14/) describe how inline storage and cache-aware lookup can reduce pointer chasing and allocation overhead. That motivated two prototypes. A flat open-addressed spatial index cut 16-worker bin construction from about 45 ms to 33 ms, but increased the measured streaming stage from about 761 ms to 804 ms; shape plus mesh was 1.471 s versus 1.440 s with the compact inline-bin hash map. I rejected the flat index because the query cost erased its build-time win. A first inline-list layout with a separate vector enlarged every map node and also streamed slower; the retained 24-byte union layout avoids that metadata growth.
|
||||||
|
- Added per-stage/per-LOD timing and worker accounting, a 27-tile neighborhood-ready metric, and a filtered one-queue edit-batch benchmark. The edit brush SDF uses explicit SSE2 square root instructions, with `/fp:precise` and `/arch:AVX2`; it does not add CRT transcendental calls or FMA contraction.
|
||||||
|
- A thread-local last-bin cache was tried and rejected: hashes matched but the LOD6 run showed no stable speed gain. A parallel far-bin builder was also rejected: hashes matched, but spatial-bin time stayed around 107 ms and whole-view time did not improve. Skipping the unused axis-bin rebuild alone saved only a few milliseconds; the capacity reservation was the useful part.
|
||||||
|
- Research: VTK's [Flying Edges documentation](https://vtk.org/doc/nightly/release/9.7/html/classvtkFlyingEdges3D.html) and the [LDAV 2015 paper](https://doi.org/10.1109/LDAV.2015.7348069) describe edge-oriented passes, count/prefix-sum allocation, preallocated output, and avoiding coincident-point merging. The current mesher still spends comparable sampled time in density-grid evaluation and vertex/triangle emission. A full multi-pass conversion is the strongest untested mesher option, but it is a larger rewrite with separate material-class, skirt, and degeneracy handling to preserve.
|
||||||
|
- Built and measured a per-worker lattice-edge vertex cache with the existing position hash retained as a fallback. The first version collapsed 2,477 vertices on the 16-worker whole view (1,792,739 vs 1,795,216) with the same triangle count, so it was rejected as a mesh change. The exact-key fallback version restored vertex counts, but totals were 1.884 / 1.484 s versus 1.886 / 1.471 s at 8 / 16 workers: no stable gain. Both prototypes were removed.
|
||||||
|
- Tested overlapping early far-tile meshing with the shape build. That path queued LOD2+ before the far-parent shapes and spatial index were built, so it could mesh against an incomplete field; its timing is invalid for a speed claim and the path was removed. Even the invalid trial was slower at 16 workers. Correct overlap needs an immutable, separately built far field before any dependent tile starts.
|
||||||
|
- The final no-hash composite whole view is 1.805 s at 8 workers and 1.454 s at 16; it still needs another 454 ms removed at 16 to meet the one-second target. The single-critical-tile split from the plugin review does not fit this LOD6 workload: there are 4,770 queued coarse tiles, and the worker pool is busy while they run. The 6-worker point, SIMD primitive batches, forward differences, Morton page order, and explicit coarse surfaces were not prototyped in this pass; explicit surfaces and a multi-pass edge mesher remain plausible larger experiments.
|
||||||
|
- Intel's [Intrinsics Guide](https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html) cautions that an intrinsic may expand to a slower instruction sequence. AVX2 batching of the irregular primitive/bin lookup was not assumed to help without a result-preserving prototype.
|
||||||
|
|
||||||
|
The final inline-bin source improves the comparable shape-build-plus-mesh result by about 9.6% at 8 workers and 10.8% at 16 versus this round's first cold baseline. The earlier round-three report's 2.095 / 1.617 s figures compare with 1.805 / 1.454 s here. Process-start timing is 1.958 / 1.629 s, so the one-second whole-view goal remains unmet.
|
||||||
|
|
||||||
|
## Determinism, routes, and surface checks
|
||||||
|
|
||||||
|
Four cold no-cache hash runs on the final built source (two at 16 workers, one each at 8 and 1) matched exactly. The field hash covers sampled density floats. The CPU-geometry hash covers each streamed tile's float positions, float normals, and indices, with tile keys included in deterministic LOD/Z/Y/X order. UV and color buffers are not stored in `FStandaloneCpuTile` and are not included in this geometry hash.
|
||||||
|
|
||||||
|
| Output | Density hash, identical in all runs | CPU geometry hash, identical in all runs |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Full-detail mesh | `0xa071e48a996e2828` | `0x6f270c621d39302f` |
|
||||||
|
| Whole clipmap | `0x630c43d7c68d5cbb` | `0xe3caf5b44244a992` |
|
||||||
|
| LOD0 | `0xc31fc55744c45928` | `0x7a70def3eb3e11ad` |
|
||||||
|
| LOD1 | `0xd74d6563f970d9f9` | `0xf47463988641b397` |
|
||||||
|
| LOD2 | `0xb0e440b4c9276bd7` | `0x9ed0581189757ee8` |
|
||||||
|
| LOD3 | `0xda4d3afb3467dd64` | `0xd8101bfb3ad87925` |
|
||||||
|
| LOD4 | `0x903537240ad6ae68` | `0x64c0b5e6a2e4f8ce` |
|
||||||
|
| LOD5 | `0x7c9e900fda72f05f` | `0x58c5139c40172196` |
|
||||||
|
| LOD6 | `0xc67d1f383fbf29ca` | `0xdc35accf7789860d` |
|
||||||
|
|
||||||
|
All runs reported zero mesh-cache hits. Each shape build retained all 5,642/5,642 passage route samples as open.
|
||||||
|
|
||||||
|
The saved earlier `final_exact_hash_*` set differs in full-detail, whole-clipmap, LOD0, and LOD1 hashes; LOD2–LOD6 match. The source/output change responsible for that older-to-current hash shift is still unexplained. The current hash set is stable across 1 / 8 / 16 workers and two 16-worker runs, but I do not claim bit identity with that earlier saved executable.
|
||||||
|
|
||||||
|
This verifies repeatability on this AVX2 host across 1, 8, and 16 workers. A second hardware host was not available, so cross-machine bit identity remains unverified here.
|
||||||
|
|
||||||
|
| Check | Final result |
|
||||||
|
| --- | --- |
|
||||||
|
| Chamber grid-stair index | 0.003 (threshold 0.400) |
|
||||||
|
| Tunnel grid-stair index | 0.028 (threshold 0.400) |
|
||||||
|
| Passage grid-stair index | 0.205 (threshold 0.400) |
|
||||||
|
| Shared tunnel joins | 280; endpoint radius and slope deltas 0.000000 / 0.00000000 |
|
||||||
|
| Whole-view level-zero sheet audit | 0 sub-voxel sheets, 0 isolated small components; 4 small boundary-touching components |
|
||||||
|
| Chamber flat-patch audit | 5.0% at the 16-voxel patch rule, largest 433 samples; audit passes, though it is not a visual sign-off |
|
||||||
|
|
||||||
|
The baseline tunnel stair audit without `--round-tunnels` measured 0.434 and failed the 0.400 threshold; the final tunnel view measures 0.028. The baseline chamber (0.041) and passage (0.276) already passed that threshold, so those two are improvements rather than fail-to-pass proofs. The earlier invalid baseline command with `--round-tunnels` remains recorded in [`round2/REPORT.md`](../round2/REPORT.md).
|
||||||
|
|
||||||
|
I reran the owner-field scan on the compact-bin source with the round-two settings: 8 workers, 960 full-resolution tiles for close views, 4-sample vertex stride, and 4,770 LOD6 tiles for far. `mesh_sample_mismatch=0` in each scan. The later vertex-map reservation only changes capacity; final no-cache density and CPU geometry hashes match the pre-reservation build exactly. The older saved scalar hashes differ, so these refreshed sign and distance results replace the stale “unchanged” claim.
|
||||||
|
|
||||||
|
| View | Lattice samples | Changed signs, before → current | Mean sampled surface distance, before → current | Max, before → current | Unmatched, before → current |
|
||||||
|
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| Chamber and tunnel region | 35,103,600 | 5.978% → 5.895% | 1.0325 m → 1.0826 m | 6.4677 m → 6.4840 m | 9,393 → 8,832 |
|
||||||
|
| Passage | 37,240,000 | 3.311% → 3.304% | 0.9814 m → 1.0292 m | 6.3422 m → 6.3750 m | 12,602 → 12,520 |
|
||||||
|
| Far view, LOD6 | 9,470,835 | 9.041% → 9.041% | 2.1667 m → 2.1588 m | 5.3070 m → 5.3070 m | 783,976 → 783,967 |
|
||||||
|
|
||||||
|
Closest-view sign disagreement fell slightly while mean surface distance rose; the far view changed almost not at all. These are sampled symmetric nearest-vertex distances, not continuous Hausdorff distances, and do not indicate that the generated world looks close to the owner.
|
||||||
|
|
||||||
|
## Captures and visual review
|
||||||
|
|
||||||
|
The final rebuilt captures are in `rebuilt_after_*.png`. Five are byte-identical to the previous exact captures; the passage differs in 4 of 1,024,000 pixels by at most 1 channel value. The current captures look unchanged at display scale. Against the owner images, the chamber still has a broad smooth ceiling and floor where the owner view has more connected forms; the tunnel has smoother, more repetitive relief; the passage retains a horizontal shelf/dark opening and lacks the owner's layered profile; the far image lacks the owner's larger connected forms. The look remains visibly different and is not a match.
|
||||||
|
|
||||||
|
| View | Before | Final after | Owner |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Chamber | [before](../round2/final_after_chamber.png) | [after](rebuilt_after_chamber.png) | [owner](../final/chamber_fixed_owner.png) |
|
||||||
|
| Tunnel | [before](../round2/final_after_tunnel.png) | [after](rebuilt_after_tunnel.png) | [owner](../final/tunnel_fixed_owner.png) |
|
||||||
|
| Passage | [before](../round2/final_after_passage.png) | [after](rebuilt_after_passage.png) | [owner](../final/passage_fixed_owner.png) |
|
||||||
|
| Far view | [before](../round2/final_after_far.png) | [after](rebuilt_after_far.png) | [owner](../final/far_owner.png) |
|
||||||
|
| Passage-floor close-up | [before](../round2/final_after_passage_floor_closeup.png) | [after](rebuilt_after_passage_floor_closeup.png) | no owner close-up |
|
||||||
|
| Tunnel bend | [before](../round2/final_after_tunnel_bend.png) | [after](rebuilt_after_tunnel_bend.png) | no owner close-up |
|
||||||
|
|
||||||
|
## Build, process limits, and failed attempts
|
||||||
|
|
||||||
|
The final build is [`vertex_map_reserve_build.log`](vertex_map_reserve_build.log). It succeeded with `/MP16`; the build host ran at Normal priority and affinity `0xFFFF`. It still reports the existing `vswhere.exe` setup diagnostic and six C4838 narrowing warnings in `VoxelStrateManager.cpp`. Benchmark processes ran Normal with `0xFFFF`; final whole-view no-cache runs peaked at 0.41 / 0.48 GiB private memory at 8 / 16 workers. All test processes were launched by this task; no external viewer was closed.
|
||||||
|
|
||||||
|
- One build wrapper invocation used a relative `Tee-Object` path. `Build.ps1` changed the working directory, so the log path resolved under `Standalone\Build` and the wrapper returned 1. I checked that no compiler remained active and reran with an absolute log path; the final build succeeded. This was a logging-path failure, not a compiler failure.
|
||||||
|
- One benchmark invocation referenced `Research\...\RunMachine.ps1` from the plugin root instead of `Standalone\Research\...`; PowerShell rejected the script path before an executable started. The corrected invocation completed.
|
||||||
|
- A `rg | head` inspection hit a Cygwin `CreateFileMapping` permission diagnostic; the source query was repeated with PowerShell and no build or benchmark was affected.
|
||||||
|
- The simple edge-cache prototype that produced fewer vertices and the early-far overlap prototype were rejected as described above. Their build/run logs remain in this directory; no such code remains in the final source.
|
||||||
|
- The parallel far-bin experiment built and ran successfully and kept exact hashes, but did not improve the bin stage or wall time; its code was removed.
|
||||||
|
- The historical `resume2_before_tunnel_audit.stdout.log` used `--round-tunnels` and is not a valid base-world comparison. The corrected no-`--round-tunnels` baseline is retained at 0.434 and failed the final 0.400 threshold, as described above.
|
||||||
|
- No compiler build or benchmark executable failed during this speed round. The flat-map and first larger inline-bin prototypes ran successfully but were rejected for slower total results. The corrected baseline stair audit still fails at 0.434 against the 0.400 threshold; this failure is preserved in the earlier report. No commit, push, or stash was made.
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
Baseline profiles: `profile_whole_w8.stdout.log`, `profile_whole_w16.stdout.log`. Final no-hash whole-view stages: `measured_whole_w8/w16.stdout.log`; process walls: `measured_whole_w8/w16.runner.log`. Full detail: `measured_detail_w8/w16.stdout.log` and `measured_detail_w8/w16.runner.log`. Final determinism on the exact rebuilt source: `final_source_hash_w1.stdout.log`, `_w8.stdout.log`, `_w16_a.stdout.log`, `_w16_b.stdout.log`. Cross-LOD edit latency: `reserve_edit_w8/w16.stdout.log`. Refreshed owner scans: `final_owner_bins_chamber_w8.stdout.log`, `final_owner_bins_passage_w8.stdout.log`, `final_owner_bins_far_w8.stdout.log`. Mesher attribution: `lod6_sample32_profile.stdout.log` and `vertex_map_reserve_profile.stdout.log`. Current audits/captures: `rebuilt_audit_*.stdout.log`, `rebuilt_shard_audit_w16.stdout.log`, `rebuilt_capture_*.stdout.log`, and `rebuilt_after_*.png`.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$ExecutableName,
|
||||||
|
[Parameter(Mandatory=$true)][string[]]$ExecutableArguments,
|
||||||
|
[Parameter(Mandatory=$true)][string]$LogStem
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$RoundDir = [IO.Path]::GetFullPath($PSScriptRoot)
|
||||||
|
$StandaloneRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
|
||||||
|
if ([IO.Path]::GetFileName($ExecutableName) -ne $ExecutableName) { throw 'ExecutableName must be a filename.' }
|
||||||
|
if ([IO.Path]::GetFileName($LogStem) -ne $LogStem) { throw 'LogStem must be a filename stem.' }
|
||||||
|
$ExecutablePath = Join-Path (Join-Path $StandaloneRoot 'bin') $ExecutableName
|
||||||
|
if (-not (Test-Path -LiteralPath $ExecutablePath -PathType Leaf)) { throw "Executable not found: $ExecutablePath" }
|
||||||
|
$StdoutPath = Join-Path $RoundDir ($LogStem + '.stdout.log')
|
||||||
|
$StderrPath = Join-Path $RoundDir ($LogStem + '.stderr.log')
|
||||||
|
|
||||||
|
$HostProcess = Get-Process -Id $PID
|
||||||
|
$HostProcess.PriorityClass = 'Normal'
|
||||||
|
$HostProcess.ProcessorAffinity = [IntPtr]0xFFFF
|
||||||
|
$RunClock = [Diagnostics.Stopwatch]::StartNew()
|
||||||
|
$Child = Start-Process -FilePath $ExecutablePath -ArgumentList $ExecutableArguments `
|
||||||
|
-WorkingDirectory $StandaloneRoot -WindowStyle Hidden -PassThru `
|
||||||
|
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath
|
||||||
|
$PeakWorkingSet = 0L
|
||||||
|
$PeakPrivateMemory = 0L
|
||||||
|
while (-not $Child.HasExited) {
|
||||||
|
$Child.Refresh()
|
||||||
|
if ($Child.HasExited) { break }
|
||||||
|
$Child.PriorityClass = 'Normal'
|
||||||
|
$Child.ProcessorAffinity = [IntPtr]0xFFFF
|
||||||
|
$PeakWorkingSet = [Math]::Max($PeakWorkingSet,$Child.WorkingSet64)
|
||||||
|
$PeakPrivateMemory = [Math]::Max($PeakPrivateMemory,$Child.PrivateMemorySize64)
|
||||||
|
Start-Sleep -Milliseconds 50
|
||||||
|
}
|
||||||
|
$Child.WaitForExit()
|
||||||
|
$RunClock.Stop()
|
||||||
|
$Child.Refresh()
|
||||||
|
Write-Output "RUN_EXIT pid=$($Child.Id) priority=Normal affinity=0xFFFF wall_s=$([math]::Round($RunClock.Elapsed.TotalSeconds,3)) peak_working_set_gb=$([math]::Round($PeakWorkingSet/1GB,2)) peak_private_gb=$([math]::Round($PeakPrivateMemory/1GB,2)) cpu_s=$([math]::Round($Child.CPU,3)) exit_code=$($Child.ExitCode)"
|
||||||
|
exit $Child.ExitCode
|
||||||
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
@@ -17,6 +17,7 @@
|
|||||||
#include <limits>
|
#include <limits>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <immintrin.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -28,6 +29,24 @@ class FStandaloneShapeWorld
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
|
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
|
||||||
|
struct FEditBrush
|
||||||
|
{
|
||||||
|
enum class EShape : uint8 { Sphere, Capsule, RoundedBox };
|
||||||
|
enum class EOperation : uint8 { Carve, AddRock };
|
||||||
|
EShape Shape=EShape::Sphere;
|
||||||
|
EOperation Operation=EOperation::Carve;
|
||||||
|
std::array<double,3> A{};
|
||||||
|
std::array<double,3> B{};
|
||||||
|
std::array<double,3> HalfExtent{};
|
||||||
|
double Radius=0.0;
|
||||||
|
double Rounding=0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FParallelTiming
|
||||||
|
{
|
||||||
|
double WallSeconds = 0.0;
|
||||||
|
double WorkerSeconds = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
struct FBuildStats
|
struct FBuildStats
|
||||||
{
|
{
|
||||||
@@ -55,6 +74,15 @@ public:
|
|||||||
double MaxUnmarkedFloorDirectionSlopeDelta = 0.0;
|
double MaxUnmarkedFloorDirectionSlopeDelta = 0.0;
|
||||||
double MaxMarkedFloorSlopeDelta = 0.0;
|
double MaxMarkedFloorSlopeDelta = 0.0;
|
||||||
double MaxFloorCurveDeviation = 0.0;
|
double MaxFloorCurveDeviation = 0.0;
|
||||||
|
double LocalPrimitiveSeconds = 0.0;
|
||||||
|
double LocalLineBinSeconds = 0.0;
|
||||||
|
double CandidateMergeSeconds = 0.0;
|
||||||
|
double FarPrimitiveSeconds = 0.0;
|
||||||
|
double FarBinSeconds = 0.0;
|
||||||
|
FParallelTiming RockIntervalLines;
|
||||||
|
FParallelTiming CandidateAxisLines;
|
||||||
|
FParallelTiming CandidateMergeLines;
|
||||||
|
FParallelTiming DetailBandLines;
|
||||||
uint64 CurvedFloorSegments = 0;
|
uint64 CurvedFloorSegments = 0;
|
||||||
uint64 MarkedFloorSlopeJoins = 0;
|
uint64 MarkedFloorSlopeJoins = 0;
|
||||||
double LegacySegmentRadiusStep = 0.0;
|
double LegacySegmentRadiusStep = 0.0;
|
||||||
@@ -70,6 +98,10 @@ public:
|
|||||||
int32 CenterSub[3]{};
|
int32 CenterSub[3]{};
|
||||||
int32 CapsuleStartSub[3]{};
|
int32 CapsuleStartSub[3]{};
|
||||||
int32 CapsuleEndSub[3]{};
|
int32 CapsuleEndSub[3]{};
|
||||||
|
double CapsuleStartWorld[3]{};
|
||||||
|
double CapsuleAxisDelta[3]{};
|
||||||
|
double CapsuleAxisLengthSquared = 0.0;
|
||||||
|
double CapsuleAxisLength = 0.0;
|
||||||
int64 Q[3][3]{};
|
int64 Q[3][3]{};
|
||||||
int32 Min[3]{};
|
int32 Min[3]{};
|
||||||
int32 Max[3]{};
|
int32 Max[3]{};
|
||||||
@@ -123,6 +155,81 @@ public:
|
|||||||
return static_cast<size_t>(H);
|
return static_cast<size_t>(H);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
struct FBinItems
|
||||||
|
{
|
||||||
|
static constexpr size_t InlineCapacity=4;
|
||||||
|
struct FHeapStorage {uint32* Data=nullptr;uint32 Capacity=0;uint32 Reserved=0;};
|
||||||
|
union FStorage
|
||||||
|
{
|
||||||
|
std::array<uint32,InlineCapacity> Inline;
|
||||||
|
FHeapStorage Heap;
|
||||||
|
FStorage():Inline{}{}
|
||||||
|
~FStorage(){}
|
||||||
|
} Storage;
|
||||||
|
uint32 Count=0;
|
||||||
|
uint32 bHeap=0;
|
||||||
|
|
||||||
|
FBinItems()=default;
|
||||||
|
FBinItems(const FBinItems&)=delete;
|
||||||
|
FBinItems& operator=(const FBinItems&)=delete;
|
||||||
|
FBinItems(FBinItems&& Other) noexcept {MoveFrom(Other);}
|
||||||
|
FBinItems& operator=(FBinItems&& Other) noexcept
|
||||||
|
{
|
||||||
|
if(this!=&Other){Release();MoveFrom(Other);}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
~FBinItems(){Release();}
|
||||||
|
|
||||||
|
void push_back(uint32 Id)
|
||||||
|
{
|
||||||
|
if(!bHeap&&Count<InlineCapacity)
|
||||||
|
{
|
||||||
|
Storage.Inline[Count++]=Id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!bHeap)
|
||||||
|
{
|
||||||
|
const std::array<uint32,InlineCapacity> Previous=Storage.Inline;
|
||||||
|
Storage.Heap={new uint32[8],8,0};
|
||||||
|
std::copy(Previous.begin(),Previous.end(),Storage.Heap.Data);
|
||||||
|
bHeap=1;
|
||||||
|
}
|
||||||
|
else if(Count==Storage.Heap.Capacity)
|
||||||
|
{
|
||||||
|
const uint32 NewCapacity=Storage.Heap.Capacity*2;
|
||||||
|
uint32* NewData=new uint32[NewCapacity];
|
||||||
|
std::copy_n(Storage.Heap.Data,Count,NewData);
|
||||||
|
delete[] Storage.Heap.Data;
|
||||||
|
Storage.Heap={NewData,NewCapacity,0};
|
||||||
|
}
|
||||||
|
Storage.Heap.Data[Count++]=Id;
|
||||||
|
}
|
||||||
|
size_t size() const {return Count;}
|
||||||
|
size_t capacity() const {return bHeap?Storage.Heap.Capacity:InlineCapacity;}
|
||||||
|
const uint32* begin() const {return bHeap?Storage.Heap.Data:Storage.Inline.data();}
|
||||||
|
const uint32* end() const {return begin()+Count;}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Release()
|
||||||
|
{
|
||||||
|
if(bHeap)delete[] Storage.Heap.Data;
|
||||||
|
bHeap=0;Count=0;
|
||||||
|
}
|
||||||
|
void MoveFrom(FBinItems& Other)
|
||||||
|
{
|
||||||
|
Count=Other.Count;bHeap=Other.bHeap;
|
||||||
|
if(bHeap)
|
||||||
|
{
|
||||||
|
Storage.Heap=Other.Storage.Heap;
|
||||||
|
Other.bHeap=0;Other.Count=0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Storage.Inline=Other.Storage.Inline;
|
||||||
|
Other.Count=0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
static constexpr int32 QScale = 1 << 28;
|
static constexpr int32 QScale = 1 << 28;
|
||||||
static constexpr int32 SubPerVoxel = 16;
|
static constexpr int32 SubPerVoxel = 16;
|
||||||
@@ -161,31 +268,62 @@ public:
|
|||||||
|
|
||||||
auto Stage=Clock::now();
|
auto Stage=Clock::now();
|
||||||
BuildLayerBounds();
|
BuildLayerBounds();
|
||||||
|
auto Substage=Clock::now();
|
||||||
BuildOwnerPrimitives();
|
BuildOwnerPrimitives();
|
||||||
|
Stats.LocalPrimitiveSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
|
||||||
|
Substage=Clock::now();
|
||||||
if(FStandaloneResearchTunnels::Enabled.load(std::memory_order_acquire))
|
if(FStandaloneResearchTunnels::Enabled.load(std::memory_order_acquire))
|
||||||
BuildRoundTunnelPrimitives();
|
BuildRoundTunnelPrimitives();
|
||||||
BuildSpatialBins(LineBinSize);
|
BuildSpatialBins(LineBinSize);
|
||||||
|
Stats.LocalLineBinSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
|
||||||
Stats.ShapeSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
|
Stats.ShapeSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
|
||||||
Stats.PrimitiveCount=Primitives.size();
|
Stats.PrimitiveCount=Primitives.size();
|
||||||
if(Primitives.empty())
|
if(Primitives.empty())
|
||||||
{Error="owner shape extraction produced no rooms, tunnels, or passages";return false;}
|
{Error="owner shape extraction produced no rooms, tunnels, or passages";return false;}
|
||||||
|
|
||||||
Stage=Clock::now();
|
Stage=Clock::now();
|
||||||
const size_t NZ=static_cast<size_t>(Size[2]);
|
const size_t NZ=static_cast<size_t>(Size[2]);
|
||||||
WordsPerZ=(NZ+63u)/64u;
|
WordsPerZ=(NZ+63u)/64u;
|
||||||
RockWords.assign(static_cast<size_t>(Size[0])*Size[1]*WordsPerZ,0);
|
RockWords.assign(static_cast<size_t>(Size[0])*Size[1]*WordsPerZ,0);
|
||||||
SurfaceDistanceQ=std::make_unique_for_overwrite<int8[]>(static_cast<size_t>(NodeCount));
|
SurfaceDistanceQ=std::make_unique_for_overwrite<int8[]>(static_cast<size_t>(NodeCount));
|
||||||
BuildPackedRockIntervals();
|
BuildPackedRockIntervals(Stats.RockIntervalLines);
|
||||||
BuildCandidateBands();
|
BuildCandidateBands(Stats.CandidateAxisLines,Stats.CandidateMergeLines,
|
||||||
|
Stats.CandidateMergeSeconds);
|
||||||
Stats.IntervalSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
|
Stats.IntervalSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
|
||||||
|
|
||||||
Stage=Clock::now();
|
Stage=Clock::now();
|
||||||
ApplyDetailBand();
|
ApplyDetailBand(Stats.DetailBandLines);
|
||||||
Stats.SurfaceSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
|
Stats.SurfaceSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
|
||||||
ValidatePassageRoutes();
|
ValidatePassageRoutes();
|
||||||
Built=true;
|
Built=true;
|
||||||
if(bIncludeFarWorld)BuildFarWorld();
|
if(bIncludeFarWorld)BuildFarWorld();
|
||||||
const uint64 WordBytes=static_cast<uint64>(RockWords.size()+CandidateWords.size())*sizeof(uint64);
|
const uint64 WordBytes=static_cast<uint64>(RockWords.size()+CandidateWords.size())*sizeof(uint64);
|
||||||
|
uint64 CandidateAxisBytes=0;
|
||||||
|
for(const auto& AxisWords:CandidateAxisWords)
|
||||||
|
CandidateAxisBytes+=static_cast<uint64>(AxisWords.capacity())*sizeof(uint64);
|
||||||
|
uint64 BinReferences=0,BinCapacity=0,InlineBins=0,OverflowBins=0;
|
||||||
|
for(const auto& Bin:SpatialBins)
|
||||||
|
{
|
||||||
|
BinReferences+=Bin.second.size();BinCapacity+=Bin.second.capacity();
|
||||||
|
if(Bin.second.size()<=FBinItems::InlineCapacity)++InlineBins;
|
||||||
|
else ++OverflowBins;
|
||||||
|
}
|
||||||
|
std::printf("[ShapeProfile] local_primitives=%.3fms line_bins=%.3fms rock_lines_wall=%.3fms rock_lines_worker=%.3fms candidate_axes_wall=%.3fms candidate_axes_worker=%.3fms candidate_merge_wall=%.3fms candidate_merge_worker=%.3fms candidate_merge_serial=%.3fms detail_lines_wall=%.3fms detail_lines_worker=%.3fms\n",
|
||||||
|
Stats.LocalPrimitiveSeconds*1000.0,Stats.LocalLineBinSeconds*1000.0,
|
||||||
|
Stats.RockIntervalLines.WallSeconds*1000.0,Stats.RockIntervalLines.WorkerSeconds*1000.0,
|
||||||
|
Stats.CandidateAxisLines.WallSeconds*1000.0,Stats.CandidateAxisLines.WorkerSeconds*1000.0,
|
||||||
|
Stats.CandidateMergeLines.WallSeconds*1000.0,Stats.CandidateMergeLines.WorkerSeconds*1000.0,
|
||||||
|
Stats.CandidateMergeSeconds*1000.0,Stats.DetailBandLines.WallSeconds*1000.0,
|
||||||
|
Stats.DetailBandLines.WorkerSeconds*1000.0);
|
||||||
|
std::printf("[ShapeAllocation] words=%.2fMiB candidate_axis_words=%.2fMiB proxy=%.2fMiB primitives=%.2fMiB spatial_bins=%llu spatial_bin_refs=%llu spatial_bin_capacity=%llu inline_bins=%llu overflow_bins=%llu far_primitive_build=%.3fms far_bin_rebuild=%.3fms\n",
|
||||||
|
static_cast<double>(WordBytes)/1048576.0,static_cast<double>(CandidateAxisBytes)/1048576.0,
|
||||||
|
static_cast<double>(Stats.SignNodes*sizeof(int8))/1048576.0,
|
||||||
|
static_cast<double>(Primitives.capacity()*sizeof(FPrimitive))/1048576.0,
|
||||||
|
static_cast<unsigned long long>(SpatialBins.size()),
|
||||||
|
static_cast<unsigned long long>(BinReferences),
|
||||||
|
static_cast<unsigned long long>(BinCapacity),
|
||||||
|
static_cast<unsigned long long>(InlineBins),
|
||||||
|
static_cast<unsigned long long>(OverflowBins),Stats.FarPrimitiveSeconds*1000.0,
|
||||||
|
Stats.FarBinSeconds*1000.0);
|
||||||
std::printf("[ShapeCompile] primitives=%llu rooms=%llu tunnels=%llu passages=%llu nodes=%llu packed=%.2f MiB surface_proxy_nodes=%llu proxy_address_capacity=%.2f MiB workers=%d\n",
|
std::printf("[ShapeCompile] primitives=%llu rooms=%llu tunnels=%llu passages=%llu nodes=%llu packed=%.2f MiB surface_proxy_nodes=%llu proxy_address_capacity=%.2f MiB workers=%d\n",
|
||||||
static_cast<unsigned long long>(Stats.PrimitiveCount),
|
static_cast<unsigned long long>(Stats.PrimitiveCount),
|
||||||
static_cast<unsigned long long>(Stats.RoomCount),
|
static_cast<unsigned long long>(Stats.RoomCount),
|
||||||
@@ -243,15 +381,51 @@ public:
|
|||||||
bool DebugCriticalAirAt(double X,double Y,double Z) const { return IsInsideCriticalAir(X,Y,Z); }
|
bool DebugCriticalAirAt(double X,double Y,double Z) const { return IsInsideCriticalAir(X,Y,Z); }
|
||||||
bool DebugSolidOverlayAt(double X,double Y,double Z) const { return IsInsideSolidOverlay(X,Y,Z); }
|
bool DebugSolidOverlayAt(double X,double Y,double Z) const { return IsInsideSolidOverlay(X,Y,Z); }
|
||||||
|
|
||||||
|
void SetEditTransaction(std::vector<FEditBrush> Brushes)
|
||||||
|
{
|
||||||
|
EditBrushes=std::move(Brushes);
|
||||||
|
EditBoundsMin={MAX_int32,MAX_int32,MAX_int32};
|
||||||
|
EditBoundsMax={MIN_int32,MIN_int32,MIN_int32};
|
||||||
|
for(const FEditBrush& Brush:EditBrushes)
|
||||||
|
{
|
||||||
|
double Extent[3]{};
|
||||||
|
if(Brush.Shape==FEditBrush::EShape::Sphere)
|
||||||
|
for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.Radius;
|
||||||
|
else if(Brush.Shape==FEditBrush::EShape::Capsule)
|
||||||
|
for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.Radius;
|
||||||
|
else
|
||||||
|
for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.HalfExtent[Axis]+Brush.Rounding;
|
||||||
|
for(int32 Axis=0;Axis<3;++Axis)
|
||||||
|
{
|
||||||
|
const double Lo=Brush.Shape==FEditBrush::EShape::Capsule
|
||||||
|
?std::min(Brush.A[Axis],Brush.B[Axis])-Extent[Axis]
|
||||||
|
:Brush.A[Axis]-Extent[Axis];
|
||||||
|
const double Hi=Brush.Shape==FEditBrush::EShape::Capsule
|
||||||
|
?std::max(Brush.A[Axis],Brush.B[Axis])+Extent[Axis]
|
||||||
|
:Brush.A[Axis]+Extent[Axis];
|
||||||
|
(&EditBoundsMin.X)[Axis]=std::min((&EditBoundsMin.X)[Axis],static_cast<int32>(std::floor(Lo)));
|
||||||
|
(&EditBoundsMax.X)[Axis]=std::max((&EditBoundsMax.X)[Axis],static_cast<int32>(std::ceil(Hi)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasEditTransaction() const { return !EditBrushes.empty(); }
|
||||||
|
FIntVector GetEditBoundsMin() const { return EditBoundsMin; }
|
||||||
|
FIntVector GetEditBoundsMax() const { return EditBoundsMax; }
|
||||||
|
|
||||||
void BuildFarWorld()
|
void BuildFarWorld()
|
||||||
{
|
{
|
||||||
if(FarBuilt)return;
|
if(FarBuilt)return;
|
||||||
const auto Start=std::chrono::steady_clock::now();
|
auto Stage=std::chrono::steady_clock::now();
|
||||||
BuildFarParentShapes();
|
BuildFarParentShapes();
|
||||||
BuildSpatialBins(SpatialBinSize);
|
Stats.FarPrimitiveSeconds=std::chrono::duration<double>(std::chrono::steady_clock::now()-Stage).count();
|
||||||
Stats.FarShapeSeconds=std::chrono::duration<double>(std::chrono::steady_clock::now()-Start).count();
|
Stage=std::chrono::steady_clock::now();
|
||||||
|
BuildSpatialBins(SpatialBinSize,false);
|
||||||
|
Stats.FarBinSeconds=std::chrono::duration<double>(std::chrono::steady_clock::now()-Stage).count();
|
||||||
|
Stats.FarShapeSeconds=Stats.FarPrimitiveSeconds+Stats.FarBinSeconds;
|
||||||
Stats.PrimitiveCount=Primitives.size();FarBuilt=true;
|
Stats.PrimitiveCount=Primitives.size();FarBuilt=true;
|
||||||
std::printf("[FarShapeStage] %.3f ms primitives=%llu\n",Stats.FarShapeSeconds*1000.0,
|
std::printf("[FarShapeStage] parent_shapes=%.3f ms spatial_bins=%.3f ms total=%.3f ms primitives=%llu\n",
|
||||||
|
Stats.FarPrimitiveSeconds*1000.0,Stats.FarBinSeconds*1000.0,Stats.FarShapeSeconds*1000.0,
|
||||||
static_cast<unsigned long long>(Stats.PrimitiveCount));
|
static_cast<unsigned long long>(Stats.PrimitiveCount));
|
||||||
std::fflush(stdout);
|
std::fflush(stdout);
|
||||||
}
|
}
|
||||||
@@ -263,11 +437,26 @@ public:
|
|||||||
int32 IX=0,IY=0,IZ=0;
|
int32 IX=0,IY=0,IZ=0;
|
||||||
const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ);
|
const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ);
|
||||||
const bool bPacked=bInteger&&Contains(IX,IY,IZ);
|
const bool bPacked=bInteger&&Contains(IX,IY,IZ);
|
||||||
const bool bRock=bPacked?IsRock(IX,IY,IZ):EvaluateFast(X,Y,Z);
|
|
||||||
if(bPacked)
|
if(bPacked)
|
||||||
|
return PackedDensityAt(IX,IY,IZ,IsRock(IX,IY,IZ),X,Y,Z);
|
||||||
|
return AnalyticDensityAt(X,Y,Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
|
||||||
{
|
{
|
||||||
const size_t Column=static_cast<size_t>(IY-Min[1])*Size[0]+static_cast<size_t>(IX-Min[0]);
|
const bool bPacked=Contains(X,Y,Z);
|
||||||
const size_t Zi=static_cast<size_t>(IZ-Min[2]);
|
if(bPacked)
|
||||||
|
return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),static_cast<float>(X),
|
||||||
|
static_cast<float>(Y),static_cast<float>(Z));
|
||||||
|
return AnalyticDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||||
|
static_cast<float>(Z));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
float PackedDensityAt(int32 X,int32 Y,int32 Z,bool bRock,float FX,float FY,float FZ) const
|
||||||
|
{
|
||||||
|
const size_t Column=static_cast<size_t>(Y-Min[1])*Size[0]+static_cast<size_t>(X-Min[0]);
|
||||||
|
const size_t Zi=static_cast<size_t>(Z-Min[2]);
|
||||||
const size_t Word=Column*WordsPerZ+(Zi>>6);
|
const size_t Word=Column*WordsPerZ+(Zi>>6);
|
||||||
const uint64 Mask=uint64{1}<<(Zi&63u);
|
const uint64 Mask=uint64{1}<<(Zi&63u);
|
||||||
if(CandidateWords[Word]&Mask)
|
if(CandidateWords[Word]&Mask)
|
||||||
@@ -277,37 +466,56 @@ public:
|
|||||||
float Density=static_cast<float>(Proxy)*(1.0f/16.0f);
|
float Density=static_cast<float>(Proxy)*(1.0f/16.0f);
|
||||||
if((Density<0.0f)!=bRock)
|
if((Density<0.0f)!=bRock)
|
||||||
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/16.0f);
|
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/16.0f);
|
||||||
return Density;
|
return ApplyEditBrushes(Density,FX,FY,FZ);
|
||||||
}
|
}
|
||||||
// Match the fixed fallback to the edge of the stored proxy band. The
|
// Match the fixed fallback to the edge of the stored proxy band. The
|
||||||
// displacement is tapered to zero there, so this avoids a magnitude
|
// displacement is tapered to zero there, so this avoids a magnitude
|
||||||
// jump along the candidate bit-mask boundary.
|
// jump along the candidate bit-mask boundary.
|
||||||
constexpr float ProxyBandEdge=DetailSupremum+0.125f;
|
constexpr float ProxyBandEdge=DetailSupremum+0.125f;
|
||||||
return bRock?-ProxyBandEdge:ProxyBandEdge;
|
return ApplyEditBrushes(bRock?-ProxyBandEdge:ProxyBandEdge,FX,FY,FZ);
|
||||||
}
|
}
|
||||||
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
|
|
||||||
bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
|
float AnalyticDensityAt(float X,float Y,float Z) const
|
||||||
float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
|
{
|
||||||
&bFloorSurface,&CaveInfluence,&FloorInfluence);
|
|
||||||
const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]
|
const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]
|
||||||
&&Z>=Min[2]&&Z<=Max[2];
|
&&Z>=Min[2]&&Z<=Max[2];
|
||||||
const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z);
|
if(!bInsideFine)
|
||||||
|
{
|
||||||
|
bool bCriticalAir=false;
|
||||||
|
float RockDistance=CoarseRockDistance(X,Y,Z,&bCriticalAir);
|
||||||
|
if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f);
|
||||||
|
const bool bRock=RockDistance>=0.0f;
|
||||||
|
float Density=-RockDistance;
|
||||||
|
if((Density<0.0f)!=bRock||std::abs(Density)<0.05f)
|
||||||
|
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f);
|
||||||
|
return ApplyEditBrushes(Density,X,Y,Z);
|
||||||
|
}
|
||||||
|
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
|
||||||
|
bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
|
||||||
|
float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
|
||||||
|
&bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir);
|
||||||
if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth
|
if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth
|
||||||
&&std::abs(RockDistance)<=DetailSupremum+0.125f)
|
&&std::abs(RockDistance)<=DetailSupremum+0.125f)
|
||||||
RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence)
|
RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence)
|
||||||
*DetailBandFade(RockDistance);
|
*DetailBandFade(RockDistance);
|
||||||
if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f);
|
if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f);
|
||||||
|
const bool bRock=RockDistance>=0.0f;
|
||||||
float Density=-RockDistance;
|
float Density=-RockDistance;
|
||||||
if((Density<0.0f)!=bRock||std::abs(Density)<0.05f)
|
if((Density<0.0f)!=bRock||std::abs(Density)<0.05f)
|
||||||
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f);
|
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f);
|
||||||
return Density;
|
return ApplyEditBrushes(Density,X,Y,Z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
EVoxelTileClass ClassifyBox(const FIntVector& Origin,int32 Step,int32 Cells) const
|
EVoxelTileClass ClassifyBox(const FIntVector& Origin,int32 Step,int32 Cells) const
|
||||||
{
|
{
|
||||||
const int32 Pad=std::max(Step,1);
|
const int32 Pad=std::max(Step,1);
|
||||||
int32 BMin[3]={Origin.X-Pad,Origin.Y-Pad,Origin.Z-Pad};
|
int32 BMin[3]={Origin.X-Pad,Origin.Y-Pad,Origin.Z-Pad};
|
||||||
int32 BMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,Origin.Z+(Cells+1)*Step};
|
int32 BMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,Origin.Z+(Cells+1)*Step};
|
||||||
|
if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0]
|
||||||
|
&&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1]
|
||||||
|
&&EditBoundsMin.Z<=BMax[2]&&EditBoundsMax.Z>=BMin[2])
|
||||||
|
return EVoxelTileClass::Mixed;
|
||||||
for(int32 BZ=FloorDiv(BMin[2],ActiveSpatialBinSize);BZ<=FloorDiv(BMax[2],ActiveSpatialBinSize);++BZ)
|
for(int32 BZ=FloorDiv(BMin[2],ActiveSpatialBinSize);BZ<=FloorDiv(BMax[2],ActiveSpatialBinSize);++BZ)
|
||||||
for(int32 BY=FloorDiv(BMin[1],ActiveSpatialBinSize);BY<=FloorDiv(BMax[1],ActiveSpatialBinSize);++BY)
|
for(int32 BY=FloorDiv(BMin[1],ActiveSpatialBinSize);BY<=FloorDiv(BMax[1],ActiveSpatialBinSize);++BY)
|
||||||
for(int32 BX=FloorDiv(BMin[0],ActiveSpatialBinSize);BX<=FloorDiv(BMax[0],ActiveSpatialBinSize);++BX)
|
for(int32 BX=FloorDiv(BMin[0],ActiveSpatialBinSize);BX<=FloorDiv(BMax[0],ActiveSpatialBinSize);++BX)
|
||||||
@@ -655,6 +863,50 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
float ApplyEditBrushes(float Density,double X,double Y,double Z) const
|
||||||
|
{
|
||||||
|
if(EditBrushes.empty()||X<EditBoundsMin.X||X>EditBoundsMax.X
|
||||||
|
||Y<EditBoundsMin.Y||Y>EditBoundsMax.Y||Z<EditBoundsMin.Z||Z>EditBoundsMax.Z)
|
||||||
|
return Density;
|
||||||
|
const double P[3]={X,Y,Z};
|
||||||
|
for(const FEditBrush& Brush:EditBrushes)
|
||||||
|
{
|
||||||
|
double Distance=0.0;
|
||||||
|
if(Brush.Shape==FEditBrush::EShape::Sphere)
|
||||||
|
{
|
||||||
|
const double DX=X-Brush.A[0],DY=Y-Brush.A[1],DZ=Z-Brush.A[2];
|
||||||
|
Distance=DeterministicSqrt(DX*DX+DY*DY+DZ*DZ)-Brush.Radius;
|
||||||
|
}
|
||||||
|
else if(Brush.Shape==FEditBrush::EShape::Capsule)
|
||||||
|
{
|
||||||
|
double AB[3]{},AP[3]{},LengthSq=0.0,Dot=0.0;
|
||||||
|
for(int32 Axis=0;Axis<3;++Axis)
|
||||||
|
{AB[Axis]=Brush.B[Axis]-Brush.A[Axis];AP[Axis]=P[Axis]-Brush.A[Axis];LengthSq+=AB[Axis]*AB[Axis];Dot+=AB[Axis]*AP[Axis];}
|
||||||
|
const double T=LengthSq>1.0e-12?std::clamp(Dot/LengthSq,0.0,1.0):0.0;
|
||||||
|
double D2=0.0;
|
||||||
|
for(int32 Axis=0;Axis<3;++Axis)
|
||||||
|
{const double D=AP[Axis]-T*AB[Axis];D2+=D*D;}
|
||||||
|
Distance=DeterministicSqrt(D2)-Brush.Radius;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double OutsideSq=0.0,MaxQ=-std::numeric_limits<double>::infinity();
|
||||||
|
for(int32 Axis=0;Axis<3;++Axis)
|
||||||
|
{
|
||||||
|
const double Q=std::abs(P[Axis]-Brush.A[Axis])-Brush.HalfExtent[Axis];
|
||||||
|
const double Outside=std::max(Q,0.0);
|
||||||
|
OutsideSq+=Outside*Outside;MaxQ=std::max(MaxQ,Q);
|
||||||
|
}
|
||||||
|
Distance=DeterministicSqrt(OutsideSq)+std::min(MaxQ,0.0)-Brush.Rounding;
|
||||||
|
}
|
||||||
|
if(Brush.Operation==FEditBrush::EOperation::Carve)
|
||||||
|
Density=std::max(Density,static_cast<float>(-Distance));
|
||||||
|
else
|
||||||
|
Density=std::min(Density,static_cast<float>(Distance));
|
||||||
|
}
|
||||||
|
return Density;
|
||||||
|
}
|
||||||
|
|
||||||
struct FAxisBins { int32 MinA=0,MinB=0,CountA=0,CountB=0; std::vector<std::vector<uint32>> Items; };
|
struct FAxisBins { int32 MinA=0,MinB=0,CountA=0,CountB=0; std::vector<std::vector<uint32>> Items; };
|
||||||
struct FLayerField
|
struct FLayerField
|
||||||
{
|
{
|
||||||
@@ -671,6 +923,12 @@ private:
|
|||||||
Out=static_cast<int32>(R);return true;
|
Out=static_cast<int32>(R);return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static double DeterministicSqrt(double Value)
|
||||||
|
{
|
||||||
|
if(!(Value>0.0))return 0.0;
|
||||||
|
return _mm_cvtsd_f64(_mm_sqrt_sd(_mm_setzero_pd(),_mm_set_sd(Value)));
|
||||||
|
}
|
||||||
|
|
||||||
bool Contains(int32 X,int32 Y,int32 Z) const
|
bool Contains(int32 X,int32 Y,int32 Z) const
|
||||||
{return X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2];}
|
{return X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2];}
|
||||||
|
|
||||||
@@ -883,14 +1141,21 @@ private:
|
|||||||
P.CapsuleStartRadius=ExtendedRadiusA;P.CapsuleEndRadius=ExtendedRadiusB;
|
P.CapsuleStartRadius=ExtendedRadiusA;P.CapsuleEndRadius=ExtendedRadiusB;
|
||||||
P.MinRadius=std::min(ExtendedRadiusA,ExtendedRadiusB);P.BlendRadius=ActiveBlendRadius;
|
P.MinRadius=std::min(ExtendedRadiusA,ExtendedRadiusB);P.BlendRadius=ActiveBlendRadius;
|
||||||
const double R=std::max(ExtendedRadiusA,ExtendedRadiusB);
|
const double R=std::max(ExtendedRadiusA,ExtendedRadiusB);
|
||||||
|
double AxisLengthSquared=0.0;
|
||||||
for(int32 Axis=0;Axis<3;++Axis)
|
for(int32 Axis=0;Axis<3;++Axis)
|
||||||
{
|
{
|
||||||
P.CapsuleStartSub[Axis]=static_cast<int32>(std::llround(Start[Axis]*SubPerVoxel));
|
P.CapsuleStartSub[Axis]=static_cast<int32>(std::llround(Start[Axis]*SubPerVoxel));
|
||||||
P.CapsuleEndSub[Axis]=static_cast<int32>(std::llround(End[Axis]*SubPerVoxel));
|
P.CapsuleEndSub[Axis]=static_cast<int32>(std::llround(End[Axis]*SubPerVoxel));
|
||||||
|
P.CapsuleStartWorld[Axis]=P.CapsuleStartSub[Axis]/static_cast<double>(SubPerVoxel);
|
||||||
|
const double CapsuleEndWorld=P.CapsuleEndSub[Axis]/static_cast<double>(SubPerVoxel);
|
||||||
|
P.CapsuleAxisDelta[Axis]=CapsuleEndWorld-P.CapsuleStartWorld[Axis];
|
||||||
|
AxisLengthSquared+=P.CapsuleAxisDelta[Axis]*P.CapsuleAxisDelta[Axis];
|
||||||
P.CenterSub[Axis]=static_cast<int32>(std::llround((Start[Axis]+End[Axis])*0.5*SubPerVoxel));
|
P.CenterSub[Axis]=static_cast<int32>(std::llround((Start[Axis]+End[Axis])*0.5*SubPerVoxel));
|
||||||
P.Min[Axis]=static_cast<int32>(std::floor(std::min(Start[Axis],End[Axis])-R))-1;
|
P.Min[Axis]=static_cast<int32>(std::floor(std::min(Start[Axis],End[Axis])-R))-1;
|
||||||
P.Max[Axis]=static_cast<int32>(std::ceil(std::max(Start[Axis],End[Axis])+R))+1;
|
P.Max[Axis]=static_cast<int32>(std::ceil(std::max(Start[Axis],End[Axis])+R))+1;
|
||||||
}
|
}
|
||||||
|
P.CapsuleAxisLengthSquared=AxisLengthSquared;
|
||||||
|
P.CapsuleAxisLength=std::sqrt(std::max(0.0,AxisLengthSquared));
|
||||||
if(bFloorClip)
|
if(bFloorClip)
|
||||||
{
|
{
|
||||||
const double FloorSlope=(FloorB-FloorA)/Length;
|
const double FloorSlope=(FloorB-FloorA)/Length;
|
||||||
@@ -1584,12 +1849,20 @@ private:
|
|||||||
ActiveBlendRadius=PreviousBlendRadius;
|
ActiveBlendRadius=PreviousBlendRadius;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildSpatialBins(int32 BinResolution)
|
void BuildSpatialBins(int32 BinResolution,bool bBuildAxisBins=true)
|
||||||
{
|
{
|
||||||
SpatialBins.clear();
|
SpatialBins.clear();
|
||||||
CriticalAirBins.clear();
|
CriticalAirBins.clear();
|
||||||
ActiveSpatialBinSize=BinResolution;
|
ActiveSpatialBinSize=BinResolution;
|
||||||
for(int32 Axis=0;Axis<3;++Axis)
|
// Primitive bounds overlap multiple cells. Reserve from the measured bins-per-primitive
|
||||||
|
// density for each grid instead of repeatedly rehashing from a 2x primitive guess.
|
||||||
|
const size_t BinsPerPrimitive=bBuildAxisBins?96u:24u;
|
||||||
|
SpatialBins.reserve(std::max<size_t>(Primitives.size()*BinsPerPrimitive,1u));
|
||||||
|
if(!bBuildAxisBins)
|
||||||
|
{
|
||||||
|
CriticalAirBins.reserve(Primitives.size()/16+1);
|
||||||
|
}
|
||||||
|
if(bBuildAxisBins)for(int32 Axis=0;Axis<3;++Axis)
|
||||||
{
|
{
|
||||||
FAxisBins& B=AxisBins[Axis];
|
FAxisBins& B=AxisBins[Axis];
|
||||||
const int32 A=(Axis+1)%3,C=(Axis+2)%3;
|
const int32 A=(Axis+1)%3,C=(Axis+2)%3;
|
||||||
@@ -1681,7 +1954,8 @@ private:
|
|||||||
const int32 MinY=static_cast<int32>(std::floor(std::min(A.Y,B.Y)-64.0));
|
const int32 MinY=static_cast<int32>(std::floor(std::min(A.Y,B.Y)-64.0));
|
||||||
const int32 MaxY=static_cast<int32>(std::ceil(std::max(A.Y,B.Y)+64.0));
|
const int32 MaxY=static_cast<int32>(std::ceil(std::max(A.Y,B.Y)+64.0));
|
||||||
if(MaxX>=Min[0]&&MinX<=Max[0]&&MaxY>=Min[1]&&MinY<=Max[1])continue;
|
if(MaxX>=Min[0]&&MinX<=Max[0]&&MaxY>=Min[1]&&MinY<=Max[1])continue;
|
||||||
AddCapsule(A,B,64.0f,true);
|
AddTaperedCapsule(A,B,64.0f,64.0f,true);
|
||||||
|
Primitives.back().BlendRadius=0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!Parents.empty())
|
if(!Parents.empty())
|
||||||
@@ -1787,16 +2061,16 @@ private:
|
|||||||
double V[3],D[3],D2=0.0,VD=0.0;
|
double V[3],D[3],D2=0.0,VD=0.0;
|
||||||
for(int32 A=0;A<3;++A)
|
for(int32 A=0;A<3;++A)
|
||||||
{
|
{
|
||||||
const double Start=P.CapsuleStartSub[A]/static_cast<double>(SubPerVoxel);
|
V[A]=Position[A]-P.CapsuleStartWorld[A];
|
||||||
const double End=P.CapsuleEndSub[A]/static_cast<double>(SubPerVoxel);
|
D[A]=P.CapsuleAxisDelta[A];
|
||||||
V[A]=Position[A]-Start;D[A]=End-Start;D2+=D[A]*D[A];VD+=V[A]*D[A];
|
VD+=V[A]*D[A];
|
||||||
}
|
}
|
||||||
const double AxisLength=std::sqrt(std::max(0.0,D2));
|
D2=P.CapsuleAxisLengthSquared;
|
||||||
const double T=D2>1.0e-12?std::clamp(VD/D2,0.0,1.0):0.0;
|
const double T=D2>1.0e-12?std::clamp(VD/D2,0.0,1.0):0.0;
|
||||||
double DistanceSquared=0.0;
|
double DistanceSquared=0.0;
|
||||||
for(int32 A=0;A<3;++A)
|
for(int32 A=0;A<3;++A)
|
||||||
{const double E=V[A]-T*D[A];DistanceSquared+=E*E;}
|
{const double E=V[A]-T*D[A];DistanceSquared+=E*E;}
|
||||||
const double Radius=CapsuleRadiusAtT(P,T,AxisLength);
|
const double Radius=CapsuleRadiusAtT(P,T,P.CapsuleAxisLength);
|
||||||
return static_cast<float>(std::sqrt(DistanceSquared)-Radius);
|
return static_cast<float>(std::sqrt(DistanceSquared)-Radius);
|
||||||
}
|
}
|
||||||
if(P.bRoundedBox)
|
if(P.bRoundedBox)
|
||||||
@@ -1949,7 +2223,7 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildPackedRockIntervals()
|
void BuildPackedRockIntervals(FParallelTiming& Timing)
|
||||||
{
|
{
|
||||||
const int32 NX=Size[0],NY=Size[1],NZ=Size[2];
|
const int32 NX=Size[0],NY=Size[1],NZ=Size[2];
|
||||||
ParallelLines(static_cast<size_t>(NX)*NY,[&](size_t Line)
|
ParallelLines(static_cast<size_t>(NX)*NY,[&](size_t Line)
|
||||||
@@ -1981,10 +2255,11 @@ private:
|
|||||||
for(const FInterval& R:Solid)FillBits(RockWords,Base,R.Lo,R.Hi,true);
|
for(const FInterval& R:Solid)FillBits(RockWords,Base,R.Lo,R.Hi,true);
|
||||||
// Critical passage air is the final authored override, including at landing floors.
|
// Critical passage air is the final authored override, including at landing floors.
|
||||||
for(const FInterval& R:ProtectedAir)FillBits(RockWords,Base,R.Lo,R.Hi,false);
|
for(const FInterval& R:ProtectedAir)FillBits(RockWords,Base,R.Lo,R.Hi,false);
|
||||||
});
|
},&Timing);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildCandidateBands()
|
void BuildCandidateBands(FParallelTiming& AxisTiming,FParallelTiming& MergeTiming,
|
||||||
|
double& MergeSeconds)
|
||||||
{
|
{
|
||||||
const int32 N[3]={Size[0],Size[1],Size[2]};
|
const int32 N[3]={Size[0],Size[1],Size[2]};
|
||||||
for(int32 Axis=0;Axis<3;++Axis)
|
for(int32 Axis=0;Axis<3;++Axis)
|
||||||
@@ -2010,14 +2285,15 @@ private:
|
|||||||
FillBits(CandidateAxisWords[Axis],Base,Lo-Min[Axis],Hi-Min[Axis],true);
|
FillBits(CandidateAxisWords[Axis],Base,Lo-Min[Axis],Hi-Min[Axis],true);
|
||||||
FillBits(CandidateAxisWords[Axis],Base,Lo2-Min[Axis],Hi2-Min[Axis],true);
|
FillBits(CandidateAxisWords[Axis],Base,Lo2-Min[Axis],Hi2-Min[Axis],true);
|
||||||
}
|
}
|
||||||
});
|
},&AxisTiming);
|
||||||
}
|
}
|
||||||
CandidateWords.assign(RockWords.size(),0);
|
const auto MergeStart=std::chrono::steady_clock::now();
|
||||||
for(int32 Axis=0;Axis<3;++Axis)
|
CandidateWords=CandidateAxisWords[2];
|
||||||
|
for(int32 Axis=0;Axis<2;++Axis)
|
||||||
{
|
{
|
||||||
const int32 A=(Axis+1)%3,B=(Axis+2)%3;
|
const int32 A=(Axis+1)%3,B=(Axis+2)%3;
|
||||||
const size_t Words=(static_cast<size_t>(Size[Axis])+63u)/64u;
|
const size_t Words=(static_cast<size_t>(Size[Axis])+63u)/64u;
|
||||||
for(size_t Line=0;Line<static_cast<size_t>(Size[A])*Size[B];++Line)
|
ParallelLines(static_cast<size_t>(Size[A])*Size[B],[&,Axis,A,B,Words](size_t Line)
|
||||||
{
|
{
|
||||||
const int32 CA=Min[A]+static_cast<int32>(Line%Size[A]);
|
const int32 CA=Min[A]+static_cast<int32>(Line%Size[A]);
|
||||||
const int32 CB=Min[B]+static_cast<int32>(Line/Size[A]);
|
const int32 CB=Min[B]+static_cast<int32>(Line/Size[A]);
|
||||||
@@ -2033,10 +2309,11 @@ private:
|
|||||||
const size_t Column=static_cast<size_t>(Coord[1]-Min[1])*Size[0]
|
const size_t Column=static_cast<size_t>(Coord[1]-Min[1])*Size[0]
|
||||||
+static_cast<size_t>(Coord[0]-Min[0]);
|
+static_cast<size_t>(Coord[0]-Min[0]);
|
||||||
const size_t Zi=static_cast<size_t>(Coord[2]-Min[2]);
|
const size_t Zi=static_cast<size_t>(Coord[2]-Min[2]);
|
||||||
CandidateWords[Column*WordsPerZ+(Zi>>6)]|=uint64{1}<<(Zi&63u);
|
std::atomic_ref<uint64>(CandidateWords[Column*WordsPerZ+(Zi>>6)])
|
||||||
}
|
.fetch_or(uint64{1}<<(Zi&63u),std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},&MergeTiming);
|
||||||
}
|
}
|
||||||
const int32 LayerLo=LayerRanges.empty()?1:LayerRanges[0].Lo;
|
const int32 LayerLo=LayerRanges.empty()?1:LayerRanges[0].Lo;
|
||||||
const int32 LayerHi=LayerRanges.empty()?0:LayerRanges[0].Hi;
|
const int32 LayerHi=LayerRanges.empty()?0:LayerRanges[0].Hi;
|
||||||
@@ -2053,15 +2330,20 @@ private:
|
|||||||
FillBits(CandidateWords,Base,Lo1,Hi1,true);
|
FillBits(CandidateWords,Base,Lo1,Hi1,true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const double MergeElapsed=std::chrono::duration<double>(
|
||||||
|
std::chrono::steady_clock::now()-MergeStart).count();
|
||||||
|
MergeSeconds=std::max(0.0,MergeElapsed-MergeTiming.WallSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApplyDetailBand()
|
void ApplyDetailBand(FParallelTiming& Timing)
|
||||||
{
|
{
|
||||||
std::atomic<uint64> CandidateCount{0},ProxyCount{0},FlipCount{0};
|
std::atomic<uint64> CandidateCount{0},ProxyCount{0},FlipCount{0};
|
||||||
ParallelLines(static_cast<size_t>(Size[0])*Size[1],[&](size_t Line)
|
ParallelLines(static_cast<size_t>(Size[0])*Size[1],[&](size_t Line)
|
||||||
{
|
{
|
||||||
const int32 X=Min[0]+static_cast<int32>(Line%Size[0]);
|
const int32 X=Min[0]+static_cast<int32>(Line%Size[0]);
|
||||||
const int32 Y=Min[1]+static_cast<int32>(Line/Size[0]);
|
const int32 Y=Min[1]+static_cast<int32>(Line/Size[0]);
|
||||||
|
FDetailNoiseLineContext NoiseLine(static_cast<float>(X),static_cast<float>(Y),
|
||||||
|
static_cast<uint32>(Owner->Seed));
|
||||||
uint64 LocalCandidates=0,LocalProxies=0,LocalFlips=0;
|
uint64 LocalCandidates=0,LocalProxies=0,LocalFlips=0;
|
||||||
const size_t WordBase=Line*WordsPerZ;
|
const size_t WordBase=Line*WordsPerZ;
|
||||||
for(size_t W=0;W<WordsPerZ;++W)
|
for(size_t W=0;W<WordsPerZ;++W)
|
||||||
@@ -2074,13 +2356,13 @@ private:
|
|||||||
const size_t Zi=W*64+Bit;if(Zi>=static_cast<size_t>(Size[2]))continue;
|
const size_t Zi=W*64+Bit;if(Zi>=static_cast<size_t>(Size[2]))continue;
|
||||||
const int32 Z=Min[2]+static_cast<int32>(Zi);
|
const int32 Z=Min[2]+static_cast<int32>(Zi);
|
||||||
++LocalCandidates;
|
++LocalCandidates;
|
||||||
const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z);
|
|
||||||
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
|
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
|
||||||
bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
|
bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
|
||||||
const float SDF=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
|
const float SDF=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
|
||||||
&bFloorSurface,&CaveInfluence,&FloorInfluence);
|
&bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir);
|
||||||
if(std::abs(SDF)>DetailSupremum+0.125f)continue;
|
if(std::abs(SDF)>DetailSupremum+0.125f)continue;
|
||||||
float Displaced=SDF+DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence)
|
float Displaced=SDF+DetailNoiseOnLine(X,Y,Z,SurfaceSDF,CaveInfluence,
|
||||||
|
FloorInfluence,NoiseLine)
|
||||||
*DetailBandFade(SDF);
|
*DetailBandFade(SDF);
|
||||||
if(bCriticalAir)Displaced=std::min(Displaced,-1.0f/16.0f);
|
if(bCriticalAir)Displaced=std::min(Displaced,-1.0f/16.0f);
|
||||||
const bool bNewRock=Displaced>=0.0f;
|
const bool bNewRock=Displaced>=0.0f;
|
||||||
@@ -2101,7 +2383,7 @@ private:
|
|||||||
CandidateCount.fetch_add(LocalCandidates,std::memory_order_relaxed);
|
CandidateCount.fetch_add(LocalCandidates,std::memory_order_relaxed);
|
||||||
ProxyCount.fetch_add(LocalProxies,std::memory_order_relaxed);
|
ProxyCount.fetch_add(LocalProxies,std::memory_order_relaxed);
|
||||||
FlipCount.fetch_add(LocalFlips,std::memory_order_relaxed);
|
FlipCount.fetch_add(LocalFlips,std::memory_order_relaxed);
|
||||||
});
|
},&Timing);
|
||||||
Stats.SurfaceCandidates=CandidateCount.load(std::memory_order_relaxed);
|
Stats.SurfaceCandidates=CandidateCount.load(std::memory_order_relaxed);
|
||||||
Stats.SurfaceProxyNodes=ProxyCount.load(std::memory_order_relaxed);
|
Stats.SurfaceProxyNodes=ProxyCount.load(std::memory_order_relaxed);
|
||||||
Stats.DetailFlips=FlipCount.load(std::memory_order_relaxed);
|
Stats.DetailFlips=FlipCount.load(std::memory_order_relaxed);
|
||||||
@@ -2238,13 +2520,53 @@ private:
|
|||||||
float* OutSurfaceSDF=nullptr,
|
float* OutSurfaceSDF=nullptr,
|
||||||
bool* OutFloorSurface=nullptr,
|
bool* OutFloorSurface=nullptr,
|
||||||
float* OutCaveInfluence=nullptr,
|
float* OutCaveInfluence=nullptr,
|
||||||
float* OutFloorInfluence=nullptr) const
|
float* OutFloorInfluence=nullptr,
|
||||||
|
bool* OutCriticalAir=nullptr) const
|
||||||
{
|
{
|
||||||
const float Base=StrataDistance(Z);
|
const float Base=StrataDistance(Z);
|
||||||
|
float Cave=100000.0f;
|
||||||
|
float Overlay=-100000.0f;
|
||||||
float CaveFloorInfluence=0.0f;
|
float CaveFloorInfluence=0.0f;
|
||||||
const float Cave=CaveDistance(X,Y,Z,nullptr,
|
bool bCriticalAir=false;
|
||||||
(OutFloorSurface||OutFloorInfluence)?&CaveFloorInfluence:nullptr);
|
const auto Bin=SpatialBins.find(PointBin(X,Y,Z));
|
||||||
const float Overlay=SolidOverlayDistance(X,Y,Z);
|
if(Bin!=SpatialBins.end())
|
||||||
|
{
|
||||||
|
for(uint32 Id:Bin->second)
|
||||||
|
{
|
||||||
|
const FPrimitive& P=Primitives[Id];
|
||||||
|
if(P.bCarve)
|
||||||
|
{
|
||||||
|
if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
|
||||||
|
&&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P,
|
||||||
|
static_cast<int32>(X),static_cast<int32>(Y),static_cast<int32>(Z)))
|
||||||
|
bCriticalAir=true;
|
||||||
|
if(X<P.Min[0]-DetailBand||X>P.Max[0]+DetailBand
|
||||||
|
||Y<P.Min[1]-DetailBand||Y>P.Max[1]+DetailBand
|
||||||
|
||Z<P.Min[2]-DetailBand||Z>P.Max[2]+DetailBand)continue;
|
||||||
|
float Distance=ShapeDistance(P,X,Y,Z);
|
||||||
|
float PrimitiveFloorInfluence=0.0f;
|
||||||
|
if(P.bHasFloorClip)
|
||||||
|
{
|
||||||
|
const double Floor=FloorAt(P,X,Y);
|
||||||
|
const float FloorT=(static_cast<float>(std::abs(Z-Floor))
|
||||||
|
-static_cast<float>(P.FloorBlendRadius))
|
||||||
|
/static_cast<float>(std::max(DetailBand*2,1));
|
||||||
|
PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT);
|
||||||
|
Distance=SmoothMaxDistance(Distance,static_cast<float>(Floor-Z),
|
||||||
|
static_cast<float>(P.FloorBlendRadius));
|
||||||
|
}
|
||||||
|
const float MetadataWidth=static_cast<float>(std::max(P.BlendRadius,6.0));
|
||||||
|
const float PrimitiveWeight=SmoothUnit(0.5f+(Cave-Distance)/(2.0f*MetadataWidth));
|
||||||
|
const float Combined=SmoothMinDistance(Cave,Distance,static_cast<float>(P.BlendRadius));
|
||||||
|
CaveFloorInfluence=std::lerp(CaveFloorInfluence,PrimitiveFloorInfluence,PrimitiveWeight);
|
||||||
|
Cave=Combined;
|
||||||
|
}
|
||||||
|
else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
|
||||||
|
&&Z>=P.Min[2]&&Z<=P.Max[2])
|
||||||
|
Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(OutCriticalAir)*OutCriticalAir=bCriticalAir;
|
||||||
const float CarveOrOverlay=std::max(Cave,Overlay);
|
const float CarveOrOverlay=std::max(Cave,Overlay);
|
||||||
const ESurfaceFamily Family=Base<=CarveOrOverlay?ESurfaceFamily::Strata:
|
const ESurfaceFamily Family=Base<=CarveOrOverlay?ESurfaceFamily::Strata:
|
||||||
(Cave>=Overlay?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay);
|
(Cave>=Overlay?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay);
|
||||||
@@ -2265,6 +2587,44 @@ private:
|
|||||||
return RockDistance;
|
return RockDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float CoarseRockDistance(double X,double Y,double Z,bool* OutCriticalAir) const
|
||||||
|
{
|
||||||
|
const float Base=StrataDistance(Z);
|
||||||
|
float Cave=100000.0f,Overlay=-100000.0f;
|
||||||
|
bool bCriticalAir=false;
|
||||||
|
const auto Bin=SpatialBins.find(PointBin(X,Y,Z));
|
||||||
|
if(Bin!=SpatialBins.end())
|
||||||
|
{
|
||||||
|
for(uint32 Id:Bin->second)
|
||||||
|
{
|
||||||
|
const FPrimitive& P=Primitives[Id];
|
||||||
|
if(P.bCarve)
|
||||||
|
{
|
||||||
|
if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
|
||||||
|
&&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P,
|
||||||
|
static_cast<int32>(X),static_cast<int32>(Y),static_cast<int32>(Z)))
|
||||||
|
bCriticalAir=true;
|
||||||
|
if(X<P.Min[0]-DetailBand||X>P.Max[0]+DetailBand
|
||||||
|
||Y<P.Min[1]-DetailBand||Y>P.Max[1]+DetailBand
|
||||||
|
||Z<P.Min[2]-DetailBand||Z>P.Max[2]+DetailBand)continue;
|
||||||
|
float Distance=ShapeDistance(P,X,Y,Z);
|
||||||
|
if(P.bHasFloorClip)
|
||||||
|
{
|
||||||
|
const double Floor=FloorAt(P,X,Y);
|
||||||
|
Distance=SmoothMaxDistance(Distance,static_cast<float>(Floor-Z),
|
||||||
|
static_cast<float>(P.FloorBlendRadius));
|
||||||
|
}
|
||||||
|
Cave=SmoothMinDistance(Cave,Distance,static_cast<float>(P.BlendRadius));
|
||||||
|
}
|
||||||
|
else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
|
||||||
|
&&Z>=P.Min[2]&&Z<=P.Max[2])
|
||||||
|
Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(OutCriticalAir)*OutCriticalAir=bCriticalAir;
|
||||||
|
return SmoothMinDistance(Base,std::max(Cave,Overlay),StrataCaveBlendRadius);
|
||||||
|
}
|
||||||
|
|
||||||
float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF,
|
float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF,
|
||||||
const FStrateGenerationParams& Params,
|
const FStrateGenerationParams& Params,
|
||||||
float CaveInfluence) const
|
float CaveInfluence) const
|
||||||
@@ -2334,6 +2694,77 @@ private:
|
|||||||
return 1.00f*Macro+(0.80f+0.25f*Macro)*(0.85f*Mid+0.18f*Fine);
|
return 1.00f*Macro+(0.80f+0.25f*Macro)*(0.85f*Mid+0.18f*Fine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FValueNoiseLineContext
|
||||||
|
{
|
||||||
|
int32 X0=0,Y0=0,Z0=std::numeric_limits<int32>::min();
|
||||||
|
int32 Wavelength=1;
|
||||||
|
uint32 Seed=0;
|
||||||
|
float Inv=1.0f,TX=0.0f,TY=0.0f;
|
||||||
|
std::array<float,8> Corners{};
|
||||||
|
|
||||||
|
FValueNoiseLineContext(float X,float Y,int32 InWavelength,uint32 InSeed)
|
||||||
|
:Wavelength(InWavelength),Seed(InSeed),Inv(1.0f/static_cast<float>(InWavelength))
|
||||||
|
{
|
||||||
|
const float FX=X*Inv,FY=Y*Inv;
|
||||||
|
X0=FMath::FloorToInt(FX);Y0=FMath::FloorToInt(FY);
|
||||||
|
TX=FStandaloneResearchTunnels::Fade(FX-static_cast<float>(X0));
|
||||||
|
TY=FStandaloneResearchTunnels::Fade(FY-static_cast<float>(Y0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetPlane(int32 Z,bool bUpper)
|
||||||
|
{
|
||||||
|
const size_t I=bUpper?4u:0u;
|
||||||
|
Corners[I+0]=FStandaloneResearchTunnels::NoiseValue(X0,Y0,Z,Seed);
|
||||||
|
Corners[I+1]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0,Z,Seed);
|
||||||
|
Corners[I+2]=FStandaloneResearchTunnels::NoiseValue(X0,Y0+1,Z,Seed);
|
||||||
|
Corners[I+3]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0+1,Z,Seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
float Sample(float Z)
|
||||||
|
{
|
||||||
|
const float FZ=Z*Inv;
|
||||||
|
const int32 NewZ0=FMath::FloorToInt(FZ);
|
||||||
|
const float TZ=FStandaloneResearchTunnels::Fade(FZ-static_cast<float>(NewZ0));
|
||||||
|
if(NewZ0!=Z0)
|
||||||
|
{
|
||||||
|
if(NewZ0==Z0+1)
|
||||||
|
{
|
||||||
|
Corners[0]=Corners[4];Corners[1]=Corners[5];
|
||||||
|
Corners[2]=Corners[6];Corners[3]=Corners[7];
|
||||||
|
SetPlane(NewZ0+1,true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetPlane(NewZ0,false);SetPlane(NewZ0+1,true);
|
||||||
|
}
|
||||||
|
Z0=NewZ0;
|
||||||
|
}
|
||||||
|
const float Z00=FMath::Lerp(Corners[0],Corners[1],TX);
|
||||||
|
const float Z10=FMath::Lerp(Corners[2],Corners[3],TX);
|
||||||
|
const float Z01=FMath::Lerp(Corners[4],Corners[5],TX);
|
||||||
|
const float Z11=FMath::Lerp(Corners[6],Corners[7],TX);
|
||||||
|
return FMath::Lerp(FMath::Lerp(Z00,Z10,TY),FMath::Lerp(Z01,Z11,TY),TZ);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FDetailNoiseLineContext
|
||||||
|
{
|
||||||
|
FValueNoiseLineContext Macro,Mid,Fine;
|
||||||
|
FDetailNoiseLineContext(float X,float Y,uint32 Seed)
|
||||||
|
:Macro(X,Y,32,Seed^0xB5297A4Du),Mid(X,Y,16,Seed^0x63D83595u),
|
||||||
|
Fine(X,Y,6,Seed^0xA511E9B3u)
|
||||||
|
{}
|
||||||
|
|
||||||
|
float SurfaceReliefAt(float Z)
|
||||||
|
{
|
||||||
|
const float MacroValue=Macro.Sample(Z);
|
||||||
|
const float MidValue=Mid.Sample(Z);
|
||||||
|
const float FineValue=Fine.Sample(Z);
|
||||||
|
return 1.00f*MacroValue+(0.80f+0.25f*MacroValue)
|
||||||
|
*(0.85f*MidValue+0.18f*FineValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
float DetailNoise(double X,double Y,double Z,float SurfaceSDF,
|
float DetailNoise(double X,double Y,double Z,float SurfaceSDF,
|
||||||
float CaveInfluence,float FloorInfluence) const
|
float CaveInfluence,float FloorInfluence) const
|
||||||
{
|
{
|
||||||
@@ -2353,13 +2784,27 @@ private:
|
|||||||
return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f);
|
return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float DetailNoiseOnLine(double X,double Y,double Z,float SurfaceSDF,
|
||||||
|
float CaveInfluence,float FloorInfluence,
|
||||||
|
FDetailNoiseLineContext& NoiseLine) const
|
||||||
|
{
|
||||||
|
if(DetailVariant!=EDetailVariant::Coherent)
|
||||||
|
return DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence);
|
||||||
|
const float Relief=NoiseLine.SurfaceReliefAt(static_cast<float>(Z));
|
||||||
|
const FLayerField* Field=FindLayerField(Z);
|
||||||
|
const float OwnerRelief=Field?OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field->Params,
|
||||||
|
CaveInfluence):0.0f;
|
||||||
|
const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f)
|
||||||
|
+1.85f*std::clamp(FloorInfluence,0.0f,1.0f);
|
||||||
|
return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f);
|
||||||
|
}
|
||||||
|
|
||||||
bool EvaluateFast(double X,double Y,double Z) const
|
bool EvaluateFast(double X,double Y,double Z) const
|
||||||
{
|
{
|
||||||
const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z);
|
|
||||||
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
|
ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f;
|
||||||
bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
|
bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f;
|
||||||
float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
|
float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF,
|
||||||
&bFloorSurface,&CaveInfluence,&FloorInfluence);
|
&bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir);
|
||||||
const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2];
|
const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2];
|
||||||
if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth
|
if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth
|
||||||
&&std::abs(RockDistance)<=DetailSupremum+0.125f)
|
&&std::abs(RockDistance)<=DetailSupremum+0.125f)
|
||||||
@@ -2421,19 +2866,44 @@ private:
|
|||||||
return Count?PairAB/std::sqrt(std::max(1.0e-12,PairAA*PairBB)):0.0;
|
return Count?PairAB/std::sqrt(std::max(1.0e-12,PairAA*PairBB)):0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<class Fn> void ParallelLines(size_t Count,Fn&& Work) const
|
template<class Fn> void ParallelLines(size_t Count,Fn&& Work,FParallelTiming* Timing=nullptr) const
|
||||||
{
|
{
|
||||||
|
const auto WallStart=std::chrono::steady_clock::now();
|
||||||
if(WorkerCount<=1||Count<512)
|
if(WorkerCount<=1||Count<512)
|
||||||
{for(size_t I=0;I<Count;++I)Work(I);return;}
|
{
|
||||||
std::atomic<size_t> Next{0};std::vector<std::thread> Threads;Threads.reserve(WorkerCount);
|
const auto WorkerStart=std::chrono::steady_clock::now();
|
||||||
|
for(size_t I=0;I<Count;++I)Work(I);
|
||||||
|
if(Timing)
|
||||||
|
{
|
||||||
|
const double WorkerSeconds=std::chrono::duration<double>(std::chrono::steady_clock::now()-WorkerStart).count();
|
||||||
|
Timing->WallSeconds+=std::chrono::duration<double>(std::chrono::steady_clock::now()-WallStart).count();
|
||||||
|
Timing->WorkerSeconds+=WorkerSeconds;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::atomic<size_t> Next{0};std::atomic<uint64> WorkerNanoseconds{0};
|
||||||
|
std::vector<std::thread> Threads;Threads.reserve(WorkerCount);
|
||||||
for(int32 W=0;W<WorkerCount;++W)
|
for(int32 W=0;W<WorkerCount;++W)
|
||||||
Threads.emplace_back([&](){for(;;){const size_t I=Next.fetch_add(1,std::memory_order_relaxed);if(I>=Count)break;Work(I);}});
|
Threads.emplace_back([&]()
|
||||||
|
{
|
||||||
|
const auto WorkerStart=std::chrono::steady_clock::now();
|
||||||
|
for(;;){const size_t I=Next.fetch_add(1,std::memory_order_relaxed);if(I>=Count)break;Work(I);}
|
||||||
|
WorkerNanoseconds.fetch_add(static_cast<uint64>(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
|
std::chrono::steady_clock::now()-WorkerStart).count()),std::memory_order_relaxed);
|
||||||
|
});
|
||||||
for(std::thread& T:Threads)T.join();
|
for(std::thread& T:Threads)T.join();
|
||||||
|
if(Timing)
|
||||||
|
{
|
||||||
|
Timing->WallSeconds+=std::chrono::duration<double>(std::chrono::steady_clock::now()-WallStart).count();
|
||||||
|
Timing->WorkerSeconds+=static_cast<double>(WorkerNanoseconds.load(std::memory_order_relaxed))*1.0e-9;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FStandaloneOwnerWorld* Owner=nullptr;
|
FStandaloneOwnerWorld* Owner=nullptr;
|
||||||
int32 WorkerCount=6;
|
int32 WorkerCount=6;
|
||||||
int32 Min[3]{},Max[3]{},ClipMin[3]{},ClipMax[3]{},Size[3]{};
|
int32 Min[3]{},Max[3]{},ClipMin[3]{},ClipMax[3]{},Size[3]{};
|
||||||
|
FIntVector EditBoundsMin=FIntVector(MAX_int32,MAX_int32,MAX_int32);
|
||||||
|
FIntVector EditBoundsMax=FIntVector(MIN_int32,MIN_int32,MIN_int32);
|
||||||
int32 ActiveSpatialBinSize=LineBinSize;
|
int32 ActiveSpatialBinSize=LineBinSize;
|
||||||
double ActiveBlendRadius=0.0;
|
double ActiveBlendRadius=0.0;
|
||||||
double ActiveFloorBlendRadius=0.0;
|
double ActiveFloorBlendRadius=0.0;
|
||||||
@@ -2444,9 +2914,10 @@ private:
|
|||||||
std::vector<FInterval> LayerRanges;
|
std::vector<FInterval> LayerRanges;
|
||||||
std::vector<FLayerField> LayerFields;
|
std::vector<FLayerField> LayerFields;
|
||||||
std::vector<FPrimitive> Primitives;
|
std::vector<FPrimitive> Primitives;
|
||||||
|
std::vector<FEditBrush> EditBrushes;
|
||||||
std::array<FAxisBins,3> AxisBins;
|
std::array<FAxisBins,3> AxisBins;
|
||||||
std::unordered_map<FBinKey,std::vector<uint32>,FBinHash> SpatialBins;
|
std::unordered_map<FBinKey,FBinItems,FBinHash> SpatialBins;
|
||||||
std::unordered_map<FBinKey,std::vector<uint32>,FBinHash> CriticalAirBins;
|
std::unordered_map<FBinKey,FBinItems,FBinHash> CriticalAirBins;
|
||||||
std::vector<uint64> RockWords;
|
std::vector<uint64> RockWords;
|
||||||
std::unique_ptr<int8[]> SurfaceDistanceQ;
|
std::unique_ptr<int8[]> SurfaceDistanceQ;
|
||||||
std::array<std::vector<uint64>,3> CandidateAxisWords;
|
std::array<std::vector<uint64>,3> CandidateAxisWords;
|
||||||
|
|||||||
@@ -670,7 +670,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
|||||||
}
|
}
|
||||||
if(!FineTiles){Error="no full-detail tiles are available for shape compilation";return false;}
|
if(!FineTiles){Error="no full-detail tiles are available for shape compilation";return false;}
|
||||||
ShapeWorld=std::make_unique<FStandaloneShapeWorld>();
|
ShapeWorld=std::make_unique<FStandaloneShapeWorld>();
|
||||||
if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,WorkerCount,ShapeDetailVariant,bIncludeFarWorld,Error))
|
if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,WorkerCount,
|
||||||
|
ShapeDetailVariant,bIncludeFarWorld,Error))
|
||||||
{ShapeWorld.reset();return false;}
|
{ShapeWorld.reset();return false;}
|
||||||
FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get());
|
FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get());
|
||||||
bFarShapeStagePending=bShapeWorld&&!ShapeWorld->HasFarWorld();
|
bFarShapeStagePending=bShapeWorld&&!ShapeWorld->HasFarWorld();
|
||||||
@@ -1721,11 +1722,24 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
|||||||
Desired.Tiles.begin(),Desired.Tiles.end(),[&](const FVoxelTileKey& Tile)
|
Desired.Tiles.begin(),Desired.Tiles.end(),[&](const FVoxelTileKey& Tile)
|
||||||
{return Tile.Level<World.FullResClipLevels;}));
|
{return Tile.Level<World.FullResClipLevels;}));
|
||||||
std::array<std::vector<FHashedTile>,MaxLevels+1> HashRows;
|
std::array<std::vector<FHashedTile>,MaxLevels+1> HashRows;
|
||||||
|
std::array<std::vector<FHashedTile>,MaxLevels+1> MeshHashRows;
|
||||||
std::array<uint64,MaxLevels+1> SamplesByLevel{};
|
std::array<uint64,MaxLevels+1> SamplesByLevel{};
|
||||||
std::array<uint64,MaxLevels+1> VerticesByLevel{};
|
std::array<uint64,MaxLevels+1> VerticesByLevel{};
|
||||||
std::array<uint64,MaxLevels+1> IndicesByLevel{};
|
std::array<uint64,MaxLevels+1> IndicesByLevel{};
|
||||||
std::array<uint64,MaxLevels+1> TilesByLevel{};
|
std::array<uint64,MaxLevels+1> TilesByLevel{};
|
||||||
std::array<double,MaxLevels+1> WorkerSecondsByLevel{};
|
std::array<double,MaxLevels+1> WorkerSecondsByLevel{};
|
||||||
|
std::array<double,MaxLevels+1> FirstCompletionByLevel{};
|
||||||
|
std::array<double,MaxLevels+1> LastCompletionByLevel{};
|
||||||
|
std::array<uint64,MaxLevels+1> OutputBytesByLevel{};
|
||||||
|
FirstCompletionByLevel.fill(std::numeric_limits<double>::infinity());
|
||||||
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> PlayerNeighborhoodTiles;
|
||||||
|
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||||
|
if(Tile.Level==0&&std::abs(Tile.Coord.X-Desired.CenterChunk.X)<=1
|
||||||
|
&&std::abs(Tile.Coord.Y-Desired.CenterChunk.Y)<=1
|
||||||
|
&&std::abs(Tile.Coord.Z-Desired.CenterChunk.Z)<=1)
|
||||||
|
PlayerNeighborhoodTiles.insert(Tile);
|
||||||
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> PlayerNeighborhoodReady;
|
||||||
|
auto PlayerNeighborhoodCompleted=std::chrono::steady_clock::time_point{};
|
||||||
std::vector<FStandaloneCpuTile> CacheTiles;
|
std::vector<FStandaloneCpuTile> CacheTiles;
|
||||||
if(!BakeCachePath.empty())CacheTiles.reserve(Desired.Tiles.size());
|
if(!BakeCachePath.empty())CacheTiles.reserve(Desired.Tiles.size());
|
||||||
const auto Start=std::chrono::steady_clock::now();
|
const auto Start=std::chrono::steady_clock::now();
|
||||||
@@ -1758,6 +1772,12 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
|||||||
{
|
{
|
||||||
++Finished;
|
++Finished;
|
||||||
const int32 Level=Tile.Key.Level;
|
const int32 Level=Tile.Key.Level;
|
||||||
|
if(PlayerNeighborhoodTiles.contains(Tile.Key))
|
||||||
|
{
|
||||||
|
PlayerNeighborhoodReady.insert(Tile.Key);
|
||||||
|
if(PlayerNeighborhoodReady.size()==PlayerNeighborhoodTiles.size())
|
||||||
|
PlayerNeighborhoodCompleted=Tile.CompletedAt;
|
||||||
|
}
|
||||||
if(Level>=0&&Level<=MaxLevels)
|
if(Level>=0&&Level<=MaxLevels)
|
||||||
{
|
{
|
||||||
SamplesByLevel[Level]+=Tile.DensitySamples;
|
SamplesByLevel[Level]+=Tile.DensitySamples;
|
||||||
@@ -1765,7 +1785,13 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
|||||||
IndicesByLevel[Level]+=Tile.Indices.size();
|
IndicesByLevel[Level]+=Tile.Indices.size();
|
||||||
++TilesByLevel[Level];
|
++TilesByLevel[Level];
|
||||||
WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds;
|
WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds;
|
||||||
|
const double CompletedSeconds=std::chrono::duration<double>(Tile.CompletedAt-Start).count();
|
||||||
|
FirstCompletionByLevel[Level]=std::min(FirstCompletionByLevel[Level],CompletedSeconds);
|
||||||
|
LastCompletionByLevel[Level]=std::max(LastCompletionByLevel[Level],CompletedSeconds);
|
||||||
|
OutputBytesByLevel[Level]+=static_cast<uint64>(Tile.Vertices.capacity())*sizeof(FStandaloneCpuVertex)
|
||||||
|
+static_cast<uint64>(Tile.Indices.capacity())*sizeof(uint32);
|
||||||
if(Tile.bHasDensityHash)HashRows[Level].push_back({Tile.Key,Tile.DensityHash});
|
if(Tile.bHasDensityHash)HashRows[Level].push_back({Tile.Key,Tile.DensityHash});
|
||||||
|
if(Tile.bHasMeshHash)MeshHashRows[Level].push_back({Tile.Key,Tile.MeshHash});
|
||||||
}
|
}
|
||||||
if(Level<World.FullResClipLevels)++FullDetailFinished;
|
if(Level<World.FullResClipLevels)++FullDetailFinished;
|
||||||
if(bShardAudit&&Level==0)
|
if(bShardAudit&&Level==0)
|
||||||
@@ -1793,12 +1819,17 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
|||||||
FullDetailSeconds=std::chrono::duration<double>(
|
FullDetailSeconds=std::chrono::duration<double>(
|
||||||
std::chrono::steady_clock::now()-Start).count();
|
std::chrono::steady_clock::now()-Start).count();
|
||||||
std::vector<FHashedTile> FullRows;
|
std::vector<FHashedTile> FullRows;
|
||||||
|
std::vector<FHashedTile> FullMeshRows;
|
||||||
for(int32 Level=0;Level<World.FullResClipLevels&&Level<=MaxLevels;++Level)
|
for(int32 Level=0;Level<World.FullResClipLevels&&Level<=MaxLevels;++Level)
|
||||||
|
{
|
||||||
FullRows.insert(FullRows.end(),HashRows[Level].begin(),HashRows[Level].end());
|
FullRows.insert(FullRows.end(),HashRows[Level].begin(),HashRows[Level].end());
|
||||||
|
FullMeshRows.insert(FullMeshRows.end(),MeshHashRows[Level].begin(),MeshHashRows[Level].end());
|
||||||
|
}
|
||||||
if(bHashDensity||!BakeCachePath.empty())
|
if(bHashDensity||!BakeCachePath.empty())
|
||||||
std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 density_hash=0x%016llx\n",
|
std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 density_hash=0x%016llx mesh_hash=0x%016llx\n",
|
||||||
FullDetailSeconds,FullDetailFinished,FullDetailExpected,
|
FullDetailSeconds,FullDetailFinished,FullDetailExpected,
|
||||||
static_cast<unsigned long long>(HashTileRows(FullRows)));
|
static_cast<unsigned long long>(HashTileRows(FullRows)),
|
||||||
|
static_cast<unsigned long long>(HashTileRows(FullMeshRows)));
|
||||||
else
|
else
|
||||||
std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 hash=off\n",
|
std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 hash=off\n",
|
||||||
FullDetailSeconds,FullDetailFinished,FullDetailExpected);
|
FullDetailSeconds,FullDetailFinished,FullDetailExpected);
|
||||||
@@ -1807,18 +1838,34 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
|||||||
}
|
}
|
||||||
const double FullClipSeconds=std::chrono::duration<double>(
|
const double FullClipSeconds=std::chrono::duration<double>(
|
||||||
std::chrono::steady_clock::now()-Start).count();
|
std::chrono::steady_clock::now()-Start).count();
|
||||||
std::vector<FHashedTile> AllRows;
|
if(!PlayerNeighborhoodTiles.empty()&&PlayerNeighborhoodReady.size()==PlayerNeighborhoodTiles.size())
|
||||||
if(bHashDensity||!BakeCachePath.empty())
|
std::printf("[PlayerNeighborhood] lod0_tiles=%zu/%zu mesh_ready=%.3fs collision_cook=unavailable_in_standalone\n",
|
||||||
for(auto& LevelRows:HashRows)AllRows.insert(AllRows.end(),LevelRows.begin(),LevelRows.end());
|
PlayerNeighborhoodReady.size(),PlayerNeighborhoodTiles.size(),
|
||||||
if(bHashDensity||!BakeCachePath.empty())
|
std::chrono::duration<double>(PlayerNeighborhoodCompleted-Start).count());
|
||||||
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs tiles=%d density_hash=0x%016llx\n",
|
|
||||||
FullClipSeconds,Finished,DesiredTotal,
|
|
||||||
std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0),
|
|
||||||
DesiredTotal,static_cast<unsigned long long>(HashTileRows(AllRows)));
|
|
||||||
else
|
else
|
||||||
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs tiles=%d hash=off\n",
|
std::printf("[PlayerNeighborhood] lod0_tiles=%zu/%zu mesh_ready=missing collision_cook=unavailable_in_standalone\n",
|
||||||
FullClipSeconds,Finished,DesiredTotal,
|
PlayerNeighborhoodReady.size(),PlayerNeighborhoodTiles.size());
|
||||||
std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0),DesiredTotal);
|
const double TotalWorkerSeconds=std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0);
|
||||||
|
const double WorkerBusyPercent=100.0*TotalWorkerSeconds
|
||||||
|
/(std::max(1,WorkerCount)*std::max(0.001,FullClipSeconds));
|
||||||
|
std::vector<FHashedTile> AllRows;
|
||||||
|
std::vector<FHashedTile> AllMeshRows;
|
||||||
|
if(bHashDensity||!BakeCachePath.empty())
|
||||||
|
for(int32 Level=0;Level<=MaxLevels;++Level)
|
||||||
|
{
|
||||||
|
AllRows.insert(AllRows.end(),HashRows[Level].begin(),HashRows[Level].end());
|
||||||
|
AllMeshRows.insert(AllMeshRows.end(),MeshHashRows[Level].begin(),MeshHashRows[Level].end());
|
||||||
|
}
|
||||||
|
if(bHashDensity||!BakeCachePath.empty())
|
||||||
|
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs worker_busy=%.1f%% worker_idle=%.1f%% tiles=%d density_hash=0x%016llx mesh_hash=0x%016llx\n",
|
||||||
|
FullClipSeconds,Finished,DesiredTotal,TotalWorkerSeconds,WorkerBusyPercent,
|
||||||
|
std::max(0.0,100.0-WorkerBusyPercent),
|
||||||
|
DesiredTotal,static_cast<unsigned long long>(HashTileRows(AllRows)),
|
||||||
|
static_cast<unsigned long long>(HashTileRows(AllMeshRows)));
|
||||||
|
else
|
||||||
|
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs worker_busy=%.1f%% worker_idle=%.1f%% tiles=%d hash=off\n",
|
||||||
|
FullClipSeconds,Finished,DesiredTotal,TotalWorkerSeconds,WorkerBusyPercent,
|
||||||
|
std::max(0.0,100.0-WorkerBusyPercent),DesiredTotal);
|
||||||
std::printf("[StreamCacheUse] hits=%llu requested=%d\n",
|
std::printf("[StreamCacheUse] hits=%llu requested=%d\n",
|
||||||
static_cast<unsigned long long>(Streamer.GetCacheHits()),DesiredTotal);
|
static_cast<unsigned long long>(Streamer.GetCacheHits()),DesiredTotal);
|
||||||
if(bShardAudit)
|
if(bShardAudit)
|
||||||
@@ -1835,16 +1882,23 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
|||||||
{
|
{
|
||||||
if(TilesByLevel[Level]==0)continue;
|
if(TilesByLevel[Level]==0)continue;
|
||||||
if(bHashDensity||!BakeCachePath.empty())
|
if(bHashDensity||!BakeCachePath.empty())
|
||||||
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs vertices=%llu indices=%llu density_hash=0x%016llx\n",
|
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs first_ready=%.3fs complete=%.3fs worker_busy=%.1f%% output=%.2fMiB vertices=%llu indices=%llu density_hash=0x%016llx mesh_hash=0x%016llx\n",
|
||||||
Level,static_cast<unsigned long long>(TilesByLevel[Level]),
|
Level,static_cast<unsigned long long>(TilesByLevel[Level]),
|
||||||
static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level],
|
static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level],
|
||||||
|
FirstCompletionByLevel[Level],LastCompletionByLevel[Level],
|
||||||
|
100.0*WorkerSecondsByLevel[Level]/(WorkerCount*std::max(0.001,LastCompletionByLevel[Level])),
|
||||||
|
static_cast<double>(OutputBytesByLevel[Level])/1048576.0,
|
||||||
static_cast<unsigned long long>(VerticesByLevel[Level]),
|
static_cast<unsigned long long>(VerticesByLevel[Level]),
|
||||||
static_cast<unsigned long long>(IndicesByLevel[Level]),
|
static_cast<unsigned long long>(IndicesByLevel[Level]),
|
||||||
static_cast<unsigned long long>(HashTileRows(HashRows[Level])));
|
static_cast<unsigned long long>(HashTileRows(HashRows[Level])),
|
||||||
|
static_cast<unsigned long long>(HashTileRows(MeshHashRows[Level])));
|
||||||
else
|
else
|
||||||
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs vertices=%llu indices=%llu hash=off\n",
|
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs first_ready=%.3fs complete=%.3fs worker_busy=%.1f%% output=%.2fMiB vertices=%llu indices=%llu hash=off\n",
|
||||||
Level,static_cast<unsigned long long>(TilesByLevel[Level]),
|
Level,static_cast<unsigned long long>(TilesByLevel[Level]),
|
||||||
static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level],
|
static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level],
|
||||||
|
FirstCompletionByLevel[Level],LastCompletionByLevel[Level],
|
||||||
|
100.0*WorkerSecondsByLevel[Level]/(WorkerCount*std::max(0.001,LastCompletionByLevel[Level])),
|
||||||
|
static_cast<double>(OutputBytesByLevel[Level])/1048576.0,
|
||||||
static_cast<unsigned long long>(VerticesByLevel[Level]),
|
static_cast<unsigned long long>(VerticesByLevel[Level]),
|
||||||
static_cast<unsigned long long>(IndicesByLevel[Level]));
|
static_cast<unsigned long long>(IndicesByLevel[Level]));
|
||||||
}
|
}
|
||||||
@@ -1864,6 +1918,78 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
|||||||
if(bShardAudit&&(MeshSheetStats.ThinSheets!=0||MeshSheetStats.SmallComponentsInView!=0))return 13;
|
if(bShardAudit&&(MeshSheetStats.ThinSheets!=0||MeshSheetStats.SmallComponentsInView!=0))return 13;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int RunEditTransactionBenchmark(FStandaloneOwnerWorld& World,FStandaloneShapeWorld& Shape,
|
||||||
|
const FVector& CameraPositionVoxels,int32 WorkerCount)
|
||||||
|
{
|
||||||
|
using Clock=std::chrono::steady_clock;
|
||||||
|
// Place the spell across the LOD0/LOD1 clipmap seam so one edit exercises every visible
|
||||||
|
// level it intersects, instead of timing only the center LOD0 tiles.
|
||||||
|
const double X=CameraPositionVoxels.X+3.0*CHUNK_SIZE;
|
||||||
|
const double Y=CameraPositionVoxels.Y,Z=CameraPositionVoxels.Z;
|
||||||
|
std::vector<FStandaloneShapeWorld::FEditBrush> Brushes(3);
|
||||||
|
Brushes[0].Shape=FStandaloneShapeWorld::FEditBrush::EShape::Sphere;
|
||||||
|
Brushes[0].Operation=FStandaloneShapeWorld::FEditBrush::EOperation::Carve;
|
||||||
|
Brushes[0].A={X,Y,Z};Brushes[0].Radius=20.0;
|
||||||
|
Brushes[1].Shape=FStandaloneShapeWorld::FEditBrush::EShape::RoundedBox;
|
||||||
|
Brushes[1].Operation=FStandaloneShapeWorld::FEditBrush::EOperation::Carve;
|
||||||
|
Brushes[1].A={X,Y,Z+12.0};Brushes[1].HalfExtent={11.0,11.0,4.0};Brushes[1].Rounding=3.0;
|
||||||
|
Brushes[2].Shape=FStandaloneShapeWorld::FEditBrush::EShape::Capsule;
|
||||||
|
Brushes[2].Operation=FStandaloneShapeWorld::FEditBrush::EOperation::AddRock;
|
||||||
|
Brushes[2].A={X-22.0,Y,Z-8.0};Brushes[2].B={X+22.0,Y,Z-8.0};Brushes[2].Radius=3.0;
|
||||||
|
const auto EditStart=Clock::now();
|
||||||
|
Shape.SetEditTransaction(std::move(Brushes));
|
||||||
|
|
||||||
|
FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels);
|
||||||
|
const FIntVector EditMin=Shape.GetEditBoundsMin(),EditMax=Shape.GetEditBoundsMax();
|
||||||
|
std::vector<FVoxelTileKey> Affected;Affected.reserve(128);
|
||||||
|
std::array<uint64,MaxLevels+1> AffectedByLevel{},FinishedByLevel{};
|
||||||
|
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||||
|
{
|
||||||
|
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
|
||||||
|
const int32 Pad=std::max(2,Tile.StepVoxels());
|
||||||
|
if(Origin.X-Pad>EditMax.X||Origin.X+Extent+Pad<EditMin.X
|
||||||
|
||Origin.Y-Pad>EditMax.Y||Origin.Y+Extent+Pad<EditMin.Y
|
||||||
|
||Origin.Z-Pad>EditMax.Z||Origin.Z+Extent+Pad<EditMin.Z)continue;
|
||||||
|
Affected.push_back(Tile);
|
||||||
|
if(Tile.Level>=0&&Tile.Level<=MaxLevels)++AffectedByLevel[Tile.Level];
|
||||||
|
}
|
||||||
|
if(Affected.empty())
|
||||||
|
{
|
||||||
|
std::fprintf(stderr,"[EditTransaction] failed: no desired tiles intersect the edit bounds.\n");
|
||||||
|
return 9;
|
||||||
|
}
|
||||||
|
FStandaloneStreamingGenerator Streamer(World,WorkerCount,false,nullptr,-1);
|
||||||
|
Streamer.SetDesiredTileFilter(Affected);
|
||||||
|
const auto Start=Clock::now();
|
||||||
|
const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels);
|
||||||
|
int32 Finished=0;
|
||||||
|
double WorkerSeconds=0.0;
|
||||||
|
while(Finished<Update.DesiredTiles)
|
||||||
|
{
|
||||||
|
std::vector<FStandaloneCpuTile> Ready=Streamer.TakeReady();
|
||||||
|
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
|
||||||
|
for(const FStandaloneCpuTile& Tile:Ready)
|
||||||
|
{
|
||||||
|
++Finished;WorkerSeconds+=Tile.WorkerSeconds;
|
||||||
|
if(Tile.Key.Level>=0&&Tile.Key.Level<=MaxLevels)++FinishedByLevel[Tile.Key.Level];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const double RemeshWall=std::chrono::duration<double>(Clock::now()-Start).count();
|
||||||
|
Streamer.Stop();
|
||||||
|
std::printf("[EditTransaction] operation=crater_batch transaction_count=1 brushes=3 diameter=10.0m offset_from_player=%.2fm affected_tiles=%d/%zu workers=%d cache_hits=%llu remesh_wall=%.3fs worker_sum=%.3fs edit_to_ready=%.3fs status=%s\n",
|
||||||
|
3.0*CHUNK_SIZE*VOXEL_SIZE/100.0,
|
||||||
|
Finished,Affected.size(),WorkerCount,static_cast<unsigned long long>(Streamer.GetCacheHits()),
|
||||||
|
RemeshWall,WorkerSeconds,std::chrono::duration<double>(Clock::now()-EditStart).count(),
|
||||||
|
Finished==static_cast<int32>(Affected.size())?"PASS":"FAIL");
|
||||||
|
for(int32 Level=0;Level<=MaxLevels;++Level)
|
||||||
|
if(AffectedByLevel[Level])
|
||||||
|
std::printf("[EditTransactionLevel] lod=%d tiles=%llu remeshed=%llu status=%s\n",Level,
|
||||||
|
static_cast<unsigned long long>(AffectedByLevel[Level]),
|
||||||
|
static_cast<unsigned long long>(FinishedByLevel[Level]),
|
||||||
|
AffectedByLevel[Level]==FinishedByLevel[Level]?"PASS":"FAIL");
|
||||||
|
return Finished==static_cast<int32>(Affected.size())?0:10;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int Argc,char** Argv)
|
int main(int Argc,char** Argv)
|
||||||
@@ -1878,6 +2004,7 @@ int main(int Argc,char** Argv)
|
|||||||
bool bStreamBenchmark=false;
|
bool bStreamBenchmark=false;
|
||||||
bool bShardAudit=false;
|
bool bShardAudit=false;
|
||||||
bool bStreamHash=false;
|
bool bStreamHash=false;
|
||||||
|
bool bEditBenchmark=false;
|
||||||
bool bRoundWallCompare=false;
|
bool bRoundWallCompare=false;
|
||||||
int32 RoundCompareTileCount=16;
|
int32 RoundCompareTileCount=16;
|
||||||
std::string BakeMeshCachePath;
|
std::string BakeMeshCachePath;
|
||||||
@@ -1907,6 +2034,8 @@ int main(int Argc,char** Argv)
|
|||||||
else if(Arg=="--round-tunnels-legacy")
|
else if(Arg=="--round-tunnels-legacy")
|
||||||
{App.bRoundTunnels=true;App.bRoundTunnelEnhanced=false;}
|
{App.bRoundTunnels=true;App.bRoundTunnelEnhanced=false;}
|
||||||
else if(Arg=="--stream-benchmark")bStreamBenchmark=true;
|
else if(Arg=="--stream-benchmark")bStreamBenchmark=true;
|
||||||
|
else if(Arg=="--edit-benchmark")
|
||||||
|
{bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;}
|
||||||
else if(Arg=="--stream-hash")bStreamHash=true;
|
else if(Arg=="--stream-hash")bStreamHash=true;
|
||||||
else if(Arg=="--shard-audit")bShardAudit=true;
|
else if(Arg=="--shard-audit")bShardAudit=true;
|
||||||
else if(Arg=="--compare-round-walls")
|
else if(Arg=="--compare-round-walls")
|
||||||
@@ -1984,6 +2113,9 @@ int main(int Argc,char** Argv)
|
|||||||
return RunRoundWallComparison(*App.World,App.Position,RoundCompareTileCount);
|
return RunRoundWallComparison(*App.World,App.Position,RoundCompareTileCount);
|
||||||
if(bStreamBenchmark)
|
if(bStreamBenchmark)
|
||||||
{
|
{
|
||||||
|
if(bEditBenchmark)
|
||||||
|
return RunEditTransactionBenchmark(*App.World,*App.ShapeWorld,
|
||||||
|
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),App.WorkerCount);
|
||||||
const double FullDetailWidth=static_cast<double>(2*App.ClipRadius+2)
|
const double FullDetailWidth=static_cast<double>(2*App.ClipRadius+2)
|
||||||
*CHUNK_SIZE*VOXEL_SIZE/100.0;
|
*CHUNK_SIZE*VOXEL_SIZE/100.0;
|
||||||
std::printf("Standalone clip override: exported radius=%d (112 m cube) -> radius=%d (%.0f m LOD0 cube), voxel size=25 cm.\n",
|
std::printf("Standalone clip override: exported radius=%d (112 m cube) -> radius=%d (%.0f m LOD0 cube), voxel size=25 cm.\n",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <bit>
|
#include <bit>
|
||||||
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -36,11 +37,36 @@ struct FStandaloneCpuTile
|
|||||||
std::vector<uint32> Indices;
|
std::vector<uint32> Indices;
|
||||||
uint64 DensitySamples=0;
|
uint64 DensitySamples=0;
|
||||||
double WorkerSeconds=0.0;
|
double WorkerSeconds=0.0;
|
||||||
|
std::chrono::steady_clock::time_point CompletedAt{};
|
||||||
uint64 DensityHash=14695981039346656037ull;
|
uint64 DensityHash=14695981039346656037ull;
|
||||||
bool bHasDensityHash=false;
|
bool bHasDensityHash=false;
|
||||||
|
uint64 MeshHash=14695981039346656037ull;
|
||||||
|
bool bHasMeshHash=false;
|
||||||
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
|
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inline uint64 HashStandaloneCpuTileMesh(const FStandaloneCpuTile& Tile)
|
||||||
|
{
|
||||||
|
uint64 Hash=14695981039346656037ull;
|
||||||
|
const auto AddWord=[&](uint64 Word,int32 Bytes)
|
||||||
|
{
|
||||||
|
for(int32 I=0;I<Bytes;++I)
|
||||||
|
{
|
||||||
|
Hash^=static_cast<uint8>(Word>>(I*8));
|
||||||
|
Hash*=1099511628211ull;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AddWord(Tile.Vertices.size(),8);
|
||||||
|
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
|
||||||
|
{
|
||||||
|
for(const float Value:Vertex.Position)AddWord(std::bit_cast<uint32>(Value),4);
|
||||||
|
for(const float Value:Vertex.Normal)AddWord(std::bit_cast<uint32>(Value),4);
|
||||||
|
}
|
||||||
|
AddWord(Tile.Indices.size(),8);
|
||||||
|
for(const uint32 Index:Tile.Indices)AddWord(Index,4);
|
||||||
|
return Hash;
|
||||||
|
}
|
||||||
|
|
||||||
struct FStandaloneMeshCacheMetadata
|
struct FStandaloneMeshCacheMetadata
|
||||||
{
|
{
|
||||||
uint64 SourceDataHash=0;
|
uint64 SourceDataHash=0;
|
||||||
@@ -308,6 +334,13 @@ public:
|
|||||||
return UpdateDesired(CameraPositionVoxels,ResidentKeys,CrossingIndex);
|
return UpdateDesired(CameraPositionVoxels,ResidentKeys,CrossingIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SetDesiredTileFilter(const std::vector<FVoxelTileKey>& Keys)
|
||||||
|
{
|
||||||
|
TileFilter.clear();TileFilter.reserve(Keys.size()*2+1);
|
||||||
|
for(const FVoxelTileKey& Key:Keys)TileFilter.insert(Key);
|
||||||
|
bUseTileFilter=true;
|
||||||
|
}
|
||||||
|
|
||||||
void Stop()
|
void Stop()
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
@@ -330,6 +363,9 @@ public:
|
|||||||
if(MaxLevelFilter>=0)
|
if(MaxLevelFilter>=0)
|
||||||
NewDesired.Tiles.erase(std::remove_if(NewDesired.Tiles.begin(),NewDesired.Tiles.end(),
|
NewDesired.Tiles.erase(std::remove_if(NewDesired.Tiles.begin(),NewDesired.Tiles.end(),
|
||||||
[&](const FVoxelTileKey& Key){return Key.Level>MaxLevelFilter;}),NewDesired.Tiles.end());
|
[&](const FVoxelTileKey& Key){return Key.Level>MaxLevelFilter;}),NewDesired.Tiles.end());
|
||||||
|
if(bUseTileFilter)
|
||||||
|
NewDesired.Tiles.erase(std::remove_if(NewDesired.Tiles.begin(),NewDesired.Tiles.end(),
|
||||||
|
[&](const FVoxelTileKey& Key){return !TileFilter.contains(Key);}),NewDesired.Tiles.end());
|
||||||
FStandaloneStreamingUpdate Stats;
|
FStandaloneStreamingUpdate Stats;
|
||||||
Stats.CenterChunk=NewDesired.CenterChunk;
|
Stats.CenterChunk=NewDesired.CenterChunk;
|
||||||
Stats.DesiredTiles=static_cast<int32>(NewDesired.Tiles.size());
|
Stats.DesiredTiles=static_cast<int32>(NewDesired.Tiles.size());
|
||||||
@@ -409,12 +445,16 @@ private:
|
|||||||
{
|
{
|
||||||
Out=*Cached;Out.RequestedCrossing=Job.RequestedCrossing;
|
Out=*Cached;Out.RequestedCrossing=Job.RequestedCrossing;
|
||||||
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
|
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
|
||||||
|
Out.CompletedAt=FClock::now();
|
||||||
|
if(bHashDensity)
|
||||||
|
{Out.MeshHash=HashStandaloneCpuTileMesh(Out);Out.bHasMeshHash=true;}
|
||||||
CacheHitCount.fetch_add(1,std::memory_order_relaxed);
|
CacheHitCount.fetch_add(1,std::memory_order_relaxed);
|
||||||
return Out;
|
return Out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UVoxelMarchingCubesMesher Mesher;
|
UVoxelMarchingCubesMesher Mesher;
|
||||||
Mesher.SetGenerator(World->Generator.get());
|
Mesher.SetGenerator(World->Generator.get());
|
||||||
|
Mesher.bStandaloneCompactPayload=true;
|
||||||
Mesher.bGenerateSkirts=World->bGenerateSkirts;
|
Mesher.bGenerateSkirts=World->bGenerateSkirts;
|
||||||
Mesher.SkirtCells=static_cast<float>(World->SkirtCells);
|
Mesher.SkirtCells=static_cast<float>(World->SkirtCells);
|
||||||
Mesher.LODOctaveDrop=World->LODOctaveDrop;
|
Mesher.LODOctaveDrop=World->LODOctaveDrop;
|
||||||
@@ -460,7 +500,10 @@ private:
|
|||||||
}
|
}
|
||||||
Out.Indices.reserve(static_cast<size_t>(Mesh.Triangles.Num()));
|
Out.Indices.reserve(static_cast<size_t>(Mesh.Triangles.Num()));
|
||||||
for(const int32 Index:Mesh.Triangles)Out.Indices.push_back(static_cast<uint32>(Index));
|
for(const int32 Index:Mesh.Triangles)Out.Indices.push_back(static_cast<uint32>(Index));
|
||||||
|
if(bHashDensity)
|
||||||
|
{Out.MeshHash=HashStandaloneCpuTileMesh(Out);Out.bHasMeshHash=true;}
|
||||||
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
|
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
|
||||||
|
Out.CompletedAt=FClock::now();
|
||||||
return Out;
|
return Out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,6 +543,8 @@ private:
|
|||||||
std::vector<FStandaloneCpuTile> Ready;
|
std::vector<FStandaloneCpuTile> Ready;
|
||||||
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Desired;
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Desired;
|
||||||
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Outstanding;
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Outstanding;
|
||||||
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> TileFilter;
|
||||||
|
bool bUseTileFilter=false;
|
||||||
FStandaloneDesiredTileSet CurrentDesired;
|
FStandaloneDesiredTileSet CurrentDesired;
|
||||||
int32 FullResTileCount=0;
|
int32 FullResTileCount=0;
|
||||||
};
|
};
|
||||||
@@ -4080,6 +4080,19 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ,
|
|||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float UVoxelGenerator::GetDensityAtLattice(int32 WorldX, int32 WorldY, int32 WorldZ,
|
||||||
|
float* OutPreEditDensity) const
|
||||||
|
{
|
||||||
|
if (const FStandaloneShapeWorld* ShapeWorld=FStandaloneShapeWorldAccess::GetActive())
|
||||||
|
{
|
||||||
|
const float ShapeDensity=ShapeWorld->DensityAtLattice(WorldX,WorldY,WorldZ);
|
||||||
|
if (OutPreEditDensity) *OutPreEditDensity=ShapeDensity;
|
||||||
|
return ShapeDensity;
|
||||||
|
}
|
||||||
|
return GetDensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
||||||
|
static_cast<float>(WorldZ),OutPreEditDensity);
|
||||||
|
}
|
||||||
|
|
||||||
float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
|
float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float WorldZ,
|
||||||
const FStrateGenerationParams& Params,
|
const FStrateGenerationParams& Params,
|
||||||
uint32 ParamsFingerprint, uint32 LayoutVersion,
|
uint32 ParamsFingerprint, uint32 LayoutVersion,
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
// plus de (ré)allocation de hash-map par tuile (chaque worker a son propre exemplaire).
|
// plus de (ré)allocation de hash-map par tuile (chaque worker a son propre exemplaire).
|
||||||
static thread_local TMap<FIntVector, int32> VertexMap;
|
static thread_local TMap<FIntVector, int32> VertexMap;
|
||||||
VertexMap.Reset();
|
VertexMap.Reset();
|
||||||
|
const int32 VertexCapacityHint=FMath::Clamp(InCellsPerAxis,2,CHUNK_SIZE);
|
||||||
|
VertexMap.Reserve(VertexCapacityHint*VertexCapacityHint*2);
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// F17 — CLASSE DE SURFACE (sol vs plafond sky-cap), par vertex → par triangle
|
// F17 — CLASSE DE SURFACE (sol vs plafond sky-cap), par vertex → par triangle
|
||||||
@@ -158,8 +160,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
// ~zéro sur une tuile de sol pur, borné par lattice² sur une tuile de cap.
|
// ~zéro sur une tuile de sol pur, borné par lattice² sur une tuile de cap.
|
||||||
static thread_local TArray<uint8> VertexClasses; // 0 = sol, 1 = sky-cap
|
static thread_local TArray<uint8> VertexClasses; // 0 = sol, 1 = sky-cap
|
||||||
static thread_local TMap<FIntVector, FVector2f> SurfColMemo; // (Xq,Yq,chunkZ) → (TerrainZ, CeilSurf) ; X=FLT_MAX ⇒ pas SurfaceWorld
|
static thread_local TMap<FIntVector, FVector2f> SurfColMemo; // (Xq,Yq,chunkZ) → (TerrainZ, CeilSurf) ; X=FLT_MAX ⇒ pas SurfaceWorld
|
||||||
|
if (!bStandaloneCompactPayload)
|
||||||
|
{
|
||||||
VertexClasses.Reset();
|
VertexClasses.Reset();
|
||||||
SurfColMemo.Reset();
|
SurfColMemo.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
// Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b) —
|
// Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b) —
|
||||||
// plus d'échantillonnage de densité par vertex. RawNormal pointe solide→air ; on la
|
// plus d'échantillonnage de densité par vertex. RawNormal pointe solide→air ; on la
|
||||||
@@ -189,18 +194,13 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
}
|
}
|
||||||
MeshData.Normals.Add(Normal);
|
MeshData.Normals.Add(Normal);
|
||||||
|
|
||||||
|
if (!bStandaloneCompactPayload)
|
||||||
|
{
|
||||||
// UVs planaires — le triplanar mapping se fait dans le matériau.
|
// UVs planaires — le triplanar mapping se fait dans le matériau.
|
||||||
MeshData.UVs.Add(FVector2D(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE));
|
MeshData.UVs.Add(FVector2D(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE));
|
||||||
|
|
||||||
//---------------------------------------------------------------------
|
//-----------------------------------------------------------------
|
||||||
// VERTEX COLOUR — masques pour le matériau triplanar maître (F6).
|
// VERTEX COLOUR — masks for biome palette, slope and border blend.
|
||||||
// R = index de palette du biome DOMINANT (0-255) — re-skin par biome.
|
|
||||||
// G = pente (0 = sol/plafond plat, 1 = paroi verticale) — roche sur falaises.
|
|
||||||
// B = poids de fondu de bordure (0 au cœur d'un biome → ~0.5 à la frontière).
|
|
||||||
// A = index de palette du biome VOISIN — le matériau lerp(R,A) par B → bords sans couture.
|
|
||||||
// La hauteur/snow-line se déduit de WorldPosition.Z dans le matériau (pas besoin de canal).
|
|
||||||
// Coût: une résolution biome par vertex UNIQUE (déduplication) ; nul si la strate n'a pas
|
|
||||||
// de biomes (GetBiomeMaterialAt sort en O(1) → palette 0 partout).
|
|
||||||
int32 PalD = 0, PalN = 0; float BlendW = 0.0f;
|
int32 PalD = 0, PalN = 0; float BlendW = 0.0f;
|
||||||
Generator->GetBiomeMaterialAt(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE,
|
Generator->GetBiomeMaterialAt(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE,
|
||||||
WorldPos.Z / VOXEL_SIZE, PalD, PalN, BlendW);
|
WorldPos.Z / VOXEL_SIZE, PalD, PalN, BlendW);
|
||||||
@@ -209,9 +209,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
const uint8 G = (uint8)FMath::Clamp(FMath::RoundToInt((1.0f - FMath::Abs((float)Normal.Z)) * 255.0f), 0, 255);
|
const uint8 G = (uint8)FMath::Clamp(FMath::RoundToInt((1.0f - FMath::Abs((float)Normal.Z)) * 255.0f), 0, 255);
|
||||||
const uint8 Bc = (uint8)FMath::Clamp(FMath::RoundToInt(BlendW * 255.0f), 0, 255);
|
const uint8 Bc = (uint8)FMath::Clamp(FMath::RoundToInt(BlendW * 255.0f), 0, 255);
|
||||||
MeshData.Colors.Add(FColor(R, G, Bc, A));
|
MeshData.Colors.Add(FColor(R, G, Bc, A));
|
||||||
|
}
|
||||||
|
|
||||||
// F17 — classe de surface (voir le bloc en tête de fonction). Down-facing seulement :
|
if (!bStandaloneCompactPayload)
|
||||||
// les vertex de sol/falaise (N.Z ≥ -0.1) sont "sol" sans payer la requête colonne.
|
{
|
||||||
|
// F17 — classify down-facing vertices for material-only ceiling grouping.
|
||||||
uint8 SurfClass = 0;
|
uint8 SurfClass = 0;
|
||||||
if (Normal.Z < -0.1f)
|
if (Normal.Z < -0.1f)
|
||||||
{
|
{
|
||||||
@@ -225,9 +227,6 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
if (!Col)
|
if (!Col)
|
||||||
{
|
{
|
||||||
float TerrainZ = 0.0f, CeilSurf = 0.0f;
|
float TerrainZ = 0.0f, CeilSurf = 0.0f;
|
||||||
// Un vertex du cap vit à la FRONTIÈRE HAUTE de la strate : arrondi/interpolation
|
|
||||||
// peuvent le faire flotter dans le chunk gap/seal juste AU-DESSUS (hors
|
|
||||||
// SurfaceWorld ⇒ sonde ratée ⇒ faux "sol"). On retente un chunk plus bas.
|
|
||||||
const bool bSurf = Generator->GetSurfaceHeightAt(
|
const bool bSurf = Generator->GetSurfaceHeightAt(
|
||||||
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z,
|
(float)(MemoKey.X * Step), (float)(MemoKey.Y * Step), MemoKey.Z,
|
||||||
TerrainZ, CeilSurf)
|
TerrainZ, CeilSurf)
|
||||||
@@ -237,10 +236,6 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
Col = &SurfColMemo.Add(MemoKey, bSurf ? FVector2f(TerrainZ, CeilSurf)
|
Col = &SurfColMemo.Add(MemoKey, bSurf ? FVector2f(TerrainZ, CeilSurf)
|
||||||
: FVector2f(FLT_MAX, -FLT_MAX));
|
: FVector2f(FLT_MAX, -FLT_MAX));
|
||||||
}
|
}
|
||||||
// Plus proche du plafond que du terrain ⇒ sky-cap. (Hors SurfaceWorld : sol.)
|
|
||||||
// + garde : un vrai vertex de cap est AU niveau du cap (± slop d'interpolation) ;
|
|
||||||
// loin AU-DESSUS = plancher/toit de la strate d'à côté atteint via le retry chunkZ-1
|
|
||||||
// (strates contiguës) → reste sol.
|
|
||||||
if (Col->X != FLT_MAX && FMath::Abs(Zv - Col->Y) < FMath::Abs(Zv - Col->X)
|
if (Col->X != FLT_MAX && FMath::Abs(Zv - Col->Y) < FMath::Abs(Zv - Col->X)
|
||||||
&& Zv <= Col->Y + 2.0f * StepF)
|
&& Zv <= Col->Y + 2.0f * StepF)
|
||||||
{
|
{
|
||||||
@@ -248,6 +243,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
VertexClasses.Add(SurfClass);
|
VertexClasses.Add(SurfClass);
|
||||||
|
}
|
||||||
|
|
||||||
return NewIndex;
|
return NewIndex;
|
||||||
};
|
};
|
||||||
@@ -919,7 +915,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY)
|
((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY)
|
||||||
* ReachBlocksPerAxis + ReachBlockX;
|
* ReachBlocksPerAxis + ReachBlockX;
|
||||||
float PreEditDensity = 0.0f;
|
float PreEditDensity = 0.0f;
|
||||||
DensityGrid[LocalIndex] = Generator->GetDensityAt(
|
DensityGrid[LocalIndex] = Generator->GetDensityAtLattice(
|
||||||
OriginVoxels.X + gx * Step,
|
OriginVoxels.X + gx * Step,
|
||||||
OriginVoxels.Y + gy * Step,
|
OriginVoxels.Y + gy * Step,
|
||||||
OriginVoxels.Z + gz * Step,
|
OriginVoxels.Z + gz * Step,
|
||||||
@@ -1244,14 +1240,18 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
|
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
|
||||||
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]);
|
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]);
|
||||||
|
|
||||||
// F17 — vote majoritaire (≥ 2 vertex sky-cap ⇒ triangle sky-cap).
|
TArray<int32>* Dst = &GroundTris;
|
||||||
|
if (!bStandaloneCompactPayload)
|
||||||
|
{
|
||||||
|
// F17 — majority vote (at least two ceiling vertices => ceiling triangle).
|
||||||
const int32 CapVotes = (int32)VertexClasses[Idx0]
|
const int32 CapVotes = (int32)VertexClasses[Idx0]
|
||||||
+ (int32)VertexClasses[Idx1]
|
+ (int32)VertexClasses[Idx1]
|
||||||
+ (int32)VertexClasses[Idx2];
|
+ (int32)VertexClasses[Idx2];
|
||||||
TArray<int32>& Dst = (CapVotes >= 2) ? CapTris : GroundTris;
|
Dst = (CapVotes >= 2) ? &CapTris : &GroundTris;
|
||||||
Dst.Add(Idx0);
|
}
|
||||||
Dst.Add(Idx2);
|
Dst->Add(Idx0);
|
||||||
Dst.Add(Idx1);
|
Dst->Add(Idx2);
|
||||||
|
Dst->Add(Idx1);
|
||||||
}
|
}
|
||||||
if (bSampleCell)
|
if (bSampleCell)
|
||||||
{
|
{
|
||||||
@@ -1314,8 +1314,11 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
const int32 Idx = MeshData.Vertices.Num();
|
const int32 Idx = MeshData.Vertices.Num();
|
||||||
MeshData.Vertices.Add(Pos);
|
MeshData.Vertices.Add(Pos);
|
||||||
MeshData.Normals.Add(Nrm);
|
MeshData.Normals.Add(Nrm);
|
||||||
|
if (!bStandaloneCompactPayload)
|
||||||
|
{
|
||||||
MeshData.UVs.Add(FVector2D(Pos.X / VOXEL_SIZE, Pos.Y / VOXEL_SIZE));
|
MeshData.UVs.Add(FVector2D(Pos.X / VOXEL_SIZE, Pos.Y / VOXEL_SIZE));
|
||||||
MeshData.Colors.Add(Col); // hérite la couleur du vertex source (parallèle aux autres tableaux)
|
MeshData.Colors.Add(Col); // parallel with the other vertex arrays
|
||||||
|
}
|
||||||
return Idx;
|
return Idx;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1339,8 +1342,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
|
|
||||||
const FVector NA = MeshData.Normals[iA];
|
const FVector NA = MeshData.Normals[iA];
|
||||||
const FVector NB = MeshData.Normals[iB];
|
const FVector NB = MeshData.Normals[iB];
|
||||||
const FColor CA = MeshData.Colors[iA];
|
const FColor CA = bStandaloneCompactPayload ? FColor::White : MeshData.Colors[iA];
|
||||||
const FColor CB = MeshData.Colors[iB];
|
const FColor CB = bStandaloneCompactPayload ? FColor::White : MeshData.Colors[iB];
|
||||||
const int32 iA2 = AddSkirtVert(PA - NA * SkirtDepth, NA, CA);
|
const int32 iA2 = AddSkirtVert(PA - NA * SkirtDepth, NA, CA);
|
||||||
const int32 iB2 = AddSkirtVert(PB - NB * SkirtDepth, NB, CB);
|
const int32 iB2 = AddSkirtVert(PB - NB * SkirtDepth, NB, CB);
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,11 @@ public:
|
|||||||
float GetDensityAt(float WorldX, float WorldY, float WorldZ,
|
float GetDensityAt(float WorldX, float WorldY, float WorldZ,
|
||||||
float* OutPreEditDensity = nullptr) const;
|
float* OutPreEditDensity = nullptr) const;
|
||||||
|
|
||||||
|
// Exact integer-grid dispatch used by the standalone tile mesher. It skips the
|
||||||
|
// float-to-lattice tolerance check while retaining the active field's same values.
|
||||||
|
float GetDensityAtLattice(int32 WorldX, int32 WorldY, int32 WorldZ,
|
||||||
|
float* OutPreEditDensity = nullptr) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a worker-local lattice hand-off for the following GetDensityAt calls. The mesher uses
|
* Open a worker-local lattice hand-off for the following GetDensityAt calls. The mesher uses
|
||||||
* this around one tile's density-grid fill. The first sample in each resolved chunk lets the
|
* this around one tile's density-grid fill. The first sample in each resolved chunk lets the
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ public:
|
|||||||
// SETTINGS
|
// SETTINGS
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|
||||||
|
// Standalone CPU streaming consumes positions, normals and indices only. When enabled,
|
||||||
|
// GenerateMesh omits UVs, colors and material-only ceiling grouping. Runtime callers leave
|
||||||
|
// this false and keep the complete FVoxelMeshData contract.
|
||||||
|
bool bStandaloneCompactPayload = false;
|
||||||
|
|
||||||
// Seuil de l'isosurface dans le champ de densité.
|
// Seuil de l'isosurface dans le champ de densité.
|
||||||
// Convention MC: densité < IsoLevel = solide, >= = air.
|
// Convention MC: densité < IsoLevel = solide, >= = air.
|
||||||
float IsoLevel = 0.0f;
|
float IsoLevel = 0.0f;
|
||||||
|
|||||||