From fcb16fd265945540457e4ada0eb5a17c51bc0b8e Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Fri, 9 Oct 2026 00:38:55 +0200 Subject: [PATCH] wip(assembler): M1 round 2 -- surface recipe closer to legacy; native still slower and visibly buggy Luna, M1 round 2 of 4: round 1's mismatch was an underfit recipe (two synthetic profiles, missing large-scale surface terms). Now: biome IDs match 4,096/4,096, mean height offset -1.366 vox, density MAE 274.04 -> 17.23 vox, sign mismatches 55.2% -> 18.14%. Bound audit 298 boxes / 8,046 points, 0 misses, negative control caught. Hash c5e0371612e927af at W1/W8/W16. Surface query 8-14 ms / 4,096 points. Matched 3D capture pairs added. Problems (my review of the captures + her timings): - matched 120-tile views: full-detail readiness 5.227 s native vs 2.882 s legacy -- SLOWER; - surface capture shows a rectangular flat-topped raised block with vertical walls (tile/patch-level height bug, inferred); - cliff capture is broken (floating shard, debris trail), not a cliff. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- Standalone/ASSEMBLER_M1_REPORT.md | 112 ++- Standalone/Core/API.md | 53 +- .../include/VoxelForgeCore/VoxelForgeCore.h | 48 +- Standalone/Core/src/WorldAssemblerInternal.h | 885 +++++++++++++++++- .../gpu_field_port/LUNA_DIALOGUE.md | 46 + .../round2/captures/border-native.jpg | Bin 0 -> 10654 bytes .../round2/captures/border-owner.jpg | Bin 0 -> 10915 bytes .../round2/captures/cliff-native.jpg | Bin 0 -> 9147 bytes .../round2/captures/cliff-owner.jpg | Bin 0 -> 17961 bytes .../round2/captures/coast-native.jpg | Bin 0 -> 9768 bytes .../round2/captures/coast-owner.jpg | Bin 0 -> 9979 bytes .../round2/captures/surface-native.jpg | Bin 0 -> 9774 bytes .../round2/captures/surface-owner.jpg | Bin 0 -> 9089 bytes .../round2/world_assembly.json | 1 + Standalone/Source/StandaloneViewer.cpp | 321 ++++++- 15 files changed, 1375 insertions(+), 91 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM1/round2/captures/border-native.jpg create mode 100644 Standalone/Research/WorldAssemblerM1/round2/captures/border-owner.jpg create mode 100644 Standalone/Research/WorldAssemblerM1/round2/captures/cliff-native.jpg create mode 100644 Standalone/Research/WorldAssemblerM1/round2/captures/cliff-owner.jpg create mode 100644 Standalone/Research/WorldAssemblerM1/round2/captures/coast-native.jpg create mode 100644 Standalone/Research/WorldAssemblerM1/round2/captures/coast-owner.jpg create mode 100644 Standalone/Research/WorldAssemblerM1/round2/captures/surface-native.jpg create mode 100644 Standalone/Research/WorldAssemblerM1/round2/captures/surface-owner.jpg create mode 100644 Standalone/Research/WorldAssemblerM1/round2/world_assembly.json diff --git a/Standalone/ASSEMBLER_M1_REPORT.md b/Standalone/ASSEMBLER_M1_REPORT.md index 0a360c5..4ea3f26 100644 --- a/Standalone/ASSEMBLER_M1_REPORT.md +++ b/Standalone/ASSEMBLER_M1_REPORT.md @@ -139,11 +139,117 @@ also not measured by audit mode. Earlier small-view D3D attempt timings (owner 0 view had no geometry. The 23.085 s matched-workload gate remains open. The ~1.57 s aim remains reasonable, but round 1 has no valid full-view evidence to support or reject it. +## Round 2 results + +### Round 1 mismatch diagnosis + +Round 1 was structurally underfit, not mis-scaled or shifted by a constant height datum. The sample +used two synthetic biome profiles (IDs 4097 and 4098) and a reduced height model. The owner export +uses four resolved surface profiles and relief/moisture climate ranges; the baseline also omitted +the mountain and relief response, height warp, output-space profile blend, coast/beach and cliff +shaping, and per-biome cap detail added in evaluator 2. Both sides use 25 cm voxels, and the current +4,096-column check finds only a -1.366-voxel mean height offset. The owner and Core biome IDs now match on all 4,096 +diagnostic columns. These measurements point to missing structure and profile assignment rather +than a unit conversion, datum offset, detail-noise phase, or biome-ID selection error. + +This round changed those terms as a group, so the before/after result does not isolate each term's +individual contribution. On the same 4,096 density-query points, mean absolute error fell from +274.0383 to 17.2262 voxels, maximum density error from 841.4257 to 392.4136 voxels, and sign +mismatches from 2,261/4,096 (55.2%) to 743/4,096 (18.14%). The separate 4,096-column height +diagnostic now reports MAE 13.060 voxels, maximum error 203.728 voxels, correlation 0.96496 and +linear-fit slope 0.95136. Residual peaks and sign errors remain visible; this is not parity. + +### Evaluator, manifest and audit + +Evaluator 2 keeps schema 1 and fixed signed 1/256-voxel units while adding deterministic +climate-based assignment across the owner's four profiles, output-space ground and cap blending, +per-profile relief/height warp/beach/cliff/cap settings, fixed-season water outputs, and stable-ID +bounded 3D overhang exceptions. Player carve/fill edits remain unrestricted; route constraints only +apply to automatic system generation or changes. This sample has one bounded exception +(`09812c26310b90c9`) and no route yet; route planning and capsule sweep remain round 3. + +The evaluator-v2 review manifest is 4,514 bytes. Recipe hash is `dbe949967c0dfbce`, plan hash is +`165a00c4a77ebae0`, and manifest hash is `203d7043158069f6`. The 4,096-sample evaluator hash is +`c5e0371612e927af` at W1, W8 and W16. Bounds audit passed with 298 boxes and 8,046 points: zero +missed and zero unknown bounds; 16 biome-border boxes, 16 cliff boxes, 9 cap boxes, 248 +negative-coordinate boxes, and one exception box with 27 exception samples. The deliberately +wrong one-sided bound was detected. + +The final interval audit followed one failed audit with 102 missed bounds. Inspection found the cap +range was overwritten by a global plan range and that plateau/beach and cliff envelopes were +incomplete. Those envelopes were corrected before the final run. The current result is a sampled +audit of this evaluator and recipe, not a mathematical proof for arbitrary future inputs. + +### Matched 3D captures + +Each pair uses the same camera for owner and native Core, with full-detail tiles ready before +capture. Images are 640×400 JPEGs; each file is 9,089–17,961 bytes. The native surface, border and +coast pairs broadly agree in silhouette. The cliff pair shows a remaining large difference: the +owner has an extreme noisy overhang, while the prototype currently emits the bounded, smoother +shelf. Water height/fill is emitted as a field sample, but a water overlay mesh is not implemented, +so the coast captures do not show rendered water. + +| Site and shared viewer camera (x, y, z; yaw, pitch) | Owner reference | Native Core | +|---|---|---| +| Surface `(0, -35, -487.943; 0, -22)` | ![Surface owner reference](Research/WorldAssemblerM1/round2/captures/surface-owner.jpg) | ![Surface native Core](Research/WorldAssemblerM1/round2/captures/surface-native.jpg) | +| Biome border `(128, -243, -492.912; 0, -22)` | ![Biome border owner reference](Research/WorldAssemblerM1/round2/captures/border-owner.jpg) | ![Biome border native Core](Research/WorldAssemblerM1/round2/captures/border-native.jpg) | +| Coast `(336, -67, -477.755; 0, -22)` | ![Coast owner reference](Research/WorldAssemblerM1/round2/captures/coast-owner.jpg) | ![Coast native Core](Research/WorldAssemblerM1/round2/captures/coast-native.jpg) | +| Cliff `(-168, 70, -445.712; 0, 34)` | ![Cliff owner reference](Research/WorldAssemblerM1/round2/captures/cliff-owner.jpg) | ![Cliff native Core](Research/WorldAssemblerM1/round2/captures/cliff-native.jpg) | + +`Present` continued to return `DXGI_STATUS_OCCLUDED`, including after bringing the viewer forward. +The first surface back-buffer file was black and was excluded. The foreground retry's copied +back-buffer was nonblack and visibly contained generated geometry; the final pairs above use that +verified capture path. This validates the native mesh/viewer path for these captures, but the +presentation status itself remains unresolved. + +### Query, cave compile and full-view timings + +| Measurement | Result | Scope | +|---|---:|---| +| Surface point queries W1 | 13.569 ms | 4,096 evaluator points, point evaluation only | +| Surface point queries W8 | 8.085 ms | same points, same field hash | +| Surface point queries W16 | 12.396 ms | same points, same field hash | +| Legacy owner density queries | 61.974 ms | same 4,096 points; reference timing, not a performance gate | +| Core world compile and query bridge | 36.295 ms; 45/45 exact | Surface recipe; `World::densityAt`, no render mesh | +| Existing cave-only shape compile | 0.100 s | no-manifest `type_3_Maze.json` smoke, from round 1 | +| Owner full-detail readiness | mean 2.882 s | 120 near tiles (64 LOD0 + 56 LOD1), 16 workers, four capture sites | +| Native Core full-detail readiness | mean 5.227 s | same tile count, workers and sites; range 4.401–6.636 s | + +| Capture site | Owner | Native Core | +|---|---:|---:| +| Surface | 2.873 s | 4.401 s | +| Biome border | 2.904 s | 4.907 s | +| Coast | 2.877 s | 4.962 s | +| Cliff | 2.874 s | 6.636 s | + +The cave compile and surface query figures are separate workloads. The surface viewer took longer: +the native 5.227 s mean is 81% above the owner's 2.882 s on these matched views. This 120-tile +clip-radius-1 workload is not the agreed 23.085 s matched-workload gate. All viewer runs used 16 +workers; maximum observed full-detail time was 6.636 s at the upward-facing cliff camera. + +### Round 2 run log + +- The first evaluator build failed because the JSON parser version has no `IsBool`/`IsNull` methods. + The parser was changed to inspect the JSON value variant, and the next isolated build passed. +- The first expanded interval audit failed with 102 escaped samples. The envelope fixes described + above were applied and the final audit passed. No failing result was discarded from the report. +- The initial native D3D surface capture was black with an occluded presentation status. A + foreground recapture still reported the same status but its back-buffer contained visible mesh + geometry; only the verified foreground copy was used. All four final owner/native pairs are + downscaled and below 18 KB per image. +- Successful final standalone builds completed in under one minute with `/MP16`, Normal priority + and 16 CPUs. Existing `vswhere.exe` lookup output and C4838 narrowing warnings at + `VoxelStrateManager.cpp:3974-3975` remain nonfatal. No process exceeded the 15-minute limit or + 8 GB cap; no `0xC0000142` retry occurred. The crash-folder check found only the pre-existing + `UECC-Windows-F1A3EFD5469F5DD62E68EBBACAC84376_0000` from 2026-09-17 and no new UECC folder. +- No owner project or BuildHost project was built. No `.uasset` was opened or changed. No new source + files were added. + ### Remaining work by round -- **Round 2:** Match the legacy reference more closely; add output-space biome blending, coast/beach, - cap detail, cliffs and bounded 3D overhang features with stable IDs and certified influence - bounds. Expand bound audits across borders, caps, negative coordinates and exceptions. +- **Round 2:** Evaluator-v2 surface recipe, matched 3D comparisons and expanded bounds audit are + implemented. The measured residual mismatch, rendered-water gap, and owner/native cliff difference + remain open for round 3 review; round 2 does not claim surface parity. - **Round 3:** Add the finite-strate capsule route and support constraints, sweep it against the final composed field, and reject or repair any failing automatic plan. Verify full-evaluator W1/W8/W16 hashes. Measure collision readiness separately from query and mesh readiness; demonstrate a diff --git a/Standalone/Core/API.md b/Standalone/Core/API.md index 963ac56..4f601a6 100644 --- a/Standalone/Core/API.md +++ b/Standalone/Core/API.md @@ -79,22 +79,33 @@ Pass `analyticOnly = false` to retain the reference packed-page compiler. Packed parent-cell coordinates and an 8-voxel apron; each covers a 128-voxel cube or one larger coarse tile. This path remains available for parity checks and workloads that need the packed lattice. -## World assembler manifest v1 (prototype) +## World assembler manifest v1 (prototype; evaluator v1 and v2) `ResolvedWorldSnapshot::FromJson` accepts an optional `world_assembly` object alongside the owner snapshot. The object is the shipped-plan payload shape: a fixed-scale recipe and finite-strate plan -with hashes, no generated mesh or voxel samples. The reviewed sample payload is -[`round1-manifest.json`](../Research/WorldAssemblerM1/round1-manifest.json). Round 4 still needs to -connect this payload to the season asset/hash path. +with hashes, no generated mesh or voxel samples. The current evaluator-v2 review payload is +[`round2/world_assembly.json`](../Research/WorldAssemblerM1/round2/world_assembly.json); the +evaluator-v1 sample remains at [`round1-manifest.json`](../Research/WorldAssemblerM1/round1-manifest.json). +Round 4 still needs to connect this payload to the season asset/hash path. Every coordinate and scalar uses signed integer units of 1/256 voxel (`units_per_voxel: 256`), -independent of the plugin's 25 cm Core voxel and of tile size. Schema and evaluator versions are -both 1. JSON integer fields are canonicalized in declared field order and hashed as 4- or 8-byte -little-endian words with FNV-1a 64. Hashes are emitted as 16 lowercase hexadecimal characters; -JSON key order and whitespace do not affect them. The root, recipe, plan and each stratum carry a -content hash. The recipe stores its seed, warped biome-map settings and one to eight biome profiles; -the plan stores finite-strate extents, cap settings, stable-ID features and route points. Bounds, -feature count and route count are validated before a manifest is accepted. +independent of the plugin's 25 cm Core voxel and of tile size. The schema version is 1; evaluator +versions 1 and 2 are accepted, with evaluator 2 used for new recipes. Evaluator 1 retains its +original evaluation and hash semantics. JSON integer fields are canonicalized in declared field +order and hashed as 4- or 8-byte little-endian words with FNV-1a 64. Hashes are emitted as 16 +lowercase hexadecimal characters; JSON key order and whitespace do not affect them. The root, +recipe, plan and each stratum carry a content hash. The recipe stores its seed, warped biome-map +settings and one to eight biome profiles; the plan stores finite-strate extents, cap settings, +stable-ID features, fixed-season water and route points. Bounds, feature count and route count are +validated before a manifest is accepted. + +Evaluator 2 adds climate-based biome assignment, output-space blending of both ground and cap +profiles, per-biome relief and height warp, beach width, cliff limits, and optional per-biome cap +profiles. Feature types 1, 2 and 3 represent a cliff plane, a bounded 3D overhang box and an authored +cap feature. An overhang feature has a stable ID and a finite AABB clipped to its stratum; intervals +outside the box can use its certified influence bound, while intersecting intervals remain on the +evaluation path. Water surface and fill are separate sample outputs. The season water plane is fixed; +this block does not generate a water mesh. ```cpp std::string error; @@ -105,18 +116,20 @@ const auto* loaded = snapshot.worldAssembly(); // null when the owner snapshot h `Heightfield2D` is the first field block. `point(xQ, yQ, zQ)` evaluates the biome map and blended height profiles at fixed-scale coordinates, returning the ground and cap heights, primary and -neighbor biome IDs, Q16 blend weight, and signed density. Negative density is solid. Its block -contract reads `biome` and writes `height` plus `rock`; `water`, `waterSurface`, `route` and -`surfaceTags` are declared channels for later blocks. The detail envelope reports maximum absolute -vertical displacement and minimum feature size. Influence is global in XY and limited to the -finite stratum's Z extent. +neighbor biome IDs, Q16 blend weight and signed density. Evaluator 2 also returns the fixed water +surface and water-fill sample values. Negative density is solid. Its contract reads `biome` and +writes `height`, `rock`, `water` and `waterSurface`; route and `surfaceTags` remain declared channels +for later blocks. The detail envelope reports maximum absolute displacement and minimum feature +size. Influence is global in XY and limited to the finite stratum's Z extent, except for the +explicitly bounded evaluator-v2 feature boxes. `interval(minXQ, minYQ, minZQ, maxXQ, maxYQ, maxZQ)` returns a conservative density range or `known == false`; callers must keep evaluating when it is unknown. The current heightfield range -uses the maximum recipe-profile amplitude and cap undulation over the whole biome set. The public -`AuditHeightfieldBounds` samples points inside randomized boxes and includes a deliberately false -one-sided range as a negative control. A pass means no sampled point escaped and the negative -control was rejected; it is an audit of this block implementation, not a proof for future blocks. +uses conservative profile, beach, cliff, cap and bounded-feature envelopes. The public +`AuditHeightfieldBounds` samples points in randomized boxes and targeted biome-border, cliff, cap, +negative-coordinate and overhang boxes, and includes a deliberately false one-sided range as a +negative control. A pass means no sampled point escaped and the negative control was rejected; it +is an audit of this block implementation, not a proof for future blocks. ## Queries and tiles diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 92f90b9..a08d239 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -103,7 +103,7 @@ namespace VoxelForgeCore // L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles. inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256; inline constexpr std::uint32_t WorldAssemblySchemaVersion = 1; - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 1; + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 2; enum class FieldChannel : std::uint32_t { @@ -124,6 +124,29 @@ namespace VoxelForgeCore std::int64_t mountainWavelengthSubvoxels = 256 * 128; std::int64_t detailAmplitudeSubvoxels = 0; std::int64_t detailWavelengthSubvoxels = 256 * 16; + // Evaluator 2: climate box and the resolved surface-operation parameters. + // Evaluateur 2 : plage climatique et parametres resolus des operations de surface. + std::int64_t reliefMinimumQ16 = 0, reliefMaximumQ16 = 1ll << 16; + std::int64_t moistureMinimumQ16 = 0, moistureMaximumQ16 = 1ll << 16; + std::int64_t reliefStrengthQ16 = 0, reliefContrastQ16 = 1ll << 16; + std::int64_t reliefWavelengthSubvoxels = 256 * 1024; + std::int64_t heightWarpStrengthSubvoxels = 0; + std::int64_t heightWarpWavelengthSubvoxels = 256 * 128; + std::int64_t beachWidthSubvoxels = 0; + std::int64_t cliffStrengthQ16 = 0, cliffSlopeThresholdQ16 = 1ll << 15; + std::int64_t cliffSharpnessQ16 = 2ll << 16; + std::int64_t cliffSampleDistanceSubvoxels = 256 * 2; + std::int64_t cliffMaximumDisplacementSubvoxels = 0; + bool hasCapProfile = false; + std::int64_t capHeightSubvoxels = 0; + std::int64_t capWarpStrengthSubvoxels = 0; + std::int64_t capWarpWavelengthSubvoxels = 256 * 128; + std::int64_t capUndulationSubvoxels = 0; + std::int64_t capWavelengthSubvoxels = 256 * 512; + std::int64_t capDetailAmplitudeSubvoxels = 0; + std::int64_t capDetailWavelengthSubvoxels = 256 * 16; + std::int64_t capRidgeAmplitudeSubvoxels = 0; + std::int64_t capRidgeWavelengthSubvoxels = 256 * 64; }; struct WorldAssemblyRecipeV1 @@ -136,6 +159,11 @@ namespace VoxelForgeCore std::int64_t biomeWarpSubvoxels = 256 * 250; std::int64_t biomeWarpWavelengthSubvoxels = 256 * 556; std::int64_t biomeBorderBlendSubvoxels = 256 * 120; + // Climate fields used to assign deterministic biome sites. + // Champs climatiques utilises pour affecter les sites de biome de facon deterministe. + std::int64_t biomeClimateReliefWavelengthSubvoxels = 256 * 3333; + std::int64_t biomeClimateMoistureWavelengthSubvoxels = 256 * 2500; + std::int64_t biomeClimateReliefContrastQ16 = 2ll << 16; std::vector biomes; std::uint64_t contentHash = 0; }; @@ -167,6 +195,14 @@ namespace VoxelForgeCore std::int64_t capHeightSubvoxels = 0; std::int64_t capUndulationSubvoxels = 0; std::int64_t capWavelengthSubvoxels = 256 * 512; + // Optional evaluator-2 cap layers; all zero preserves the evaluator-1 payload hash. + // Couches de plafond optionnelles; zero conserve le hash historique des payloads v1. + std::int64_t capDetailAmplitudeSubvoxels = 0; + std::int64_t capDetailWavelengthSubvoxels = 256 * 16; + std::int64_t capRidgeAmplitudeSubvoxels = 0; + std::int64_t capRidgeWavelengthSubvoxels = 256 * 64; + std::int64_t waterSurfaceSubvoxels = 0; + bool waterEnabled = false; std::vector features; std::vector route; std::uint64_t contentHash = 0; @@ -204,6 +240,8 @@ namespace VoxelForgeCore std::uint32_t neighborBiomeId = 0; std::uint32_t biomeWeightQ16 = 0; std::int64_t densitySubvoxels = 0; + std::int64_t waterSurfaceSubvoxels = 0; + std::int64_t waterFillSubvoxels = 0; }; struct FieldInterval @@ -254,6 +292,8 @@ namespace VoxelForgeCore FieldInfluenceBounds influenceBounds() const noexcept; FieldDetailEnvelope detailEnvelope() const noexcept; FieldBlockContract contract() const noexcept; + const WorldAssemblyRecipeV1* recipe() const noexcept { return recipe_; } + const WorldAssemblyStratumPlanV1* plan() const noexcept { return plan_; } private: const WorldAssemblyRecipeV1* recipe_ = nullptr; @@ -266,6 +306,12 @@ namespace VoxelForgeCore std::uint64_t pointSamples = 0; std::uint64_t missedBounds = 0; std::uint64_t unknownBounds = 0; + std::uint64_t biomeBorderBoxes = 0; + std::uint64_t cliffBoxes = 0; + std::uint64_t capBoxes = 0; + std::uint64_t negativeCoordinateBoxes = 0; + std::uint64_t exceptionBoxes = 0; + std::uint64_t exceptionPointSamples = 0; bool negativeControlDetected = false; bool passed = false; }; diff --git a/Standalone/Core/src/WorldAssemblerInternal.h b/Standalone/Core/src/WorldAssemblerInternal.h index e642782..7da6a76 100644 --- a/Standalone/Core/src/WorldAssemblerInternal.h +++ b/Standalone/Core/src/WorldAssemblerInternal.h @@ -99,7 +99,7 @@ namespace world_assembly_detail inline bool ValidateRecipe(const WorldAssemblyRecipeV1& recipe, std::string& error) { if (recipe.schemaVersion != WorldAssemblySchemaVersion - || recipe.evaluatorVersion != WorldAssemblyEvaluatorVersion + || (recipe.evaluatorVersion != 1 && recipe.evaluatorVersion != WorldAssemblyEvaluatorVersion) || recipe.unitsPerVoxel != WorldAssemblySubvoxelsPerVoxel) { error = "world assembly recipe version, evaluator or fixed scale is unsupported"; return false; } if (recipe.biomes.empty() || recipe.biomes.size() > MaxBiomeCount) @@ -118,16 +118,48 @@ namespace world_assembly_detail if (biome.stableId == 0 || std::find(ids.begin(), ids.end(), biome.stableId) != ids.end()) { error = "world assembly biome IDs must be nonzero and unique"; return false; } ids.push_back(biome.stableId); - const std::array values = {biome.baseHeightSubvoxels, + const std::array values = {biome.baseHeightSubvoxels, biome.continentAmplitudeSubvoxels, biome.continentWavelengthSubvoxels, biome.mountainAmplitudeSubvoxels, biome.mountainWavelengthSubvoxels, - biome.detailAmplitudeSubvoxels, biome.detailWavelengthSubvoxels}; + biome.detailAmplitudeSubvoxels, biome.detailWavelengthSubvoxels, + biome.reliefMinimumQ16, biome.reliefMaximumQ16, biome.moistureMinimumQ16, + biome.moistureMaximumQ16, biome.reliefStrengthQ16, biome.reliefContrastQ16, + biome.reliefWavelengthSubvoxels, biome.heightWarpStrengthSubvoxels, + biome.heightWarpWavelengthSubvoxels, biome.beachWidthSubvoxels, + biome.cliffStrengthQ16, biome.cliffSlopeThresholdQ16, biome.cliffSharpnessQ16, + biome.cliffSampleDistanceSubvoxels, biome.cliffMaximumDisplacementSubvoxels, + biome.capHeightSubvoxels, biome.capWarpStrengthSubvoxels, + biome.capWarpWavelengthSubvoxels, biome.capUndulationSubvoxels, + biome.capWavelengthSubvoxels, biome.capDetailAmplitudeSubvoxels, + biome.capDetailWavelengthSubvoxels, biome.capRidgeAmplitudeSubvoxels, + biome.capRidgeWavelengthSubvoxels}; if (!std::all_of(values.begin(), values.end(), ValidScalar) || biome.continentAmplitudeSubvoxels < 0 || biome.mountainAmplitudeSubvoxels < 0 || biome.detailAmplitudeSubvoxels < 0 || biome.continentWavelengthSubvoxels <= 0 - || biome.mountainWavelengthSubvoxels <= 0 || biome.detailWavelengthSubvoxels <= 0) + || biome.mountainWavelengthSubvoxels <= 0 || biome.detailWavelengthSubvoxels <= 0 + || biome.reliefMinimumQ16 < 0 || biome.reliefMaximumQ16 > Q16 + || biome.reliefMinimumQ16 > biome.reliefMaximumQ16 + || biome.moistureMinimumQ16 < 0 || biome.moistureMaximumQ16 > Q16 + || biome.moistureMinimumQ16 > biome.moistureMaximumQ16 + || biome.reliefStrengthQ16 < 0 || biome.reliefStrengthQ16 > Q16 + || biome.reliefContrastQ16 < 0 || biome.reliefWavelengthSubvoxels <= 0 + || biome.heightWarpStrengthSubvoxels < 0 || biome.heightWarpWavelengthSubvoxels <= 0 + || biome.beachWidthSubvoxels < 0 || biome.cliffStrengthQ16 < 0 + || biome.cliffSlopeThresholdQ16 <= 0 || biome.cliffSharpnessQ16 < 0 + || biome.cliffSampleDistanceSubvoxels <= 0 + || biome.cliffMaximumDisplacementSubvoxels < 0) { error = "world assembly biome profile has an invalid fixed-scale value"; return false; } + if (biome.capWarpStrengthSubvoxels < 0 || biome.capWarpWavelengthSubvoxels <= 0 + || biome.capUndulationSubvoxels < 0 || biome.capWavelengthSubvoxels <= 0 + || biome.capDetailAmplitudeSubvoxels < 0 || biome.capDetailWavelengthSubvoxels <= 0 + || biome.capRidgeAmplitudeSubvoxels < 0 || biome.capRidgeWavelengthSubvoxels <= 0) + { error = "world assembly biome cap profile has an invalid fixed-scale value"; return false; } } + if (recipe.evaluatorVersion >= 2 + && (recipe.biomeClimateReliefWavelengthSubvoxels <= 0 + || recipe.biomeClimateMoistureWavelengthSubvoxels <= 0 + || recipe.biomeClimateReliefContrastQ16 < 0)) + { error = "world assembly climate parameters are outside the supported fixed-scale range"; return false; } return true; } @@ -138,7 +170,14 @@ namespace world_assembly_detail || stratum.features.size() > MaxFeatureCount || stratum.route.size() > MaxRoutePointCount) { error = "world assembly stratum plan has an invalid extent or exceeds its bounded payload"; return false; } if (!ValidScalar(stratum.capHeightSubvoxels) || !ValidScalar(stratum.capUndulationSubvoxels) - || !ValidScalar(stratum.capWavelengthSubvoxels)) + || !ValidScalar(stratum.capWavelengthSubvoxels) + || !ValidScalar(stratum.capDetailAmplitudeSubvoxels) + || !ValidScalar(stratum.capDetailWavelengthSubvoxels) + || !ValidScalar(stratum.capRidgeAmplitudeSubvoxels) + || !ValidScalar(stratum.capRidgeWavelengthSubvoxels) + || !ValidScalar(stratum.waterSurfaceSubvoxels) + || stratum.capDetailAmplitudeSubvoxels < 0 || stratum.capDetailWavelengthSubvoxels <= 0 + || stratum.capRidgeAmplitudeSubvoxels < 0 || stratum.capRidgeWavelengthSubvoxels <= 0) { error = "world assembly cap contains a value outside the fixed-scale range"; return false; } std::vector ids; ids.reserve(stratum.features.size()); @@ -155,6 +194,23 @@ namespace world_assembly_detail { error = "world assembly feature has an invalid influence bound"; return false; } if (!std::all_of(feature.parametersSubvoxels.begin(), feature.parametersSubvoxels.end(), ValidScalar)) { error = "world assembly feature parameter is outside the fixed-scale range"; return false; } + if (feature.type == 2) + { + const std::int64_t stratumMinZ = static_cast(stratum.bottomChunkZ) + * 32 * WorldAssemblySubvoxelsPerVoxel; + const std::int64_t stratumMaxZ = (static_cast(stratum.topChunkZ) + 1) + * 32 * WorldAssemblySubvoxelsPerVoxel; + constexpr std::int64_t maximumExceptionExtent = 1024 + * WorldAssemblySubvoxelsPerVoxel; + for (std::size_t axis = 0; axis < 3; ++axis) + if (feature.maximumSubvoxels[axis] <= feature.minimumSubvoxels[axis] + || feature.maximumSubvoxels[axis] - feature.minimumSubvoxels[axis] + > maximumExceptionExtent) + { error = "world assembly overhang exception is not a bounded positive-volume box"; return false; } + if (feature.minimumSubvoxels[2] < stratumMinZ + || feature.maximumSubvoxels[2] > stratumMaxZ) + { error = "world assembly overhang exception escapes its finite stratum Z bounds"; return false; } + } } for (const auto& point : stratum.route) { @@ -175,6 +231,13 @@ namespace world_assembly_detail HashWord(hash, static_cast(recipe.biomeWarpSubvoxels), 8); HashWord(hash, static_cast(recipe.biomeWarpWavelengthSubvoxels), 8); HashWord(hash, static_cast(recipe.biomeBorderBlendSubvoxels), 8); + if (recipe.evaluatorVersion >= 2) + { + HashWord(hash, 0x535552465632ull, 8); + HashWord(hash, static_cast(recipe.biomeClimateReliefWavelengthSubvoxels), 8); + HashWord(hash, static_cast(recipe.biomeClimateMoistureWavelengthSubvoxels), 8); + HashWord(hash, static_cast(recipe.biomeClimateReliefContrastQ16), 8); + } HashWord(hash, recipe.biomes.size(), 4); for (const auto& biome : recipe.biomes) { @@ -186,6 +249,24 @@ namespace world_assembly_detail HashWord(hash, static_cast(biome.mountainWavelengthSubvoxels), 8); HashWord(hash, static_cast(biome.detailAmplitudeSubvoxels), 8); HashWord(hash, static_cast(biome.detailWavelengthSubvoxels), 8); + if (recipe.evaluatorVersion >= 2) + { + for (const auto value : {biome.reliefMinimumQ16, biome.reliefMaximumQ16, + biome.moistureMinimumQ16, biome.moistureMaximumQ16, biome.reliefStrengthQ16, + biome.reliefContrastQ16, biome.reliefWavelengthSubvoxels, + biome.heightWarpStrengthSubvoxels, biome.heightWarpWavelengthSubvoxels, + biome.beachWidthSubvoxels, biome.cliffStrengthQ16, + biome.cliffSlopeThresholdQ16, biome.cliffSharpnessQ16, + biome.cliffSampleDistanceSubvoxels, biome.cliffMaximumDisplacementSubvoxels}) + HashWord(hash, static_cast(value), 8); + HashWord(hash, biome.hasCapProfile ? 1u : 0u, 1); + for (const auto value : {biome.capHeightSubvoxels, + biome.capWarpStrengthSubvoxels, biome.capWarpWavelengthSubvoxels, + biome.capUndulationSubvoxels, biome.capWavelengthSubvoxels, + biome.capDetailAmplitudeSubvoxels, biome.capDetailWavelengthSubvoxels, + biome.capRidgeAmplitudeSubvoxels, biome.capRidgeWavelengthSubvoxels}) + HashWord(hash, static_cast(value), 8); + } } return hash; } @@ -199,6 +280,19 @@ namespace world_assembly_detail HashWord(hash, static_cast(stratum.capHeightSubvoxels), 8); HashWord(hash, static_cast(stratum.capUndulationSubvoxels), 8); HashWord(hash, static_cast(stratum.capWavelengthSubvoxels), 8); + const bool hasExtendedCap = stratum.capDetailAmplitudeSubvoxels != 0 + || stratum.capRidgeAmplitudeSubvoxels != 0 || stratum.waterEnabled + || stratum.waterSurfaceSubvoxels != 0; + if (hasExtendedCap) + { + HashWord(hash, 0x4341505632ull, 8); + HashWord(hash, static_cast(stratum.capDetailAmplitudeSubvoxels), 8); + HashWord(hash, static_cast(stratum.capDetailWavelengthSubvoxels), 8); + HashWord(hash, static_cast(stratum.capRidgeAmplitudeSubvoxels), 8); + HashWord(hash, static_cast(stratum.capRidgeWavelengthSubvoxels), 8); + HashWord(hash, static_cast(stratum.waterSurfaceSubvoxels), 8); + HashWord(hash, stratum.waterEnabled ? 1u : 0u, 1); + } HashWord(hash, stratum.features.size(), 4); for (const auto& feature : stratum.features) { @@ -248,6 +342,11 @@ namespace world_assembly_detail || !ReadHash(value.Get("content_hash"), recipe.contentHash) || !value.Get("biomes").IsArray()) { error = "world assembly recipe is missing a required versioned fixed-scale field"; return false; } + if (recipe.evaluatorVersion >= 2 + && (!ReadInteger(value.Get("climate_relief_wavelength_q"), recipe.biomeClimateReliefWavelengthSubvoxels) + || !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels) + || !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16))) + { error = "world assembly recipe is missing evaluator-2 climate parameters"; return false; } for (const auto& item : value.Get("biomes").AsArray()) { WorldAssemblyBiomeProfileV1 biome; @@ -260,6 +359,35 @@ namespace world_assembly_detail || !ReadInteger(item.Get("detail_amplitude_q"), biome.detailAmplitudeSubvoxels) || !ReadInteger(item.Get("detail_wavelength_q"), biome.detailWavelengthSubvoxels)) { error = "world assembly biome profile is missing a required fixed-scale field"; return false; } + if (recipe.evaluatorVersion >= 2 + && (!ReadInteger(item.Get("relief_min_q16"), biome.reliefMinimumQ16) + || !ReadInteger(item.Get("relief_max_q16"), biome.reliefMaximumQ16) + || !ReadInteger(item.Get("moisture_min_q16"), biome.moistureMinimumQ16) + || !ReadInteger(item.Get("moisture_max_q16"), biome.moistureMaximumQ16) + || !ReadInteger(item.Get("relief_strength_q16"), biome.reliefStrengthQ16) + || !ReadInteger(item.Get("relief_contrast_q16"), biome.reliefContrastQ16) + || !ReadInteger(item.Get("relief_wavelength_q"), biome.reliefWavelengthSubvoxels) + || !ReadInteger(item.Get("height_warp_strength_q"), biome.heightWarpStrengthSubvoxels) + || !ReadInteger(item.Get("height_warp_wavelength_q"), biome.heightWarpWavelengthSubvoxels) + || !ReadInteger(item.Get("beach_width_q"), biome.beachWidthSubvoxels) + || !ReadInteger(item.Get("cliff_strength_q16"), biome.cliffStrengthQ16) + || !ReadInteger(item.Get("cliff_slope_threshold_q16"), biome.cliffSlopeThresholdQ16) + || !ReadInteger(item.Get("cliff_sharpness_q16"), biome.cliffSharpnessQ16) + || !ReadInteger(item.Get("cliff_sample_distance_q"), biome.cliffSampleDistanceSubvoxels) + || !ReadInteger(item.Get("cliff_max_displacement_q"), biome.cliffMaximumDisplacementSubvoxels) + || !std::holds_alternative(item.Get("cap_enabled").Data) + || !ReadInteger(item.Get("cap_height_q"), biome.capHeightSubvoxels) + || !ReadInteger(item.Get("cap_warp_strength_q"), biome.capWarpStrengthSubvoxels) + || !ReadInteger(item.Get("cap_warp_wavelength_q"), biome.capWarpWavelengthSubvoxels) + || !ReadInteger(item.Get("cap_undulation_q"), biome.capUndulationSubvoxels) + || !ReadInteger(item.Get("cap_wavelength_q"), biome.capWavelengthSubvoxels) + || !ReadInteger(item.Get("cap_detail_amplitude_q"), biome.capDetailAmplitudeSubvoxels) + || !ReadInteger(item.Get("cap_detail_wavelength_q"), biome.capDetailWavelengthSubvoxels) + || !ReadInteger(item.Get("cap_ridge_amplitude_q"), biome.capRidgeAmplitudeSubvoxels) + || !ReadInteger(item.Get("cap_ridge_wavelength_q"), biome.capRidgeWavelengthSubvoxels))) + { error = "world assembly biome profile is missing evaluator-2 surface parameters"; return false; } + if (recipe.evaluatorVersion >= 2) + biome.hasCapProfile = std::get(item.Get("cap_enabled").Data); recipe.biomes.push_back(biome); } return true; @@ -284,6 +412,25 @@ namespace world_assembly_detail || !ReadHash(item.Get("content_hash"), stratum.contentHash) || !item.Get("features").IsArray() || !item.Get("route").IsArray()) { error = "world assembly stratum is missing a required fixed-scale field"; return false; } + const auto readOptional = [&item](const char* name, std::int64_t& output) + { + const VFJson::Value& field = item.Get(name); + if (std::holds_alternative(field.Data)) return true; + return field.IsNumber() && ReadInteger(field, output); + }; + const VFJson::Value& waterEnabled = item.Get("water_enabled"); + const bool hasWaterBoolean=std::holds_alternative(waterEnabled.Data); + const bool hasWaterNumber=waterEnabled.IsNumber(); + const bool hasWaterNull=std::holds_alternative(waterEnabled.Data); + if (!readOptional("cap_detail_amplitude_q", stratum.capDetailAmplitudeSubvoxels) + || !readOptional("cap_detail_wavelength_q", stratum.capDetailWavelengthSubvoxels) + || !readOptional("cap_ridge_amplitude_q", stratum.capRidgeAmplitudeSubvoxels) + || !readOptional("cap_ridge_wavelength_q", stratum.capRidgeWavelengthSubvoxels) + || !readOptional("water_surface_q", stratum.waterSurfaceSubvoxels) + || (!hasWaterBoolean&&!hasWaterNumber&&!hasWaterNull)) + { error = "world assembly stratum has malformed optional evaluator-2 fields"; return false; } + if(hasWaterBoolean)stratum.waterEnabled=waterEnabled.Boolean(false); + if(hasWaterNumber)stratum.waterEnabled=waterEnabled.Number()!=0.0; for (const auto& featureValue : item.Get("features").AsArray()) { WorldAssemblyFeatureV1 feature; @@ -412,6 +559,401 @@ namespace world_assembly_detail return biome.baseHeightSubvoxels + continent + mountain + detail; } + inline std::uint32_t Mix32(std::uint32_t value) + { + value ^= value >> 16; value *= 0x45d9f3bu; + value ^= value >> 16; value *= 0x45d9f3bu; + value ^= value >> 16; + return value; + } + + inline std::int64_t SeedOffsetQ16(std::uint32_t seed, std::uint32_t siteHundredths) + { + const std::uint32_t site = siteHundredths * 2654435761u; + return static_cast(Mix32(seed ^ site) & 0x3fffu) * Q16; + } + + inline std::uint32_t HashCorner(std::int64_t x, std::int64_t y, std::int64_t z) + { + std::uint32_t hash = static_cast(x) * 0x9E3779B1u + ^ static_cast(y) * 0x85EBCA77u + ^ static_cast(z) * 0xC2B2AE3Du; + hash ^= hash >> 15; hash *= 0x2C1B3C6Du; + hash ^= hash >> 12; hash *= 0x297A2D39u; + hash ^= hash >> 15; + return hash; + } + + inline std::int32_t FadeQ16(std::int32_t value) + { + const std::int64_t t2 = (static_cast(value) * value) / Q16; + const std::int64_t t3 = (t2 * value) / Q16; + const std::int64_t t4 = (t3 * value) / Q16; + const std::int64_t t5 = (t4 * value) / Q16; + return static_cast(6 * t5 - 15 * t4 + 10 * t3); + } + + inline std::int32_t GradientDotQ16(std::uint32_t hash, std::int32_t x, + std::int32_t y, std::int32_t z) + { + const std::uint32_t h = hash & 15u; + const std::int32_t u = (h & 8u) == 0u ? x : y; + const std::int32_t v = (h & 12u) == 0u ? y : ((h & 13u) == 12u ? x : z); + return ((h & 1u) == 0u ? u : -u) + ((h & 2u) == 0u ? v : -v); + } + + inline std::int32_t Perlin3DQ16(std::int64_t xQ16, std::int64_t yQ16, + std::int64_t zQ16) + { + const std::int64_t cellX = FloorDiv(xQ16, Q16); + const std::int64_t cellY = FloorDiv(yQ16, Q16); + const std::int64_t cellZ = FloorDiv(zQ16, Q16); + const std::int32_t fx = static_cast(FloorMod(xQ16, Q16)); + const std::int32_t fy = static_cast(FloorMod(yQ16, Q16)); + const std::int32_t fz = static_cast(FloorMod(zQ16, Q16)); + const std::int32_t sx = FadeQ16(fx), sy = FadeQ16(fy), sz = FadeQ16(fz); + const std::int32_t fx1 = fx - static_cast(Q16); + const std::int32_t fy1 = fy - static_cast(Q16); + const std::int32_t fz1 = fz - static_cast(Q16); + const auto n = [&](std::int64_t dx, std::int64_t dy, std::int64_t dz, + std::int32_t px, std::int32_t py, std::int32_t pz) + { return GradientDotQ16(HashCorner(cellX + dx, cellY + dy, cellZ + dz), px, py, pz); }; + const std::int32_t n000=n(0,0,0,fx,fy,fz), n100=n(1,0,0,fx1,fy,fz); + const std::int32_t n010=n(0,1,0,fx,fy1,fz), n110=n(1,1,0,fx1,fy1,fz); + const std::int32_t n001=n(0,0,1,fx,fy,fz1), n101=n(1,0,1,fx1,fy,fz1); + const std::int32_t n011=n(0,1,1,fx,fy1,fz1), n111=n(1,1,1,fx1,fy1,fz1); + const std::int32_t x00=LerpQ16(n000,n100,static_cast(sx)); + const std::int32_t x10=LerpQ16(n010,n110,static_cast(sx)); + const std::int32_t x01=LerpQ16(n001,n101,static_cast(sx)); + const std::int32_t x11=LerpQ16(n011,n111,static_cast(sx)); + const std::int32_t y0=LerpQ16(x00,x10,static_cast(sy)); + const std::int32_t y1=LerpQ16(x01,x11,static_cast(sy)); + return LerpQ16(y0,y1,static_cast(sz)); + } + + inline std::int64_t NoiseCoordinateQ16(std::int64_t positionSubvoxels, + std::int64_t wavelengthSubvoxels) + { return FloorDiv(positionSubvoxels * Q16, wavelengthSubvoxels); } + + inline std::int32_t FractalNoiseQ16(std::int64_t xQ16, std::int64_t yQ16, + std::int64_t zQ16, std::uint32_t octaves, bool ridged = false) + { + std::int64_t total = 0, weightSum = 0, weight = Q16, ridgeWeight = Q16; + for (std::uint32_t octave = 0; octave < octaves; ++octave) + { + const std::int64_t scale = 1ll << octave; + const std::int64_t noise = Perlin3DQ16(xQ16 * scale,yQ16 * scale,zQ16 * scale); + std::int64_t value = noise; + if (ridged) + { + const std::int64_t scaled = noise * 5 / 4; + const std::int64_t folded = Q16 - std::abs(scaled); + value = MulQ16(MulQ16(folded,folded),ridgeWeight); + ridgeWeight = std::clamp(value * 2,0,Q16); + } + total += MulQ16(value,weight); + weightSum += weight; + weight /= 2; + } + if (weightSum == 0) return 0; + const std::int64_t average = (total * Q16) / weightSum; + return static_cast(ridged ? average * 2 - Q16 : average); + } + + inline std::int32_t ReliefQ16(std::uint32_t seed, std::int64_t x, + std::int64_t y, std::int64_t wavelength, std::int64_t contrastQ16) + { + const std::int32_t noise = FractalNoiseQ16( + NoiseCoordinateQ16(x,wavelength)+SeedOffsetQ16(seed,730), + NoiseCoordinateQ16(y,wavelength)+SeedOffsetQ16(seed,210), + SeedOffsetQ16(seed,50),2); + const std::int64_t raw = Q16/2 + noise/2; + const std::int64_t contrasted = Q16/2 + MulQ16(raw-Q16/2,contrastQ16); + return static_cast(SmoothQ16(static_cast( + std::clamp(contrasted,0,Q16)))); + } + + inline std::int32_t MoistureQ16(std::uint32_t seed, std::int64_t x, + std::int64_t y, std::int64_t wavelength) + { + const std::int32_t noise = FractalNoiseQ16( + NoiseCoordinateQ16(x,wavelength)+SeedOffsetQ16(seed,470), + NoiseCoordinateQ16(y,wavelength)+SeedOffsetQ16(seed,890), + SeedOffsetQ16(seed,130),2); + return static_cast(std::clamp(Q16/2+noise/2,0,Q16)); + } + + inline std::int64_t WarpDisplacement(std::int32_t noiseQ16, + std::int64_t amplitudeSubvoxels) + { return (static_cast(noiseQ16)*amplitudeSubvoxels*5)/(4*Q16); } + + inline std::int64_t StructuralHeightV2(const WorldAssemblyRecipeV1& recipe, + const WorldAssemblyBiomeProfileV1& biome, std::int64_t x, std::int64_t y, + std::int32_t* outRelief = nullptr) + { + const std::uint32_t seed=recipe.seed; + std::int64_t qx=x,qy=y; + if(biome.heightWarpStrengthSubvoxels>0) + { + const std::int64_t xw=NoiseCoordinateQ16(x,biome.heightWarpWavelengthSubvoxels); + const std::int64_t yw=NoiseCoordinateQ16(y,biome.heightWarpWavelengthSubvoxels); + const std::int32_t wx=Perlin3DQ16(xw+SeedOffsetQ16(seed,31),yw+4*Q16+13*Q16/64,SeedOffsetQ16(seed,170)); + const std::int32_t wy=Perlin3DQ16(xw+8*Q16+3*Q16/5,yw+SeedOffsetQ16(seed,53),SeedOffsetQ16(seed,290)); + qx+=WarpDisplacement(wx,biome.heightWarpStrengthSubvoxels); + qy+=WarpDisplacement(wy,biome.heightWarpStrengthSubvoxels); + } + const std::int32_t relief=ReliefQ16(seed,x,y,biome.reliefWavelengthSubvoxels, + biome.reliefContrastQ16); + const std::int32_t mountainous=static_cast(Q16 + +MulQ16(relief-Q16,biome.reliefStrengthQ16)); + if(outRelief)*outRelief=mountainous; + const std::int64_t contX=NoiseCoordinateQ16(qx,biome.continentWavelengthSubvoxels)+SeedOffsetQ16(seed,310); + const std::int64_t contY=NoiseCoordinateQ16(qy,biome.continentWavelengthSubvoxels)+SeedOffsetQ16(seed,570); + const std::int64_t contZ=SeedOffsetQ16(seed,70); + const std::int32_t continent=FractalNoiseQ16(contX,contY,contZ,4); + const std::int64_t continentScale=Q16*45/100+MulQ16(mountainous,Q16*55/100); + std::int64_t height=biome.baseHeightSubvoxels + +MulQ16(MulQ16(continent,biome.continentAmplitudeSubvoxels),continentScale)/2; + if(biome.mountainAmplitudeSubvoxels>0) + { + const std::int64_t mountainX=NoiseCoordinateQ16(qx,biome.mountainWavelengthSubvoxels)+SeedOffsetQ16(seed,990); + const std::int64_t mountainY=NoiseCoordinateQ16(qy,biome.mountainWavelengthSubvoxels)+SeedOffsetQ16(seed,770); + const std::int64_t mountainZ=SeedOffsetQ16(seed,90); + const std::int32_t ridge=FractalNoiseQ16(mountainX,mountainY,mountainZ,4,true); + const std::int64_t ridgeHeight=std::clamp((ridge+Q16)/2,0,Q16); + height+=MulQ16(MulQ16(ridgeHeight,biome.mountainAmplitudeSubvoxels),mountainous); + } + const std::int64_t detailX=NoiseCoordinateQ16(x,biome.detailWavelengthSubvoxels)+11*Q16; + const std::int64_t detailY=NoiseCoordinateQ16(y,biome.detailWavelengthSubvoxels)+22*Q16; + const std::int64_t detailZ=SeedOffsetQ16(seed,130); + height+=MulQ16(FractalNoiseQ16(detailX,detailY,detailZ,3),biome.detailAmplitudeSubvoxels); + return height; + } + + inline std::int64_t SurfaceHeightV2(const WorldAssemblyRecipeV1& recipe, + const WorldAssemblyStratumPlanV1& plan, const WorldAssemblyBiomeProfileV1& biome, + std::int64_t x,std::int64_t y) + { + std::int64_t terrain=StructuralHeightV2(recipe,biome,x,y); + if(biome.cliffStrengthQ16>0 && biome.cliffMaximumDisplacementSubvoxels>0) + { + const std::int64_t d=biome.cliffSampleDistanceSubvoxels; + const std::int64_t xp=StructuralHeightV2(recipe,biome,x+d,y); + const std::int64_t xm=StructuralHeightV2(recipe,biome,x-d,y); + const std::int64_t yp=StructuralHeightV2(recipe,biome,x,y+d); + const std::int64_t ym=StructuralHeightV2(recipe,biome,x,y-d); + const std::int64_t gx=((xp-xm)*Q16)/(2*d),gy=((yp-ym)*Q16)/(2*d); + const std::uint64_t square=static_cast(gx*gx+gy*gy); + std::uint64_t remainder=square,root=0,bit=1ull<<62; + while(bit>remainder)bit>>=2; + while(bit) + { + if(remainder>=root+bit) + {remainder-=root+bit;root=(root>>1)+bit;} + else root>>=1; + bit>>=2; + } + const std::int64_t slope=static_cast(root); + const std::int64_t threshold=std::max(1,biome.cliffSlopeThresholdQ16); + const std::int64_t gate=std::clamp( + (slope-threshold)*Q16/threshold,0,Q16); + if(gate>0) + { + const std::int64_t reference=(xp+xm+yp+ym)/4; + const std::int64_t gain=MulQ16(MulQ16(biome.cliffStrengthQ16,gate), + biome.cliffSharpnessQ16); + const std::int64_t displacement=std::clamp( + MulQ16(terrain-reference,gain),-biome.cliffMaximumDisplacementSubvoxels, + biome.cliffMaximumDisplacementSubvoxels); + terrain+=displacement; + } + } + if(biome.beachWidthSubvoxels>0 && plan.waterEnabled) + { + const std::int64_t distance=std::abs(terrain-plan.waterSurfaceSubvoxels); + if(distance(std::clamp( + distance*Q16/biome.beachWidthSubvoxels,0,Q16)); + const std::uint32_t smooth=SmoothQ16(t); + terrain=plan.waterSurfaceSubvoxels+MulQ16(terrain-plan.waterSurfaceSubvoxels,smooth); + } + } + return terrain; + } + + inline std::uint64_t IntegerSqrt(std::uint64_t value) + { + std::uint64_t remainder=value,root=0,bit=1ull<<62; + while(bit>remainder)bit>>=2; + while(bit) + { + if(remainder>=root+bit){remainder-=root+bit;root=(root>>1)+bit;} + else root>>=1; + bit>>=2; + } + return root; + } + + struct BiomeSelectionV2 + { + std::size_t primary=0,neighbor=0; + std::uint32_t primaryWeightQ16=static_cast(Q16); + std::int64_t warpedX=0,warpedY=0; + }; + + inline std::size_t ClassifyBiomeSiteV2(const WorldAssemblyRecipeV1& recipe, + std::int64_t siteX,std::int64_t siteY,std::uint32_t siteHash) + { + const std::int32_t relief=ReliefQ16(recipe.seed,siteX,siteY, + recipe.biomeClimateReliefWavelengthSubvoxels,recipe.biomeClimateReliefContrastQ16); + const std::int32_t moisture=MoistureQ16(recipe.seed,siteX,siteY, + recipe.biomeClimateMoistureWavelengthSubvoxels); + std::size_t best=0; + std::uint64_t bestScore=std::numeric_limits::max(); + for(std::size_t i=0;ibiome.reliefMaximumQ16 + ?relief-biome.reliefMaximumQ16:0); + const std::int64_t dm=moisturebiome.moistureMaximumQ16 + ?moisture-biome.moistureMaximumQ16:0); + const std::uint32_t jitterHash=Mix32(siteHash^(0x9e3779b9u*static_cast(i+1))); + const std::uint64_t jitter=(static_cast(jitterHash>>8)*429497u)/0x1000000u; + const std::uint64_t score=static_cast(dr*dr+dm*dm)+jitter; + if(score(1,recipe.biomeCellSizeSubvoxels); + const std::int64_t cellX=FloorDiv(out.warpedX,cellSize); + const std::int64_t cellY=FloorDiv(out.warpedY,cellSize); + const std::uint32_t biomeSeed=recipe.seed^0x42494f4du; + std::uint64_t nearest=std::numeric_limits::max(); + std::uint64_t second=nearest; + for(std::int64_t dy=-1;dy<=1;++dy) + for(std::int64_t dx=-1;dx<=1;++dx) + { + const std::int64_t cx=cellX+dx,cy=cellY+dy; + std::uint32_t hash=biomeSeed; + hash^=Mix32(static_cast(cx)+0x7fffffffu); + hash^=Mix32((static_cast(cy)+0x7fffffffu)*2654435761u); + hash=Mix32(hash); + const std::uint32_t jx=hash&0xffffffu; + const std::uint32_t jy=Mix32(hash^0x68bc21ebu)&0xffffffu; + const std::int64_t siteX=cx*cellSize+(static_cast(jx)*cellSize)/0x1000000ll; + const std::int64_t siteY=cy*cellSize+(static_cast(jy)*cellSize)/0x1000000ll; + const std::int64_t dxq=siteX-out.warpedX,dyq=siteY-out.warpedY; + const std::uint64_t distance=static_cast(dxq*dxq+dyq*dyq); + if(distance0 + &&second!=std::numeric_limits::max()) + { + const std::int64_t d1=static_cast(IntegerSqrt(nearest)); + const std::int64_t d2=static_cast(IntegerSqrt(second)); + const std::int64_t t=std::clamp((d2-d1)*Q16 + /std::max(recipe.biomeBorderBlendSubvoxels,1),0,Q16); + const std::uint32_t neighborWeight=static_cast( + (Q16-SmoothQ16(static_cast(t)))/2); + out.primaryWeightQ16=static_cast(Q16)-neighborWeight; + } + return out; + } + + inline std::int64_t CapHeightV2(const WorldAssemblyRecipeV1& recipe, + const WorldAssemblyStratumPlanV1& plan,const WorldAssemblyBiomeProfileV1& biome, + std::int64_t x,std::int64_t y) + { + const std::int64_t base=biome.hasCapProfile?biome.capHeightSubvoxels:plan.capHeightSubvoxels; + const std::int64_t warpStrength=biome.hasCapProfile + ?biome.capWarpStrengthSubvoxels:0; + const std::int64_t warpWave=biome.hasCapProfile + ?biome.capWarpWavelengthSubvoxels:plan.capWavelengthSubvoxels; + const std::int64_t undulation=biome.hasCapProfile + ?biome.capUndulationSubvoxels:plan.capUndulationSubvoxels; + const std::int64_t swellWave=biome.hasCapProfile + ?biome.capWavelengthSubvoxels:plan.capWavelengthSubvoxels; + const std::int64_t detail=biome.hasCapProfile + ?biome.capDetailAmplitudeSubvoxels:plan.capDetailAmplitudeSubvoxels; + const std::int64_t detailWave=biome.hasCapProfile + ?biome.capDetailWavelengthSubvoxels:plan.capDetailWavelengthSubvoxels; + const std::int64_t ridge=biome.hasCapProfile + ?biome.capRidgeAmplitudeSubvoxels:plan.capRidgeAmplitudeSubvoxels; + const std::int64_t ridgeWave=biome.hasCapProfile + ?biome.capRidgeWavelengthSubvoxels:plan.capRidgeWavelengthSubvoxels; + std::int64_t qx=x,qy=y; + if(warpStrength>0) + { + const std::int64_t nx=NoiseCoordinateQ16(x,warpWave); + const std::int64_t ny=NoiseCoordinateQ16(y,warpWave); + const std::int32_t wx=Perlin3DQ16(nx+SeedOffsetQ16(recipe.seed,71),ny+2*Q16+Q16*3/10, + SeedOffsetQ16(recipe.seed,330)); + const std::int32_t wy=Perlin3DQ16(nx+6*Q16+Q16/10,ny+SeedOffsetQ16(recipe.seed,19), + SeedOffsetQ16(recipe.seed,470)); + qx+=WarpDisplacement(wx,warpStrength);qy+=WarpDisplacement(wy,warpStrength); + } + std::int64_t cap=base; + if(undulation!=0) + { + const std::int64_t nx=NoiseCoordinateQ16(qx,swellWave)+SeedOffsetQ16(recipe.seed,190); + const std::int64_t ny=NoiseCoordinateQ16(qy,swellWave)+13*Q16; + const std::int32_t swell=FractalNoiseQ16(nx,ny,SeedOffsetQ16(recipe.seed,50),3); + cap+=WarpDisplacement(swell,undulation); + } + if(detail>0) + { + const std::int64_t nx=NoiseCoordinateQ16(x,detailWave)+5*Q16; + const std::int64_t ny=NoiseCoordinateQ16(y,detailWave)+6*Q16; + const std::int32_t noise=FractalNoiseQ16(nx,ny,SeedOffsetQ16(recipe.seed,210),3); + cap-=WarpDisplacement(std::abs(noise),detail); + } + if(ridge>0) + { + const std::int64_t nx=NoiseCoordinateQ16(qx,ridgeWave)+31*Q16; + const std::int64_t ny=NoiseCoordinateQ16(qy,ridgeWave)+47*Q16; + const std::int32_t ridgeNoise=FractalNoiseQ16(nx,ny,SeedOffsetQ16(recipe.seed,110),4,true); + const std::int64_t hang=std::clamp((ridgeNoise+Q16)/2,0,Q16); + cap-=MulQ16(hang,ridge); + } + return cap; + } + + inline std::uint32_t FeatureFadeQ16(const WorldAssemblyFeatureV1& feature, + std::int64_t x,std::int64_t y,std::int64_t feather) + { + const auto& lo=feature.minimumSubvoxels;const auto& hi=feature.maximumSubvoxels; + if(xhi[0]||yhi[1])return 0; + if(feather<=0)return static_cast(Q16); + const std::int64_t edge=std::min({x-lo[0],hi[0]-x,y-lo[1],hi[1]-y}); + return SmoothQ16(static_cast(std::clamp(edge*Q16/feather,0,Q16))); + } + inline bool InInterval(const FieldInterval& interval, std::int64_t density) { return interval.known && density >= interval.minimumDensitySubvoxels && density <= interval.maximumDensitySubvoxels; } @@ -492,6 +1034,12 @@ std::string WorldAssemblyManifest::ToJson() const << ",\"biome_warp_q\":" << value.recipe.biomeWarpSubvoxels << ",\"biome_warp_wavelength_q\":" << value.recipe.biomeWarpWavelengthSubvoxels << ",\"biome_border_blend_q\":" << value.recipe.biomeBorderBlendSubvoxels + ; + if (value.recipe.evaluatorVersion >= 2) + out << ",\"climate_relief_wavelength_q\":" << value.recipe.biomeClimateReliefWavelengthSubvoxels + << ",\"climate_moisture_wavelength_q\":" << value.recipe.biomeClimateMoistureWavelengthSubvoxels + << ",\"climate_relief_contrast_q16\":" << value.recipe.biomeClimateReliefContrastQ16; + out << ",\"content_hash\":\"" << world_assembly_detail::HashText(value.recipe.contentHash) << "\",\"biomes\":["; for (std::size_t i = 0; i < value.recipe.biomes.size(); ++i) @@ -504,7 +1052,34 @@ std::string WorldAssemblyManifest::ToJson() const << ",\"mountain_amplitude_q\":" << biome.mountainAmplitudeSubvoxels << ",\"mountain_wavelength_q\":" << biome.mountainWavelengthSubvoxels << ",\"detail_amplitude_q\":" << biome.detailAmplitudeSubvoxels - << ",\"detail_wavelength_q\":" << biome.detailWavelengthSubvoxels << '}'; + << ",\"detail_wavelength_q\":" << biome.detailWavelengthSubvoxels; + if (value.recipe.evaluatorVersion >= 2) + out << ",\"relief_min_q16\":" << biome.reliefMinimumQ16 + << ",\"relief_max_q16\":" << biome.reliefMaximumQ16 + << ",\"moisture_min_q16\":" << biome.moistureMinimumQ16 + << ",\"moisture_max_q16\":" << biome.moistureMaximumQ16 + << ",\"relief_strength_q16\":" << biome.reliefStrengthQ16 + << ",\"relief_contrast_q16\":" << biome.reliefContrastQ16 + << ",\"relief_wavelength_q\":" << biome.reliefWavelengthSubvoxels + << ",\"height_warp_strength_q\":" << biome.heightWarpStrengthSubvoxels + << ",\"height_warp_wavelength_q\":" << biome.heightWarpWavelengthSubvoxels + << ",\"beach_width_q\":" << biome.beachWidthSubvoxels + << ",\"cliff_strength_q16\":" << biome.cliffStrengthQ16 + << ",\"cliff_slope_threshold_q16\":" << biome.cliffSlopeThresholdQ16 + << ",\"cliff_sharpness_q16\":" << biome.cliffSharpnessQ16 + << ",\"cliff_sample_distance_q\":" << biome.cliffSampleDistanceSubvoxels + << ",\"cliff_max_displacement_q\":" << biome.cliffMaximumDisplacementSubvoxels + << ",\"cap_enabled\":" << (biome.hasCapProfile ? "true" : "false") + << ",\"cap_height_q\":" << biome.capHeightSubvoxels + << ",\"cap_warp_strength_q\":" << biome.capWarpStrengthSubvoxels + << ",\"cap_warp_wavelength_q\":" << biome.capWarpWavelengthSubvoxels + << ",\"cap_undulation_q\":" << biome.capUndulationSubvoxels + << ",\"cap_wavelength_q\":" << biome.capWavelengthSubvoxels + << ",\"cap_detail_amplitude_q\":" << biome.capDetailAmplitudeSubvoxels + << ",\"cap_detail_wavelength_q\":" << biome.capDetailWavelengthSubvoxels + << ",\"cap_ridge_amplitude_q\":" << biome.capRidgeAmplitudeSubvoxels + << ",\"cap_ridge_wavelength_q\":" << biome.capRidgeWavelengthSubvoxels; + out << '}'; } out << "]},\"plan\":{\"schema_version\":" << value.plan.schemaVersion << ",\"content_hash\":\"" << world_assembly_detail::HashText(value.plan.contentHash) @@ -518,6 +1093,18 @@ std::string WorldAssemblyManifest::ToJson() const << ",\"cap_height_q\":" << stratum.capHeightSubvoxels << ",\"cap_undulation_q\":" << stratum.capUndulationSubvoxels << ",\"cap_wavelength_q\":" << stratum.capWavelengthSubvoxels + ; + const bool hasExtendedCap = stratum.capDetailAmplitudeSubvoxels != 0 + || stratum.capRidgeAmplitudeSubvoxels != 0 || stratum.waterEnabled + || stratum.waterSurfaceSubvoxels != 0; + if (hasExtendedCap) + out << ",\"cap_detail_amplitude_q\":" << stratum.capDetailAmplitudeSubvoxels + << ",\"cap_detail_wavelength_q\":" << stratum.capDetailWavelengthSubvoxels + << ",\"cap_ridge_amplitude_q\":" << stratum.capRidgeAmplitudeSubvoxels + << ",\"cap_ridge_wavelength_q\":" << stratum.capRidgeWavelengthSubvoxels + << ",\"water_surface_q\":" << stratum.waterSurfaceSubvoxels + << ",\"water_enabled\":" << (stratum.waterEnabled ? "true" : "false"); + out << ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash) << "\",\"features\":["; for (std::size_t f = 0; f < stratum.features.size(); ++f) @@ -556,7 +1143,7 @@ Heightfield2D::Heightfield2D(const WorldAssemblyRecipeV1& recipe, bool Heightfield2D::isValid() const noexcept { if (!recipe_ || !plan_ || recipe_->schemaVersion != WorldAssemblySchemaVersion - || recipe_->evaluatorVersion != WorldAssemblyEvaluatorVersion + || (recipe_->evaluatorVersion != 1 && recipe_->evaluatorVersion != WorldAssemblyEvaluatorVersion) || recipe_->unitsPerVoxel != WorldAssemblySubvoxelsPerVoxel || recipe_->biomes.empty() || recipe_->biomes.size() > world_assembly_detail::MaxBiomeCount || recipe_->biomeCellSizeSubvoxels <= 0 || recipe_->biomeWarpWavelengthSubvoxels <= 0 @@ -569,7 +1156,8 @@ bool Heightfield2D::isValid() const noexcept return biome.stableId != 0 && biome.continentAmplitudeSubvoxels >= 0 && biome.mountainAmplitudeSubvoxels >= 0 && biome.detailAmplitudeSubvoxels >= 0 && biome.continentWavelengthSubvoxels > 0 && biome.mountainWavelengthSubvoxels > 0 - && biome.detailWavelengthSubvoxels > 0; + && biome.detailWavelengthSubvoxels > 0 && biome.reliefWavelengthSubvoxels > 0 + && biome.heightWarpWavelengthSubvoxels > 0 && biome.cliffSampleDistanceSubvoxels > 0; }); } @@ -581,6 +1169,62 @@ HeightfieldPointSample Heightfield2D::point(std::int64_t x, std::int64_t y, || world_assembly_detail::OutsideRange(y) || world_assembly_detail::OutsideRange(z)) return out; using namespace world_assembly_detail; + if(recipe_->evaluatorVersion>=2) + { + const BiomeSelectionV2 selection=SelectBiomeV2(*recipe_,x,y); + const auto& primary=recipe_->biomes[selection.primary]; + const auto& neighbor=recipe_->biomes[selection.neighbor]; + const std::int64_t primaryHeight=SurfaceHeightV2(*recipe_,*plan_,primary,x,y); + const std::int64_t neighborHeight=selection.primary==selection.neighbor?primaryHeight + :SurfaceHeightV2(*recipe_,*plan_,neighbor,x,y); + std::int64_t ground=primaryHeight+((neighborHeight-primaryHeight) + *(static_cast(Q16)-selection.primaryWeightQ16))/Q16; + const std::int64_t primaryCap=CapHeightV2(*recipe_,*plan_,primary,x,y); + const std::int64_t neighborCap=selection.primary==selection.neighbor?primaryCap + :CapHeightV2(*recipe_,*plan_,neighbor,x,y); + std::int64_t cap=primaryCap+((neighborCap-primaryCap) + *(static_cast(Q16)-selection.primaryWeightQ16))/Q16; + for(const auto& feature:plan_->features) + { + if(feature.type==1) + { + const std::uint32_t fade=FeatureFadeQ16(feature,x,y,feature.parametersSubvoxels[1]); + if(fade)ground+=MulQ16(feature.parametersSubvoxels[0],fade); + } + else if(feature.type==3) + { + const std::uint32_t fade=FeatureFadeQ16(feature,x,y,feature.parametersSubvoxels[2]); + const std::int64_t wave=std::max(1,feature.parametersSubvoxels[1]); + if(fade) + { + const std::uint32_t featureSeed=static_cast(feature.stableId); + const std::int64_t nx=NoiseCoordinateQ16(x,wave)+SeedOffsetQ16(featureSeed,1700); + const std::int64_t ny=NoiseCoordinateQ16(y,wave)+SeedOffsetQ16(featureSeed,2900); + const std::int32_t noise=FractalNoiseQ16(nx,ny,SeedOffsetQ16(featureSeed,4100),3); + cap+=MulQ16(MulQ16(noise,feature.parametersSubvoxels[0]),fade); + } + } + } + out.groundHeightSubvoxels=ground; + out.capHeightSubvoxels=cap; + out.biomeId=primary.stableId; + out.neighborBiomeId=neighbor.stableId; + out.biomeWeightQ16=selection.primaryWeightQ16; + out.densitySubvoxels=std::min(z-ground,cap-z); + for(const auto& feature:plan_->features) + { + if(feature.type!=2)continue; + const auto& lo=feature.minimumSubvoxels;const auto& hi=feature.maximumSubvoxels; + if(xhi[0]||yhi[1]||zhi[2])continue; + const std::int64_t depth=std::min({x-lo[0],hi[0]-x,y-lo[1],hi[1]-y,z-lo[2],hi[2]-z}); + out.densitySubvoxels=std::min(out.densitySubvoxels,-depth); + } + out.waterSurfaceSubvoxels=plan_->waterSurfaceSubvoxels; + if(plan_->waterEnabled && out.densitySubvoxels>=0 && z>ground + &&zwaterSurfaceSubvoxels && groundwaterSurfaceSubvoxels) + out.waterFillSubvoxels=std::min(z-ground,plan_->waterSurfaceSubvoxels-z); + return out; + } const std::int64_t warpX = MulQ16(recipe_->biomeWarpSubvoxels, Noise2D(x, y, recipe_->biomeWarpWavelengthSubvoxels, recipe_->seed, 0x574152505f585f31ull)); const std::int64_t warpY = MulQ16(recipe_->biomeWarpSubvoxels, @@ -657,15 +1301,55 @@ FieldInterval Heightfield2D::interval(std::int64_t minX, std::int64_t minY, || world_assembly_detail::OutsideRange(maxZ)) return out; std::int64_t groundMin = std::numeric_limits::max(); std::int64_t groundMax = std::numeric_limits::min(); + std::int64_t capMin=plan_->capHeightSubvoxels-plan_->capUndulationSubvoxels; + std::int64_t capMax=plan_->capHeightSubvoxels+plan_->capUndulationSubvoxels; + if(recipe_->evaluatorVersion>=2) + { + for(const auto& biome:recipe_->biomes) + { + const std::int64_t displacement=biome.continentAmplitudeSubvoxels + +biome.mountainAmplitudeSubvoxels+2*biome.detailAmplitudeSubvoxels + +biome.cliffMaximumDisplacementSubvoxels; + groundMin=std::min(groundMin,biome.baseHeightSubvoxels-displacement); + groundMax=std::max(groundMax,biome.baseHeightSubvoxels+displacement); + if(plan_->waterEnabled&&biome.beachWidthSubvoxels>0) + { + groundMin=std::min(groundMin,plan_->waterSurfaceSubvoxels-biome.beachWidthSubvoxels); + groundMax=std::max(groundMax,plan_->waterSurfaceSubvoxels+biome.beachWidthSubvoxels); + } + if(biome.hasCapProfile) + { + const std::int64_t profileCapDown=biome.capUndulationSubvoxels*5/2 + +biome.capDetailAmplitudeSubvoxels*5/2+biome.capRidgeAmplitudeSubvoxels; + const std::int64_t profileCapUp=biome.capUndulationSubvoxels*5/2; + capMin=std::min(capMin,biome.capHeightSubvoxels-profileCapDown); + capMax=std::max(capMax,biome.capHeightSubvoxels+profileCapUp); + } + } + std::int64_t localCliffMagnitude=0,localCapMagnitude=0; + for(const auto& feature:plan_->features) + { + if(feature.type==1)localCliffMagnitude=std::max(localCliffMagnitude, + std::abs(feature.parametersSubvoxels[0])); + if(feature.type==3)localCapMagnitude+=std::abs(feature.parametersSubvoxels[0]); + } + groundMin-=localCliffMagnitude;groundMax+=localCliffMagnitude; + const std::int64_t capDown=plan_->capUndulationSubvoxels*5/2 + +plan_->capDetailAmplitudeSubvoxels*5/2+plan_->capRidgeAmplitudeSubvoxels; + const std::int64_t capUp=plan_->capUndulationSubvoxels*5/2+localCapMagnitude; + capMin=std::min(capMin,plan_->capHeightSubvoxels-capDown-localCapMagnitude); + capMax=std::max(capMax,plan_->capHeightSubvoxels+capUp); + } for (const auto& biome : recipe_->biomes) { - const std::int64_t amplitude = biome.continentAmplitudeSubvoxels - + biome.mountainAmplitudeSubvoxels + biome.detailAmplitudeSubvoxels; - groundMin = std::min(groundMin, biome.baseHeightSubvoxels - amplitude); - groundMax = std::max(groundMax, biome.baseHeightSubvoxels + amplitude); + if(recipe_->evaluatorVersion<2) + { + const std::int64_t amplitude=biome.continentAmplitudeSubvoxels + +biome.mountainAmplitudeSubvoxels+biome.detailAmplitudeSubvoxels; + groundMin=std::min(groundMin,biome.baseHeightSubvoxels-amplitude); + groundMax=std::max(groundMax,biome.baseHeightSubvoxels+amplitude); + } } - const std::int64_t capMin = plan_->capHeightSubvoxels - plan_->capUndulationSubvoxels; - const std::int64_t capMax = plan_->capHeightSubvoxels + plan_->capUndulationSubvoxels; const std::int64_t groundLower = minZ - groundMax; const std::int64_t groundUpper = maxZ - groundMin; const std::int64_t capLower = capMin - maxZ; @@ -673,6 +1357,22 @@ FieldInterval Heightfield2D::interval(std::int64_t minX, std::int64_t minY, out.known = true; out.minimumDensitySubvoxels = std::min(groundLower, capLower); out.maximumDensitySubvoxels = std::min(groundUpper, capUpper); + if(recipe_->evaluatorVersion>=2) + { + for(const auto& feature:plan_->features) + { + if(feature.type!=2)continue; + bool overlap=true; + for(std::size_t axis=0;axis<3;++axis) + overlap=overlap&&feature.minimumSubvoxels[axis]<=std::array{maxX,maxY,maxZ}[axis] + &&feature.maximumSubvoxels[axis]>=std::array{minX,minY,minZ}[axis]; + if(!overlap)continue; + const std::int64_t depth=std::min({(feature.maximumSubvoxels[0]-feature.minimumSubvoxels[0])/2, + (feature.maximumSubvoxels[1]-feature.minimumSubvoxels[1])/2, + (feature.maximumSubvoxels[2]-feature.minimumSubvoxels[2])/2}); + out.minimumDensitySubvoxels=std::min(out.minimumDensitySubvoxels,-depth); + } + } return out; } @@ -689,14 +1389,34 @@ FieldDetailEnvelope Heightfield2D::detailEnvelope() const noexcept { FieldDetailEnvelope out; if (!isValid()) return out; - std::int64_t maximum = plan_->capUndulationSubvoxels; + std::int64_t maximum = plan_->capUndulationSubvoxels*5/2 + +plan_->capDetailAmplitudeSubvoxels*5/2+plan_->capRidgeAmplitudeSubvoxels; std::int64_t minimumFeature = std::numeric_limits::max(); for (const auto& biome : recipe_->biomes) { - maximum = std::max(maximum, biome.continentAmplitudeSubvoxels - + biome.mountainAmplitudeSubvoxels + biome.detailAmplitudeSubvoxels); - minimumFeature = std::min({minimumFeature, biome.continentWavelengthSubvoxels, - biome.mountainWavelengthSubvoxels, biome.detailWavelengthSubvoxels}); + const std::int64_t surfaceAmplitude=biome.continentAmplitudeSubvoxels + +biome.mountainAmplitudeSubvoxels+biome.detailAmplitudeSubvoxels + +biome.cliffMaximumDisplacementSubvoxels+biome.beachWidthSubvoxels; + maximum = std::max(maximum,surfaceAmplitude); + if(biome.hasCapProfile) + maximum=std::max(maximum,biome.capUndulationSubvoxels*5/2 + +biome.capDetailAmplitudeSubvoxels*5/2+biome.capRidgeAmplitudeSubvoxels); + minimumFeature = std::min({minimumFeature, biome.continentWavelengthSubvoxels/8, + biome.mountainWavelengthSubvoxels/8, biome.detailWavelengthSubvoxels/4, + biome.reliefWavelengthSubvoxels/2, biome.heightWarpWavelengthSubvoxels, + biome.cliffSampleDistanceSubvoxels}); + } + minimumFeature=std::min({minimumFeature,plan_->capWavelengthSubvoxels/4, + plan_->capDetailWavelengthSubvoxels/4,plan_->capRidgeWavelengthSubvoxels/8}); + for(const auto& feature:plan_->features) + { + if(feature.type==1)maximum=std::max(maximum,std::abs(feature.parametersSubvoxels[0])); + if(feature.type==2) + maximum=std::max(maximum,std::min({ + (feature.maximumSubvoxels[0]-feature.minimumSubvoxels[0])/2, + (feature.maximumSubvoxels[1]-feature.minimumSubvoxels[1])/2, + (feature.maximumSubvoxels[2]-feature.minimumSubvoxels[2])/2})); + if(feature.type==3)maximum=std::max(maximum,std::abs(feature.parametersSubvoxels[0])); } out.maximumAbsoluteDisplacementSubvoxels = maximum; out.minimumFeatureSizeSubvoxels = std::max(1, @@ -708,15 +1428,18 @@ FieldDetailEnvelope Heightfield2D::detailEnvelope() const noexcept FieldBlockContract Heightfield2D::contract() const noexcept { return {ChannelBit(FieldChannel::biome), - ChannelBit(FieldChannel::height) | ChannelBit(FieldChannel::rock), - WorldAssemblyEvaluatorVersion}; + ChannelBit(FieldChannel::height) | ChannelBit(FieldChannel::rock) + | ChannelBit(FieldChannel::water) | ChannelBit(FieldChannel::waterSurface), + recipe_?recipe_->evaluatorVersion:WorldAssemblyEvaluatorVersion}; } WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block, std::uint32_t boxCount, std::uint64_t seed) { WorldAssemblyBoundsAuditResult result; - if (!block.isValid()) return result; + const auto* recipe=block.recipe(); + const auto* plan=block.plan(); + if (!block.isValid() || !recipe || !plan) return result; boxCount = std::clamp(boxCount, 1, 100000); const auto random = [&seed]() { @@ -725,33 +1448,110 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block }; const auto signedVoxel = [&random]() { return static_cast(random() % 12001) - 6000; }; + const auto testBox=[&](std::int64_t x,std::int64_t y,std::int64_t z, + std::int64_t halfX,std::int64_t halfY,std::int64_t halfZ, + std::uint64_t* category,bool countExceptionPoints,const char* tag) + { + const FieldInterval bounds=block.interval(x-halfX,y-halfY,z-halfZ, + x+halfX,y+halfY,z+halfZ); + ++result.boxesTested; + if(category)++*category; + if(x<0||y<0)++result.negativeCoordinateBoxes; + if(!bounds.known){++result.unknownBounds;return;} + for(int iz=0;iz<=2;++iz) + for(int iy=0;iy<=2;++iy) + for(int ix=0;ix<=2;++ix) + { + const std::int64_t px=x-halfX+halfX*ix; + const std::int64_t py=y-halfY+halfY*iy; + const std::int64_t pz=z-halfZ+halfZ*iz; + const auto sample=block.point(px,py,pz); + ++result.pointSamples; + if(countExceptionPoints)++result.exceptionPointSamples; + if(!world_assembly_detail::InInterval(bounds,sample.densitySubvoxels)) + { + ++result.missedBounds; + if(result.missedBounds<=6) + std::fprintf(stderr,"[WorldAssemblerBoundMiss] tag=%s box=(%lld,%lld,%lld) half=(%lld,%lld,%lld) bounds=[%lld,%lld] point=(%lld,%lld,%lld) density=%lld\n", + tag,static_cast(x),static_cast(y),static_cast(z), + static_cast(halfX),static_cast(halfY),static_cast(halfZ), + static_cast(bounds.minimumDensitySubvoxels), + static_cast(bounds.maximumDensitySubvoxels), + static_cast(px),static_cast(py),static_cast(pz), + static_cast(sample.densitySubvoxels)); + } + } + }; for (std::uint32_t i = 0; i < boxCount; ++i) { - const std::int64_t x = signedVoxel() * WorldAssemblySubvoxelsPerVoxel; - const std::int64_t y = signedVoxel() * WorldAssemblySubvoxelsPerVoxel; + std::int64_t x = signedVoxel() * WorldAssemblySubvoxelsPerVoxel; + std::int64_t y = signedVoxel() * WorldAssemblySubvoxelsPerVoxel; + if(i%4==0){x=-std::abs(x);y=-std::abs(y);} const auto column = block.point(x, y, 0); const std::int64_t z = column.groundHeightSubvoxels + (static_cast(random() % 257) - 128) * WorldAssemblySubvoxelsPerVoxel; const std::int64_t halfX = static_cast(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel; const std::int64_t halfY = static_cast(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel; const std::int64_t halfZ = static_cast(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel; - const FieldInterval bounds = block.interval(x - halfX, y - halfY, z - halfZ, - x + halfX, y + halfY, z + halfZ); - ++result.boxesTested; - if (!bounds.known) { ++result.unknownBounds; continue; } - for (int iz = 0; iz <= 2; ++iz) - for (int iy = 0; iy <= 2; ++iy) - for (int ix = 0; ix <= 2; ++ix) + testBox(x,y,z,halfX,halfY,halfZ,nullptr,false,"random"); + } + const std::int64_t cell=recipe->biomeCellSizeSubvoxels; + const std::int64_t searchExtent=2*cell; + const std::int64_t borderStep=std::max(cell/8, + 32*WorldAssemblySubvoxelsPerVoxel); + for(std::int64_t y=-searchExtent;y<=searchExtent&&result.biomeBorderBoxes<16;y+=borderStep) + for(std::int64_t x=-searchExtent;x<=searchExtent&&result.biomeBorderBoxes<16;x+=borderStep) + { + const auto column=block.point(x,y,0); + if(column.biomeId==column.neighborBiomeId||column.biomeWeightQ16>=65536)continue; + testBox(x,y,column.groundHeightSubvoxels,16*WorldAssemblySubvoxelsPerVoxel, + 16*WorldAssemblySubvoxelsPerVoxel,8*WorldAssemblySubvoxelsPerVoxel, + &result.biomeBorderBoxes,false,"border"); + } + const std::int64_t capStep=std::max(cell/2, + 64*WorldAssemblySubvoxelsPerVoxel); + for(std::int64_t y=-capStep;y<=capStep;y+=capStep) + for(std::int64_t x=-capStep;x<=capStep;x+=capStep) + { + const auto column=block.point(x,y,0); + testBox(x,y,column.capHeightSubvoxels,8*WorldAssemblySubvoxelsPerVoxel, + 8*WorldAssemblySubvoxelsPerVoxel,16*WorldAssemblySubvoxelsPerVoxel, + &result.capBoxes,false,"cap"); + } + bool hasCliffProfile=false; + for(const auto& biome:recipe->biomes)hasCliffProfile|=biome.cliffStrengthQ16>0; + if(hasCliffProfile) + { + const std::int64_t d=16*WorldAssemblySubvoxelsPerVoxel; + const std::int64_t step=std::max(cell/10, + 64*WorldAssemblySubvoxelsPerVoxel); + for(std::int64_t y=-2*cell;y<=2*cell&&result.cliffBoxes<16;y+=step) + for(std::int64_t x=-2*cell;x<=2*cell&&result.cliffBoxes<16;x+=step) { - const std::int64_t px = x - halfX + (2 * halfX * ix) / 2; - const std::int64_t py = y - halfY + (2 * halfY * iy) / 2; - const std::int64_t pz = z - halfZ + (2 * halfZ * iz) / 2; - const auto sample = block.point(px, py, pz); - ++result.pointSamples; - if (!world_assembly_detail::InInterval(bounds, sample.densitySubvoxels)) - ++result.missedBounds; + const auto center=block.point(x,y,0); + const auto xp=block.point(x+d,y,0),xm=block.point(x-d,y,0); + const auto yp=block.point(x,y+d,0),ym=block.point(x,y-d,0); + const std::int64_t gradient=std::max(std::abs(xp.groundHeightSubvoxels-xm.groundHeightSubvoxels), + std::abs(yp.groundHeightSubvoxels-ym.groundHeightSubvoxels)); + if(gradientfeatures) + { + if(feature.type!=2)continue; + const std::int64_t x=feature.minimumSubvoxels[0] + +(feature.maximumSubvoxels[0]-feature.minimumSubvoxels[0])/2; + const std::int64_t y=feature.minimumSubvoxels[1] + +(feature.maximumSubvoxels[1]-feature.minimumSubvoxels[1])/2; + const std::int64_t z=feature.minimumSubvoxels[2] + +(feature.maximumSubvoxels[2]-feature.minimumSubvoxels[2])/2; + testBox(x,y,z,(feature.maximumSubvoxels[0]-feature.minimumSubvoxels[0])/2, + (feature.maximumSubvoxels[1]-feature.minimumSubvoxels[1])/2, + (feature.maximumSubvoxels[2]-feature.minimumSubvoxels[2])/2, + &result.exceptionBoxes,true,"exception"); + } const auto ground = block.point(0, 0, 0).groundHeightSubvoxels; const auto controlPoint = block.point(0, 0, ground + 17).densitySubvoxels; // Negative control: this fabricated one-sided range excludes its known sample. The same @@ -759,7 +1559,14 @@ WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block // Controle negatif : cette plage unilaterale exclut un echantillon connu; l'audit doit rejeter cette borne mensongere. const FieldInterval deliberatelyWrong{true, controlPoint + 1, controlPoint + 2}; result.negativeControlDetected = !world_assembly_detail::InInterval(deliberatelyWrong, controlPoint); - result.passed = result.boxesTested == boxCount && result.missedBounds == 0 + const bool borderCoverage=recipe->biomes.size()<2||recipe->biomeBorderBlendSubvoxels==0 + ||result.biomeBorderBoxes>0; + const bool cliffCoverage=!hasCliffProfile||result.cliffBoxes>0; + const bool exceptionCoverage=std::none_of(plan->features.begin(),plan->features.end(), + [](const auto& feature){return feature.type==2;})||result.exceptionBoxes>0; + result.passed = result.boxesTested >= boxCount && result.negativeCoordinateBoxes>0 + && borderCoverage&&cliffCoverage&&result.capBoxes>0&&exceptionCoverage + && result.missedBounds == 0 && result.unknownBounds == 0 && result.negativeControlDetected; return result; } diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md index ec94dce..ae6d7b5 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md @@ -638,3 +638,49 @@ the capsule-swept route. The approximately 1.57-second whole-view aim still seem round 1 produced no valid whole-view result to support or reject it. — Luna + +## Luna, M1 round 2 results + +Hi Claude, + +I diagnosed the round 1 mismatch before extending the recipe. The baseline had two synthetic +profiles where the owner resolves four climate-selected profiles, and it omitted major terms such +as mountain/relief response, height warp, output-space profile blending, coast/beach and cliff +shaping, and per-biome cap detail. +Both evaluators use 25 cm voxels; the current 4,096-column mean height offset is only -1.366 voxels, +and owner biome IDs match 4,096/4,096 samples. Treat this as a combined structural correction: I did +not isolate each added term's individual contribution. + +On the same 4,096 density points, MAE fell from 274.0383 to 17.2262 voxels, maximum error from +841.4257 to 392.4136 voxels, and sign mismatches from 55.2% to 18.14%. The remaining error is still +visible. Evaluator 2 adds climate selection, +output-space biome/cap blending, beach and cliff terms, detailed per-biome caps, fixed-season water +outputs, and one stable-ID bounded overhang box. Schema stays v1; the new recipe and four owner +profiles use evaluator 2. Route points remain empty until round 3. Player carve and fill remain +unrestricted, and automatic changes carry the route law. + +The 4,096 sample hash is `c5e0371612e927af` at W1/W8/W16. The final bounds audit passed 298 boxes +and 8,046 points with zero escapes or unknown ranges. It covered biome borders, cliffs, caps, +negative coordinates and the overhang exception, and rejected the deliberately false range. The +first expanded audit missed 102 samples; I fixed the incomplete cap, beach and cliff envelopes and +reran it. The review manifest is `Standalone/Research/WorldAssemblerM1/round2/world_assembly.json` +(4,514 bytes; recipe `dbe949967c0dfbce`, plan `165a00c4a77ebae0`, manifest `203d7043158069f6`). + +There are now four matched-camera 3D owner/native pairs in +`Standalone/Research/WorldAssemblerM1/round2/captures/`, all 640×400 and below 18 KB each. Surface, +border and coast broadly agree. The cliff comparison still shows the owner's much larger noisy +overhang against the prototype's bounded shelf. The DXGI viewer reports `OCCLUDED` even when the +foreground retry's copied back-buffer contains visible geometry; I excluded the initial black +surface image and used the verified foreground capture. + +Surface point evaluation takes 13.569/8.085/12.396 ms at W1/W8/W16 for 4,096 points; owner density +queries took 61.974 ms for those points. The surface Core compile/query bridge took 36.295 ms for +45 exact queries without a render mesh. The existing +cave-only shape compile reference is 0.100 s. Full-detail rendering on matched 120-tile views took +2.882 s owner versus 5.227 s Core on average. This is not the 23.085 s matched-workload gate. + +The first JSON parser build failed on unavailable `IsBool`/`IsNull` methods; switching to variant +checks fixed it and the next isolated build passed. No crash, `0xC0000142` retry, new UECC folder, +owner build or `.uasset` edit occurred. No source files were added, and I did not commit. + +— Luna diff --git a/Standalone/Research/WorldAssemblerM1/round2/captures/border-native.jpg b/Standalone/Research/WorldAssemblerM1/round2/captures/border-native.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eb4a83d6fb4f7320f9e9dc2c586089f252316c51 GIT binary patch literal 10654 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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM1/round2/captures/cliff-native.jpg b/Standalone/Research/WorldAssemblerM1/round2/captures/cliff-native.jpg new file mode 100644 index 0000000000000000000000000000000000000000..506e876c0f3c12d92c5516ba062379f2424592a7 GIT binary patch literal 9147 zcmbVR2|QHm-#;^BEQ6#*p|YgW3@I{%l96UI_8CjvDP2*TA!TXRwH2v}vd1LGm>Ek+ z%Luo+*-3ZGAW5s`ingoLCh|UKEa|@g|NH*mm(MZh%z2)3p5ONUet(bg&hh8aYzGRJ z0wE9x$OrsF8KXS6NatbOK1$lXexyrK?Rp!l|KY!j_H8u4GdRpq5 zx|(WgS_Ey~CAg*drRs|emmBIY*VA9B4?hGUD<`KQuP_ILnWL|vrlJ3T{u!@=ROJy# z$Y>N|F@#h_pi~j#b&xg$A<$s8@Pa3Q5J;2^T2@Y80iy^8l+A{a2owq_g90YVfYA}) zJ|v@xo}+7GD?69AO>S`rc4bU*p1j_=%hhUh$Hhd#Z!Gho{%ZO}{Yww(s!Y$z%nD?cKNkz`;Y|M`PoT#UDSxPDxEm&p4Br z#o^`i1;T>DqT(x8MP=8@D=L4lsjaJTXl%OO+}hTDzvDsY!>;b0-oC&3UknVseE0st z@W)S|zkD5m?Lt7P3A3gx`-fesz%HbW3`#~0whMtg01lL@3|iMjc8)DgZd=IQ#Vcdv zvFnoaE>|n)nbL>Ud_yIec}s}hxOcE=Qp^4`!;bvFESom$hh1He5()u=hf;-Xps&~1 z1j8^>znbtfpEe#}_V(V$&F@=s-0wd2udxKRBUPe2etx!Xq9{-{8vaUi&P4 z0l0SNMK6MH!`J3)XvT2J*M7i8*Lkor(&ObR_s)G>`HP6%6dQvh6x~COeFhN^w;WLO z^7A-hC`zEnYp2sHm#ILJp5g1vhl@k6{6Q*Sf8`|MNw!M!8D>MUjlntl0an6Bi{jY> z!Qp=IOXOF|*?#r?z>-+SFc}Fr>HutoxbVRn*G}8E|=S`#khxFFxzlxG|mPr z!VPVtdm{DCSNe<2$aN`f+2vEdPot}kH9mTuAM$CmD9sI39${nP4X%YWlFDPS66dG2 z@3A002ekJz!LeF*mLm;TA0FJ*brxfxH()gO>}l?391aVC_)grRk|FpNm{njNokmiG zrzXN9)7+46*FH;v-#0aZ;CZUMt&}TlEq$uZkp1lg)secj`>(hIHkdN)rS6q&3rEYT z{Y$^SdhkKJxgy}I@F}(6<-7-yh;N5bF|Up`jc!moXa9lB<9+qM7sbokLp)&q?(w>6 zUOFmoQMCqkwo7Y->-DbxcKh3^R`1|TC$~uFqTwP?6@bjl6wBj$HGnwzm8_IFpNgD zJb%|GByno(dlleLZO5-i!TNNtIQ{Yi1a74f*5g3LDax(H>5NYK?Y;pAGs;e|_6|Dux%P?UH=DYo{L8bSoW_l< zj2s*tht3^FAKKS_$Hx6icBv{;Y@W~|dj!15Ka~i-^|6HGvz#ejMXVW%*NZMpO71HcHwCeyNR^~^qk&>jYuGvB*14wru4#3# zxVj!PtM^LMy1(tjTY>-~TwEZ2%JGb8I`6DS$Seqv+4Au`)$#ds*SBIrn$tO&oeJW8M0c*jJJf^kRfdJ-OMQ4ACX_EG4`cheORL#)1II zGI=S{N)q5&ERDjPOhdjv3@$mQBsA=9356-WbbuHHLKyhYKcNLd*2pJ9V1^hGEco@t zT2Yq3{ewyg^xJ1?dGw>PG!o`QQ?-Qsdy;3o+XlO>uXq89#jJVdFAs5yH-#H_$M-8A zbPiuyb8~~qlR=lr!4JJ7TIx}HTRM+FUG+N1!E3)YikQV0>j&PoiW6%H`$1e@xB6!? zlFE?UoZTehZvH)iv05%NYr)*=!3}ck(3loYXjAnZpW!0!Bkh~7Fq1MC?&r$*Ta$I% z#whZ68`%dfdT&CMs=f6l(e;+qcSGvU8a1$0Clc3Jy)awedWehhRlo63ZQDUL%~G?K ztq8t*2tC=U9;HWYHCRV5Asu(t!G6MK3X#6~)*H@w#_qqawrN9>h?`QG(T(nr9WeT!d zpM~e5Co-PT1D1+t9H$l}92Jo8{?YT;crNl^9NWlPHw1h*+&UP+B5eE{j=>+01b*h$x zHa75*I}CSs_*%l+M1hm$c*ZL_j zfPK;+H(6%B#UKggbA*pnk4GsoaQtRzh`N9Id`VO(|Y&HKtUDyM9FuFE1_IW3(r~Yg4biSKv z>%oB-j8cC4n|Rjd&_m_f&v#x`8i&?A@Z@~Rjh|n$$R%*TjZy3FUq7E2he|lZj1Pf^ z&gb?%>B0J^uG7zgPm>wa(w6?Nn!CN&)z0N!z{!IzmGM3-Xe7AjtSgNJxrizR4a%#r zhgvn&?I*(6b>jmq_%O9h2% zN>U`_fHSCoVWlS%5Du;=EW8jv1|85X4qF9sS>Q8|?SMxxCw)wSkw~n;=S9zhS$7ui ztRr}YK9h@vi$eGbDTAr z?ep5dsd9#1epEsHs(2Hvv9Q6_`l-lF8kk1j~YIu@-yI=E#x`LIWq=WS7D!#MPE z`Kon{$a1J{Qhw~@CetY;7 z%P?oirRdYxqHW1nLs-WjSv!nF3ZoUHhTPKSw2=KfeGhEJI1mgd*e-Xf6EXu%0A5BC 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

