The passage mouth fade multiplied the signed carve distance by zero, which turned native rock into an open surface (the reason earlier rounds stamped walk pads at mouths); it now blends between the uncarved field and the full signed carve. The FI style-0 route followed a lifted grade envelope over the crater (the "bridge"); it now runs on the rim and follows the native surface. New built-shape and crease audits with thresholds frozen before the fix (L36 and box controls fail). 24/24 passage rows, 30/30 determinism, 16/16 invariants, 18/18 interface/ring. Unreal: one recipe setter (uncompiled). Research R1 (read-only, web): ranked proposals for natural walkable mouths, floor-limited carve first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2465 lines
137 KiB
C++
2465 lines
137 KiB
C++
#pragma once
|
|
|
|
#include "VoxelForgeCore/ThreadLocalStorage.h"
|
|
#include "Json.h"
|
|
#include "VoxelForgeCore/VoxelForgeCore.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <charconv>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <numeric>
|
|
#include <sstream>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
|
|
namespace VoxelForgeCore
|
|
{
|
|
namespace floating_island_detail
|
|
{
|
|
constexpr std::int64_t Q = WorldAssemblySubvoxelsPerVoxel;
|
|
constexpr std::int64_t Q16 = 1ll << 16;
|
|
constexpr std::int64_t MaxScalar = 1000000000000ll;
|
|
constexpr std::int64_t MaxDensity = MaxScalar;
|
|
constexpr double Pi = 3.141592653589793238462643383279502884;
|
|
|
|
inline std::uint64_t StartHash() { return 14695981039346656037ull; }
|
|
|
|
inline void HashWord(std::uint64_t& hash, std::uint64_t word, int bytes)
|
|
{
|
|
for (int i = 0; i < bytes; ++i)
|
|
{
|
|
hash ^= static_cast<std::uint8_t>(word >> (i * 8));
|
|
hash *= 1099511628211ull;
|
|
}
|
|
}
|
|
|
|
inline std::uint64_t Mix(std::uint64_t value)
|
|
{
|
|
value += 0x9e3779b97f4a7c15ull;
|
|
value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ull;
|
|
value = (value ^ (value >> 27)) * 0x94d049bb133111ebull;
|
|
return value ^ (value >> 31);
|
|
}
|
|
|
|
inline std::uint64_t Roll(std::uint32_t seed, std::uint64_t site, std::uint64_t salt)
|
|
{ return Mix((static_cast<std::uint64_t>(seed) << 32) ^ site ^ salt); }
|
|
|
|
inline std::uint32_t UnitQ16(std::uint32_t seed, std::uint64_t site, std::uint64_t salt)
|
|
{ return static_cast<std::uint32_t>(Roll(seed, site, salt) >> 48); }
|
|
|
|
inline std::int64_t SignedRange(std::uint32_t seed, std::uint64_t site,
|
|
std::uint64_t salt, std::int64_t magnitude)
|
|
{
|
|
const std::uint32_t unit = UnitQ16(seed, site, salt);
|
|
return (static_cast<std::int64_t>(unit) * 2 * magnitude / Q16) - magnitude;
|
|
}
|
|
|
|
inline std::string HashText(std::uint64_t hash)
|
|
{
|
|
constexpr char Hex[] = "0123456789abcdef";
|
|
std::string output(16, '0');
|
|
for (int i = 15; i >= 0; --i)
|
|
{
|
|
output[static_cast<std::size_t>(i)] = Hex[hash & 0xfu];
|
|
hash >>= 4;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
inline bool ReadInteger(const VFJson::Value& value, std::int64_t& output)
|
|
{
|
|
if (!value.IsNumber()) return false;
|
|
const double number = value.Number();
|
|
if (!std::isfinite(number) || std::trunc(number) != number
|
|
|| std::abs(number) > 9007199254740991.0) return false;
|
|
output = static_cast<std::int64_t>(number);
|
|
return true;
|
|
}
|
|
|
|
inline bool ReadU32(const VFJson::Value& value, std::uint32_t& output)
|
|
{
|
|
std::int64_t parsed = 0;
|
|
if (!ReadInteger(value, parsed) || parsed < 0
|
|
|| parsed > std::numeric_limits<std::uint32_t>::max()) return false;
|
|
output = static_cast<std::uint32_t>(parsed);
|
|
return true;
|
|
}
|
|
|
|
inline bool ReadHash(const VFJson::Value& value, std::uint64_t& output)
|
|
{
|
|
if (!value.IsString()) return false;
|
|
std::string_view text = value.String();
|
|
if (text.starts_with("0x") || text.starts_with("0X")) text.remove_prefix(2);
|
|
if (text.empty() || text.size() > 16) return false;
|
|
const auto result = std::from_chars(text.data(), text.data() + text.size(), output, 16);
|
|
return result.ec == std::errc{} && result.ptr == text.data() + text.size();
|
|
}
|
|
|
|
inline bool ReadArray(const VFJson::Value& value, std::array<std::int64_t, 3>& output)
|
|
{
|
|
if (!value.IsArray() || value.AsArray().size() != output.size()) return false;
|
|
for (std::size_t i = 0; i < output.size(); ++i)
|
|
if (!ReadInteger(value.AsArray()[i], output[i])) return false;
|
|
return true;
|
|
}
|
|
|
|
inline bool ValidScalar(std::int64_t value)
|
|
{ return value >= -MaxScalar && value <= MaxScalar; }
|
|
|
|
inline double Q16ToDouble(std::int64_t value)
|
|
{ return static_cast<double>(value) / static_cast<double>(Q16); }
|
|
|
|
inline std::int64_t RoundQ(double value)
|
|
{
|
|
if (!std::isfinite(value)) return value < 0.0 ? -MaxDensity : MaxDensity;
|
|
return static_cast<std::int64_t>(std::llround(std::clamp(value,
|
|
-static_cast<double>(MaxDensity), static_cast<double>(MaxDensity))));
|
|
}
|
|
|
|
inline double SmoothStep(double value)
|
|
{
|
|
value = std::clamp(value, 0.0, 1.0);
|
|
return value * value * (3.0 - 2.0 * value);
|
|
}
|
|
|
|
inline double SmoothMin(double a, double b, double k)
|
|
{
|
|
if (!(k > 0.0)) return std::min(a, b);
|
|
const double h = std::max(k - std::abs(a - b), 0.0) / k;
|
|
return std::min(a, b) - h * h * k * 0.25;
|
|
}
|
|
|
|
inline std::uint64_t StableId(std::uint32_t seed, std::uint64_t site, std::uint64_t salt)
|
|
{
|
|
std::uint64_t hash = StartHash();
|
|
HashWord(hash, seed, 4); HashWord(hash, site, 8); HashWord(hash, salt, 8);
|
|
return hash == 0 ? 1 : hash;
|
|
}
|
|
|
|
inline std::uint64_t HashRouteConstraints(const WorldAssemblyRouteConstraintsV1& value)
|
|
{
|
|
std::uint64_t hash = StartHash();
|
|
HashWord(hash, value.version, 4);
|
|
HashWord(hash, static_cast<std::uint64_t>(value.capsuleRadiusSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(value.capsuleHeightSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(value.maximumGradeQ16), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(value.sweepSpacingSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(value.supportProbeRadiusSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(value.supportSearchDepthSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(value.minimumClearanceSubvoxels), 8);
|
|
HashWord(hash, value.minimumSupportProbes, 4);
|
|
return hash;
|
|
}
|
|
|
|
inline std::uint64_t HashRecipe(const FloatingIslandRecipeV1& recipe)
|
|
{
|
|
std::uint64_t hash = StartHash();
|
|
HashWord(hash, recipe.schemaVersion, 4);
|
|
HashWord(hash, recipe.unitsPerVoxel, 4);
|
|
HashWord(hash, recipe.ownerVoxelSizeMicrons, 4);
|
|
HashWord(hash, recipe.coreVoxelSizeMicrons, 4);
|
|
HashWord(hash, recipe.ownerToCoreScaleNumerator, 4);
|
|
HashWord(hash, recipe.ownerToCoreScaleDenominator, 4);
|
|
HashWord(hash, recipe.seed, 4);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutOriginXSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutOriginYSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutCenterTopZSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutMinimumZSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.layoutMaximumZSubvoxels), 8);
|
|
if(recipe.geometryVersion>=4)
|
|
HashWord(hash,static_cast<std::uint64_t>(recipe.layoutChunkWidthSubvoxels),8);
|
|
HashWord(hash, recipe.primaryIslandCount, 4);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.islandDensityQ16), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.spacingSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.minimumRadiusSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.maximumRadiusSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.thicknessRatioQ16), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.verticalJitterQ16), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.topFlattenQ16), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.taperEndMinimumQ16), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.taperEndMaximumQ16), 8);
|
|
if (recipe.geometryVersion >= 2)
|
|
{
|
|
HashWord(hash, recipe.geometryVersion, 4);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.crustDepthBonusQ16), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.rootDepthRatioQ16), 8);
|
|
}
|
|
if (recipe.geometryVersion >= 5)
|
|
{
|
|
HashWord(hash, recipe.anchorIslandOrdinal, 4);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.anchorPlacementRadiusSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.anchorRootRadiusSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.anchorIslandMinimumRadiusSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.anchorRootBottomZSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.anchorMouthOffsetSubvoxels), 8);
|
|
}
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.blendRadiusSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.contourWarpSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.roughnessSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.boundarySealSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeHalfWidthSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeThicknessSubvoxels), 8);
|
|
if(recipe.terrainFollowingRoute)HashWord(hash,1,1);
|
|
HashWord(hash, HashRouteConstraints(recipe.routeConstraints), 8);
|
|
return hash;
|
|
}
|
|
|
|
inline std::uint64_t HashPlan(const FloatingIslandPlanV1& plan)
|
|
{
|
|
std::uint64_t hash = StartHash();
|
|
HashWord(hash, plan.islands.size(), 4);
|
|
for (const auto& island : plan.islands)
|
|
{
|
|
HashWord(hash, island.stableId, 8); HashWord(hash, island.ordinal, 4);
|
|
for (auto value : island.centerSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(island.radiusSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(island.topHalfSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(island.topZSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(island.bottomZSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(island.taperEndQ16), 8);
|
|
for (auto value : island.influenceMinimumSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
|
for (auto value : island.influenceMaximumSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
|
}
|
|
HashWord(hash, plan.clusterEdges.size(), 4);
|
|
for (const auto& edge : plan.clusterEdges)
|
|
{
|
|
HashWord(hash, edge.stableId, 8); HashWord(hash, edge.fromIslandId, 8);
|
|
HashWord(hash, edge.toIslandId, 8);
|
|
for (auto value : edge.startSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
|
for (auto value : edge.endSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(edge.halfWidthSubvoxels), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(edge.thicknessSubvoxels), 8);
|
|
for (auto value : edge.influenceMinimumSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
|
for (auto value : edge.influenceMaximumSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
|
HashWord(hash, edge.sharpTagReady ? 1u : 0u, 1);
|
|
}
|
|
HashWord(hash, plan.route.size(), 4);
|
|
for (const auto& point : plan.route)
|
|
{
|
|
HashWord(hash, point.stableFeatureId, 8);
|
|
for (auto value : point.positionSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
|
|
HashWord(hash, static_cast<std::uint64_t>(point.radiusSubvoxels), 8);
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
inline std::uint64_t HashManifest(const FloatingIslandManifestV1& manifest)
|
|
{
|
|
std::uint64_t hash = StartHash();
|
|
HashWord(hash, manifest.recipe.contentHash, 8);
|
|
HashWord(hash, manifest.plan.contentHash, 8);
|
|
return hash;
|
|
}
|
|
|
|
inline bool ValidateRecipe(const FloatingIslandRecipeV1& recipe, std::string& error)
|
|
{
|
|
const std::array<std::int64_t, 32> values = {
|
|
recipe.layoutOriginXSubvoxels, recipe.layoutOriginYSubvoxels,
|
|
recipe.layoutCenterTopZSubvoxels, recipe.layoutMinimumZSubvoxels,
|
|
recipe.layoutMaximumZSubvoxels, recipe.layoutChunkWidthSubvoxels, recipe.islandDensityQ16,
|
|
recipe.spacingSubvoxels, recipe.minimumRadiusSubvoxels,
|
|
recipe.maximumRadiusSubvoxels, recipe.thicknessRatioQ16,
|
|
recipe.verticalJitterQ16, recipe.topFlattenQ16,
|
|
recipe.taperEndMinimumQ16, recipe.taperEndMaximumQ16,
|
|
recipe.crustDepthBonusQ16, recipe.rootDepthRatioQ16,
|
|
recipe.blendRadiusSubvoxels, recipe.contourWarpSubvoxels,
|
|
recipe.roughnessSubvoxels, recipe.boundarySealSubvoxels,
|
|
recipe.bridgeHalfWidthSubvoxels, recipe.bridgeThicknessSubvoxels,
|
|
static_cast<std::int64_t>(recipe.anchorIslandOrdinal),
|
|
recipe.anchorPlacementRadiusSubvoxels, recipe.anchorRootRadiusSubvoxels,
|
|
recipe.anchorIslandMinimumRadiusSubvoxels, recipe.anchorRootBottomZSubvoxels,
|
|
recipe.anchorMouthOffsetSubvoxels,
|
|
recipe.routeConstraints.capsuleRadiusSubvoxels,
|
|
recipe.routeConstraints.capsuleHeightSubvoxels,
|
|
recipe.routeConstraints.maximumGradeQ16};
|
|
const std::uint32_t ScaleGcd = std::gcd(recipe.ownerVoxelSizeMicrons,
|
|
recipe.coreVoxelSizeMicrons);
|
|
const std::uint32_t ExpectedNumerator = ScaleGcd > 0
|
|
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
|
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
|
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
|
if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 8
|
|
|| (recipe.geometryVersion == 1
|
|
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
|
|| recipe.unitsPerVoxel != Q
|
|
|| recipe.ownerVoxelSizeMicrons != 125000
|
|
|| recipe.coreVoxelSizeMicrons < 125000 || recipe.coreVoxelSizeMicrons > 250000
|
|
|| recipe.ownerToCoreScaleNumerator != ExpectedNumerator
|
|
|| recipe.ownerToCoreScaleDenominator != ExpectedDenominator
|
|
|| recipe.primaryIslandCount < 3 || recipe.primaryIslandCount > 9
|
|
|| recipe.layoutChunkWidthSubvoxels <= 0
|
|
|| static_cast<long double>(recipe.layoutChunkWidthSubvoxels)*recipe.coreVoxelSizeMicrons
|
|
< 250000.0L*Q
|
|
|| static_cast<long double>(recipe.layoutChunkWidthSubvoxels)*recipe.coreVoxelSizeMicrons
|
|
> 64000000.0L*Q
|
|
|| recipe.layoutMaximumZSubvoxels <= recipe.layoutMinimumZSubvoxels
|
|
|| recipe.layoutMaximumZSubvoxels - recipe.layoutMinimumZSubvoxels > 2048 * Q
|
|
|| std::any_of(values.begin(), values.end(), [](auto value) { return !ValidScalar(value); }))
|
|
{ error = "floating-island recipe version, scale, layout or scalar range is invalid"; return false; }
|
|
const auto& constraints = recipe.routeConstraints;
|
|
const std::int64_t minimumSpacing = recipe.geometryVersion >= 2 ? 32 * Q : 64 * Q;
|
|
const std::int64_t minimumRadius = recipe.geometryVersion >= 2 ? 12 * Q : 24 * Q;
|
|
if (recipe.islandDensityQ16 < 6554 || recipe.islandDensityQ16 > (1ll << 15)
|
|
|| recipe.spacingSubvoxels < minimumSpacing || recipe.spacingSubvoxels > 256 * Q
|
|
|| recipe.minimumRadiusSubvoxels < minimumRadius
|
|
|| recipe.maximumRadiusSubvoxels < recipe.minimumRadiusSubvoxels
|
|
|| recipe.maximumRadiusSubvoxels > 64 * Q
|
|
|| recipe.thicknessRatioQ16 < 29491 || recipe.thicknessRatioQ16 > 49152
|
|
|| recipe.verticalJitterQ16 < 13107 || recipe.verticalJitterQ16 > 39322
|
|
|| recipe.topFlattenQ16 < 39322 || recipe.topFlattenQ16 > 52429
|
|
|| recipe.taperEndMinimumQ16 < 26214 || recipe.taperEndMaximumQ16 > 52429
|
|
|| recipe.taperEndMinimumQ16 > recipe.taperEndMaximumQ16
|
|
|| (recipe.geometryVersion >= 2
|
|
&& (recipe.crustDepthBonusQ16 < 6554 || recipe.crustDepthBonusQ16 > 16384
|
|
|| recipe.rootDepthRatioQ16 < 32768 || recipe.rootDepthRatioQ16 > 65536))
|
|
|| recipe.blendRadiusSubvoxels < 2 * Q || recipe.blendRadiusSubvoxels > 6 * Q
|
|
|| recipe.contourWarpSubvoxels <= 0
|
|
|| recipe.contourWarpSubvoxels > recipe.maximumRadiusSubvoxels / 2
|
|
|| recipe.roughnessSubvoxels < 0
|
|
|| recipe.roughnessSubvoxels > recipe.minimumRadiusSubvoxels / 4
|
|
|| recipe.boundarySealSubvoxels < 2 * Q
|
|
|| recipe.bridgeHalfWidthSubvoxels < constraints.capsuleRadiusSubvoxels + Q
|
|
|| recipe.bridgeThicknessSubvoxels < 2 * Q
|
|
|| constraints.version != 1 || constraints.capsuleRadiusSubvoxels <= 0
|
|
|| constraints.capsuleHeightSubvoxels < 2 * constraints.capsuleRadiusSubvoxels
|
|
|| constraints.maximumGradeQ16 <= 0 || constraints.maximumGradeQ16 > Q16
|
|
|| constraints.sweepSpacingSubvoxels <= 0 || constraints.sweepSpacingSubvoxels > Q / 2
|
|
|| !ValidScalar(constraints.sweepSpacingSubvoxels)
|
|
|| !ValidScalar(constraints.supportProbeRadiusSubvoxels)
|
|
|| !ValidScalar(constraints.supportSearchDepthSubvoxels)
|
|
|| !ValidScalar(constraints.minimumClearanceSubvoxels)
|
|
|| constraints.supportProbeRadiusSubvoxels <= 0
|
|
|| constraints.supportSearchDepthSubvoxels <= 0
|
|
|| constraints.minimumClearanceSubvoxels < 0
|
|
|| constraints.minimumSupportProbes == 0 || constraints.minimumSupportProbes > 5)
|
|
{ error = "floating-island shape or route parameters are outside the supported range"; return false; }
|
|
if (recipe.geometryVersion >= 5
|
|
&& (recipe.anchorIslandOrdinal != recipe.primaryIslandCount - 1
|
|
|| recipe.anchorPlacementRadiusSubvoxels <= 0
|
|
|| recipe.anchorRootRadiusSubvoxels < recipe.anchorMouthOffsetSubvoxels
|
|
+ recipe.routeConstraints.capsuleRadiusSubvoxels + Q
|
|
|| recipe.anchorIslandMinimumRadiusSubvoxels
|
|
< recipe.anchorMouthOffsetSubvoxels
|
|
+ recipe.routeConstraints.capsuleRadiusSubvoxels + Q
|
|
|| recipe.anchorIslandMinimumRadiusSubvoxels > 64 * Q
|
|
|| recipe.anchorRootBottomZSubvoxels < recipe.layoutMinimumZSubvoxels
|
|
+ recipe.boundarySealSubvoxels
|
|
|| recipe.anchorRootBottomZSubvoxels >= recipe.layoutCenterTopZSubvoxels
|
|
|| recipe.anchorMouthOffsetSubvoxels <= 0
|
|
|| recipe.anchorMouthOffsetSubvoxels > 64 * Q))
|
|
{ error = "floating-island bedrock anchor is outside its passage or rock-thickness bounds"; return false; }
|
|
return true;
|
|
}
|
|
|
|
inline bool Contains(const std::array<std::int64_t, 3>& minimum,
|
|
const std::array<std::int64_t, 3>& maximum,
|
|
const std::array<std::int64_t, 3>& point)
|
|
{
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
if (point[axis] < minimum[axis] || point[axis] > maximum[axis]) return false;
|
|
return true;
|
|
}
|
|
|
|
inline bool Intersects(const std::array<std::int64_t, 3>& minimumA,
|
|
const std::array<std::int64_t, 3>& maximumA,
|
|
const std::array<std::int64_t, 3>& minimumB,
|
|
const std::array<std::int64_t, 3>& maximumB)
|
|
{
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
if (maximumA[axis] < minimumB[axis] || minimumA[axis] > maximumB[axis]) return false;
|
|
return true;
|
|
}
|
|
|
|
inline std::int64_t IslandBlendMargin(const FloatingIslandRecipeV1& recipe)
|
|
{ return 3 * recipe.blendRadiusSubvoxels; }
|
|
|
|
inline double LobeOffset(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandAnchorV1& island,
|
|
double dx, double dy, double zSubvoxels)
|
|
{
|
|
const double angle = std::atan2(dy, dx);
|
|
const double z = zSubvoxels / static_cast<double>(Q);
|
|
const double seed = static_cast<double>(Roll(recipe.seed, island.stableId, 0x4c4f4245ull)
|
|
& 0xffffffffull) * (2.0 * Pi / 4294967296.0);
|
|
const double a = std::sin(3.0 * angle + seed + z * 0.017);
|
|
const double b = std::sin(5.0 * angle - seed * 0.73 - z * 0.023);
|
|
const double c = std::sin(8.0 * angle + seed * 1.41 + z * 0.011);
|
|
return static_cast<double>(recipe.contourWarpSubvoxels) * (0.50 * a + 0.30 * b + 0.20 * c);
|
|
}
|
|
|
|
inline double LobePhase(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandAnchorV1& island)
|
|
{
|
|
return static_cast<double>(Roll(recipe.seed, island.stableId, 0x4c4f4245ull)
|
|
& 0xffffffffull) * (2.0 * Pi / 4294967296.0);
|
|
}
|
|
|
|
inline double LobeOffsetWithPhase(const FloatingIslandRecipeV1& recipe,
|
|
double angle,double zSubvoxels,double phase)
|
|
{
|
|
const double z = zSubvoxels / static_cast<double>(Q);
|
|
const double a = std::sin(3.0 * angle + phase + z * 0.017);
|
|
const double b = std::sin(5.0 * angle - phase * 0.73 - z * 0.023);
|
|
const double c = std::sin(8.0 * angle + phase * 1.41 + z * 0.011);
|
|
return static_cast<double>(recipe.contourWarpSubvoxels) * (0.50 * a + 0.30 * b + 0.20 * c);
|
|
}
|
|
|
|
inline double LobeOffsetAtAngle(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandAnchorV1& island,
|
|
double angle,double zSubvoxels)
|
|
{
|
|
return LobeOffsetWithPhase(recipe,angle,zSubvoxels,LobePhase(recipe,island));
|
|
}
|
|
|
|
inline double Roughness(const FloatingIslandRecipeV1& recipe,
|
|
std::uint64_t featureId, double x, double y, double z)
|
|
{
|
|
const double phase = static_cast<double>(Roll(recipe.seed, featureId, 0x524f5547ull)
|
|
& 0xffffffffull) * (2.0 * Pi / 4294967296.0);
|
|
const double xv = x / static_cast<double>(Q);
|
|
const double yv = y / static_cast<double>(Q);
|
|
const double zv = z / static_cast<double>(Q);
|
|
if (recipe.geometryVersion < 2)
|
|
{
|
|
const double oldNoise = 0.52 * std::sin(xv * 0.071 + yv * 0.053 + zv * 0.089 + phase)
|
|
+ 0.31 * std::sin(xv * 0.127 - yv * 0.097 + zv * 0.061 - phase * 0.73)
|
|
+ 0.17 * std::cos(xv * 0.043 + yv * 0.139 - zv * 0.113 + phase * 1.39);
|
|
return static_cast<double>(recipe.roughnessSubvoxels) * oldNoise;
|
|
}
|
|
// Four oblique bands span about 7 m down to 0.9 m at 25 cm per voxel.
|
|
// Their absolute weights sum to one, keeping displacement inside the 3-voxel envelope.
|
|
const double noise = 0.52 * std::sin(xv * 0.071 + yv * 0.053 + zv * 0.089 + phase)
|
|
+ 0.31 * std::sin(xv * 0.127 - yv * 0.097 + zv * 0.061 - phase * 0.73)
|
|
+ 0.17 * std::cos(xv * 0.043 + yv * 0.139 - zv * 0.113 + phase * 1.39);
|
|
const double fine = 0.46 * std::sin(xv * 0.22 + yv * 0.31 - zv * 0.17 + phase * 0.41)
|
|
+ 0.32 * std::cos(xv * 0.38 - yv * 0.21 + zv * 0.29 - phase * 0.67)
|
|
+ 0.22 * std::sin(xv * 0.74 + yv * 0.53 + zv * 0.61 + phase * 1.17);
|
|
return static_cast<double>(recipe.roughnessSubvoxels)
|
|
* std::clamp(0.42 * noise + 0.58 * fine, -1.0, 1.0);
|
|
}
|
|
|
|
inline double IslandTopSurface(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandAnchorV1& island,
|
|
double x, double y)
|
|
{
|
|
const double dx = x - island.centerSubvoxels[0];
|
|
const double dy = y - island.centerSubvoxels[1];
|
|
const double radial = std::hypot(dx, dy);
|
|
const double effectiveRadius = std::max(1.0,
|
|
static_cast<double>(island.radiusSubvoxels) + LobeOffset(recipe, island, dx, dy,
|
|
static_cast<double>(island.topZSubvoxels)));
|
|
const double edge = std::clamp(radial / effectiveRadius, 0.0, 1.0);
|
|
const double drop = (1.0 - Q16ToDouble(recipe.topFlattenQ16))
|
|
* static_cast<double>(island.topHalfSubvoxels) * 2.0 * edge * edge;
|
|
return static_cast<double>(island.topZSubvoxels) - drop;
|
|
}
|
|
|
|
struct FIslandColumnCacheEntry
|
|
{
|
|
bool bValid=false;
|
|
const FloatingIslandRecipeV1* RecipeIdentity=nullptr;
|
|
const FloatingIslandPlanV1* PlanIdentity=nullptr;
|
|
std::uint64_t RecipeContentHash=0,PlanContentHash=0;
|
|
std::uint32_t Seed=0;
|
|
std::uint64_t StableId=0;
|
|
std::int64_t SampleX=0,SampleY=0,CenterX=0,CenterY=0,TopZ=0,Radius=0,TopHalf=0;
|
|
std::int64_t TopFlattenQ16=0,ContourWarp=0;
|
|
double DeltaX=0.0,DeltaY=0.0,Radial=0.0,Angle=0.0,LobePhase=0.0,TopSurface=0.0;
|
|
};
|
|
|
|
using FSharedIslandColumnCache =
|
|
VoxelForgeCoreShared::TSharedColumnValueCache<FIslandColumnCacheEntry,16384,4>;
|
|
|
|
inline const FIslandColumnCacheEntry& IslandColumnCache(
|
|
const FloatingIslandRecipeV1& recipe,const FloatingIslandPlanV1& plan,
|
|
const FloatingIslandAnchorV1& island,double x,double y)
|
|
{
|
|
// Density samples arrive on integer Core lattice columns; reuse the exact column metrics
|
|
// across Z samples without changing the field value or touching shared world state.
|
|
// Les echantillons suivent les colonnes Core entieres; reutiliser les memes metriques XY.
|
|
constexpr std::size_t Ways=4;
|
|
constexpr std::size_t SetCount=2048;
|
|
static_assert((SetCount&(SetCount-1))==0);
|
|
static thread_local VoxelForgeCoreThreadLocal::FLazy<
|
|
std::array<FIslandColumnCacheEntry,SetCount*Ways>> Cache;
|
|
auto& cache=Cache.Get();
|
|
const std::int64_t SampleX=static_cast<std::int64_t>(x);
|
|
const std::int64_t SampleY=static_cast<std::int64_t>(y);
|
|
std::uint64_t IndexKey=island.stableId
|
|
^(static_cast<std::uint64_t>(SampleX)*0x9e3779b97f4a7c15ull)
|
|
^(static_cast<std::uint64_t>(SampleY)*0xc2b2ae3d27d4eb4full)
|
|
^(static_cast<std::uint64_t>(recipe.seed)<<23)
|
|
^recipe.contentHash^std::rotl(plan.contentHash,17)
|
|
^static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&recipe))
|
|
^std::rotl(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),31);
|
|
IndexKey^=IndexKey>>33;
|
|
IndexKey*=0xff51afd7ed558ccdull;
|
|
IndexKey^=IndexKey>>33;
|
|
const std::size_t Base=static_cast<std::size_t>(IndexKey&(SetCount-1))*Ways;
|
|
FIslandColumnCacheEntry* Replacement=&cache[Base+((IndexKey>>11)&(Ways-1))];
|
|
for(std::size_t Way=0;Way<Ways;++Way)
|
|
{
|
|
FIslandColumnCacheEntry& Entry=cache[Base+Way];
|
|
const bool bKeyMatches=Entry.bValid&&Entry.RecipeIdentity==&recipe
|
|
&&Entry.PlanIdentity==&plan&&Entry.RecipeContentHash==recipe.contentHash
|
|
&&Entry.PlanContentHash==plan.contentHash&&Entry.Seed==recipe.seed
|
|
&&Entry.StableId==island.stableId
|
|
&&Entry.SampleX==SampleX&&Entry.SampleY==SampleY
|
|
&&Entry.CenterX==island.centerSubvoxels[0]
|
|
&&Entry.CenterY==island.centerSubvoxels[1]
|
|
&&Entry.TopZ==island.topZSubvoxels
|
|
&&Entry.Radius==island.radiusSubvoxels
|
|
&&Entry.TopHalf==island.topHalfSubvoxels
|
|
&&Entry.TopFlattenQ16==recipe.topFlattenQ16
|
|
&&Entry.ContourWarp==recipe.contourWarpSubvoxels;
|
|
if(bKeyMatches)return Entry;
|
|
if(!Entry.bValid)Replacement=&Entry;
|
|
}
|
|
|
|
const auto build=[&]()
|
|
{
|
|
FIslandColumnCacheEntry Entry;
|
|
Entry.bValid=true;
|
|
Entry.RecipeIdentity=&recipe;
|
|
Entry.PlanIdentity=&plan;
|
|
Entry.RecipeContentHash=recipe.contentHash;
|
|
Entry.PlanContentHash=plan.contentHash;
|
|
Entry.Seed=recipe.seed;
|
|
Entry.StableId=island.stableId;
|
|
Entry.SampleX=SampleX;
|
|
Entry.SampleY=SampleY;
|
|
Entry.CenterX=island.centerSubvoxels[0];
|
|
Entry.CenterY=island.centerSubvoxels[1];
|
|
Entry.TopZ=island.topZSubvoxels;
|
|
Entry.Radius=island.radiusSubvoxels;
|
|
Entry.TopHalf=island.topHalfSubvoxels;
|
|
Entry.TopFlattenQ16=recipe.topFlattenQ16;
|
|
Entry.ContourWarp=recipe.contourWarpSubvoxels;
|
|
Entry.DeltaX=x-static_cast<double>(island.centerSubvoxels[0]);
|
|
Entry.DeltaY=y-static_cast<double>(island.centerSubvoxels[1]);
|
|
Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY);
|
|
Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX);
|
|
Entry.LobePhase=LobePhase(recipe,island);
|
|
const double EffectiveRadius=std::max(1.0,static_cast<double>(island.radiusSubvoxels)
|
|
+LobeOffsetWithPhase(recipe,Entry.Angle,static_cast<double>(island.topZSubvoxels),
|
|
Entry.LobePhase));
|
|
const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0);
|
|
const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16))
|
|
*static_cast<double>(island.topHalfSubvoxels)*2.0*Edge*Edge;
|
|
Entry.TopSurface=static_cast<double>(island.topZSubvoxels)-Drop;
|
|
return Entry;
|
|
};
|
|
FIslandColumnCacheEntry Entry{};
|
|
if(auto* shared=FSharedIslandColumnCache::threadActive())
|
|
{
|
|
const VoxelForgeCoreShared::FColumnCacheKey key{
|
|
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&recipe)),
|
|
recipe.contentHash,
|
|
static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),
|
|
plan.contentHash,SampleX,SampleY,island.stableId};
|
|
bool computed=false;
|
|
Entry=shared->getOrCompute(key,build,computed);
|
|
}
|
|
else Entry=build();
|
|
*Replacement=Entry;
|
|
return *Replacement;
|
|
}
|
|
|
|
inline std::int64_t ScaledQ16(std::int64_t value, std::int64_t scaleQ16)
|
|
{ return (value * scaleQ16 + Q16 / 2) / Q16; }
|
|
|
|
inline std::int64_t IslandCrustBottom(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandAnchorV1& island)
|
|
{
|
|
const std::int64_t ratio = recipe.thicknessRatioQ16
|
|
+ (recipe.geometryVersion >= 2 ? recipe.crustDepthBonusQ16 : 0);
|
|
return island.topZSubvoxels - ScaledQ16(island.radiusSubvoxels, ratio);
|
|
}
|
|
|
|
inline std::int64_t IslandBottom(const FloatingIslandRecipeV1& recipe,
|
|
std::int64_t topZ, std::int64_t radius)
|
|
{
|
|
const std::int64_t crustRatio = recipe.thicknessRatioQ16
|
|
+ (recipe.geometryVersion >= 2 ? recipe.crustDepthBonusQ16 : 0);
|
|
const std::int64_t rootRatio = recipe.geometryVersion >= 2
|
|
? recipe.rootDepthRatioQ16 : 0;
|
|
return topZ - ScaledQ16(radius, crustRatio) - ScaledQ16(radius, rootRatio);
|
|
}
|
|
|
|
inline std::int32_t RootCount(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandAnchorV1& island)
|
|
{
|
|
// Keep four separate side lobes and a full-depth central buttress to close the measured
|
|
// underside cavity without driving the outer roots to one point.
|
|
// Garder quatre lobes lateraux et un renfort central profond pour fermer la cavite mesuree.
|
|
return 5;
|
|
}
|
|
|
|
inline double UnitHash(std::uint64_t hash)
|
|
{ return static_cast<double>(hash & 0xffffull) / 65535.0; }
|
|
|
|
struct FRootLobeParameters
|
|
{
|
|
double RootX=0.0,RootY=0.0,CenterZ=0.0;
|
|
double RadiusXSquared=1.0,RadiusYSquared=1.0,VerticalRadiusSquared=1.0;
|
|
double MinimumRadius=1.0;
|
|
};
|
|
|
|
struct FRootLobeCacheEntry
|
|
{
|
|
bool bValid=false;
|
|
std::uint32_t Seed=0;
|
|
std::uint64_t StableId=0;
|
|
std::int32_t RootIndex=0,GeometryVersion=0;
|
|
std::array<std::int64_t,3> Center{};
|
|
std::int64_t TopZ=0,Radius=0,TaperEndQ16=0;
|
|
std::int64_t ThicknessRatioQ16=0,CrustDepthBonusQ16=0,RootDepthRatioQ16=0;
|
|
FRootLobeParameters Parameters{};
|
|
};
|
|
|
|
inline double RootLobeDensity(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandAnchorV1& island,
|
|
std::int32_t rootIndex, double x, double y, double z)
|
|
{
|
|
// Cache derived root constants per worker. The complete immutable input key keeps this
|
|
// value-only cache safe across worlds, epochs and worker schedules.
|
|
// Mettre en cache les constantes par worker; la cle complete evite tout partage d'etat.
|
|
constexpr std::size_t CacheSize=1024;
|
|
static_assert((CacheSize&(CacheSize-1))==0);
|
|
static thread_local VoxelForgeCoreThreadLocal::FLazy<
|
|
std::array<FRootLobeCacheEntry,CacheSize>> Cache;
|
|
auto& cache=Cache.Get();
|
|
const std::uint64_t IndexKey=island.stableId^(island.stableId>>33)
|
|
^(static_cast<std::uint64_t>(recipe.seed)<<17)
|
|
^(static_cast<std::uint64_t>(static_cast<std::uint32_t>(rootIndex))*0x9e3779b97f4a7c15ull);
|
|
FRootLobeCacheEntry& Entry=cache[static_cast<std::size_t>(IndexKey)&(CacheSize-1)];
|
|
const bool bKeyMatches=Entry.bValid&&Entry.Seed==recipe.seed
|
|
&&Entry.StableId==island.stableId&&Entry.RootIndex==rootIndex
|
|
&&Entry.GeometryVersion==recipe.geometryVersion
|
|
&&Entry.Center==island.centerSubvoxels
|
|
&&Entry.TopZ==island.topZSubvoxels&&Entry.Radius==island.radiusSubvoxels
|
|
&&Entry.TaperEndQ16==island.taperEndQ16
|
|
&&Entry.ThicknessRatioQ16==recipe.thicknessRatioQ16
|
|
&&Entry.CrustDepthBonusQ16==recipe.crustDepthBonusQ16
|
|
&&Entry.RootDepthRatioQ16==recipe.rootDepthRatioQ16;
|
|
if(!bKeyMatches)
|
|
{
|
|
constexpr std::array<std::array<double, 2>, 4> pattern{{
|
|
{{-0.92, -0.12}}, {{0.28, 0.98}}, {{0.83, -0.42}}, {{-0.16, -0.94}}}};
|
|
const std::uint64_t salt = 0x524f4f540000ull + static_cast<std::uint64_t>(rootIndex);
|
|
const std::uint64_t roll = Roll(recipe.seed, island.stableId, salt);
|
|
const double radius = static_cast<double>(island.radiusSubvoxels);
|
|
const double rootDepth = radius * Q16ToDouble(recipe.rootDepthRatioQ16);
|
|
const double crustBottom = static_cast<double>(IslandCrustBottom(recipe, island));
|
|
const bool centralButtress = rootIndex == 4;
|
|
const double orbit = centralButtress ? 0.0
|
|
: radius * (0.17 + 0.07 * UnitHash(roll));
|
|
const double rootX = static_cast<double>(island.centerSubvoxels[0])
|
|
+ (centralButtress ? 0.0 : pattern[static_cast<std::size_t>(rootIndex)][0] * orbit);
|
|
const double rootY = static_cast<double>(island.centerSubvoxels[1])
|
|
+ (centralButtress ? 0.0 : pattern[static_cast<std::size_t>(rootIndex)][1] * orbit);
|
|
const double shortRootVariation = UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x4c454eull));
|
|
const double lengthRatio = centralButtress ? 1.0 : (rootIndex == 0 ? 1.0
|
|
: Q16ToDouble(island.taperEndQ16) * (0.82 + 0.18 * shortRootVariation));
|
|
const double lowerDepth = rootDepth * lengthRatio;
|
|
const double embedDepth = rootDepth * (centralButtress ? 0.22 : (rootIndex == 0 ? 0.30 : 0.22));
|
|
const double verticalRadius = 0.5 * (lowerDepth + embedDepth);
|
|
const double centerZ = crustBottom + 0.5 * (embedDepth - lowerDepth);
|
|
const double rx = radius * (centralButtress ? 0.25
|
|
: 0.15 + 0.055 * UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x5258ull)));
|
|
const double ry = radius * (centralButtress ? 0.25
|
|
: 0.15 + 0.065 * UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x5259ull)));
|
|
Entry.bValid=true;
|
|
Entry.Seed=recipe.seed;
|
|
Entry.StableId=island.stableId;
|
|
Entry.RootIndex=rootIndex;
|
|
Entry.GeometryVersion=recipe.geometryVersion;
|
|
Entry.Center=island.centerSubvoxels;
|
|
Entry.TopZ=island.topZSubvoxels;
|
|
Entry.Radius=island.radiusSubvoxels;
|
|
Entry.TaperEndQ16=island.taperEndQ16;
|
|
Entry.ThicknessRatioQ16=recipe.thicknessRatioQ16;
|
|
Entry.CrustDepthBonusQ16=recipe.crustDepthBonusQ16;
|
|
Entry.RootDepthRatioQ16=recipe.rootDepthRatioQ16;
|
|
Entry.Parameters={rootX,rootY,centerZ,rx*rx,ry*ry,
|
|
verticalRadius*verticalRadius,std::min({rx,ry,verticalRadius})};
|
|
}
|
|
const FRootLobeParameters& Parameters=Entry.Parameters;
|
|
const double q = (x - Parameters.RootX) * (x - Parameters.RootX) / Parameters.RadiusXSquared
|
|
+ (y - Parameters.RootY) * (y - Parameters.RootY) / Parameters.RadiusYSquared
|
|
+ (z - Parameters.CenterZ) * (z - Parameters.CenterZ) / Parameters.VerticalRadiusSquared;
|
|
return (std::sqrt(std::max(q, 0.0)) - 1.0) * Parameters.MinimumRadius;
|
|
}
|
|
|
|
inline double ApplySurfaceDetail(const FloatingIslandRecipeV1& recipe,
|
|
std::uint64_t featureId, double structural,
|
|
double x, double y, double z, double detailScale = 1.0)
|
|
{
|
|
if (recipe.geometryVersion < 2)
|
|
return structural + Roughness(recipe, featureId, x, y, z);
|
|
const double amplitude = static_cast<double>(recipe.roughnessSubvoxels) * detailScale;
|
|
const double band = 2.0 * amplitude;
|
|
if (!(amplitude > 0.0) || std::abs(structural) >= band) return structural;
|
|
const double fade = 1.0 - SmoothStep(std::abs(structural) / band);
|
|
return structural + Roughness(recipe, featureId, x, y, z) * detailScale * fade;
|
|
}
|
|
|
|
inline double IslandDensity(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandPlanV1& plan,
|
|
const FloatingIslandAnchorV1& island,
|
|
double x, double y, double z)
|
|
{
|
|
if (recipe.geometryVersion < 2)
|
|
{
|
|
const double dx = x - static_cast<double>(island.centerSubvoxels[0]);
|
|
const double dy = y - static_cast<double>(island.centerSubvoxels[1]);
|
|
const double radial = std::hypot(dx, dy);
|
|
const double topZ = static_cast<double>(island.topZSubvoxels);
|
|
const double bottomZ = static_cast<double>(island.bottomZSubvoxels);
|
|
const double verticalSpan = std::max(topZ - bottomZ, 1.0);
|
|
const double height = std::clamp((z - bottomZ) / verticalSpan, 0.0, 1.0);
|
|
const double taperEnd = Q16ToDouble(island.taperEndQ16);
|
|
const double taper = SmoothStep(std::clamp(height / std::max(taperEnd, 0.01), 0.0, 1.0));
|
|
const double effectiveRadius = std::max(0.0,
|
|
static_cast<double>(island.radiusSubvoxels) + LobeOffset(recipe, island, dx, dy, z));
|
|
const double envelope = effectiveRadius * taper;
|
|
const double top = IslandTopSurface(recipe, island,
|
|
static_cast<double>(island.centerSubvoxels[0]) + dx,
|
|
static_cast<double>(island.centerSubvoxels[1]) + dy);
|
|
return ApplySurfaceDetail(recipe, island.stableId,
|
|
std::max({z - top, radial - envelope, bottomZ - z}), x, y, z);
|
|
}
|
|
|
|
const FIslandColumnCacheEntry& Column=IslandColumnCache(recipe,plan,island,x,y);
|
|
const double dx=Column.DeltaX;
|
|
const double dy=Column.DeltaY;
|
|
const double radial=Column.Radial;
|
|
const double topZ = static_cast<double>(island.topZSubvoxels);
|
|
const double radius = static_cast<double>(island.radiusSubvoxels);
|
|
const double effectiveRadius = std::max(0.0,
|
|
radius + LobeOffsetWithPhase(recipe,Column.Angle,z,Column.LobePhase));
|
|
const double edge = std::clamp(radial / std::max(effectiveRadius, 1.0), 0.0, 1.0);
|
|
const double lowerSurface = static_cast<double>(IslandCrustBottom(recipe, island))
|
|
+ radius * 0.16 * edge * edge;
|
|
const double top = Column.TopSurface;
|
|
const double topDistance = z - top;
|
|
const double sideDistance = radial - effectiveRadius;
|
|
const double undersideDistance = lowerSurface - z;
|
|
double sdf = std::max({topDistance, sideDistance, undersideDistance});
|
|
const std::int32_t rootCount = RootCount(recipe, island);
|
|
const double rootBlend = std::clamp(radius * 0.12, 2.0 * Q, 5.0 * Q);
|
|
for (std::int32_t root = 0; root < rootCount; ++root)
|
|
sdf = SmoothMin(sdf, RootLobeDensity(recipe, island, root, x, y, z), rootBlend);
|
|
if (recipe.geometryVersion >= 5 && recipe.geometryVersion < 8
|
|
&& island.ordinal == recipe.anchorIslandOrdinal
|
|
&& recipe.anchorRootBottomZSubvoxels < island.bottomZSubvoxels)
|
|
{
|
|
// The central buttress must meet the island crust underside. IslandBottom
|
|
// also includes the four shorter root lobes and sits below that surface.
|
|
const double rootTop = static_cast<double>(IslandCrustBottom(recipe, island));
|
|
const double rootBottom = static_cast<double>(recipe.anchorRootBottomZSubvoxels);
|
|
const double depth = std::max(1.0, rootTop - rootBottom);
|
|
const double blend = std::clamp(static_cast<double>(recipe.anchorRootRadiusSubvoxels)
|
|
* 0.12, 3.0 * Q, 6.0 * Q);
|
|
// A slightly flared lower buttress joins the island's own root lobes to bedrock.
|
|
// Une base legerement evasee relie les racines de l'ile a la roche de fond.
|
|
const double t = std::clamp((rootTop - z) / depth, 0.0, 1.0);
|
|
const double rootRadius = static_cast<double>(recipe.anchorRootRadiusSubvoxels)
|
|
* (0.92 + 0.08 * SmoothStep(t));
|
|
double rootSdf = std::max({Column.Radial - rootRadius,
|
|
rootBottom - z, z - rootTop});
|
|
rootSdf = ApplySurfaceDetail(recipe, island.stableId ^ 0x414e43484f52524full,
|
|
rootSdf, x, y, z, 0.75);
|
|
sdf = SmoothMin(sdf, rootSdf, blend);
|
|
}
|
|
const double topDetailScale = topDistance >= std::max(sideDistance, undersideDistance)
|
|
|| z >= static_cast<double>(IslandCrustBottom(recipe, island)) + radius * 0.45
|
|
? 0.25 : 1.0;
|
|
return ApplySurfaceDetail(recipe, island.stableId, sdf, x, y, z, topDetailScale);
|
|
}
|
|
|
|
inline double ConnectorDensity(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandConnectorV1& edge,
|
|
double x, double y, double z,
|
|
bool* topSurface = nullptr,
|
|
double* alongFraction = nullptr)
|
|
{
|
|
const double ax = static_cast<double>(edge.startSubvoxels[0]);
|
|
const double ay = static_cast<double>(edge.startSubvoxels[1]);
|
|
const double az = static_cast<double>(edge.startSubvoxels[2]);
|
|
const double dx = static_cast<double>(edge.endSubvoxels[0]) - ax;
|
|
const double dy = static_cast<double>(edge.endSubvoxels[1]) - ay;
|
|
const double length = std::hypot(dx, dy);
|
|
if (!(length > 0.0)) return MaxDensity;
|
|
const double ux = dx / length, uy = dy / length;
|
|
const double px = x - ax, py = y - ay;
|
|
const double along = px * ux + py * uy;
|
|
const double side = std::abs(-px * uy + py * ux);
|
|
const double qx = side - static_cast<double>(edge.halfWidthSubvoxels);
|
|
const double qy = std::max(-along, along - length);
|
|
const double outside = std::hypot(std::max(qx, 0.0), std::max(qy, 0.0));
|
|
const double horizontal = outside + std::min(std::max(qx, qy), 0.0);
|
|
const double t = std::clamp(along / length, 0.0, 1.0);
|
|
if (alongFraction) *alongFraction = t;
|
|
const double deck = az + (static_cast<double>(edge.endSubvoxels[2]) - az) * t;
|
|
const std::uint64_t westernEdgeId = StableId(recipe.seed, 0x200000000ull,
|
|
0x425249444745ull);
|
|
const bool westernSupportConnector=edge.stableId==westernEdgeId;
|
|
if(recipe.geometryVersion>=2&&WorldAssemblyEvaluatorVersion>=183
|
|
&&westernSupportConnector)
|
|
{
|
|
const double phase=static_cast<double>(Roll(recipe.seed,edge.stableId,0x5749445448ull)
|
|
&0xffffffffull)*(2.0*Pi/4294967296.0);
|
|
const double localHalfWidth=std::max(
|
|
static_cast<double>(recipe.routeConstraints.capsuleRadiusSubvoxels+Q),
|
|
static_cast<double>(edge.halfWidthSubvoxels)
|
|
+0.48*static_cast<double>(Q)*std::sin(6.0*Pi*t+phase));
|
|
const double localThickness=static_cast<double>(edge.thicknessSubvoxels);
|
|
const double rounding=std::min({1.5*static_cast<double>(Q),
|
|
localHalfWidth*0.6,localThickness*0.45});
|
|
const double roundedX=side-(localHalfWidth-rounding);
|
|
const double roundedY=std::abs(along-0.5*length)-(0.5*length-rounding);
|
|
const double roundedZ=std::abs(z-(deck-0.5*localThickness))
|
|
-(0.5*localThickness-rounding);
|
|
const double roundedOutside=std::sqrt(
|
|
std::pow(std::max(roundedX,0.0),2)
|
|
+std::pow(std::max(roundedY,0.0),2)
|
|
+std::pow(std::max(roundedZ,0.0),2));
|
|
const double roundedInside=std::min(std::max(roundedX,
|
|
std::max(roundedY,roundedZ)),0.0);
|
|
if(topSurface)*topSurface=z-deck>=std::max(horizontal,
|
|
deck-localThickness-z);
|
|
return roundedOutside+roundedInside-rounding;
|
|
}
|
|
const double arch = recipe.geometryVersion >= 2
|
|
&& WorldAssemblyEvaluatorVersion < 172
|
|
? 2.0 * static_cast<double>(edge.thicknessSubvoxels) * (4.0 * t * (1.0 - t))
|
|
: 0.0;
|
|
const double upper = z - deck;
|
|
const double lower = deck - static_cast<double>(edge.thicknessSubvoxels) - arch - z;
|
|
if (topSurface) *topSurface = upper >= std::max(horizontal, lower);
|
|
return std::max({horizontal, upper, lower});
|
|
}
|
|
|
|
inline std::int64_t FieldDensity(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandPlanV1& plan,
|
|
std::int64_t x, std::int64_t y, std::int64_t z)
|
|
{
|
|
if (plan.islands.empty()) return MaxDensity;
|
|
const double blend = static_cast<double>(recipe.blendRadiusSubvoxels);
|
|
double result = static_cast<double>(MaxDensity);
|
|
for (const auto& island : plan.islands)
|
|
{
|
|
double group = IslandDensity(recipe, plan, island,
|
|
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z));
|
|
for (const auto& edge : plan.clusterEdges)
|
|
{
|
|
if (edge.fromIslandId != island.stableId) continue;
|
|
bool topSurface = false;
|
|
double alongFraction = 0.0;
|
|
double connector = ConnectorDensity(recipe, edge,
|
|
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z),
|
|
&topSurface, &alongFraction);
|
|
if (recipe.geometryVersion >= 2)
|
|
connector = ApplySurfaceDetail(recipe, edge.stableId, connector,
|
|
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z),
|
|
topSurface || alongFraction < 0.12 || alongFraction > 0.88 ? 0.25 : 1.0);
|
|
group = SmoothMin(group, connector, blend);
|
|
}
|
|
result = std::min(result, group);
|
|
}
|
|
return std::clamp(RoundQ(result), -MaxDensity, MaxDensity);
|
|
}
|
|
|
|
inline std::int64_t FieldDensityForTile(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandPlanV1& plan, std::int64_t x, std::int64_t y, std::int64_t z,
|
|
const FloatingIslandTileFeatureListV1& featureList)
|
|
{
|
|
if (plan.islands.empty()) return MaxDensity;
|
|
if (featureList.groupBounds.size() != plan.islands.size())
|
|
return FieldDensity(recipe, plan, x, y, z);
|
|
const auto& featureIndices = featureList.featureIndices;
|
|
const std::size_t islandCount = plan.islands.size();
|
|
const double blend = static_cast<double>(recipe.blendRadiusSubvoxels);
|
|
double result = static_cast<double>(MaxDensity);
|
|
|
|
const auto evaluateLocalGroup = [&](std::size_t islandIndex)
|
|
{
|
|
const auto& island = plan.islands[islandIndex];
|
|
double group = IslandDensity(recipe, plan, island,
|
|
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z));
|
|
for (const std::uint32_t edgeIndex : featureIndices)
|
|
{
|
|
if (edgeIndex < islandCount) continue;
|
|
const std::size_t planIndex = static_cast<std::size_t>(edgeIndex) - islandCount;
|
|
if (planIndex >= plan.clusterEdges.size()) continue;
|
|
const auto& edge = plan.clusterEdges[planIndex];
|
|
if (edge.fromIslandId != island.stableId) continue;
|
|
bool topSurface = false;
|
|
double alongFraction = 0.0;
|
|
double connector = ConnectorDensity(recipe, edge,
|
|
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z),
|
|
&topSurface, &alongFraction);
|
|
if (recipe.geometryVersion >= 2)
|
|
connector = ApplySurfaceDetail(recipe, edge.stableId, connector,
|
|
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z),
|
|
topSurface || alongFraction < 0.12 || alongFraction > 0.88 ? 0.25 : 1.0);
|
|
group = SmoothMin(group, connector, blend);
|
|
}
|
|
return group;
|
|
};
|
|
|
|
const auto evaluateDistantGroup = [&](std::size_t islandIndex)
|
|
{
|
|
const auto& island = plan.islands[islandIndex];
|
|
double group = IslandDensity(recipe, plan, island,
|
|
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z));
|
|
for (const auto& edge : plan.clusterEdges)
|
|
{
|
|
if (edge.fromIslandId != island.stableId) continue;
|
|
bool topSurface = false;
|
|
double alongFraction = 0.0;
|
|
double connector = ConnectorDensity(recipe, edge,
|
|
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z),
|
|
&topSurface, &alongFraction);
|
|
if (recipe.geometryVersion >= 2)
|
|
connector = ApplySurfaceDetail(recipe, edge.stableId, connector,
|
|
static_cast<double>(x), static_cast<double>(y), static_cast<double>(z),
|
|
topSurface || alongFraction < 0.12 || alongFraction > 0.88 ? 0.25 : 1.0);
|
|
group = SmoothMin(group, connector, blend);
|
|
}
|
|
return group;
|
|
};
|
|
|
|
// Each local group keeps its complete ordered connector chain in the tile list.
|
|
// Les connecteurs d'un groupe local restent complets et dans leur ordre canonique.
|
|
for (const std::uint32_t index : featureIndices)
|
|
{
|
|
if (index >= islandCount) break;
|
|
result = std::min(result, evaluateLocalGroup(index));
|
|
}
|
|
|
|
// Air-side density magnitudes are part of the canonical field too. The per-tile list
|
|
// handles nearby groups; conservative group-box lower bounds prune distant exact SDFs.
|
|
// Les magnitudes d'air restent canoniques; les bornes de groupes eliminent les SDF lointains.
|
|
if (result >= 0.0)
|
|
{
|
|
if ((featureList.groupSearchOrder.size() != islandCount
|
|
|| featureList.groupDistanceLowerBoundsSubvoxels.size() != islandCount)
|
|
&& featureList.groupSearchComplete)
|
|
return FieldDensity(recipe, plan, x, y, z);
|
|
const std::size_t searchCount = std::min(featureList.groupSearchOrder.size(),
|
|
featureList.groupDistanceLowerBoundsSubvoxels.size());
|
|
for (std::size_t orderIndex = 0; orderIndex < searchCount; ++orderIndex)
|
|
{
|
|
const std::size_t islandIndex = featureList.groupSearchOrder[orderIndex];
|
|
if (islandIndex >= islandCount) return FieldDensity(recipe, plan, x, y, z);
|
|
if (std::binary_search(featureIndices.begin(), featureIndices.end(),
|
|
static_cast<std::uint32_t>(islandIndex))) continue;
|
|
const auto& bounds = featureList.groupBounds[islandIndex];
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
if (bounds.minimumSubvoxels[axis] > bounds.maximumSubvoxels[axis])
|
|
return FieldDensity(recipe, plan, x, y, z);
|
|
if (static_cast<double>(featureList.groupDistanceLowerBoundsSubvoxels[orderIndex]) > result)
|
|
break;
|
|
result = std::min(result, evaluateDistantGroup(islandIndex));
|
|
}
|
|
}
|
|
return std::clamp(RoundQ(result), -MaxDensity, MaxDensity);
|
|
}
|
|
|
|
inline void SetIslandBounds(const FloatingIslandRecipeV1& recipe,
|
|
FloatingIslandAnchorV1& island)
|
|
{
|
|
const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels;
|
|
const bool isAnchorRoot=recipe.geometryVersion>=5&&recipe.geometryVersion<8
|
|
&&island.ordinal==recipe.anchorIslandOrdinal;
|
|
const std::int64_t reach = std::max(island.radiusSubvoxels,
|
|
isAnchorRoot?recipe.anchorRootRadiusSubvoxels:std::int64_t{0})
|
|
+ recipe.contourWarpSubvoxels + pad;
|
|
island.influenceMinimumSubvoxels = {island.centerSubvoxels[0] - reach,
|
|
island.centerSubvoxels[1] - reach,
|
|
(isAnchorRoot?std::min(island.bottomZSubvoxels,recipe.anchorRootBottomZSubvoxels)
|
|
:island.bottomZSubvoxels)-pad};
|
|
island.influenceMaximumSubvoxels = {island.centerSubvoxels[0] + reach,
|
|
island.centerSubvoxels[1] + reach, island.topZSubvoxels + pad};
|
|
}
|
|
|
|
inline void SetConnectorBounds(const FloatingIslandRecipeV1& recipe,
|
|
FloatingIslandConnectorV1& edge)
|
|
{
|
|
const std::int64_t pad = recipe.blendRadiusSubvoxels + recipe.roughnessSubvoxels;
|
|
const std::int64_t archDepth = recipe.geometryVersion >= 2
|
|
? 2 * edge.thicknessSubvoxels : 0;
|
|
const std::int64_t xyPad = edge.halfWidthSubvoxels + pad;
|
|
edge.influenceMinimumSubvoxels = {
|
|
std::min(edge.startSubvoxels[0], edge.endSubvoxels[0]) - xyPad,
|
|
std::min(edge.startSubvoxels[1], edge.endSubvoxels[1]) - xyPad,
|
|
std::min(edge.startSubvoxels[2], edge.endSubvoxels[2]) - edge.thicknessSubvoxels - pad};
|
|
edge.influenceMinimumSubvoxels[2] -= archDepth;
|
|
edge.influenceMaximumSubvoxels = {
|
|
std::max(edge.startSubvoxels[0], edge.endSubvoxels[0]) + xyPad,
|
|
std::max(edge.startSubvoxels[1], edge.endSubvoxels[1]) + xyPad,
|
|
std::max(edge.startSubvoxels[2], edge.endSubvoxels[2]) + pad};
|
|
}
|
|
|
|
inline const FloatingIslandAnchorV1* FindIsland(const FloatingIslandPlanV1& plan,
|
|
std::uint64_t id)
|
|
{
|
|
const auto found = std::find_if(plan.islands.begin(), plan.islands.end(),
|
|
[id](const auto& island) { return island.stableId == id; });
|
|
return found == plan.islands.end() ? nullptr : &*found;
|
|
}
|
|
|
|
inline bool ValidateManifest(const FloatingIslandManifestV1& manifest, std::string& error)
|
|
{
|
|
if (!ValidateRecipe(manifest.recipe, error)) return false;
|
|
const auto& recipe = manifest.recipe;
|
|
const auto& plan = manifest.plan;
|
|
if (plan.islands.size() < recipe.primaryIslandCount
|
|
|| plan.islands.size() > recipe.primaryIslandCount + 2
|
|
|| plan.clusterEdges.size() + 1 < plan.islands.size()
|
|
|| plan.clusterEdges.size() > plan.islands.size() + 1
|
|
|| plan.route.size() < 2 || plan.route.size() > 4096)
|
|
{ error = "floating-island plan has an invalid bounded island graph or route"; return false; }
|
|
|
|
std::unordered_set<std::uint64_t> islandIds;
|
|
std::unordered_set<std::uint32_t> ordinals;
|
|
const FloatingIslandAnchorV1* anchorIsland = nullptr;
|
|
for (const auto& island : plan.islands)
|
|
{
|
|
const bool isAnchor=recipe.geometryVersion>=5
|
|
&&island.ordinal==recipe.anchorIslandOrdinal;
|
|
if(isAnchor)anchorIsland=&island;
|
|
if (island.stableId == 0 || !islandIds.insert(island.stableId).second
|
|
|| !ordinals.insert(island.ordinal).second
|
|
|| island.radiusSubvoxels < (isAnchor?recipe.anchorIslandMinimumRadiusSubvoxels
|
|
:recipe.minimumRadiusSubvoxels)
|
|
|| island.radiusSubvoxels > (isAnchor
|
|
?std::max(recipe.anchorIslandMinimumRadiusSubvoxels,recipe.maximumRadiusSubvoxels)
|
|
:recipe.maximumRadiusSubvoxels)
|
|
|| island.topHalfSubvoxels <= 0 || island.topZSubvoxels <= island.bottomZSubvoxels
|
|
|| island.taperEndQ16 < recipe.taperEndMinimumQ16
|
|
|| island.taperEndQ16 > recipe.taperEndMaximumQ16
|
|
|| island.centerSubvoxels[2] != island.topZSubvoxels - island.topHalfSubvoxels)
|
|
{ error = "floating-island anchor has an invalid or unstable identity or profile"; return false; }
|
|
if (recipe.geometryVersion >= 2
|
|
&& static_cast<double>(island.topZSubvoxels - island.bottomZSubvoxels)
|
|
/ (2.0 * static_cast<double>(island.radiusSubvoxels)) < 0.70)
|
|
{ error = "floating-island crust/root depth is below its minimum width ratio"; return false; }
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
{
|
|
if (!ValidScalar(island.centerSubvoxels[axis])
|
|
|| island.influenceMinimumSubvoxels[axis] > island.influenceMaximumSubvoxels[axis])
|
|
{ error = "floating-island anchor has invalid coordinates or an inverted bound"; return false; }
|
|
}
|
|
const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels;
|
|
const bool hasAnchorRoot=recipe.geometryVersion>=5&&recipe.geometryVersion<8&&isAnchor;
|
|
const std::int64_t reach = std::max(island.radiusSubvoxels,
|
|
hasAnchorRoot?recipe.anchorRootRadiusSubvoxels:std::int64_t{0})
|
|
+ recipe.contourWarpSubvoxels + pad;
|
|
if (island.influenceMinimumSubvoxels[0] > island.centerSubvoxels[0] - reach
|
|
|| island.influenceMaximumSubvoxels[0] < island.centerSubvoxels[0] + reach
|
|
|| island.influenceMinimumSubvoxels[1] > island.centerSubvoxels[1] - reach
|
|
|| island.influenceMaximumSubvoxels[1] < island.centerSubvoxels[1] + reach
|
|
|| island.influenceMinimumSubvoxels[2] > (hasAnchorRoot
|
|
?std::min(island.bottomZSubvoxels,recipe.anchorRootBottomZSubvoxels)
|
|
:island.bottomZSubvoxels) - pad
|
|
|| island.influenceMaximumSubvoxels[2] < island.topZSubvoxels + pad
|
|
|| island.bottomZSubvoxels < recipe.layoutMinimumZSubvoxels + recipe.boundarySealSubvoxels
|
|
|| island.topZSubvoxels > recipe.layoutMaximumZSubvoxels - recipe.boundarySealSubvoxels)
|
|
{ error = "floating-island bound omits its taper, lobe warp, blend or finite layout margin"; return false; }
|
|
}
|
|
for (std::uint32_t ordinal = 0; ordinal < recipe.primaryIslandCount; ++ordinal)
|
|
if (!ordinals.contains(ordinal))
|
|
{ error = "floating-island route chain is missing a primary seeded anchor"; return false; }
|
|
if(recipe.geometryVersion>=5)
|
|
{
|
|
if(!anchorIsland)
|
|
{error="floating-island plan is missing its unique passage anchor island";return false;}
|
|
const double anchorDistance=std::hypot(
|
|
static_cast<double>(anchorIsland->centerSubvoxels[0]),
|
|
static_cast<double>(anchorIsland->centerSubvoxels[1]));
|
|
if(anchorDistance>recipe.anchorPlacementRadiusSubvoxels
|
|
||recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels
|
|
||recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
>=anchorIsland->radiusSubvoxels)
|
|
{error="floating-island anchor island or its mouth leaves the passage radius or rock envelope";return false;}
|
|
}
|
|
|
|
std::unordered_set<std::uint64_t> edgeIds;
|
|
std::unordered_map<std::uint64_t, std::vector<std::uint64_t>> adjacency;
|
|
for (const auto& edge : plan.clusterEdges)
|
|
{
|
|
if (edge.stableId == 0 || !edgeIds.insert(edge.stableId).second
|
|
|| edge.fromIslandId == edge.toIslandId || !islandIds.contains(edge.fromIslandId)
|
|
|| !islandIds.contains(edge.toIslandId) || !edge.sharpTagReady
|
|
|| edge.halfWidthSubvoxels < recipe.routeConstraints.capsuleRadiusSubvoxels + Q
|
|
|| edge.thicknessSubvoxels < 2 * Q)
|
|
{ error = "floating-island graph edge is invalid, unconnected or missing its sharp tag"; return false; }
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
if (!ValidScalar(edge.startSubvoxels[axis]) || !ValidScalar(edge.endSubvoxels[axis])
|
|
|| edge.influenceMinimumSubvoxels[axis] > edge.influenceMaximumSubvoxels[axis])
|
|
{ error = "floating-island connector has invalid endpoint or influence data"; return false; }
|
|
const std::int64_t pad = recipe.blendRadiusSubvoxels + recipe.roughnessSubvoxels;
|
|
const std::int64_t xyPad = edge.halfWidthSubvoxels + pad;
|
|
if (edge.influenceMinimumSubvoxels[0] > std::min(edge.startSubvoxels[0], edge.endSubvoxels[0]) - xyPad
|
|
|| edge.influenceMaximumSubvoxels[0] < std::max(edge.startSubvoxels[0], edge.endSubvoxels[0]) + xyPad
|
|
|| edge.influenceMinimumSubvoxels[1] > std::min(edge.startSubvoxels[1], edge.endSubvoxels[1]) - xyPad
|
|
|| edge.influenceMaximumSubvoxels[1] < std::max(edge.startSubvoxels[1], edge.endSubvoxels[1]) + xyPad
|
|
|| edge.influenceMinimumSubvoxels[2] > std::min(edge.startSubvoxels[2], edge.endSubvoxels[2])
|
|
- edge.thicknessSubvoxels - pad
|
|
- (recipe.geometryVersion >= 2 ? 2 * edge.thicknessSubvoxels : 0)
|
|
|| edge.influenceMaximumSubvoxels[2] < std::max(edge.startSubvoxels[2], edge.endSubvoxels[2]) + pad)
|
|
{ error = "floating-island connector bound omits its swept bridge, blend or thickness"; return false; }
|
|
adjacency[edge.fromIslandId].push_back(edge.toIslandId);
|
|
adjacency[edge.toIslandId].push_back(edge.fromIslandId);
|
|
}
|
|
std::unordered_set<std::uint64_t> reached;
|
|
std::vector<std::uint64_t> pending{plan.islands.front().stableId};
|
|
while (!pending.empty())
|
|
{
|
|
const std::uint64_t current = pending.back(); pending.pop_back();
|
|
if (!reached.insert(current).second) continue;
|
|
for (const auto next : adjacency[current]) if (!reached.contains(next)) pending.push_back(next);
|
|
}
|
|
if (reached.size() != plan.islands.size())
|
|
{ error = "floating-island cluster graph is disconnected"; return false; }
|
|
|
|
std::unordered_set<std::uint64_t> routeIds;
|
|
for (const auto& point : plan.route)
|
|
{
|
|
if (point.stableFeatureId == 0 || !routeIds.insert(point.stableFeatureId).second
|
|
|| point.radiusSubvoxels < recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
|| !std::all_of(point.positionSubvoxels.begin(), point.positionSubvoxels.end(), ValidScalar))
|
|
{ error = "floating-island route point has invalid identity, clearance or position"; return false; }
|
|
const std::int64_t halfHeight = recipe.routeConstraints.capsuleHeightSubvoxels / 2;
|
|
if (point.positionSubvoxels[2] - halfHeight < recipe.layoutMinimumZSubvoxels
|
|
|| point.positionSubvoxels[2] + halfHeight > recipe.layoutMaximumZSubvoxels)
|
|
{ error = "floating-island route capsule escapes its finite vertical layout"; return false; }
|
|
}
|
|
for (std::size_t i = 1; i < plan.route.size(); ++i)
|
|
{
|
|
double square = 0.0;
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
{
|
|
const std::int64_t delta = plan.route[i].positionSubvoxels[axis]
|
|
- plan.route[i - 1].positionSubvoxels[axis];
|
|
if (std::abs(delta) > 4096 * Q)
|
|
{ error = "floating-island route segment exceeds its bounded sweep length"; return false; }
|
|
square += static_cast<double>(delta) * static_cast<double>(delta);
|
|
}
|
|
if (!(square > 0.0))
|
|
{ error = "floating-island route has a zero-length segment"; return false; }
|
|
}
|
|
return true;
|
|
}
|
|
|
|
inline double TopLevel(const FloatingIslandRecipeV1& recipe, std::uint64_t site,
|
|
double centerZ, std::int64_t radius, double maxRise)
|
|
{
|
|
const std::int64_t signedDelta = SignedRange(recipe.seed, site, 0x4c4556454cull,
|
|
static_cast<std::int64_t>(std::llround(maxRise)));
|
|
return centerZ + static_cast<double>(signedDelta);
|
|
}
|
|
|
|
inline FloatingIslandAnchorV1 MakeIsland(const FloatingIslandRecipeV1& recipe,
|
|
std::uint32_t ordinal, std::int64_t x, std::int64_t y, std::int64_t topZ,
|
|
std::uint64_t siteSalt)
|
|
{
|
|
FloatingIslandAnchorV1 island;
|
|
island.ordinal = ordinal;
|
|
island.stableId = StableId(recipe.seed, siteSalt, 0x49534c414e44ull);
|
|
const std::uint32_t radiusRoll = UnitQ16(recipe.seed, siteSalt, 0x524144495553ull);
|
|
island.radiusSubvoxels = recipe.minimumRadiusSubvoxels
|
|
+ (recipe.maximumRadiusSubvoxels - recipe.minimumRadiusSubvoxels)
|
|
* radiusRoll / 65535;
|
|
island.topHalfSubvoxels = std::max<std::int64_t>(1, island.radiusSubvoxels / 5);
|
|
island.topZSubvoxels = topZ;
|
|
island.bottomZSubvoxels = IslandBottom(recipe, topZ, island.radiusSubvoxels);
|
|
island.centerSubvoxels = {x, y, topZ - island.topHalfSubvoxels};
|
|
island.taperEndQ16 = recipe.taperEndMinimumQ16
|
|
+ (recipe.taperEndMaximumQ16 - recipe.taperEndMinimumQ16)
|
|
* UnitQ16(recipe.seed, siteSalt, 0x5441504552ull) / 65535;
|
|
SetIslandBounds(recipe, island);
|
|
return island;
|
|
}
|
|
|
|
inline FloatingIslandConnectorV1 MakeConnector(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandAnchorV1& from, const FloatingIslandAnchorV1& to,
|
|
std::uint64_t siteSalt)
|
|
{
|
|
FloatingIslandConnectorV1 edge;
|
|
edge.stableId = StableId(recipe.seed, siteSalt, 0x425249444745ull);
|
|
edge.fromIslandId = from.stableId;
|
|
edge.toIslandId = to.stableId;
|
|
const double dx = static_cast<double>(to.centerSubvoxels[0] - from.centerSubvoxels[0]);
|
|
const double dy = static_cast<double>(to.centerSubvoxels[1] - from.centerSubvoxels[1]);
|
|
const double length = std::hypot(dx, dy);
|
|
const double ux = dx / std::max(length, 1.0);
|
|
const double uy = dy / std::max(length, 1.0);
|
|
// Land each connector well inside the broad top so the full capsule footprint remains
|
|
// supported across the bridge-to-island transition, including bounded surface roughness.
|
|
// Les connecteurs recouvrent le plateau pour soutenir toute la capsule au raccord.
|
|
// Keep the capsule footprint supported while seating L37 connectors just below the native island tops.
|
|
// Garder l'appui de la capsule tout en enfouissant les connecteurs L37 juste sous les plateaux.
|
|
const double landingFraction = recipe.geometryVersion >= 2 ? 0.52 : 0.62;
|
|
const double fromReach = static_cast<double>(from.radiusSubvoxels) * landingFraction;
|
|
const double toReach = static_cast<double>(to.radiusSubvoxels) * landingFraction;
|
|
const double sx = from.centerSubvoxels[0] + ux * fromReach;
|
|
const double sy = from.centerSubvoxels[1] + uy * fromReach;
|
|
const double ex = to.centerSubvoxels[0] - ux * toReach;
|
|
const double ey = to.centerSubvoxels[1] - uy * toReach;
|
|
const bool craterMouthConnector=WorldAssemblyEvaluatorVersion>=174
|
|
&&(from.ordinal==recipe.anchorIslandOrdinal
|
|
||to.ordinal==recipe.anchorIslandOrdinal);
|
|
const bool westernSupportConnector=WorldAssemblyEvaluatorVersion>=178
|
|
&&((from.ordinal==0&&to.ordinal==1)||(from.ordinal==1&&to.ordinal==0));
|
|
const double routeDeckLift=recipe.geometryVersion>=2
|
|
?(WorldAssemblyEvaluatorVersion>=178
|
|
?(westernSupportConnector
|
|
?(WorldAssemblyEvaluatorVersion>=181?-1.0
|
|
:(WorldAssemblyEvaluatorVersion>=180?-1.25
|
|
:(WorldAssemblyEvaluatorVersion>=179?-1.5:-1.0))):-2.0)
|
|
:(craterMouthConnector?-2.0
|
|
:(WorldAssemblyEvaluatorVersion>=172?-1.0:1.0)))*Q:0.0;
|
|
const double sz = IslandTopSurface(recipe, from, sx, sy) + routeDeckLift;
|
|
const double ez = IslandTopSurface(recipe, to, ex, ey) + routeDeckLift;
|
|
edge.startSubvoxels = {RoundQ(sx), RoundQ(sy), RoundQ(sz)};
|
|
edge.endSubvoxels = {RoundQ(ex), RoundQ(ey), RoundQ(ez)};
|
|
edge.halfWidthSubvoxels = recipe.bridgeHalfWidthSubvoxels;
|
|
edge.thicknessSubvoxels = recipe.bridgeThicknessSubvoxels;
|
|
SetConnectorBounds(recipe, edge);
|
|
return edge;
|
|
}
|
|
|
|
inline std::optional<std::int64_t> FindSurfaceZ(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandPlanV1& plan, std::int64_t x, std::int64_t y)
|
|
{
|
|
const std::int64_t step = std::max<std::int64_t>(1, recipe.routeConstraints.sweepSpacingSubvoxels);
|
|
std::int64_t aboveZ = recipe.layoutMaximumZSubvoxels + 8 * Q;
|
|
std::int64_t aboveDensity = FieldDensity(recipe, plan, x, y, aboveZ);
|
|
if (aboveDensity < 0) return std::nullopt;
|
|
for (std::int64_t z = aboveZ - step; z >= recipe.layoutMinimumZSubvoxels - 8 * Q; z -= step)
|
|
{
|
|
const std::int64_t density = FieldDensity(recipe, plan, x, y, z);
|
|
if (density < 0)
|
|
{
|
|
std::int64_t outside = aboveZ, inside = z;
|
|
while (outside - inside > 1)
|
|
{
|
|
const std::int64_t middle = inside + (outside - inside) / 2;
|
|
if (FieldDensity(recipe, plan, x, y, middle) < 0) inside = middle;
|
|
else outside = middle;
|
|
}
|
|
return outside;
|
|
}
|
|
aboveZ = z;
|
|
aboveDensity = density;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
inline std::optional<std::int64_t> FindCapsuleFootingTopZ(
|
|
const FloatingIslandRecipeV1& recipe, const FloatingIslandPlanV1& plan,
|
|
std::int64_t x, std::int64_t y)
|
|
{
|
|
const double radius = static_cast<double>(recipe.routeConstraints.capsuleRadiusSubvoxels);
|
|
constexpr std::array<std::array<double, 2>, 17> footprint = {{{0.0, 0.0},
|
|
{0.6, 0.0}, {-0.6, 0.0}, {0.0, 0.6}, {0.0, -0.6},
|
|
{0.424264, 0.424264}, {-0.424264, 0.424264}, {0.424264, -0.424264}, {-0.424264, -0.424264},
|
|
{0.95, 0.0}, {-0.95, 0.0}, {0.0, 0.95}, {0.0, -0.95},
|
|
{0.671751, 0.671751}, {-0.671751, 0.671751}, {0.671751, -0.671751}, {-0.671751, -0.671751}}};
|
|
std::int64_t highest = std::numeric_limits<std::int64_t>::min();
|
|
for (const auto& offset : footprint)
|
|
{
|
|
const std::int64_t sampleX = RoundQ(static_cast<double>(x) + offset[0] * radius);
|
|
const std::int64_t sampleY = RoundQ(static_cast<double>(y) + offset[1] * radius);
|
|
const auto surface = FindSurfaceZ(recipe, plan, sampleX, sampleY);
|
|
if (!surface) return std::nullopt;
|
|
highest = std::max(highest, *surface);
|
|
}
|
|
return highest;
|
|
}
|
|
|
|
inline bool ReadConstraints(const VFJson::Value& value, WorldAssemblyRouteConstraintsV1& output)
|
|
{
|
|
std::uint32_t version = 0, probes = 0;
|
|
if (!ReadU32(value.Get("version"), version)
|
|
|| !ReadInteger(value.Get("capsule_radius_q"), output.capsuleRadiusSubvoxels)
|
|
|| !ReadInteger(value.Get("capsule_height_q"), output.capsuleHeightSubvoxels)
|
|
|| !ReadInteger(value.Get("maximum_grade_q16"), output.maximumGradeQ16)
|
|
|| !ReadInteger(value.Get("sweep_spacing_q"), output.sweepSpacingSubvoxels)
|
|
|| !ReadInteger(value.Get("support_probe_radius_q"), output.supportProbeRadiusSubvoxels)
|
|
|| !ReadInteger(value.Get("support_search_depth_q"), output.supportSearchDepthSubvoxels)
|
|
|| !ReadInteger(value.Get("minimum_clearance_q"), output.minimumClearanceSubvoxels)
|
|
|| !ReadU32(value.Get("minimum_support_probes"), probes)) return false;
|
|
output.version = version; output.minimumSupportProbes = probes;
|
|
return true;
|
|
}
|
|
|
|
inline void WriteConstraints(std::ostream& out, const WorldAssemblyRouteConstraintsV1& value)
|
|
{
|
|
out << "\"route_constraints\":{\"version\":" << value.version
|
|
<< ",\"capsule_radius_q\":" << value.capsuleRadiusSubvoxels
|
|
<< ",\"capsule_height_q\":" << value.capsuleHeightSubvoxels
|
|
<< ",\"maximum_grade_q16\":" << value.maximumGradeQ16
|
|
<< ",\"sweep_spacing_q\":" << value.sweepSpacingSubvoxels
|
|
<< ",\"support_probe_radius_q\":" << value.supportProbeRadiusSubvoxels
|
|
<< ",\"support_search_depth_q\":" << value.supportSearchDepthSubvoxels
|
|
<< ",\"minimum_clearance_q\":" << value.minimumClearanceSubvoxels
|
|
<< ",\"minimum_support_probes\":" << value.minimumSupportProbes << '}';
|
|
}
|
|
|
|
struct RandomStream
|
|
{
|
|
std::uint64_t state;
|
|
std::uint64_t next()
|
|
{
|
|
state = Mix(state);
|
|
return state;
|
|
}
|
|
};
|
|
}
|
|
|
|
std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
|
std::string_view json, std::string& error,
|
|
std::int64_t resolvedLayoutChunkWidthSubvoxels)
|
|
{
|
|
VFJson::Value root;
|
|
if (!VFJson::Parser(std::string(json)).Parse(root, error)) return std::nullopt;
|
|
FloatingIslandManifestV1 manifest;
|
|
std::uint32_t schema = 0;
|
|
const auto& recipeValue = root.Get("recipe");
|
|
const auto& planValue = root.Get("plan");
|
|
std::uint64_t manifestHash = 0, recipeHash = 0, planHash = 0;
|
|
const auto& geometryVersionValue = recipeValue.Get("geometry_version");
|
|
if (geometryVersionValue.IsNumber())
|
|
{
|
|
if (!floating_island_detail::ReadU32(geometryVersionValue, manifest.recipe.geometryVersion)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("crust_depth_bonus_q16"),
|
|
manifest.recipe.crustDepthBonusQ16)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("root_depth_ratio_q16"),
|
|
manifest.recipe.rootDepthRatioQ16))
|
|
{ error = "floating-island geometry profile is incomplete"; return std::nullopt; }
|
|
}
|
|
else
|
|
{
|
|
// Frozen M3 round-1 manifests omit profile metadata and retain their original field.
|
|
manifest.recipe.geometryVersion = 1;
|
|
manifest.recipe.crustDepthBonusQ16 = 0;
|
|
manifest.recipe.rootDepthRatioQ16 = 0;
|
|
}
|
|
if(manifest.recipe.geometryVersion>=4)
|
|
{
|
|
if(!floating_island_detail::ReadInteger(recipeValue.Get("layout_chunk_width_q"),
|
|
manifest.recipe.layoutChunkWidthSubvoxels))
|
|
{error="floating-island recipe is missing its resolved layout chunk width";return std::nullopt;}
|
|
if(resolvedLayoutChunkWidthSubvoxels>0
|
|
&& manifest.recipe.layoutChunkWidthSubvoxels!=resolvedLayoutChunkWidthSubvoxels)
|
|
{error="floating-island recipe layout width disagrees with the resolved world layout";return std::nullopt;}
|
|
}
|
|
if(manifest.recipe.geometryVersion>=5)
|
|
{
|
|
if(!floating_island_detail::ReadU32(recipeValue.Get("anchor_island_ordinal"),
|
|
manifest.recipe.anchorIslandOrdinal)
|
|
||!floating_island_detail::ReadInteger(recipeValue.Get("anchor_placement_radius_q"),
|
|
manifest.recipe.anchorPlacementRadiusSubvoxels)
|
|
||!floating_island_detail::ReadInteger(recipeValue.Get("anchor_root_radius_q"),
|
|
manifest.recipe.anchorRootRadiusSubvoxels)
|
|
||!floating_island_detail::ReadInteger(recipeValue.Get("anchor_island_minimum_radius_q"),
|
|
manifest.recipe.anchorIslandMinimumRadiusSubvoxels)
|
|
||!floating_island_detail::ReadInteger(recipeValue.Get("anchor_root_bottom_z_q"),
|
|
manifest.recipe.anchorRootBottomZSubvoxels)
|
|
||!floating_island_detail::ReadInteger(recipeValue.Get("anchor_mouth_offset_q"),
|
|
manifest.recipe.anchorMouthOffsetSubvoxels))
|
|
{error="floating-island geometry version 7 is missing a passage-rooted anchor field";return std::nullopt;}
|
|
}
|
|
manifest.recipe.terrainFollowingRoute=recipeValue.Get("terrain_following_route").Boolean(false);
|
|
if (!floating_island_detail::ReadU32(root.Get("schema_version"), schema)
|
|
|| !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash)
|
|
|| !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion)
|
|
|| !floating_island_detail::ReadU32(recipeValue.Get("units_per_voxel"), manifest.recipe.unitsPerVoxel)
|
|
|| !floating_island_detail::ReadU32(recipeValue.Get("owner_voxel_size_microns"), manifest.recipe.ownerVoxelSizeMicrons)
|
|
|| !floating_island_detail::ReadU32(recipeValue.Get("core_voxel_size_microns"), manifest.recipe.coreVoxelSizeMicrons)
|
|
|| !floating_island_detail::ReadU32(recipeValue.Get("owner_to_core_scale_numerator"), manifest.recipe.ownerToCoreScaleNumerator)
|
|
|| !floating_island_detail::ReadU32(recipeValue.Get("owner_to_core_scale_denominator"), manifest.recipe.ownerToCoreScaleDenominator)
|
|
|| !floating_island_detail::ReadU32(recipeValue.Get("seed"), manifest.recipe.seed)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("origin_x_q"), manifest.recipe.layoutOriginXSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("origin_y_q"), manifest.recipe.layoutOriginYSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("center_top_z_q"), manifest.recipe.layoutCenterTopZSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("layout_min_z_q"), manifest.recipe.layoutMinimumZSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("layout_max_z_q"), manifest.recipe.layoutMaximumZSubvoxels)
|
|
|| !floating_island_detail::ReadU32(recipeValue.Get("primary_island_count"), manifest.recipe.primaryIslandCount)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("island_density_q16"), manifest.recipe.islandDensityQ16)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("spacing_q"), manifest.recipe.spacingSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("minimum_radius_q"), manifest.recipe.minimumRadiusSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("maximum_radius_q"), manifest.recipe.maximumRadiusSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("thickness_ratio_q16"), manifest.recipe.thicknessRatioQ16)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("vertical_jitter_q16"), manifest.recipe.verticalJitterQ16)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("top_flatten_q16"), manifest.recipe.topFlattenQ16)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("taper_end_min_q16"), manifest.recipe.taperEndMinimumQ16)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("taper_end_max_q16"), manifest.recipe.taperEndMaximumQ16)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("blend_radius_q"), manifest.recipe.blendRadiusSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("contour_warp_q"), manifest.recipe.contourWarpSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("roughness_q"), manifest.recipe.roughnessSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("boundary_seal_q"), manifest.recipe.boundarySealSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("bridge_half_width_q"), manifest.recipe.bridgeHalfWidthSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(recipeValue.Get("bridge_thickness_q"), manifest.recipe.bridgeThicknessSubvoxels)
|
|
|| !floating_island_detail::ReadHash(recipeValue.Get("content_hash"), recipeHash)
|
|
|| !floating_island_detail::ReadConstraints(recipeValue.Get("route_constraints"), manifest.recipe.routeConstraints)
|
|
|| !floating_island_detail::ReadHash(planValue.Get("content_hash"), planHash)
|
|
|| schema != 1)
|
|
{ error = "floating-island manifest is missing a required versioned recipe or plan field"; return std::nullopt; }
|
|
if(manifest.recipe.geometryVersion<4 && resolvedLayoutChunkWidthSubvoxels>0)
|
|
manifest.recipe.layoutChunkWidthSubvoxels=resolvedLayoutChunkWidthSubvoxels;
|
|
else if(manifest.recipe.geometryVersion<4)
|
|
{ error="legacy floating-island manifest requires the world's resolved layout chunk width"; return std::nullopt; }
|
|
manifest.recipe.contentHash = recipeHash;
|
|
manifest.contentHash = manifestHash;
|
|
manifest.plan.contentHash = planHash;
|
|
|
|
const auto& islands = planValue.Get("islands").AsArray();
|
|
for (const auto& value : islands)
|
|
{
|
|
FloatingIslandAnchorV1 island;
|
|
if (!floating_island_detail::ReadHash(value.Get("stable_id"), island.stableId)
|
|
|| !floating_island_detail::ReadU32(value.Get("ordinal"), island.ordinal)
|
|
|| !floating_island_detail::ReadArray(value.Get("center_q"), island.centerSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(value.Get("radius_q"), island.radiusSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(value.Get("top_half_q"), island.topHalfSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(value.Get("top_z_q"), island.topZSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(value.Get("bottom_z_q"), island.bottomZSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(value.Get("taper_end_q16"), island.taperEndQ16)
|
|
|| !floating_island_detail::ReadArray(value.Get("bounds_min_q"), island.influenceMinimumSubvoxels)
|
|
|| !floating_island_detail::ReadArray(value.Get("bounds_max_q"), island.influenceMaximumSubvoxels))
|
|
{ error = "floating-island manifest contains a malformed seeded anchor"; return std::nullopt; }
|
|
manifest.plan.islands.push_back(island);
|
|
}
|
|
const auto& edges = planValue.Get("cluster_edges").AsArray();
|
|
for (const auto& value : edges)
|
|
{
|
|
FloatingIslandConnectorV1 edge;
|
|
if (!floating_island_detail::ReadHash(value.Get("stable_id"), edge.stableId)
|
|
|| !floating_island_detail::ReadHash(value.Get("from_island_id"), edge.fromIslandId)
|
|
|| !floating_island_detail::ReadHash(value.Get("to_island_id"), edge.toIslandId)
|
|
|| !floating_island_detail::ReadArray(value.Get("start_q"), edge.startSubvoxels)
|
|
|| !floating_island_detail::ReadArray(value.Get("end_q"), edge.endSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(value.Get("half_width_q"), edge.halfWidthSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(value.Get("thickness_q"), edge.thicknessSubvoxels)
|
|
|| !floating_island_detail::ReadArray(value.Get("bounds_min_q"), edge.influenceMinimumSubvoxels)
|
|
|| !floating_island_detail::ReadArray(value.Get("bounds_max_q"), edge.influenceMaximumSubvoxels)
|
|
|| !std::holds_alternative<bool>(value.Get("sharp_tag_ready").Data))
|
|
{ error = "floating-island manifest contains a malformed graph connector"; return std::nullopt; }
|
|
edge.sharpTagReady = value.Get("sharp_tag_ready").Boolean();
|
|
manifest.plan.clusterEdges.push_back(edge);
|
|
}
|
|
const auto& route = planValue.Get("route").AsArray();
|
|
for (const auto& value : route)
|
|
{
|
|
WorldAssemblyRoutePointV1 point;
|
|
if (!floating_island_detail::ReadHash(value.Get("feature_id"), point.stableFeatureId)
|
|
|| !floating_island_detail::ReadArray(value.Get("position_q"), point.positionSubvoxels)
|
|
|| !floating_island_detail::ReadInteger(value.Get("radius_q"), point.radiusSubvoxels))
|
|
{ error = "floating-island manifest contains a malformed route waypoint"; return std::nullopt; }
|
|
manifest.plan.route.push_back(point);
|
|
}
|
|
if (!manifest.validate(error)) return std::nullopt;
|
|
return manifest;
|
|
}
|
|
|
|
bool FloatingIslandManifestV1::finalize(std::string& error)
|
|
{
|
|
if (!floating_island_detail::ValidateManifest(*this, error)) return false;
|
|
recipe.contentHash = floating_island_detail::HashRecipe(recipe);
|
|
plan.contentHash = floating_island_detail::HashPlan(plan);
|
|
contentHash = floating_island_detail::HashManifest(*this);
|
|
return true;
|
|
}
|
|
|
|
bool FloatingIslandManifestV1::validate(std::string& error) const
|
|
{
|
|
FloatingIslandManifestV1 copy = *this;
|
|
const std::uint64_t expectedRecipe = recipe.contentHash;
|
|
const std::uint64_t expectedPlan = plan.contentHash;
|
|
const std::uint64_t expectedManifest = contentHash;
|
|
if (!copy.finalize(error)) return false;
|
|
if (expectedRecipe != copy.recipe.contentHash || expectedPlan != copy.plan.contentHash
|
|
|| expectedManifest != copy.contentHash)
|
|
{ error = "floating-island recipe, cluster plan or manifest hash does not match its data"; return false; }
|
|
return true;
|
|
}
|
|
|
|
std::string FloatingIslandManifestV1::ToJson() const
|
|
{
|
|
std::string error;
|
|
FloatingIslandManifestV1 value = *this;
|
|
if (!value.finalize(error)) return {};
|
|
const auto& recipe = value.recipe;
|
|
const auto& plan = value.plan;
|
|
std::ostringstream out;
|
|
out << "{\"schema_version\":1,\"content_hash\":\""
|
|
<< floating_island_detail::HashText(value.contentHash) << "\",\"recipe\":{"
|
|
<< "\"schema_version\":" << recipe.schemaVersion
|
|
<< ",\"units_per_voxel\":" << recipe.unitsPerVoxel
|
|
<< ",\"owner_voxel_size_microns\":" << recipe.ownerVoxelSizeMicrons
|
|
<< ",\"core_voxel_size_microns\":" << recipe.coreVoxelSizeMicrons
|
|
<< ",\"owner_to_core_scale_numerator\":" << recipe.ownerToCoreScaleNumerator
|
|
<< ",\"owner_to_core_scale_denominator\":" << recipe.ownerToCoreScaleDenominator
|
|
<< ",\"seed\":" << recipe.seed
|
|
<< ",\"origin_x_q\":" << recipe.layoutOriginXSubvoxels
|
|
<< ",\"origin_y_q\":" << recipe.layoutOriginYSubvoxels
|
|
<< ",\"center_top_z_q\":" << recipe.layoutCenterTopZSubvoxels
|
|
<< ",\"layout_min_z_q\":" << recipe.layoutMinimumZSubvoxels
|
|
<< ",\"layout_max_z_q\":" << recipe.layoutMaximumZSubvoxels
|
|
<< ",\"primary_island_count\":" << recipe.primaryIslandCount
|
|
<< ",\"island_density_q16\":" << recipe.islandDensityQ16
|
|
<< ",\"spacing_q\":" << recipe.spacingSubvoxels
|
|
<< ",\"minimum_radius_q\":" << recipe.minimumRadiusSubvoxels
|
|
<< ",\"maximum_radius_q\":" << recipe.maximumRadiusSubvoxels
|
|
<< ",\"thickness_ratio_q16\":" << recipe.thicknessRatioQ16
|
|
<< ",\"vertical_jitter_q16\":" << recipe.verticalJitterQ16
|
|
<< ",\"top_flatten_q16\":" << recipe.topFlattenQ16
|
|
<< ",\"taper_end_min_q16\":" << recipe.taperEndMinimumQ16
|
|
<< ",\"taper_end_max_q16\":" << recipe.taperEndMaximumQ16;
|
|
if (recipe.geometryVersion >= 2)
|
|
out << ",\"geometry_version\":" << recipe.geometryVersion
|
|
<< ",\"crust_depth_bonus_q16\":" << recipe.crustDepthBonusQ16
|
|
<< ",\"root_depth_ratio_q16\":" << recipe.rootDepthRatioQ16;
|
|
if(recipe.geometryVersion>=4)
|
|
out << ",\"layout_chunk_width_q\":" << recipe.layoutChunkWidthSubvoxels;
|
|
if(recipe.geometryVersion>=5)
|
|
out << ",\"anchor_island_ordinal\":" << recipe.anchorIslandOrdinal
|
|
<< ",\"anchor_placement_radius_q\":" << recipe.anchorPlacementRadiusSubvoxels
|
|
<< ",\"anchor_root_radius_q\":" << recipe.anchorRootRadiusSubvoxels
|
|
<< ",\"anchor_island_minimum_radius_q\":" << recipe.anchorIslandMinimumRadiusSubvoxels
|
|
<< ",\"anchor_root_bottom_z_q\":" << recipe.anchorRootBottomZSubvoxels
|
|
<< ",\"anchor_mouth_offset_q\":" << recipe.anchorMouthOffsetSubvoxels;
|
|
if(recipe.terrainFollowingRoute)out << ",\"terrain_following_route\":true";
|
|
out << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels
|
|
<< ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels
|
|
<< ",\"roughness_q\":" << recipe.roughnessSubvoxels
|
|
<< ",\"boundary_seal_q\":" << recipe.boundarySealSubvoxels
|
|
<< ",\"bridge_half_width_q\":" << recipe.bridgeHalfWidthSubvoxels
|
|
<< ",\"bridge_thickness_q\":" << recipe.bridgeThicknessSubvoxels << ',';
|
|
floating_island_detail::WriteConstraints(out, recipe.routeConstraints);
|
|
out << ",\"content_hash\":\"" << floating_island_detail::HashText(recipe.contentHash)
|
|
<< "\"},\"plan\":{\"content_hash\":\""
|
|
<< floating_island_detail::HashText(plan.contentHash) << "\",\"islands\":[";
|
|
for (std::size_t i = 0; i < plan.islands.size(); ++i)
|
|
{
|
|
if (i) out << ',';
|
|
const auto& island = plan.islands[i];
|
|
out << "{\"stable_id\":\"" << floating_island_detail::HashText(island.stableId)
|
|
<< "\",\"ordinal\":" << island.ordinal << ",\"center_q\":["
|
|
<< island.centerSubvoxels[0] << ',' << island.centerSubvoxels[1] << ',' << island.centerSubvoxels[2]
|
|
<< "],\"radius_q\":" << island.radiusSubvoxels
|
|
<< ",\"top_half_q\":" << island.topHalfSubvoxels
|
|
<< ",\"top_z_q\":" << island.topZSubvoxels
|
|
<< ",\"bottom_z_q\":" << island.bottomZSubvoxels
|
|
<< ",\"taper_end_q16\":" << island.taperEndQ16
|
|
<< ",\"bounds_min_q\":[" << island.influenceMinimumSubvoxels[0] << ','
|
|
<< island.influenceMinimumSubvoxels[1] << ',' << island.influenceMinimumSubvoxels[2]
|
|
<< "],\"bounds_max_q\":[" << island.influenceMaximumSubvoxels[0] << ','
|
|
<< island.influenceMaximumSubvoxels[1] << ',' << island.influenceMaximumSubvoxels[2] << "]}";
|
|
}
|
|
out << "],\"cluster_edges\":[";
|
|
for (std::size_t i = 0; i < plan.clusterEdges.size(); ++i)
|
|
{
|
|
if (i) out << ',';
|
|
const auto& edge = plan.clusterEdges[i];
|
|
out << "{\"stable_id\":\"" << floating_island_detail::HashText(edge.stableId)
|
|
<< "\",\"from_island_id\":\"" << floating_island_detail::HashText(edge.fromIslandId)
|
|
<< "\",\"to_island_id\":\"" << floating_island_detail::HashText(edge.toIslandId)
|
|
<< "\",\"start_q\":[" << edge.startSubvoxels[0] << ',' << edge.startSubvoxels[1] << ','
|
|
<< edge.startSubvoxels[2] << "],\"end_q\":[" << edge.endSubvoxels[0] << ','
|
|
<< edge.endSubvoxels[1] << ',' << edge.endSubvoxels[2]
|
|
<< "],\"half_width_q\":" << edge.halfWidthSubvoxels
|
|
<< ",\"thickness_q\":" << edge.thicknessSubvoxels
|
|
<< ",\"bounds_min_q\":[" << edge.influenceMinimumSubvoxels[0] << ','
|
|
<< edge.influenceMinimumSubvoxels[1] << ',' << edge.influenceMinimumSubvoxels[2]
|
|
<< "],\"bounds_max_q\":[" << edge.influenceMaximumSubvoxels[0] << ','
|
|
<< edge.influenceMaximumSubvoxels[1] << ',' << edge.influenceMaximumSubvoxels[2]
|
|
<< "],\"sharp_tag_ready\":" << (edge.sharpTagReady ? "true" : "false") << '}';
|
|
}
|
|
out << "],\"route\":[";
|
|
for (std::size_t i = 0; i < plan.route.size(); ++i)
|
|
{
|
|
if (i) out << ',';
|
|
const auto& point = plan.route[i];
|
|
out << "{\"feature_id\":\"" << floating_island_detail::HashText(point.stableFeatureId)
|
|
<< "\",\"position_q\":[" << point.positionSubvoxels[0] << ','
|
|
<< point.positionSubvoxels[1] << ',' << point.positionSubvoxels[2]
|
|
<< "],\"radius_q\":" << point.radiusSubvoxels << '}';
|
|
}
|
|
out << "]}}";
|
|
return out.str();
|
|
}
|
|
|
|
std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
const FloatingIslandRecipeV1& inputRecipe, std::string& error)
|
|
{
|
|
FloatingIslandManifestV1 manifest;
|
|
manifest.recipe = inputRecipe;
|
|
auto& recipe = manifest.recipe;
|
|
if (recipe.routeConstraints.version == 0)
|
|
{
|
|
recipe.routeConstraints.version = 1;
|
|
recipe.routeConstraints.capsuleRadiusSubvoxels = 348;
|
|
recipe.routeConstraints.capsuleHeightSubvoxels = 1802;
|
|
recipe.routeConstraints.maximumGradeQ16 = 17560;
|
|
recipe.routeConstraints.sweepSpacingSubvoxels = 128;
|
|
recipe.routeConstraints.supportProbeRadiusSubvoxels = 230;
|
|
recipe.routeConstraints.supportSearchDepthSubvoxels = 512;
|
|
recipe.routeConstraints.minimumClearanceSubvoxels = 128;
|
|
recipe.routeConstraints.minimumSupportProbes = 3;
|
|
}
|
|
if (!floating_island_detail::ValidateRecipe(recipe, error)) return std::nullopt;
|
|
|
|
auto& islands = manifest.plan.islands;
|
|
islands.reserve(static_cast<std::size_t>(recipe.primaryIslandCount) + 2);
|
|
const std::uint32_t count = recipe.primaryIslandCount;
|
|
const std::uint32_t middle = count / 2;
|
|
std::vector<std::int64_t> tops(count, recipe.layoutCenterTopZSubvoxels);
|
|
std::vector<std::int64_t> radii(count);
|
|
for (std::uint32_t i = 0; i < count; ++i)
|
|
{
|
|
const std::uint64_t site = 0x100000000ull + i;
|
|
const std::uint32_t radiusRoll = floating_island_detail::UnitQ16(recipe.seed, site, 0x524144495553ull);
|
|
radii[i] = recipe.minimumRadiusSubvoxels
|
|
+ (recipe.maximumRadiusSubvoxels - recipe.minimumRadiusSubvoxels) * radiusRoll / 65535;
|
|
}
|
|
std::vector<std::array<std::int64_t, 2>> primaryXY(count);
|
|
const std::int64_t rowJitter = recipe.spacingSubvoxels / 10;
|
|
for (std::uint32_t i = 0; i < count; ++i)
|
|
{
|
|
const std::uint64_t site = 0x100000000ull + i;
|
|
if (recipe.geometryVersion < 2)
|
|
{
|
|
const std::int64_t yOffset = i == middle ? 0
|
|
: floating_island_detail::SignedRange(recipe.seed, site, 0x594a4954544552ull, rowJitter);
|
|
primaryXY[i] = {recipe.layoutOriginXSubvoxels
|
|
- static_cast<std::int64_t>(middle) * recipe.spacingSubvoxels
|
|
+ static_cast<std::int64_t>(i) * recipe.spacingSubvoxels,
|
|
recipe.layoutOriginYSubvoxels + yOffset};
|
|
}
|
|
else
|
|
{
|
|
const std::int64_t along = (static_cast<std::int64_t>(i)
|
|
- static_cast<std::int64_t>(middle)) * recipe.spacingSubvoxels * 7 / 10;
|
|
const std::int64_t side = (i % 2 == 0 ? 1 : -1) * recipe.spacingSubvoxels / 3;
|
|
const std::int64_t jitter = floating_island_detail::SignedRange(
|
|
recipe.seed, site, 0x594a4954544552ull, rowJitter / 2);
|
|
primaryXY[i] = {recipe.layoutOriginXSubvoxels + along,
|
|
recipe.layoutOriginYSubvoxels + side + jitter};
|
|
}
|
|
}
|
|
const double maxGrade = floating_island_detail::Q16ToDouble(recipe.routeConstraints.maximumGradeQ16);
|
|
const double levelFactor = floating_island_detail::Q16ToDouble(recipe.verticalJitterQ16) * 1.5;
|
|
const auto riseLimit = [&](std::uint32_t a, std::uint32_t b)
|
|
{
|
|
const double centerDistance = recipe.geometryVersion < 2
|
|
? static_cast<double>(recipe.spacingSubvoxels)
|
|
: std::hypot(static_cast<double>(primaryXY[b][0] - primaryXY[a][0]),
|
|
static_cast<double>(primaryXY[b][1] - primaryXY[a][1]));
|
|
const double gap = centerDistance
|
|
- 0.78 * (static_cast<double>(radii[a]) + static_cast<double>(radii[b]));
|
|
return std::max(0.0, gap * maxGrade * levelFactor);
|
|
};
|
|
for (std::uint32_t i = middle + 1; i < count; ++i)
|
|
{
|
|
const auto site = 0x100000000ull + i;
|
|
tops[i] = tops[i - 1] + floating_island_detail::SignedRange(recipe.seed, site,
|
|
0x4c4556454cull, static_cast<std::int64_t>(std::ceil(riseLimit(i - 1, i))));
|
|
}
|
|
for (std::int32_t i = static_cast<std::int32_t>(middle) - 1; i >= 0; --i)
|
|
{
|
|
const auto site = 0x100000000ull + static_cast<std::uint32_t>(i);
|
|
tops[static_cast<std::size_t>(i)] = tops[static_cast<std::size_t>(i + 1)]
|
|
+ floating_island_detail::SignedRange(recipe.seed, site, 0x4c4556454cull,
|
|
static_cast<std::int64_t>(std::ceil(riseLimit(static_cast<std::uint32_t>(i),
|
|
static_cast<std::uint32_t>(i + 1)))));
|
|
}
|
|
if (recipe.geometryVersion >= 2)
|
|
{
|
|
// Step the outer route islands around the middle island so the connected path still reads as separate floating masses.
|
|
// Decaler les iles externes autour de l'ile centrale pour distinguer les masses tout en gardant la route continue.
|
|
constexpr std::int64_t VerticalStagger = 3 * floating_island_detail::Q;
|
|
for (std::uint32_t i = 0; i < count; ++i)
|
|
{
|
|
if (i < middle) tops[i] -= VerticalStagger;
|
|
else if (i > middle) tops[i] += VerticalStagger;
|
|
}
|
|
}
|
|
for (std::uint32_t i = 0; i < count; ++i)
|
|
{
|
|
const std::uint64_t site = 0x100000000ull + i;
|
|
auto island = floating_island_detail::MakeIsland(recipe, i,
|
|
primaryXY[i][0], primaryXY[i][1], tops[i], site);
|
|
island.radiusSubvoxels = radii[i];
|
|
if(recipe.geometryVersion>=5&&i==recipe.anchorIslandOrdinal)
|
|
island.radiusSubvoxels=std::max(island.radiusSubvoxels,
|
|
recipe.anchorIslandMinimumRadiusSubvoxels);
|
|
island.topHalfSubvoxels = std::max<std::int64_t>(1, island.radiusSubvoxels / 5);
|
|
island.centerSubvoxels[2] = island.topZSubvoxels - island.topHalfSubvoxels;
|
|
island.bottomZSubvoxels = floating_island_detail::IslandBottom(recipe,
|
|
island.topZSubvoxels, island.radiusSubvoxels);
|
|
floating_island_detail::SetIslandBounds(recipe, island);
|
|
islands.push_back(island);
|
|
}
|
|
|
|
auto& edges = manifest.plan.clusterEdges;
|
|
for (std::uint32_t i = 0; i + 1 < count; ++i)
|
|
edges.push_back(floating_island_detail::MakeConnector(recipe, islands[i], islands[i + 1],
|
|
0x200000000ull + i));
|
|
|
|
// Two optional side sites make density a real recipe parameter. The primary chain is never
|
|
// probabilistic, so a sparse roll cannot remove the guaranteed entrance-to-exit path.
|
|
// Deux sites lateraux dependent de la densite; la chaine principale reste toujours garantie.
|
|
for (std::uint32_t branch = 0; branch < 2; ++branch)
|
|
{
|
|
const std::uint64_t site = 0x300000000ull + branch;
|
|
if (floating_island_detail::UnitQ16(recipe.seed, site, 0x53495445524f4c4cull)
|
|
>= static_cast<std::uint32_t>(recipe.islandDensityQ16)) continue;
|
|
const std::uint32_t hostIndex = recipe.geometryVersion < 2
|
|
? (branch == 0 ? 0 : count - 1)
|
|
: (branch == 0 ? middle : std::min(middle + 1, count - 1));
|
|
const auto& host = islands[hostIndex];
|
|
const std::int64_t xOffset = recipe.geometryVersion < 2 ? 0
|
|
: (branch == 0 ? -1 : 1) * recipe.spacingSubvoxels * 9 / 20;
|
|
const std::int64_t yOffset = branch == 0 ? -recipe.spacingSubvoxels : recipe.spacingSubvoxels;
|
|
const std::int64_t gap = recipe.spacingSubvoxels - static_cast<std::int64_t>(
|
|
std::llround(0.78 * static_cast<double>(host.radiusSubvoxels + recipe.minimumRadiusSubvoxels)));
|
|
const std::int64_t rise = static_cast<std::int64_t>(std::ceil(std::max(0.0,
|
|
static_cast<double>(gap) * maxGrade * levelFactor)));
|
|
const std::int64_t sideRise = recipe.geometryVersion < 2 ? rise
|
|
: std::min<std::int64_t>(24 * floating_island_detail::Q,
|
|
(recipe.layoutMaximumZSubvoxels - recipe.layoutMinimumZSubvoxels)
|
|
* recipe.verticalJitterQ16 / (8 * floating_island_detail::Q16));
|
|
std::int64_t topZ = host.topZSubvoxels
|
|
+ floating_island_detail::SignedRange(recipe.seed, site, 0x4252414e43484c56ull, sideRise);
|
|
const std::uint32_t radiusRoll = floating_island_detail::UnitQ16(recipe.seed, site, 0x524144495553ull);
|
|
const std::int64_t branchRadius = recipe.minimumRadiusSubvoxels
|
|
+ (recipe.maximumRadiusSubvoxels - recipe.minimumRadiusSubvoxels) * radiusRoll / 65535;
|
|
if (recipe.geometryVersion >= 2)
|
|
{
|
|
const std::int64_t maximumTop = recipe.layoutMaximumZSubvoxels
|
|
- recipe.boundarySealSubvoxels - branchRadius / 5;
|
|
const std::int64_t minimumTop = recipe.layoutMinimumZSubvoxels
|
|
+ recipe.boundarySealSubvoxels + floating_island_detail::IslandBottom(recipe, 0, branchRadius) * -1;
|
|
topZ = std::clamp(topZ, minimumTop, maximumTop);
|
|
}
|
|
auto island = floating_island_detail::MakeIsland(recipe,
|
|
recipe.primaryIslandCount + branch,
|
|
host.centerSubvoxels[0] + xOffset, host.centerSubvoxels[1] + yOffset, topZ, site);
|
|
island.radiusSubvoxels = branchRadius;
|
|
island.topHalfSubvoxels = std::max<std::int64_t>(1, branchRadius / 5);
|
|
island.centerSubvoxels[2] = topZ - island.topHalfSubvoxels;
|
|
island.bottomZSubvoxels = floating_island_detail::IslandBottom(recipe, topZ, branchRadius);
|
|
floating_island_detail::SetIslandBounds(recipe, island);
|
|
const std::uint64_t islandId = island.stableId;
|
|
islands.push_back(island);
|
|
edges.push_back(floating_island_detail::MakeConnector(recipe, host, islands.back(),
|
|
0x400000000ull + branch));
|
|
(void)islandId;
|
|
}
|
|
|
|
FloatingIslandFieldV1 planningField(recipe, manifest.plan);
|
|
if (!planningField.isValid())
|
|
{ error = "planned floating-island features did not pass the Core field contract"; return std::nullopt; }
|
|
auto& route = manifest.plan.route;
|
|
const std::int64_t capsuleHalf = recipe.routeConstraints.capsuleHeightSubvoxels / 2;
|
|
const std::int64_t routeRadius = 4 * floating_island_detail::Q;
|
|
std::uint32_t routeOrdinal = 0;
|
|
for (std::uint32_t segment = 0; segment + 1 < count; ++segment)
|
|
{
|
|
const auto& a = islands[segment];
|
|
const auto& b = islands[segment + 1];
|
|
const double dx = static_cast<double>(b.centerSubvoxels[0] - a.centerSubvoxels[0]);
|
|
const double dy = static_cast<double>(b.centerSubvoxels[1] - a.centerSubvoxels[1]);
|
|
const std::uint32_t pieces = static_cast<std::uint32_t>(std::max(1.0,
|
|
std::ceil(std::hypot(dx, dy) / (4.0 * floating_island_detail::Q))));
|
|
for (std::uint32_t piece = segment == 0 ? 0 : 1; piece <= pieces; ++piece)
|
|
{
|
|
const double t = static_cast<double>(piece) / pieces;
|
|
const std::int64_t x = floating_island_detail::RoundQ(
|
|
static_cast<double>(a.centerSubvoxels[0]) + dx * t);
|
|
const std::int64_t y = floating_island_detail::RoundQ(
|
|
static_cast<double>(a.centerSubvoxels[1]) + dy * t);
|
|
std::int64_t routeX=x,routeY=y;
|
|
if(WorldAssemblyEvaluatorVersion>=192&&recipe.terrainFollowingRoute
|
|
&&recipe.geometryVersion>=5
|
|
&&segment+2==count&&pieces>1)
|
|
{
|
|
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
|
const std::uint32_t side=static_cast<std::uint32_t>(
|
|
floating_island_detail::UnitQ16(recipe.seed,anchor.stableId,
|
|
0x414e43484f524d50ull)&1u);
|
|
const double length=std::hypot(dx,dy);
|
|
const double bend=10.0*floating_island_detail::Q
|
|
*std::pow(std::sin(floating_island_detail::Pi*t),2.0);
|
|
const double sign=side?-1.0:1.0;
|
|
if(length>0.0)
|
|
{
|
|
routeX=floating_island_detail::RoundQ(
|
|
static_cast<double>(routeX)+sign*(-dy/length)*bend);
|
|
routeY=floating_island_detail::RoundQ(
|
|
static_cast<double>(routeY)+sign*(dx/length)*bend);
|
|
}
|
|
}
|
|
if(recipe.geometryVersion>=5&&segment+2==count&&piece==pieces)
|
|
{
|
|
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
|
const std::uint32_t side=static_cast<std::uint32_t>(
|
|
floating_island_detail::UnitQ16(recipe.seed,anchor.stableId,
|
|
0x414e43484f524d50ull)&1u);
|
|
const double routeAngle=std::atan2(dy,dx);
|
|
const double angle=routeAngle+(side?1.0:-1.0)
|
|
*floating_island_detail::Pi/2.0;
|
|
const double mouthX=static_cast<double>(anchor.centerSubvoxels[0])
|
|
+static_cast<double>(recipe.anchorMouthOffsetSubvoxels)*std::cos(angle);
|
|
const double mouthY=static_cast<double>(anchor.centerSubvoxels[1])
|
|
+static_cast<double>(recipe.anchorMouthOffsetSubvoxels)*std::sin(angle);
|
|
routeX=floating_island_detail::RoundQ(mouthX);
|
|
routeY=floating_island_detail::RoundQ(mouthY);
|
|
}
|
|
const auto surface = WorldAssemblyEvaluatorVersion>=199
|
|
&&recipe.terrainFollowingRoute
|
|
?floating_island_detail::FindSurfaceZ(recipe,manifest.plan,routeX,routeY)
|
|
:floating_island_detail::FindCapsuleFootingTopZ(
|
|
recipe,manifest.plan,routeX,routeY);
|
|
if (!surface)
|
|
{ error = "island planner could not find a full capsule-width support surface along its connector chain"; return std::nullopt; }
|
|
WorldAssemblyRoutePointV1 point;
|
|
point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
|
|
0x500000000ull + routeOrdinal, 0x524f555445ull);
|
|
point.positionSubvoxels = {routeX, routeY, *surface + capsuleHalf
|
|
+ recipe.routeConstraints.minimumClearanceSubvoxels
|
|
+ (recipe.geometryVersion >= 2 ? floating_island_detail::Q / 8 : 0)};
|
|
point.radiusSubvoxels = routeRadius;
|
|
route.push_back(point);
|
|
++routeOrdinal;
|
|
}
|
|
}
|
|
// Find the lowest route profile that stays above every sampled footing surface while obeying
|
|
// the grade limit. This raises valleys instead of allowing a short terrain bump to create a
|
|
// collision or a grade spike in the serialized walk line.
|
|
// Profil minimal au-dessus des appuis : les bosses locales ne percent pas la capsule.
|
|
const double maximumGrade = floating_island_detail::Q16ToDouble(
|
|
recipe.routeConstraints.maximumGradeQ16) * 0.95;
|
|
std::vector<std::int64_t> baseHeights;
|
|
baseHeights.reserve(route.size());
|
|
for (const auto& point : route) baseHeights.push_back(point.positionSubvoxels[2]);
|
|
for (std::size_t i = 0; i < route.size(); ++i)
|
|
{
|
|
double profile = static_cast<double>(baseHeights[i]);
|
|
for (std::size_t j = 0; j < route.size(); ++j)
|
|
{
|
|
const double dx = static_cast<double>(route[i].positionSubvoxels[0]
|
|
- route[j].positionSubvoxels[0]);
|
|
const double dy = static_cast<double>(route[i].positionSubvoxels[1]
|
|
- route[j].positionSubvoxels[1]);
|
|
profile = std::max(profile, static_cast<double>(baseHeights[j])
|
|
- maximumGrade * std::hypot(dx, dy));
|
|
}
|
|
route[i].positionSubvoxels[2] = floating_island_detail::RoundQ(profile);
|
|
}
|
|
if (!manifest.finalize(error)) return std::nullopt;
|
|
const std::string encoded = manifest.ToJson();
|
|
const auto roundTrip = FloatingIslandManifestV1::FromJson(encoded, error);
|
|
if (!roundTrip || roundTrip->ToJson() != encoded || roundTrip->contentHash != manifest.contentHash)
|
|
{ if (error.empty()) error = "floating-island plan did not canonical-round-trip"; return std::nullopt; }
|
|
return manifest;
|
|
}
|
|
|
|
FloatingIslandFieldV1::FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe,
|
|
const FloatingIslandPlanV1& plan) noexcept : recipe_(&recipe), plan_(&plan)
|
|
{}
|
|
|
|
bool FloatingIslandFieldV1::isValid() const noexcept
|
|
{
|
|
if (!recipe_ || !plan_ || recipe_->schemaVersion != 1
|
|
|| recipe_->unitsPerVoxel != floating_island_detail::Q
|
|
|| recipe_->primaryIslandCount < 3 || plan_->islands.size() < recipe_->primaryIslandCount
|
|
|| plan_->islands.size() > 4096 || plan_->clusterEdges.size() > 4096
|
|
|| recipe_->minimumRadiusSubvoxels <= 0 || recipe_->maximumRadiusSubvoxels < recipe_->minimumRadiusSubvoxels
|
|
|| recipe_->blendRadiusSubvoxels < 0 || recipe_->contourWarpSubvoxels < 0) return false;
|
|
return std::all_of(plan_->islands.begin(), plan_->islands.end(), [](const auto& island)
|
|
{
|
|
return island.stableId != 0 && island.radiusSubvoxels > 0
|
|
&& island.topZSubvoxels > island.bottomZSubvoxels;
|
|
});
|
|
}
|
|
|
|
std::int64_t FloatingIslandFieldV1::densitySubvoxels(std::int64_t x,
|
|
std::int64_t y, std::int64_t z) const noexcept
|
|
{
|
|
// The selected evaluator validates once; do not scan the plan twice per scoped sample.
|
|
// L'evaluateur choisi valide une seule fois; ne pas reparcourir le plan a chaque echantillon.
|
|
if (const auto* list = ScopedWorldAssemblyFeatureQueriesV1::find(recipe_, plan_, x, y, z))
|
|
return densitySubvoxelsForTile(x, y, z, *list);
|
|
if (!isValid() || !floating_island_detail::ValidScalar(x)
|
|
|| !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z))
|
|
return floating_island_detail::MaxDensity;
|
|
return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
|
}
|
|
|
|
std::vector<WorldAssemblyFeatureBoundsV1> FloatingIslandFieldV1::solidSupportBounds() const
|
|
{
|
|
std::vector<WorldAssemblyFeatureBoundsV1> features;
|
|
features.reserve(plan_->islands.size() + plan_->clusterEdges.size());
|
|
for (const auto& island : plan_->islands)
|
|
features.push_back({island.stableId, island.influenceMinimumSubvoxels,
|
|
island.influenceMaximumSubvoxels});
|
|
for (const auto& edge : plan_->clusterEdges)
|
|
features.push_back({edge.stableId, edge.influenceMinimumSubvoxels,
|
|
edge.influenceMaximumSubvoxels});
|
|
|
|
return features;
|
|
}
|
|
|
|
FloatingIslandTileFeatureListV1 FloatingIslandFieldV1::buildFeatureTileList(
|
|
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
|
const std::array<std::int64_t, 3>& tileMaximumSubvoxels) const
|
|
{
|
|
FloatingIslandTileFeatureListV1 result;
|
|
// The measured four-island plan is cheaper to scan; larger plans amortize group certificates.
|
|
// Le plan mesure de quatre iles coute moins en parcours complet; les grands plans amortissent les bornes.
|
|
result.candidateEvaluationUseful=plan_->islands.size()>4;
|
|
result.minimumSubvoxels = tileMinimumSubvoxels;
|
|
result.maximumSubvoxels = tileMaximumSubvoxels;
|
|
if (!isValid()) return result;
|
|
const auto features = solidSupportBounds();
|
|
|
|
result.featureIndices = BuildWorldAssemblyFeatureTileListV1(features,
|
|
tileMinimumSubvoxels, tileMaximumSubvoxels);
|
|
const std::size_t islandCount = plan_->islands.size();
|
|
result.groupBounds.resize(islandCount);
|
|
for (std::size_t islandIndex = 0; islandIndex < islandCount; ++islandIndex)
|
|
{
|
|
result.groupBounds[islandIndex].minimumSubvoxels = plan_->islands[islandIndex].influenceMinimumSubvoxels;
|
|
result.groupBounds[islandIndex].maximumSubvoxels = plan_->islands[islandIndex].influenceMaximumSubvoxels;
|
|
}
|
|
|
|
const auto findSourceIsland = [&](const FloatingIslandConnectorV1& edge)
|
|
{
|
|
return std::find_if(plan_->islands.begin(), plan_->islands.end(),
|
|
[&edge](const auto& island) { return island.stableId == edge.fromIslandId; });
|
|
};
|
|
for (const auto& edge : plan_->clusterEdges)
|
|
{
|
|
const auto source = findSourceIsland(edge);
|
|
if (source == plan_->islands.end())
|
|
{ result.groupBounds.clear(); result.featureIndices.clear(); return result; }
|
|
auto& group = result.groupBounds[static_cast<std::size_t>(source - plan_->islands.begin())];
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
{
|
|
group.minimumSubvoxels[axis] = std::min(group.minimumSubvoxels[axis],
|
|
edge.influenceMinimumSubvoxels[axis]);
|
|
group.maximumSubvoxels[axis] = std::max(group.maximumSubvoxels[axis],
|
|
edge.influenceMaximumSubvoxels[axis]);
|
|
}
|
|
}
|
|
|
|
result.groupSearchOrder.resize(islandCount);
|
|
std::iota(result.groupSearchOrder.begin(), result.groupSearchOrder.end(), 0u);
|
|
const auto tileDistanceLowerBound = [&](
|
|
const std::array<std::int64_t, 3>& featureMinimum,
|
|
const std::array<std::int64_t, 3>& featureMaximum)
|
|
{
|
|
std::int64_t maximumGap = 0;
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
{
|
|
if (featureMinimum[axis] > tileMaximumSubvoxels[axis])
|
|
maximumGap = std::max(maximumGap,
|
|
featureMinimum[axis] - tileMaximumSubvoxels[axis]);
|
|
else if (featureMaximum[axis] < tileMinimumSubvoxels[axis])
|
|
maximumGap = std::max(maximumGap,
|
|
tileMinimumSubvoxels[axis] - featureMaximum[axis]);
|
|
}
|
|
return maximumGap;
|
|
};
|
|
result.groupDistanceLowerBoundsSubvoxels.resize(islandCount);
|
|
for (std::size_t islandIndex = 0; islandIndex < islandCount; ++islandIndex)
|
|
{
|
|
const auto& island = plan_->islands[islandIndex];
|
|
const std::int64_t islandGap = tileDistanceLowerBound(
|
|
island.influenceMinimumSubvoxels, island.influenceMaximumSubvoxels);
|
|
double islandLowerBound = static_cast<double>(islandGap)
|
|
- floating_island_detail::IslandBlendMargin(*recipe_) - recipe_->roughnessSubvoxels;
|
|
if (recipe_->geometryVersion >= 2)
|
|
{
|
|
// Root lobes use a scaled ellipsoid distance, so their positive values can be
|
|
// substantially smaller than distance to the island AABB. Supported root radii
|
|
// guarantee minAxis/maxAxis >= 0.22; include their smooth-min budget before pruning.
|
|
// Les racines utilisent une distance d'ellipsoide mise a l'echelle; borner leur pente.
|
|
const double radius = static_cast<double>(island.radiusSubvoxels);
|
|
const double rootBlend = std::clamp(radius * 0.12,
|
|
2.0 * floating_island_detail::Q, 5.0 * floating_island_detail::Q);
|
|
const double rootSmoothBudget = static_cast<double>(
|
|
floating_island_detail::RootCount(*recipe_, island)) * rootBlend * 0.25;
|
|
const double rootLowerBound = 0.22 * static_cast<double>(islandGap)
|
|
- recipe_->roughnessSubvoxels - rootSmoothBudget;
|
|
islandLowerBound = std::min(islandLowerBound, rootLowerBound);
|
|
}
|
|
|
|
double groupLowerBound = islandLowerBound;
|
|
for (const auto& edge : plan_->clusterEdges)
|
|
{
|
|
if (edge.fromIslandId != island.stableId) continue;
|
|
const std::int64_t edgeGap = tileDistanceLowerBound(
|
|
edge.influenceMinimumSubvoxels, edge.influenceMaximumSubvoxels);
|
|
const double edgeLowerBound = static_cast<double>(edgeGap) - recipe_->roughnessSubvoxels;
|
|
groupLowerBound = std::min(groupLowerBound, edgeLowerBound)
|
|
- static_cast<double>(recipe_->blendRadiusSubvoxels) * 0.25;
|
|
}
|
|
// Floor with one subvoxel of slack so quantization cannot make a lower bound optimistic.
|
|
// Arrondir vers le bas avec une marge pour ne jamais surestimer la borne.
|
|
result.groupDistanceLowerBoundsSubvoxels[islandIndex] =
|
|
static_cast<std::int64_t>(std::floor(groupLowerBound)) - 1;
|
|
}
|
|
std::sort(result.groupSearchOrder.begin(), result.groupSearchOrder.end(),
|
|
[&result](std::uint32_t a, std::uint32_t b)
|
|
{
|
|
const auto distanceA = result.groupDistanceLowerBoundsSubvoxels[a];
|
|
const auto distanceB = result.groupDistanceLowerBoundsSubvoxels[b];
|
|
return distanceA == distanceB ? a < b : distanceA < distanceB;
|
|
});
|
|
// Store distance in search order so query samples use one sequential early-out walk.
|
|
// Ranger les distances dans le meme ordre pour arreter le parcours au premier echec.
|
|
std::vector<std::int64_t> orderedDistances;
|
|
orderedDistances.reserve(islandCount);
|
|
for (const std::uint32_t islandIndex : result.groupSearchOrder)
|
|
orderedDistances.push_back(result.groupDistanceLowerBoundsSubvoxels[islandIndex]);
|
|
result.groupDistanceLowerBoundsSubvoxels = std::move(orderedDistances);
|
|
|
|
std::vector<std::uint8_t> selectedGroups(islandCount, 0);
|
|
for (const std::uint32_t featureIndex : result.featureIndices)
|
|
{
|
|
if (featureIndex < islandCount) selectedGroups[featureIndex] = 1;
|
|
else
|
|
{
|
|
const std::size_t edgeIndex = static_cast<std::size_t>(featureIndex) - islandCount;
|
|
if (edgeIndex >= plan_->clusterEdges.size())
|
|
{ result.groupBounds.clear(); result.featureIndices.clear(); return result; }
|
|
const auto source = findSourceIsland(plan_->clusterEdges[edgeIndex]);
|
|
if (source == plan_->islands.end())
|
|
{ result.groupBounds.clear(); result.featureIndices.clear(); return result; }
|
|
selectedGroups[static_cast<std::size_t>(source - plan_->islands.begin())] = 1;
|
|
}
|
|
}
|
|
|
|
// A connector is composed into its source island's group. Keep that source and every
|
|
// outgoing connector in the tile list so the ordered smooth-min chain remains exact.
|
|
// Un connecteur fait partie de son ile source : garder toute la chaine de fondu exacte.
|
|
for (std::size_t islandIndex = 0; islandIndex < islandCount; ++islandIndex)
|
|
{
|
|
if (!selectedGroups[islandIndex]) continue;
|
|
auto insertSorted = [&](std::uint32_t value)
|
|
{
|
|
const auto insertion = std::lower_bound(result.featureIndices.begin(),
|
|
result.featureIndices.end(), value);
|
|
if (insertion == result.featureIndices.end() || *insertion != value)
|
|
result.featureIndices.insert(insertion, value);
|
|
};
|
|
insertSorted(static_cast<std::uint32_t>(islandIndex));
|
|
for (std::size_t edgeIndex = 0; edgeIndex < plan_->clusterEdges.size(); ++edgeIndex)
|
|
if (plan_->clusterEdges[edgeIndex].fromIslandId == plan_->islands[islandIndex].stableId)
|
|
insertSorted(static_cast<std::uint32_t>(islandCount + edgeIndex));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::int64_t FloatingIslandFieldV1::densitySubvoxelsForTile(std::int64_t x,
|
|
std::int64_t y, std::int64_t z,
|
|
const FloatingIslandTileFeatureListV1& featureList) const noexcept
|
|
{
|
|
if (!isValid() || !floating_island_detail::ValidScalar(x)
|
|
|| !floating_island_detail::ValidScalar(y) || !floating_island_detail::ValidScalar(z))
|
|
return floating_island_detail::MaxDensity;
|
|
std::uint32_t previous = 0;
|
|
bool first = true;
|
|
const std::size_t featureCount = plan_->islands.size() + plan_->clusterEdges.size();
|
|
if (featureList.groupBounds.size() != plan_->islands.size())
|
|
return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
|
for (const std::uint32_t index : featureList.featureIndices)
|
|
{
|
|
if (index >= featureCount || (!first && index <= previous))
|
|
return floating_island_detail::FieldDensity(*recipe_, *plan_, x, y, z);
|
|
first = false;
|
|
previous = index;
|
|
}
|
|
return floating_island_detail::FieldDensityForTile(*recipe_, *plan_, x, y, z, featureList);
|
|
}
|
|
|
|
FieldInterval FloatingIslandFieldV1::interval(std::int64_t minX, std::int64_t minY,
|
|
std::int64_t minZ, std::int64_t maxX, std::int64_t maxY, std::int64_t maxZ) const noexcept
|
|
{
|
|
FieldInterval result;
|
|
if (!isValid() || minX > maxX || minY > maxY || minZ > maxZ
|
|
|| !floating_island_detail::ValidScalar(minX) || !floating_island_detail::ValidScalar(minY)
|
|
|| !floating_island_detail::ValidScalar(minZ) || !floating_island_detail::ValidScalar(maxX)
|
|
|| !floating_island_detail::ValidScalar(maxY) || !floating_island_detail::ValidScalar(maxZ)) return result;
|
|
const std::array<std::int64_t, 3> minimum{minX, minY, minZ};
|
|
const std::array<std::int64_t, 3> maximum{maxX, maxY, maxZ};
|
|
for (const auto& island : plan_->islands)
|
|
if (floating_island_detail::Intersects(minimum, maximum,
|
|
island.influenceMinimumSubvoxels, island.influenceMaximumSubvoxels)) return result;
|
|
for (const auto& edge : plan_->clusterEdges)
|
|
if (floating_island_detail::Intersects(minimum, maximum,
|
|
edge.influenceMinimumSubvoxels, edge.influenceMaximumSubvoxels)) return result;
|
|
// Root is void. A box outside every lobe/taper/blend/connector influence bound proves air.
|
|
// La racine est le vide : une boite hors des influences bornees prouve l'air.
|
|
result.known = true;
|
|
result.minimumDensitySubvoxels = 0;
|
|
result.maximumDensitySubvoxels = floating_island_detail::MaxDensity;
|
|
return result;
|
|
}
|
|
|
|
FloatingIslandInfluenceBounds FloatingIslandFieldV1::influenceBounds() const noexcept
|
|
{
|
|
FloatingIslandInfluenceBounds result;
|
|
if (!isValid()) return result;
|
|
result.finite = true;
|
|
result.minimumSubvoxels.fill(std::numeric_limits<std::int64_t>::max());
|
|
result.maximumSubvoxels.fill(std::numeric_limits<std::int64_t>::min());
|
|
const auto include = [&](const auto& feature)
|
|
{
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
{
|
|
result.minimumSubvoxels[axis] = std::min(result.minimumSubvoxels[axis], feature.influenceMinimumSubvoxels[axis]);
|
|
result.maximumSubvoxels[axis] = std::max(result.maximumSubvoxels[axis], feature.influenceMaximumSubvoxels[axis]);
|
|
}
|
|
};
|
|
for (const auto& island : plan_->islands) include(island);
|
|
for (const auto& edge : plan_->clusterEdges) include(edge);
|
|
return result;
|
|
}
|
|
|
|
FieldDetailEnvelope FloatingIslandFieldV1::detailEnvelope() const noexcept
|
|
{
|
|
FieldDetailEnvelope result;
|
|
if (!isValid()) return result;
|
|
result.maximumAbsoluteDisplacementSubvoxels = recipe_->contourWarpSubvoxels
|
|
+ recipe_->roughnessSubvoxels + floating_island_detail::IslandBlendMargin(*recipe_);
|
|
result.minimumFeatureSizeSubvoxels = std::numeric_limits<std::int64_t>::max();
|
|
for (const auto& island : plan_->islands)
|
|
result.minimumFeatureSizeSubvoxels = std::min(result.minimumFeatureSizeSubvoxels,
|
|
island.radiusSubvoxels * 2);
|
|
return result;
|
|
}
|
|
|
|
FieldBlockContract FloatingIslandFieldV1::contract() const noexcept
|
|
{
|
|
FieldBlockContract result;
|
|
result.readsChannels = ChannelBit(FieldChannel::island);
|
|
result.writesChannels = ChannelBit(FieldChannel::rock) | ChannelBit(FieldChannel::route)
|
|
| ChannelBit(FieldChannel::surfaceTags);
|
|
result.evaluatorVersion = 1;
|
|
return result;
|
|
}
|
|
|
|
FloatingIslandBoundsAuditResult AuditFloatingIslandBounds(const FloatingIslandFieldV1& field,
|
|
std::uint32_t boxCount, std::uint64_t seed)
|
|
{
|
|
FloatingIslandBoundsAuditResult result;
|
|
if (!field.isValid() || !field.recipe() || !field.plan()) return result;
|
|
const auto& recipe = *field.recipe();
|
|
const auto& plan = *field.plan();
|
|
const auto bounds = field.influenceBounds();
|
|
if (!bounds.finite) return result;
|
|
for (const auto& island : plan.islands) ++result.islandBoundsTested;
|
|
for (const auto& edge : plan.clusterEdges) ++result.connectorBoundsTested;
|
|
|
|
floating_island_detail::RandomStream random{seed};
|
|
const auto randomBetween = [&](std::int64_t low, std::int64_t high)
|
|
{
|
|
if (high <= low) return low;
|
|
return low + static_cast<std::int64_t>(random.next() % static_cast<std::uint64_t>(high - low + 1));
|
|
};
|
|
constexpr std::array<std::int32_t, 3> samples{0, 1, 2};
|
|
for (std::uint32_t boxIndex = 0; boxIndex < boxCount; ++boxIndex)
|
|
{
|
|
std::array<std::int64_t, 3> minimum{}, maximum{};
|
|
for (std::size_t axis = 0; axis < 3; ++axis)
|
|
{
|
|
const std::int64_t pad = 48 * floating_island_detail::Q;
|
|
const std::int64_t low = bounds.minimumSubvoxels[axis] - pad;
|
|
const std::int64_t high = bounds.maximumSubvoxels[axis] + pad;
|
|
const std::int64_t extent = std::max<std::int64_t>(1, randomBetween(1,
|
|
20 * floating_island_detail::Q));
|
|
const std::int64_t start = randomBetween(low, high);
|
|
minimum[axis] = start;
|
|
maximum[axis] = std::min(high + extent, start + extent);
|
|
}
|
|
const auto interval = field.interval(minimum[0], minimum[1], minimum[2],
|
|
maximum[0], maximum[1], maximum[2]);
|
|
++result.boxesTested;
|
|
if (!interval.known) ++result.unknownBounds;
|
|
for (const auto sx : samples)
|
|
for (const auto sy : samples)
|
|
for (const auto sz : samples)
|
|
{
|
|
const std::array<std::int64_t, 3> point{
|
|
minimum[0] + (maximum[0] - minimum[0]) * sx / 2,
|
|
minimum[1] + (maximum[1] - minimum[1]) * sy / 2,
|
|
minimum[2] + (maximum[2] - minimum[2]) * sz / 2};
|
|
const std::int64_t density = field.densitySubvoxels(point[0], point[1], point[2]);
|
|
++result.pointSamples;
|
|
if (interval.known && (density < interval.minimumDensitySubvoxels
|
|
|| density > interval.maximumDensitySubvoxels)) ++result.missedBounds;
|
|
if (density < 0)
|
|
{
|
|
bool enclosed = false;
|
|
for (const auto& island : plan.islands)
|
|
enclosed = enclosed || floating_island_detail::Contains(island.influenceMinimumSubvoxels,
|
|
island.influenceMaximumSubvoxels, point);
|
|
for (const auto& edge : plan.clusterEdges)
|
|
enclosed = enclosed || floating_island_detail::Contains(edge.influenceMinimumSubvoxels,
|
|
edge.influenceMaximumSubvoxels, point);
|
|
if (!enclosed) ++result.missedBounds;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Probe the void beside each primary connector. These boxes should be certifiably air.
|
|
for (std::size_t i = 0; i < plan.clusterEdges.size(); ++i)
|
|
{
|
|
const auto& edge = plan.clusterEdges[i];
|
|
const double dx = static_cast<double>(edge.endSubvoxels[0] - edge.startSubvoxels[0]);
|
|
const double dy = static_cast<double>(edge.endSubvoxels[1] - edge.startSubvoxels[1]);
|
|
const double length = std::hypot(dx, dy);
|
|
if (!(length > 0.0)) continue;
|
|
const double offset = static_cast<double>(recipe.maximumRadiusSubvoxels
|
|
+ recipe.contourWarpSubvoxels + 4 * recipe.blendRadiusSubvoxels
|
|
+ 3 * floating_island_detail::Q);
|
|
const std::int64_t x = floating_island_detail::RoundQ(
|
|
(edge.startSubvoxels[0] + edge.endSubvoxels[0]) * 0.5 - dy / length * offset);
|
|
const std::int64_t y = floating_island_detail::RoundQ(
|
|
(edge.startSubvoxels[1] + edge.endSubvoxels[1]) * 0.5 + dx / length * offset);
|
|
const std::int64_t z = (edge.startSubvoxels[2] + edge.endSubvoxels[2]) / 2;
|
|
constexpr std::int64_t half = floating_island_detail::Q / 2;
|
|
const auto interval = field.interval(x - half, y - half, z - half,
|
|
x + half, y + half, z + half);
|
|
++result.boxesTested;
|
|
if (!interval.known) { ++result.unknownBounds; continue; }
|
|
if (interval.minimumDensitySubvoxels < 0) ++result.missedBounds;
|
|
else ++result.voidBoxesProvedAir;
|
|
for (const auto sx : samples)
|
|
for (const auto sy : samples)
|
|
for (const auto sz : samples)
|
|
{
|
|
const std::int64_t density = field.densitySubvoxels(
|
|
x - half + half * sx, y - half + half * sy, z - half + half * sz);
|
|
++result.pointSamples; ++result.voidPointSamples;
|
|
if (density < 0 || density < interval.minimumDensitySubvoxels
|
|
|| density > interval.maximumDensitySubvoxels) ++result.missedBounds;
|
|
}
|
|
}
|
|
|
|
// Negative control: deliberately omit the lobe-warp and blend reach from an island box.
|
|
// Controles negatifs : une fausse borne oublie le lobe et le fondu, puis doit etre rejetee.
|
|
for (const auto& island : plan.islands)
|
|
{
|
|
const std::int64_t badReach = island.radiusSubvoxels + recipe.roughnessSubvoxels;
|
|
const std::array<std::int64_t, 3> badMin{island.centerSubvoxels[0] - badReach,
|
|
island.centerSubvoxels[1] - badReach, island.bottomZSubvoxels - recipe.roughnessSubvoxels};
|
|
const std::array<std::int64_t, 3> badMax{island.centerSubvoxels[0] + badReach,
|
|
island.centerSubvoxels[1] + badReach, island.topZSubvoxels + recipe.roughnessSubvoxels};
|
|
const double radial = static_cast<double>(island.radiusSubvoxels)
|
|
+ static_cast<double>(recipe.contourWarpSubvoxels) * 0.72;
|
|
const double z = static_cast<double>(island.topZSubvoxels)
|
|
- static_cast<double>(island.topHalfSubvoxels) * 0.95;
|
|
for (int angleIndex = 0; angleIndex < 512 && !result.negativeControlDetected; ++angleIndex)
|
|
{
|
|
const double angle = floating_island_detail::Pi * 2.0 * angleIndex / 512.0;
|
|
const std::array<std::int64_t, 3> point{
|
|
floating_island_detail::RoundQ(island.centerSubvoxels[0] + radial * std::cos(angle)),
|
|
floating_island_detail::RoundQ(island.centerSubvoxels[1] + radial * std::sin(angle)),
|
|
floating_island_detail::RoundQ(z)};
|
|
if (floating_island_detail::Contains(badMin, badMax, point)) continue;
|
|
bool hiddenByAnotherFeatureBound = false;
|
|
for (const auto& other : plan.islands)
|
|
if (other.stableId != island.stableId
|
|
&& floating_island_detail::Contains(other.influenceMinimumSubvoxels,
|
|
other.influenceMaximumSubvoxels, point))
|
|
{ hiddenByAnotherFeatureBound = true; break; }
|
|
if (!hiddenByAnotherFeatureBound)
|
|
for (const auto& edge : plan.clusterEdges)
|
|
if (floating_island_detail::Contains(edge.influenceMinimumSubvoxels,
|
|
edge.influenceMaximumSubvoxels, point))
|
|
{ hiddenByAnotherFeatureBound = true; break; }
|
|
if (hiddenByAnotherFeatureBound) continue;
|
|
if (field.densitySubvoxels(point[0], point[1], point[2]) < 0)
|
|
{
|
|
const auto wrongInterval = floating_island_detail::Contains(badMin, badMax, point)
|
|
? FieldInterval{} : FieldInterval{true, 0, floating_island_detail::MaxDensity};
|
|
result.negativeControlDetected = wrongInterval.known
|
|
&& wrongInterval.minimumDensitySubvoxels >= 0;
|
|
}
|
|
}
|
|
}
|
|
result.passed = result.missedBounds == 0 && result.negativeControlDetected
|
|
&& result.voidBoxesProvedAir > 0;
|
|
return result;
|
|
}
|
|
|
|
FloatingIslandRouteAuditResult AuditFloatingIslandRoute(const FloatingIslandFieldV1& field,
|
|
std::int64_t routeOffsetSubvoxels)
|
|
{
|
|
FloatingIslandRouteAuditResult result;
|
|
if (!field.isValid() || !field.recipe() || !field.plan() || field.plan()->route.size() < 2) return result;
|
|
const auto& recipe = *field.recipe();
|
|
const auto& route = field.plan()->route;
|
|
const auto& constraints = recipe.routeConstraints;
|
|
const double maxGrade = floating_island_detail::Q16ToDouble(constraints.maximumGradeQ16);
|
|
const double radius = static_cast<double>(constraints.capsuleRadiusSubvoxels);
|
|
const double height = static_cast<double>(constraints.capsuleHeightSubvoxels);
|
|
const double halfHeight = height * 0.5;
|
|
const double cylinderHalf = std::max(0.0, halfHeight - radius);
|
|
const double spacing = static_cast<double>(constraints.sweepSpacingSubvoxels);
|
|
double minimumClearance = std::numeric_limits<double>::infinity();
|
|
const std::array<std::array<double, 2>, 17> disk = {{
|
|
{0.0, 0.0}, {0.6, 0.0}, {-0.6, 0.0}, {0.0, 0.6}, {0.0, -0.6},
|
|
{0.424264, 0.424264}, {-0.424264, 0.424264}, {0.424264, -0.424264}, {-0.424264, -0.424264},
|
|
{0.95, 0.0}, {-0.95, 0.0}, {0.0, 0.95}, {0.0, -0.95},
|
|
{0.671751, 0.671751}, {-0.671751, 0.671751}, {0.671751, -0.671751}, {-0.671751, -0.671751}}};
|
|
for (std::size_t segment = 0; segment + 1 < route.size(); ++segment)
|
|
{
|
|
++result.segmentsTested;
|
|
const auto& a = route[segment].positionSubvoxels;
|
|
const auto& b = route[segment + 1].positionSubvoxels;
|
|
const double dx = static_cast<double>(b[0] - a[0]);
|
|
const double dy = static_cast<double>(b[1] - a[1]);
|
|
const double dz = static_cast<double>(b[2] - a[2]);
|
|
const double horizontal = std::hypot(dx, dy);
|
|
if (!(horizontal > 0.0) || std::abs(dz) / horizontal > maxGrade)
|
|
{
|
|
if (result.firstGradeFailureSegment == ~std::uint64_t{0})
|
|
result.firstGradeFailureSegment = segment;
|
|
++result.gradeFailures;
|
|
}
|
|
const double length = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
const std::uint64_t centers = std::max<std::uint64_t>(1,
|
|
static_cast<std::uint64_t>(std::ceil(length / std::max(spacing, 1.0))));
|
|
for (std::uint64_t center = 0; center <= centers; ++center)
|
|
{
|
|
const double t = static_cast<double>(center) / centers;
|
|
const double cx = static_cast<double>(a[0]) + dx * t;
|
|
const double cy = static_cast<double>(a[1]) + dy * t;
|
|
const double cz = static_cast<double>(a[2]) + dz * t + routeOffsetSubvoxels;
|
|
++result.centerSamples;
|
|
const std::uint32_t verticalSteps = static_cast<std::uint32_t>(
|
|
std::max(1.0, std::ceil(height / std::max(spacing, 1.0))));
|
|
for (std::uint32_t vz = 0; vz <= verticalSteps; ++vz)
|
|
{
|
|
const double offsetZ = -halfHeight + height * vz / verticalSteps;
|
|
const double capDistance = std::max(0.0, std::abs(offsetZ) - cylinderHalf);
|
|
const double sectionRadius = std::sqrt(std::max(0.0, radius * radius - capDistance * capDistance));
|
|
for (const auto& sample : disk)
|
|
{
|
|
const double sx = cx + sample[0] * sectionRadius;
|
|
const double sy = cy + sample[1] * sectionRadius;
|
|
const std::int64_t density = field.densitySubvoxels(
|
|
floating_island_detail::RoundQ(sx), floating_island_detail::RoundQ(sy),
|
|
floating_island_detail::RoundQ(cz + offsetZ));
|
|
++result.capsuleFieldSamples;
|
|
minimumClearance = std::min(minimumClearance, static_cast<double>(density));
|
|
if (density < -1)
|
|
{
|
|
if (result.firstBlockedSegment == ~std::uint64_t{0})
|
|
{
|
|
result.firstBlockedSegment = segment;
|
|
result.firstBlockedPositionSubvoxels = {
|
|
floating_island_detail::RoundQ(sx), floating_island_detail::RoundQ(sy),
|
|
floating_island_detail::RoundQ(cz + offsetZ)};
|
|
}
|
|
++result.blockedCapsuleSamples;
|
|
}
|
|
}
|
|
}
|
|
|
|
const double foot = cz - halfHeight;
|
|
const double probeRadius = static_cast<double>(constraints.supportProbeRadiusSubvoxels);
|
|
const std::array<std::array<double, 2>, 5> probes{{
|
|
{0.0, 0.0}, {probeRadius, 0.0}, {-probeRadius, 0.0},
|
|
{0.0, probeRadius}, {0.0, -probeRadius}}};
|
|
std::uint32_t supported = 0;
|
|
for (const auto& probe : probes)
|
|
{
|
|
bool foundSupport = false;
|
|
for (double depth = static_cast<double>(constraints.minimumClearanceSubvoxels)
|
|
+ 0.5 * floating_island_detail::Q;
|
|
depth <= static_cast<double>(constraints.minimumClearanceSubvoxels
|
|
+ constraints.supportSearchDepthSubvoxels) + 1.0;
|
|
depth += 0.5 * floating_island_detail::Q)
|
|
{
|
|
const std::int64_t density = field.densitySubvoxels(
|
|
floating_island_detail::RoundQ(cx + probe[0]),
|
|
floating_island_detail::RoundQ(cy + probe[1]),
|
|
floating_island_detail::RoundQ(foot - depth));
|
|
++result.supportProbeSamples;
|
|
if (density < 0) { foundSupport = true; break; }
|
|
}
|
|
if (foundSupport) ++supported;
|
|
}
|
|
if (supported < constraints.minimumSupportProbes)
|
|
{
|
|
if (result.firstUnsupportedSegment == ~std::uint64_t{0})
|
|
{
|
|
result.firstUnsupportedSegment = segment;
|
|
result.firstUnsupportedCenterSubvoxels = {
|
|
floating_island_detail::RoundQ(cx), floating_island_detail::RoundQ(cy),
|
|
floating_island_detail::RoundQ(cz)};
|
|
}
|
|
++result.unsupportedCenterSamples;
|
|
}
|
|
}
|
|
}
|
|
result.minimumClearanceSubvoxels = std::isfinite(minimumClearance)
|
|
? floating_island_detail::RoundQ(minimumClearance) : 0;
|
|
result.passed = result.segmentsTested > 0 && result.centerSamples > 0
|
|
&& result.blockedCapsuleSamples == 0 && result.unsupportedCenterSamples == 0
|
|
&& result.gradeFailures == 0;
|
|
return result;
|
|
}
|
|
|
|
FloatingIslandThicknessAuditResult AuditFloatingIslandThickness(
|
|
const FloatingIslandPlanV1& plan, double minimumDepthToDiameterRatio)
|
|
{
|
|
FloatingIslandThicknessAuditResult result;
|
|
result.requiredDepthToDiameterRatio = minimumDepthToDiameterRatio;
|
|
if (plan.islands.empty() || !std::isfinite(minimumDepthToDiameterRatio)
|
|
|| minimumDepthToDiameterRatio <= 0.0) return result;
|
|
result.minimumDepthToDiameterRatio = std::numeric_limits<double>::infinity();
|
|
for (const auto& island : plan.islands)
|
|
{
|
|
++result.islandsTested;
|
|
if (island.radiusSubvoxels <= 0 || island.topZSubvoxels <= island.bottomZSubvoxels)
|
|
{
|
|
++result.islandsBelowMinimum;
|
|
result.minimumDepthToDiameterRatio = 0.0;
|
|
result.minimumIslandStableId = island.stableId;
|
|
continue;
|
|
}
|
|
const double ratio = static_cast<double>(island.topZSubvoxels - island.bottomZSubvoxels)
|
|
/ (2.0 * static_cast<double>(island.radiusSubvoxels));
|
|
if (ratio < result.minimumDepthToDiameterRatio)
|
|
{
|
|
result.minimumDepthToDiameterRatio = ratio;
|
|
result.minimumIslandStableId = island.stableId;
|
|
}
|
|
if (ratio + 1.0e-12 < minimumDepthToDiameterRatio) ++result.islandsBelowMinimum;
|
|
}
|
|
if (!std::isfinite(result.minimumDepthToDiameterRatio)) result.minimumDepthToDiameterRatio = 0.0;
|
|
result.passed = result.islandsTested == plan.islands.size()
|
|
&& result.islandsBelowMinimum == 0;
|
|
return result;
|
|
}
|
|
|
|
}
|