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 0000000..eb4a83d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM1/round2/captures/border-native.jpg differ diff --git a/Standalone/Research/WorldAssemblerM1/round2/captures/border-owner.jpg b/Standalone/Research/WorldAssemblerM1/round2/captures/border-owner.jpg new file mode 100644 index 0000000..9ca067f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM1/round2/captures/border-owner.jpg differ 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 0000000..506e876 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM1/round2/captures/cliff-native.jpg differ diff --git a/Standalone/Research/WorldAssemblerM1/round2/captures/cliff-owner.jpg b/Standalone/Research/WorldAssemblerM1/round2/captures/cliff-owner.jpg new file mode 100644 index 0000000..100945c Binary files /dev/null and b/Standalone/Research/WorldAssemblerM1/round2/captures/cliff-owner.jpg differ diff --git a/Standalone/Research/WorldAssemblerM1/round2/captures/coast-native.jpg b/Standalone/Research/WorldAssemblerM1/round2/captures/coast-native.jpg new file mode 100644 index 0000000..a534408 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM1/round2/captures/coast-native.jpg differ diff --git a/Standalone/Research/WorldAssemblerM1/round2/captures/coast-owner.jpg b/Standalone/Research/WorldAssemblerM1/round2/captures/coast-owner.jpg new file mode 100644 index 0000000..a0470d9 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM1/round2/captures/coast-owner.jpg differ 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 0000000..1e3dc80 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM1/round2/captures/surface-native.jpg differ 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 0000000..f46b0df Binary files /dev/null and b/Standalone/Research/WorldAssemblerM1/round2/captures/surface-owner.jpg differ 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());