nOQ`iggzEN(k7C8w&X~Khp}kY3 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 wp#AMGJ?IXP^1~ z==XC#mgq!q0$?y05G?oszvlo4KtvdUKnRNnPNE_rqT(`0aWOIRO;XYlGMhIkDsJAS zprE9xtD&T#t)ify0cvXNZ80!3P*OKCG152D)i==pqXZ@*Dk?4}E{{aY>nkfL>;J>Y z?_NMo3|0tF6N2dga5Kv{|A9+RfD0}xEF>)Y2Nw)}L?A+P!U%1Qi2N>Z z(U7=JI%er&DEp%N_S@pR=A`!uq4DF$&3f1uTR!}O_Aki(-+*QOmyrECu>Zg{3rGsV z1jZAR19k%6u0Wts0w$8#khU2j8p8g6Xt3(w#pT6mNW+HznL;H}0T84a0u9jrOkpwq zIfegC)BJM^|J$qkpHukHDg5uoM>E?>oY{{rJuqJ^ zB5=FZA(ZdrNyUdjMW5v>-@U6#wZ5U4(di_5zq_+wEY|qkppSLoZs&U@1ApNU``O7% zhYsawcMEMC8_Gx(f^K(bMAI0mxwW8Ti{)(vA~6tyonAQXjrYoc4C~M}|7F!FfVd!=R9)tlv>lsx-_ zQO3wr#;TuQzDRY;=of{v>Phs6t<8RcH4EoGVh(6?hMJc^b5zFk6wmQC(ER- zF6X=%)ygDH863bj1$B9Ti*R0Rv)L>9I-%&M(%ajAZ_V6UIFfwQe3IXi*Z11%wQJos zxfkoD+_LF#ls6vuuM2MBdHe|kfTA>GU;ilhw3$4Jsm^_1M0|E<_U^z^x|T+V<%WZW ziFaW8Oyyy}kb}>!?0Ebv{VnR)FL&DQ7sfi6AGfgsQ_O*n=buFClq$a}T^?VIX>YW$ zEe(Iz#H(NapvJC|PB>Q?yODSjo3Aj3A}Fon;q?!9%k8-In~8q|B6|d2^`2Q z^u-kUl_7Q*JCxSGq3PK-qe7g3Wl>bOg(Y!!r*4lwzBK3TuF&; zF88kv=4^zGJ*@b8rmu^!qfN_~F?g;x*g9o<{V$`A8j7*Tl3CUj)8)oCg`P@pJPPo} zORTB=rT1Bl0T{M|AtzUrx5oN!x7R+P$-lsR@g&Jt`*3S|{>goFKUaf_pXA>kYPc$q zDi*z_abQ}mIwAm-x^=qL4t6u_$Lk+Q`*&sq6s&U+wXWtl?ato@tw*QAXe++~O~r~$ zDJHV1)T=k8#&EzKG4ApG$CK9Yjb<+21IHT&=fa#4mK!Zzf8J0Z*NPcmIkeyEp2?e& zi7Yj_yVpYVbb&Cjx>R(|-VV*n??%?r$Io)%$bwt8eG6Os?f6s<^(^t1rdpR-l!3 zC$!qq_p-=FyL+ye+LgGMvo#a zO<|Eiv6@t|y1(w*brLou^l?C%(WU!_4G{2;%hkVymNQ=5>Wn30oE~NQhnuISyx&Na zFNUtE_$5jGb-TkoM(xWNRj{?&OdkLv1eI%_IiX#YS87vbdw1b876=kYEX;T$ zL+pF0%@Re&?5lO*ES{Qouo4PsY75?-ATVJ(krFJwHxVmI#ki>2a$X)`X>AgrlP^7; z?AGAE;T<+(#AQgEo`?#rYRdKd(NI&lTS=uNhkF9=Srd`_YJ+OOZTrf@4;S{V^^(O? z$2x^EwG$l4ohF?tcGT~r1AB?x!*MgklL4=MH-Pw_p2;-3BK)~Y20yd+yJPsViwSlz zA1u8Db8;GfwrjSv_F(h*Yf8Qwo5$+=vCGedlncv8m!e4$Mm7nR#v!@inZcU;HeuF` zt6z7}nR8?UR1%}=j?`IF&cs`C*w3L(c+g$e1JK+F@26u>bLMK^Q*IS%!m~`&T=Oa& z%4N7QtYmv;wndKMOI#UNLX{^YN48_Du<<#jKgSsb`g#5NUQ2o-`m`!+Ph;uvJ7c$w zo`@VlpQ}7U^EWxVdyDawMEQR-j4B)xd=3O}#&+is@#JmT5LQQi!FJ{1kmpJd_Qk{BVV zZ-@u>!|qycNu2Hm4lbzAFTbItkS5M-Sum(1yYozF(vb_&hj>b;$3-~oH=*hzP^{64 ziH$!~=|(2pZi*<%jyi92`fB0YE>!6oqr83WP!_h&&21lwOTKi9rw>kg2dhFD4{WKw zAf>ZH9uqOHt`K}aluk}-L}hPBEoz#6=g(I+E@@JWYUK#7gh2W~78bg87}wuyPomnB z9>`0!{rdZ}^R17}S>hX7(9C=3oF&U?lad98Gl?Nz3clQue)cQsUG-$(OUNSMLM#{F zlAk7>(K2-Cep21Yp$EqwEUj+a!tmBm;948 z=wgL&G$w3dXO=(3c7z}q*YX_hx#+4RU4yWI2mKI}^CUBp8(tyNCZfciC_q`KjlAVP zCC?{3pGG;Nfv|0N4ByWfCz~g1afF{scM*H*ZY(h4E5(R+pLF^Wj(H;;BF|Ok?*sr&%k9N(oXdOVCPE z=u1>>o^h^lEj`}r!d8j12fMNUCR&zFAbXx3dIrJ6hVwr$LtH`_G!$t(xcezhiU8Sj zVVgm;J(yjIP;QJ-jl2|OI8P@f;4|h6rC>C}A*Q0F%AV3#)*x2WZJa@MAoeP=$ZIlf z+sd5p+j0NRr332zHLw23t^b)EIGXBM;Zu=P4o%67XsNZY?@cb}`-Kq~sM00d+K%c5 zF~xQ<_P#6F^)8wp!&h}Z%)NXjJkqq~*68Dj^^vW3c;MU}YKTV!lxDMn}OEUC~^{0-{m zr_KojLZ8?rQX+fY?jIVs7&2X-uXwUih<~0Z`=l8C5tKNEGxF~p8EtoTA_JW7KKE|b z!SHT!_x3B>ahJS0J5HArHpfk$)m16pf;h(RNT3>4U2&G((t!_hOD9MkZAErAI9f?c z6hX;#O<7G8|FiVD71s_X&%{tpmYBr1nXGI?y%A11vHS>J@2(U3j70jBcGo!u8qSdB zp{>F;hI6F(Jg(%K7?ar&RA{=~dN?0L>JdQ?2fD8Q4N->NpPl?Iaj&s+kA>gEVN=+mZ z43q{Vc|xj05O1ut>3{cQhaH;*n@G)*|93y8zib0gKA%5V_-)-hJ4-vgF~5nLw4wO0 zY4Px@U9XLVHzHVq%7RWcYU_G$a{lK?T(qdi_RTx%T0b1$`TAAVE%(CTz-i9sqIY(_ zhYtHOouBXCVitb&T1EcZvQ$>knZ9JiEu-KL*@Pb6Ese}zE(9;C;teV3MBQq+(q!C~%v z=VtXmH433C42fPdwhuGt#AYREznwX&U+9nzEL{;6Y9+gAty8CTR6&PWQG*hanbq?+ zU&lb&oKzJ3EIMnpvRPT;aBDty>&dCeatF1eL8Mbl=MVI<-`;MMH$Q+1tp$N5n2$?C z1*(oTp9XVf5WzIA3KfIPUV?(WA%GaqaP!7VH)2b@;Nt{5U}DG|QLaM>B61`f?(Hh5 zNkSn*ST|GW9Kl6wm>x~670 zWSe8LW1HXQmqV9p-KwS9OGdPe#;NT`Q>Yd1Rwn+k{PzCV18&=OM@oy1x0 zIJPx5Wa2q_l^pkTBx>v7pRj-)!n=WRjI7}uy3DTLrkAL!wC0&rCl`NY-U845<5WOX z@4}eb-IA2)#3!r$%q7mVy{9yo>)ubB`@_qA4q4geg!nW(I_q*xrpo5@mgh;-M^{QB zh3d}dK7KDsy%GE9$CMy&6mEZ)i0c!{Sy&SvzFWI~evS zmf`jrrTwYNr_U)!4B<}iA*L^UX|5%5u+I-;{g3D~4mGw#1O*j~i$VwZ=JD>hnVn;l z=F=St4!RY`i<4z``MoGWB4qLD)d_x~x$olw^+F#xNLyKi;6_mj14REEi}0X!4QC=_ zyG>7Xsm~yW66*T?UYb>FyL&CW{kFvpk}S`5h4rQL-LW<*9hZ@{yVexbN32`) z`lgwiWT-)zOIw4zp){f`?X@9mxUaHAkR2l9JSD+^{AG+27!O8XmZ-lLIp;ChsFP>E z96RgI-2jg^dW{wAQ$>3i&et}KMU1ZyI+*kMhLc#GM}lPI%~Q!fl>%<_xlW36s;^h9 zaR12jJ1B6>2J zioOsRuiURY%WyD`RN~}qP9c*OLFYte&QqDFQ~h_^%nrafGwc{_NR|!Mx8};ATbR&t zPewMc5mnMO@mg4JFW3aHAwa-E{c^ztrw?L0m~|p@?sFt>NW|XvEL06efG`b1Zvpop zTq*Js@J!(Lk%0xY&fPEUYpL39Ll&M zSa*PgO5W)v49~Asw(z zc-Vr#Qgg9?zk5KwH6;8lYIaH|5J5Z%f_JL4EdTbrC%v8l1)D5=YT8N%xz9m=6XIo+ zqWyMdNa!g199p9I^vI4NN`kvi1zMuxSq)>aC9=7p2D0)>*epC>v$WJQpYKc*{4<*= zB%Q{SUrpzqh+LEoeZ>HGj8l%NxG{q2?+32oH6mM%e;*%hQ>(upw1zOKM6&~@>78ng zOUBAL=Gu)_jvhZW-$#u%Rbsm`_iHtGEBD0~1K674_VJsXZ`l?1KUwU^Tr$Z$0ffj|$?NL>j}A$pJDa!NHoc&~tN)7ra$##7e{a1_*g zLm|{EAV{XTCc`=eyu5<71wi#5@EUW*m@tzK)#E)B8gIa)o+%FH=ZXoUhnenxt> zQ!P5Z#LM*F3G~s)w@X}??7KxiUGQ`zmQkkXo~T^aF4VrW>7=(Kc361jsqC%sQ6w~8 z88xlcBAYfjZjgN1Prr!_MdD8ZJ`HvBC6=gyV{y6%ys}`_7F;McvnI=uIT3V@w|z7R(m&^Ou+pFHyo)fu2^VRj>8K9Xq((WpnUK2gwkp;?MgjC!C> zr7y?D9kpAmS&tM=D6vp=vNgi%sJT5kZ$tXo*SS0VbMuTXelJFib>LFt3E~h$Z*Ge7 zhc#mFV8-}MEenrMpzcqk*CE*a68Aa;YwvYC?kwd;-)r21Xw5-#I=i~$D>9yQ(eiH; zd6LvJnI7jR(lW6;q8}-%61@%dpKsVP9;|eiL5E5qngT(*!wl@^U69_|x`9@6NQW605P9Ipg7`}4=cCo(2}cr{e!IDrA?9>h{( zjO?4RVeZ%#arrNl)broKbY&o+?iaS z$4*n9B-46Bi_^vJ8E&NKV?yaoWQ3)FgSjA#O%<_xnmZ}m_0pz}pspRVvKzo<2|ZDyoxSMgia#M_Amu|dDQq5FFvs+{xCp{+UKXKFi%R8 z+|Mk9SC9S{d`+&*DCExceln{fV?VV0`0@JnXIe3vOU0f4{11Et>0ec87LG?x$@^+#glYl>zS>P4ehDs=~^lDZP;E-gw0 zy&rJ${OxT>X_0yH0X@lx3~{SEK*%LNnbgjcs9gu>hi-sA(%tNm3V8KhX)f@z&69nR zNam+)Y9%o?7k&2<4ku?j6Jw7xAssY|lUgx1qcSt3qg)#WF&h%Li&+B%bj+N(k7P2RDH|(M3Hm7)~H{MVy@@1S8__Yzm*MAu&U|I^k z`Y$o=*fnGHih4eP?+rgNs%N@&0F28{%sq-2GYpt5I+x8x`j_ameljyeZ<%F#EC>`vx zh;6ufC1Vw%emCS!&9EoMuI`iQW0J%db>+W?Scjy{d`)rutt2;}{ozNqK65KHckAkQ zZN6KS<8{qX2r$b6m^*Gakmi#y`5B>mAgW2~tzlWzNWEEjl&B|Qh7$0%a!ObDd7$#`G6*NV@uo|QH z*RuozPeVF*{2BkB&Ss_LD_!-C(E^nyQr1BSSMGPsVL?tS{gpGe;d3@nutirVG7lw9 zukS3=pI*lTr6hqiSMG1%@P-di8d?#ZtT~*aIyR=AU0f{~_O`D2x=@i81v@bC%~9cfcZSS#_G<#i!~e}V#zRV5tAv4~^>v7bytaqJ8z{3|UtE|%rA4IH33)4- z_veOd7F~9p+VF`K9{yx~OMwzrB0#~B+dH;FkwR7Nqt;5{>Tg|5MHe}w9-fZXKV87& z8A|<{@R)t}#6qxQ9Pyaqrbo}72mtqQtkj#fdly$-7Vz8d@sgkVRf|_%e5}1$)=(s+ I`FrMn0h3~%S^xk5 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM1/round2/captures/surface-native.jpg b/Standalone/Research/WorldAssemblerM1/round2/captures/surface-native.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e3dc802eab4b63b06045d01411bfdb38de0dd33 GIT binary patch literal 9774 zcmeHr2UJsOyY5aPh*3avhiaq66krSlPyrLcPyzvxKtKjZ3Br&lDq^2e0Y!1so5mo; zR6_9w1;qACF{q6ELV^whj0M52qYM@l;e5e!{&VkH>#lY0J#*JOcde7GUB3Ob_kF&% zPxnthgyu2*0{tKq3I%Nhe`tCT@`ViaF&K=#0l3XFFqk#l1UuW%aP|Tt<2fc~3(U>U z7T|Ce)^>|6tZb}sxW&XJHuet72+J%Mk)6qSXFL2dylxSc!K_)c4QJ2CV&~&6ahCZ1 zb||n+&_86`V1&^J!?qaeCj{yYhFW&&HaWTUc5- z;Fl4IB&wUc$BLD!eAhDk`~w1+-0%oqWYoIoxJ~g1KPC!zRCd}CAdg_et#+B>eRJ8#^))uqw)-M#l~|8E12h98fN zK6(1=`HOdBe~!QZ@bS~cXB}TCNbeh4|K#jH@MQ}4LhI}6>Ce*fg+lKEm!7FU#)e`r ze+_$9Z1MtImo!7zr{GLO+iW{m&RZORM=#dQp8Ck)osP8`&i?lpOaH&(?4OMNJ6{9P zTs;&xJUvs022C_0L~;rxQ6@<>Lzo2A{|_(nN_63g!c;6l@?Z0cW=2CqqJRKNkpG&O zT=w4({wI&-zajijOZWd92+3EDv9vsUbmxLrn#+iGs!fr5>AS1osTFN2I$Kgjz)3K)M)vEC8& z{o$9R$PY99Za3e<)pNF2Ug$}<>YY@FJM1Kba@Uw<=V_GdYq}(V>*1fg=XLDWl{?=B z1zm)Qn_Uf_t^LtJId$tU?b(v#Y3S!j`6h7~>0tdAli%#RY3JNRKhGLE^uXX0zO2fR z{*!gavWoW!S84Dm>Ven~CJA}YUvN(+C+(pZ1U6)9XswmQ{}B{10onJ(mT(WS8A< z)KRu6&tttcF9A!P7&{P&mQA(ZJM^>-U@D)6tj1~LBC+?C%d^h=SD2W*v`_nBZuveQ zbgc98uC+`0V!CbDF1v|eD}U~5pC-(3zBJ^fPYF{U^a_u|30amzIH zccc9QzlT^q|H@wvM*VW4eL_Sy*J6JU9~*6gJeL-IH!^GJ80)kXu74V8w_fs9 zf63HWlXr>>haW?a!+!p7+jmlUwb8TamR-K&!r961syc4tJu{}EO|NMMwLfKT&Po_e z)-PD{p%3Y0y8KqC{XV~!f%C!s<$j`aTynUlXO`05Xmr^Z=>#|0;C)@xwZ0qLHzuRZ z9S6w2T)Y?WaDMUizKVo{w<7yJ>ih;(FEG2Bw~k=*zkhs{==40WrAP0|;h>QMmhk~W z+w&-A=iA)$9;-4j#A+ELavpHd&7(O7Y?6#@~b%MYsKWx(U-panA$bP zS0V3X7MJ+^u~xgyIKC+&WiokEIbJ8-Fi}vd?EkZx8O?k_`OBgT6JN%xGnFQB8HZAq zd^tS9{h&Rw_Sn*UNraocz5NAn48 zv-Z)3A;#43G(_4>zP&1-S(Bt{2t*7R%|%H;NN||`_lfF2ghDsvXNQ+24$@6u%LhZ% z&IGh~;}#XgkAN!0EM7k|VBgLM&Pqw@(%<|B8g9rfHZXn3Nm5+goacc}iD&>-=|&kb z*l;&kzu5{fnH7)kI#HNrI>9gc1)dTRQ0431D_*(b6Dtkt6V^yRC_&XgY4I!!j_VzP1gWgrv|KbyVLPHmwa&JNSxD}^W7 z2zq3%3wlihec)Fx2H^x;z#V4!gIG0)PV4OBLD+&ay#H;7@A9j|Z+?<>B}BL*V%JQ1&~^%zW= z?0Cs}ea8Rx>c;DPuR83veCu2`4bd*F_cos-nZI3hG44F4_`8k0=tE_@z66NI&ZkQ* zKlntCrTqyPO-<±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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM1/round2/captures/surface-owner.jpg b/Standalone/Research/WorldAssemblerM1/round2/captures/surface-owner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f46b0df1c2a2d74e37da3fd1af55f0c755259d44 GIT binary patch literal 9089 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 Kp84C%-}w)}$PZZn literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM1/round2/world_assembly.json b/Standalone/Research/WorldAssemblerM1/round2/world_assembly.json new file mode 100644 index 0000000..20c49ec --- /dev/null +++ b/Standalone/Research/WorldAssemblerM1/round2/world_assembly.json @@ -0,0 +1 @@ +{"schema_version":1,"content_hash":"203d7043158069f6","recipe":{"schema_version":1,"evaluator_version":2,"units_per_voxel":256,"seed":0,"biome_cell_size_q":204800,"biome_warp_q":64000,"biome_warp_wavelength_q":142222,"biome_border_blend_q":30720,"climate_relief_wavelength_q":853333,"climate_moisture_wavelength_q":640000,"climate_relief_contrast_q16":131072,"content_hash":"dbe949967c0dfbce","biomes":[{"id":4097,"base_height_q":-516096,"continent_amplitude_q":128000,"continent_wavelength_q":256000,"mountain_amplitude_q":0,"mountain_wavelength_q":128000,"detail_amplitude_q":512,"detail_wavelength_q":6400,"relief_min_q16":0,"relief_max_q16":19661,"moisture_min_q16":0,"moisture_max_q16":19661,"relief_strength_q16":6554,"relief_contrast_q16":104858,"relief_wavelength_q":2560000,"height_warp_strength_q":12288,"height_warp_wavelength_q":32000,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":256000,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4098,"base_height_q":-484639,"continent_amplitude_q":128000,"continent_wavelength_q":256000,"mountain_amplitude_q":128000,"mountain_wavelength_q":512000,"detail_amplitude_q":512,"detail_wavelength_q":25600,"relief_min_q16":52429,"relief_max_q16":65536,"moisture_min_q16":52429,"moisture_max_q16":65536,"relief_strength_q16":45875,"relief_contrast_q16":104858,"relief_wavelength_q":170667,"height_warp_strength_q":12288,"height_warp_wavelength_q":25600,"beach_width_q":3072,"cliff_strength_q16":65536,"cliff_slope_threshold_q16":19661,"cliff_sharpness_q16":327680,"cliff_sample_distance_q":5120,"cliff_max_displacement_q":2570240,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":230400,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4099,"base_height_q":-516096,"continent_amplitude_q":20480,"continent_wavelength_q":85333,"mountain_amplitude_q":0,"mountain_wavelength_q":128000,"detail_amplitude_q":512,"detail_wavelength_q":6400,"relief_min_q16":6554,"relief_max_q16":26214,"moisture_min_q16":32768,"moisture_max_q16":52429,"relief_strength_q16":0,"relief_contrast_q16":16384,"relief_wavelength_q":170752,"height_warp_strength_q":16640,"height_warp_wavelength_q":32000,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":256000,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4100,"base_height_q":-385024,"continent_amplitude_q":20480,"continent_wavelength_q":256000,"mountain_amplitude_q":20480,"mountain_wavelength_q":512000,"detail_amplitude_q":512,"detail_wavelength_q":25600,"relief_min_q16":64881,"relief_max_q16":65536,"moisture_min_q16":52429,"moisture_max_q16":65536,"relief_strength_q16":45875,"relief_contrast_q16":104858,"relief_wavelength_q":170667,"height_warp_strength_q":12288,"height_warp_wavelength_q":25600,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":230400,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800}]},"plan":{"schema_version":1,"content_hash":"165a00c4a77ebae0","strates":[{"strate_index":1,"top_chunk_z":-16,"bottom_chunk_z":-79,"cap_height_q":-149094,"cap_undulation_q":0,"cap_wavelength_q":64000,"cap_detail_amplitude_q":1536,"cap_detail_wavelength_q":6400,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800,"water_surface_q":-505610,"water_enabled":true,"content_hash":"5f409a7a9653937c","features":[{"stable_id":"09812c26310b90c9","type":2,"min_q":[-181248,97280,-434134],"max_q":[-162816,117760,-429782],"parameters_q":[0,0,0,0]}],"route":[]}]}} \ No newline at end of file diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index 9a65835..901255b 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -7710,12 +7710,26 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; std::int64_t WorldAssemblyFixed(double voxels) {return static_cast(std::llround(voxels*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));} + std::int64_t WorldAssemblyQ16(double value) + {return static_cast(std::llround(value*65536.0));} + std::int64_t WorldAssemblyWavelength(double frequency,double fallbackVoxels) { const double wavelength=std::isfinite(frequency)&&frequency>0.0?1.0/frequency:fallbackVoxels; return std::max(1,WorldAssemblyFixed(wavelength)); } + uint64 WorldAssemblyFeatureId(uint32 seed,int64 x,int64 y,uint32 kind) + { + uint64 hash=14695981039346656037ull; + const auto add=[&](uint64 value,int bytes) + { + for(int i=0;i(value>>(8*i));hash*=1099511628211ull;} + }; + add(seed,4);add(static_cast(x),8);add(static_cast(y),8);add(kind,4); + return hash?hash:1; + } + bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root, std::string& error,const std::string& manifestOutputPath) { @@ -7738,10 +7752,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; const FBiomeContext biomeContext=owner.Manager->GetBiomeContextForChunk(chunk); const std::int64_t bottomVoxel=static_cast(bottomChunkZ)*CHUNK_SIZE; const std::int64_t heightVoxels=static_cast(topChunkZ-bottomChunkZ+1)*CHUNK_SIZE; - const double baseHeight=static_cast(bottomVoxel) - +static_cast(heightVoxels)*surface.BaseGroundRelative; - const double elevation=std::max(16.0,static_cast(surface.ElevationRange)); const FBiomeMapParams& map=biomeContext.Map; + const UVoxelStrateDefinition* definition=owner.Manager->GetStrateForChunk(chunk); VoxelForgeCore::WorldAssemblyManifest manifest; auto& recipe=manifest.recipe; @@ -7750,34 +7762,140 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; recipe.biomeWarpSubvoxels=WorldAssemblyFixed(map.WarpStrength); recipe.biomeWarpWavelengthSubvoxels=WorldAssemblyWavelength(map.WarpFrequency,556.0); recipe.biomeBorderBlendSubvoxels=WorldAssemblyFixed(map.BorderBlend); - VoxelForgeCore::WorldAssemblyBiomeProfileV1 lowland; - lowland.stableId=0x1001u; - lowland.baseHeightSubvoxels=WorldAssemblyFixed(baseHeight-elevation*0.15); - lowland.continentAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.45); - lowland.continentWavelengthSubvoxels=WorldAssemblyWavelength(surface.ContinentFrequency,800.0); - lowland.mountainAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.12*surface.MountainStrength); - lowland.mountainWavelengthSubvoxels=WorldAssemblyWavelength(surface.MountainFrequency,192.0); - lowland.detailAmplitudeSubvoxels=WorldAssemblyFixed(std::max(1.0,static_cast(surface.SurfaceRoughness))); - lowland.detailWavelengthSubvoxels=WorldAssemblyWavelength(surface.DetailFrequency,24.0); - VoxelForgeCore::WorldAssemblyBiomeProfileV1 upland=lowland; - upland.stableId=0x1002u; - upland.baseHeightSubvoxels=WorldAssemblyFixed(baseHeight+elevation*0.20); - upland.continentAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.80); - upland.continentWavelengthSubvoxels=WorldAssemblyWavelength(surface.ContinentFrequency*0.72,1120.0); - upland.mountainAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.42*std::max(0.25,static_cast(surface.MountainStrength))); - upland.mountainWavelengthSubvoxels=WorldAssemblyWavelength(surface.MountainFrequency*0.75,256.0); - upland.detailAmplitudeSubvoxels=WorldAssemblyFixed(std::max(2.0,static_cast(surface.SurfaceRoughness)*1.5)); - upland.detailWavelengthSubvoxels=WorldAssemblyWavelength(surface.DetailFrequency*0.8,30.0); - recipe.biomes={lowland,upland}; + recipe.biomeClimateReliefWavelengthSubvoxels=WorldAssemblyWavelength(map.ReliefFrequency,3333.0); + recipe.biomeClimateMoistureWavelengthSubvoxels=WorldAssemblyWavelength(map.MoistureFrequency,2500.0); + recipe.biomeClimateReliefContrastQ16=WorldAssemblyQ16(map.ReliefContrast); + const auto makeProfile=[&](uint32 stableId,const FSurfaceGenerationParams& params, + const FBiomeResolved* climate) + { + VoxelForgeCore::WorldAssemblyBiomeProfileV1 profile; + profile.stableId=stableId; + profile.baseHeightSubvoxels=WorldAssemblyFixed(static_cast(bottomVoxel) + +static_cast(heightVoxels)*params.BaseGroundRelative); + profile.continentAmplitudeSubvoxels=WorldAssemblyFixed(params.ElevationRange); + profile.continentWavelengthSubvoxels=WorldAssemblyWavelength(params.ContinentFrequency,1000.0); + profile.mountainAmplitudeSubvoxels=WorldAssemblyFixed(params.ElevationRange*params.MountainStrength); + profile.mountainWavelengthSubvoxels=WorldAssemblyWavelength(params.MountainFrequency,500.0); + profile.detailAmplitudeSubvoxels=WorldAssemblyFixed(params.SurfaceRoughness); + profile.detailWavelengthSubvoxels=WorldAssemblyWavelength(params.DetailFrequency,25.0); + profile.reliefMinimumQ16=climate?WorldAssemblyQ16(climate->ReliefMin):0; + profile.reliefMaximumQ16=climate?WorldAssemblyQ16(climate->ReliefMax):65536; + profile.moistureMinimumQ16=climate?WorldAssemblyQ16(climate->MoistureMin):0; + profile.moistureMaximumQ16=climate?WorldAssemblyQ16(climate->MoistureMax):65536; + profile.reliefStrengthQ16=WorldAssemblyQ16(params.ReliefStrength); + profile.reliefContrastQ16=WorldAssemblyQ16(params.ReliefContrast); + profile.reliefWavelengthSubvoxels=WorldAssemblyWavelength(params.ReliefFrequency,667.0); + profile.heightWarpStrengthSubvoxels=WorldAssemblyFixed(params.HeightWarpStrength); + profile.heightWarpWavelengthSubvoxels=WorldAssemblyWavelength(params.HeightWarpFrequency,125.0); + profile.beachWidthSubvoxels=WorldAssemblyFixed(params.BeachWidth); + profile.cliffStrengthQ16=WorldAssemblyQ16(params.CliffStrength); + profile.cliffSlopeThresholdQ16=std::max(1,WorldAssemblyQ16(params.CliffSlopeThreshold)); + profile.cliffSharpnessQ16=WorldAssemblyQ16(params.CliffSharpness); + profile.cliffSampleDistanceSubvoxels=WorldAssemblyFixed(std::max(0.5f,params.CliffSampleDist)); + const double structuralAbsBound=static_cast(params.ElevationRange) + +static_cast(params.ElevationRange)*params.MountainStrength + +2.0*params.SurfaceRoughness; + const double cliffAbsBound=2.0*structuralAbsBound*params.CliffStrength*params.CliffSharpness; + profile.cliffMaximumDisplacementSubvoxels=WorldAssemblyFixed(params.CliffStrength>0.0f + ?std::max(1.0,cliffAbsBound):0.0); + profile.hasCapProfile=true; + profile.capHeightSubvoxels=WorldAssemblyFixed(static_cast(bottomVoxel) + +static_cast(heightVoxels)*params.CeilingRelative); + profile.capWarpStrengthSubvoxels=WorldAssemblyFixed(params.CeilingWarpStrength); + profile.capWarpWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingWarpFrequency,100.0); + profile.capUndulationSubvoxels=WorldAssemblyFixed(params.CeilingUndulation); + profile.capWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingUndulationFrequency,500.0); + profile.capDetailAmplitudeSubvoxels=WorldAssemblyFixed(params.CeilingRoughness); + profile.capDetailWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingRoughnessFrequency,25.0); + profile.capRidgeAmplitudeSubvoxels=WorldAssemblyFixed(params.CeilingRidgeStrength); + profile.capRidgeWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingRidgeFrequency,50.0); + return profile; + }; + if(biomeContext.IsValid()&&definition) + { + for(const FBiomeResolved& resolved:biomeContext.Biomes) + { + const UVoxelBiomeDefinition* biome=definition->Biomes.IsValidIndex(resolved.Index) + ?definition->Biomes[resolved.Index]:nullptr; + FSurfaceGenerationParams params=surface; + if(biome&&biome->bOverrideTerrain&&biome->GeneratorType==ECaveGeneratorType::SurfaceWorld) + { + params=biome->SurfaceParams; + params.StrateTopWorldZ=surface.StrateTopWorldZ; + params.StrateBottomWorldZ=surface.StrateBottomWorldZ; + params.BoundarySealThickness=surface.BoundarySealThickness; + params.BaseDensity=surface.BaseDensity; + params.WaterLevelRelative=surface.WaterLevelRelative; + } + recipe.biomes.push_back(makeProfile(0x1001u+static_cast(resolved.Index),params,&resolved)); + } + } + if(recipe.biomes.empty())recipe.biomes.push_back(makeProfile(0x1001u,surface,nullptr)); VoxelForgeCore::WorldAssemblyStratumPlanV1 stratum; stratum.strateIndex=owner.Manager->GetStrateIndex( static_cast((static_cast(surfaceChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE)); stratum.topChunkZ=topChunkZ; stratum.bottomChunkZ=bottomChunkZ; - stratum.capHeightSubvoxels=WorldAssemblyFixed(static_cast(topChunkZ+1)*CHUNK_SIZE-4.0); - stratum.capUndulationSubvoxels=0; - stratum.capWavelengthSubvoxels=WorldAssemblyFixed(512.0); + const double strateBottom=static_cast(bottomVoxel); + const double strateHeight=static_cast(heightVoxels); + stratum.capHeightSubvoxels=WorldAssemblyFixed(strateBottom+strateHeight*surface.CeilingRelative); + stratum.capUndulationSubvoxels=WorldAssemblyFixed(surface.CeilingUndulation); + stratum.capWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingUndulationFrequency,250.0); + stratum.capDetailAmplitudeSubvoxels=WorldAssemblyFixed(surface.CeilingRoughness); + stratum.capDetailWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingRoughnessFrequency,25.0); + stratum.capRidgeAmplitudeSubvoxels=WorldAssemblyFixed(surface.CeilingRidgeStrength); + stratum.capRidgeWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingRidgeFrequency,50.0); + stratum.waterSurfaceSubvoxels=WorldAssemblyFixed(strateBottom+strateHeight*surface.WaterLevelRelative); + stratum.waterEnabled=definition&&definition->bHasWater&&surface.WaterLevelRelative>0.0f; + + // Select a deterministic steep edge in the resolved owner field for a bounded shelf. + // Selection d'une pente deterministe dans le champ resolu pour une saillie bornee. + constexpr int probeSide=49,probeStep=32; + std::array heights{}; + std::array valid{}; + for(int y=0;y((x-probeSide/2)*probeStep); + const float py=static_cast((y-probeSide/2)*probeStep); + float ceiling=0.0f; + const std::size_t index=static_cast(y)*probeSide+x; + valid[index]=owner.Generator->GetSurfaceHeightAt(px,py,topChunkZ,heights[index],ceiling)?1u:0u; + } + double steepest=0.0;int cliffX=0,cliffY=0;bool xAxis=true;int downhill=1; + for(int y=1;y(y)*probeSide+x; + if(!valid[center]||!valid[center-1]||!valid[center+1]||!valid[center-probeSide]||!valid[center+probeSide])continue; + const double dx=static_cast(heights[center+1]-heights[center-1]); + const double dy=static_cast(heights[center+probeSide]-heights[center-probeSide]); + const double slope=(std::abs(dx)+std::abs(dy))/(4.0*probeStep); + if(slope>steepest){steepest=slope;cliffX=(x-probeSide/2)*probeStep;cliffY=(y-probeSide/2)*probeStep; + xAxis=std::abs(dx)>=std::abs(dy);downhill=(xAxis?dx:dy)>=0.0?-1:1;} + } + if(steepest>0.05) + { + float ground=0.0f,ceiling=0.0f; + if(owner.Generator->GetSurfaceHeightAt(static_cast(cliffX),static_cast(cliffY), + topChunkZ,ground,ceiling)) + { + VoxelForgeCore::WorldAssemblyFeatureV1 shelf; + shelf.stableId=WorldAssemblyFeatureId(recipe.seed,cliffX,cliffY,0x4f564552u); + shelf.type=2; + const int reach=72,width=36; + const int minX=xAxis?cliffX+(downhill<0?-reach:0)-4:cliffX-width; + const int maxX=xAxis?cliffX+(downhill>0?reach:0)+4:cliffX+width; + const int minY=xAxis?cliffY-width:cliffY+(downhill<0?-reach:0)-4; + const int maxY=xAxis?cliffY+width:cliffY+(downhill>0?reach:0)+4; + shelf.minimumSubvoxels={WorldAssemblyFixed(minX),WorldAssemblyFixed(minY), + WorldAssemblyFixed(std::max(bottomVoxel,ground-12.0))}; + shelf.maximumSubvoxels={WorldAssemblyFixed(maxX),WorldAssemblyFixed(maxY), + WorldAssemblyFixed(std::min((topChunkZ+1)*CHUNK_SIZE,ground+5.0))}; + stratum.features.push_back(shelf); + } + } manifest.plan.strates.push_back(std::move(stratum)); if(!manifest.finalize(error))return false; const std::string manifestText=manifest.ToJson(); @@ -7802,12 +7920,13 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; if(!rootObject) {error="owner snapshot root is not a JSON object";return false;} (*rootObject)["world_assembly"]=std::move(manifestJson); - std::printf("[WorldAssemblerPrototype] recipe_version=%u evaluator_version=%u strata=%zu biomes=%zu strate_index=%d chunk_z=%d..%d serialized_bytes=%zu recipe_hash=%016llx plan_hash=%016llx manifest_hash=%016llx source=owner_surface_params_and_biome_map route_points=0 features=0\n", + std::printf("[WorldAssemblerPrototype] recipe_version=%u evaluator_version=%u strata=%zu biomes=%zu strate_index=%d chunk_z=%d..%d serialized_bytes=%zu recipe_hash=%016llx plan_hash=%016llx manifest_hash=%016llx source=owner_surface_params_and_biome_map route_points=%zu features=%zu\n", manifest.recipe.schemaVersion,manifest.recipe.evaluatorVersion,manifest.plan.strates.size(), manifest.recipe.biomes.size(),manifest.plan.strates.front().strateIndex,topChunkZ,bottomChunkZ, manifestText.size(),static_cast(manifest.recipe.contentHash), static_cast(manifest.plan.contentHash), - static_cast(manifest.contentHash)); + static_cast(manifest.contentHash),manifest.plan.strates.front().route.size(), + manifest.plan.strates.front().features.size()); return true; } @@ -7831,11 +7950,17 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; static_cast(influence.maximumZSubvoxels), static_cast(detail.maximumAbsoluteDisplacementSubvoxels), static_cast(detail.minimumFeatureSizeSubvoxels),bounds.passed?"PASS":"FAIL"); - std::printf("[WorldAssemblerBoundAudit] boxes=%llu point_samples=%llu missed_bounds=%llu unknown_bounds=%llu deliberately_wrong_bound_detected=%s status=%s\n", + std::printf("[WorldAssemblerBoundAudit] boxes=%llu point_samples=%llu missed_bounds=%llu unknown_bounds=%llu border_boxes=%llu cliff_boxes=%llu cap_boxes=%llu negative_coordinate_boxes=%llu exception_boxes=%llu exception_point_samples=%llu deliberately_wrong_bound_detected=%s status=%s\n", static_cast(bounds.boxesTested), static_cast(bounds.pointSamples), static_cast(bounds.missedBounds), static_cast(bounds.unknownBounds), + static_cast(bounds.biomeBorderBoxes), + static_cast(bounds.cliffBoxes), + static_cast(bounds.capBoxes), + static_cast(bounds.negativeCoordinateBoxes), + static_cast(bounds.exceptionBoxes), + static_cast(bounds.exceptionPointSamples), bounds.negativeControlDetected?"true":"false",bounds.passed?"PASS":"FAIL"); struct Query {std::int64_t x=0,y=0,z=0;}; @@ -7889,6 +8014,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; add(static_cast(sample.capHeightSubvoxels),8); add(sample.biomeId,4);add(sample.neighborBiomeId,4);add(sample.biomeWeightQ16,4); add(static_cast(sample.densitySubvoxels),8); + add(static_cast(sample.waterSurfaceSubvoxels),8); + add(static_cast(sample.waterFillSubvoxels),8); } return hash; }; @@ -7908,6 +8035,139 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; std::printf("[WorldAssemblerQueryTiming] samples=%zu native_W1_ms=%.3f native_W8_ms=%.3f native_W16_ms=%.3f scope=point_evaluation_only\n", queries.size(),time1,time8,time16); + double ownerHeightSum=0.0,nativeHeightSum=0.0; + double ownerHeightSquareSum=0.0,nativeHeightSquareSum=0.0,centeredCrossSum=0.0; + double heightAbsoluteSum=0.0,heightMaximumError=0.0; + std::vector heightDeltas; + heightDeltas.reserve(4096); + std::uint64_t ownerBiomeSamples=0,ownerBiomeIdMismatches=0; + const FBiomeContext ownerBiomeContext=owner.Manager->GetBiomeContextForChunk( + FIntVector(0,0,plan.topChunkZ)); + std::uint64_t heightSamples=0; + constexpr int heightProbeSide=64; + constexpr double heightProbeSpan=3200.0; + for(int y=0;y(x)/(heightProbeSide-1)-0.5)*heightProbeSpan; + const double yVoxel=(static_cast(y)/(heightProbeSide-1)-0.5)*heightProbeSpan; + float ownerGround=0.0f,ownerCap=0.0f; + if(!owner.Generator->GetSurfaceHeightAt(static_cast(xVoxel), + static_cast(yVoxel),plan.topChunkZ,ownerGround,ownerCap))continue; + const auto native=block.point( + static_cast(std::llround(xVoxel*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)), + static_cast(std::llround(yVoxel*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)),0); + const double nativeGround=static_cast(native.groundHeightSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const double ownerGroundValue=ownerGround; + ownerHeightSum+=ownerGroundValue;nativeHeightSum+=nativeGround; + ownerHeightSquareSum+=ownerGroundValue*ownerGroundValue; + nativeHeightSquareSum+=nativeGround*nativeGround; + centeredCrossSum+=ownerGroundValue*nativeGround; + const double difference=std::abs(nativeGround-ownerGroundValue); + heightAbsoluteSum+=difference;heightMaximumError=std::max(heightMaximumError,difference); + heightDeltas.push_back(nativeGround-ownerGroundValue); + FVoxelBiomeQuery ownerBiome; + owner.Generator->QueryBiomeAt(static_cast(xVoxel),static_cast(yVoxel), + plan.topChunkZ,ownerBiome); + if(ownerBiome.bHasBiomes&&ownerBiomeContext.Biomes.IsValidIndex(ownerBiome.DominantContextIndex)) + { + const std::uint32_t ownerBiomeId=0x1001u+static_cast( + ownerBiomeContext.Biomes[ownerBiome.DominantContextIndex].Index); + ++ownerBiomeSamples; + if(native.biomeId!=ownerBiomeId)++ownerBiomeIdMismatches; + } + ++heightSamples; + } + if(heightSamples>0) + { + const double ownerMean=ownerHeightSum/heightSamples; + const double nativeMean=nativeHeightSum/heightSamples; + const double ownerVariance=std::max(0.0,ownerHeightSquareSum/heightSamples-ownerMean*ownerMean); + const double nativeVariance=std::max(0.0,nativeHeightSquareSum/heightSamples-nativeMean*nativeMean); + const double covariance=centeredCrossSum/heightSamples-ownerMean*nativeMean; + const double slope=ownerVariance>0.0?covariance/ownerVariance:0.0; + const double intercept=nativeMean-slope*ownerMean; + const double correlation=(ownerVariance>0.0&&nativeVariance>0.0) + ?covariance/std::sqrt(ownerVariance*nativeVariance):0.0; + double offsetCorrectedAbsoluteSum=0.0; + for(const double delta:heightDeltas)offsetCorrectedAbsoluteSum+=std::abs(delta-(nativeMean-ownerMean)); + std::printf("[WorldAssemblerSurfaceShapeDiagnostic] samples=%llu owner_voxel_cm=%.1f core_voxel_cm=%.1f mean_owner_height=%.3f mean_native_height=%.3f mean_offset=%.3f mae=%.3f mae_after_mean_offset_removal=%.3f max_error=%.3f regression_native_equals_slope_owner_plus_offset_slope=%.5f offset=%.3f correlation=%.5f owner_biome_samples=%llu owner_biome_id_mismatches=%llu\n", + static_cast(heightSamples),VOXEL_SIZE,25.0, + ownerMean,nativeMean,nativeMean-ownerMean,heightAbsoluteSum/heightSamples, + heightDeltas.empty()?0.0:offsetCorrectedAbsoluteSum/heightDeltas.size(), + heightMaximumError,slope,intercept,correlation, + static_cast(ownerBiomeSamples), + static_cast(ownerBiomeIdMismatches)); + } + + struct FWorldAssemblerCaptureSite + {double X=0.0,Y=0.0,OwnerGround=0.0,NativeGround=0.0,Score=0.0;bool Found=false;}; + FWorldAssemblerCaptureSite borderSite,coastSite; + const auto waterVoxel=static_cast(plan.waterSurfaceSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + for(int y=-1600;y<=1600;y+=64) + for(int x=-1600;x<=1600;x+=64) + { + float ownerGround=0.0f,ownerCap=0.0f; + if(!owner.Generator->GetSurfaceHeightAt(static_cast(x),static_cast(y), + plan.topChunkZ,ownerGround,ownerCap))continue; + FVoxelBiomeQuery ownerBiome; + owner.Generator->QueryBiomeAt(static_cast(x),static_cast(y), + plan.topChunkZ,ownerBiome); + const auto native=block.point(static_cast(x) + *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,static_cast(y) + *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,0); + if(ownerBiome.bHasBiomes&&ownerBiome.NeighborWeight>borderSite.Score) + borderSite={static_cast(x),static_cast(y),ownerGround, + static_cast(native.groundHeightSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, + ownerBiome.NeighborWeight,true}; + const double waterDistance=std::abs(static_cast(ownerGround)-waterVoxel); + if(!coastSite.Found||waterDistance(x),static_cast(y),ownerGround, + static_cast(native.groundHeightSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, + waterDistance,true}; + } + const auto printCaptureSite=[&](const char* name,const FWorldAssemblerCaptureSite& site) + { + if(!site.Found)return; + const double cameraX=site.X/4.0,cameraY=(site.Y-140.0)/4.0; + const double cameraZ=(site.OwnerGround+64.0)/4.0; + std::printf("[WorldAssemblerCaptureSite] name=%s target_vox=(%.0f,%.0f,%.2f) owner_ground_vox=%.2f native_ground_vox=%.2f score=%.5f camera_m=(%.3f,%.3f,%.3f,0,-22)\n", + name,site.X,site.Y,site.OwnerGround,site.OwnerGround,site.NativeGround,site.Score, + cameraX,cameraY,cameraZ); + }; + const auto originNativeColumn=block.point(0,0,0); + float originOwnerGround=0.0f,originOwnerCap=0.0f; + if(owner.Generator->GetSurfaceHeightAt(0.0f,0.0f,plan.topChunkZ, + originOwnerGround,originOwnerCap)) + printCaptureSite("surface",{0.0,0.0,originOwnerGround, + static_cast(originNativeColumn.groundHeightSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,0.0,true}); + printCaptureSite("biome_border",borderSite); + printCaptureSite("coast",coastSite); + if(!plan.features.empty()&&plan.features.front().type==2) + { + const auto& feature=plan.features.front(); + const double fx=(feature.minimumSubvoxels[0]+feature.maximumSubvoxels[0]) + /(2.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + const double fy=(feature.minimumSubvoxels[1]+feature.maximumSubvoxels[1]) + /(2.0*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + float ownerGround=0.0f,ownerCap=0.0f; + if(owner.Generator->GetSurfaceHeightAt(static_cast(fx),static_cast(fy), + plan.topChunkZ,ownerGround,ownerCap)) + { + const auto native=block.point(static_cast(std::llround(fx + *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)),static_cast(std::llround(fy + *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)),0); + printCaptureSite("cliff",{fx,fy,ownerGround, + static_cast(native.groundHeightSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,0.0,true}); + } + } + double absoluteSum=0.0,maximumError=0.0; std::uint64_t signMismatches=0; const auto legacyStart=std::chrono::steady_clock::now(); @@ -8520,6 +8780,11 @@ int main(int Argc,char** Argv) { const auto WindowShowStart=std::chrono::steady_clock::now(); ShowWindow(App.Window,SW_SHOW); + if(!App.CapturePath.empty()) + { + BringWindowToTop(App.Window); + SetForegroundWindow(App.Window); + } std::printf("[ColdTimeline] stage=viewer_window_show duration_ms=%.3f\n", std::chrono::duration( std::chrono::steady_clock::now()-WindowShowStart).count());