wip(assembler): round L24 -- per-strate passage styles; natural Worm default; FI anchor island
Core plans inter-strate passages from each upper strate's FStratePassageConfig for every generator: Straight / Worm (the new natural default: meander, variable radius, rough walls) / Spiral / Cascading. A naturalness audit (no >8 m flat runs, >=0.20 m wall relief) fails on L23's smooth tube. FloatingIslands selects Spiral, with a deterministic unique anchor island (ordinal 2) joined to the bedrock floor by a rock root. The SW arrival no longer stamps flat shelves. Capsule audits 0 blocked / 0 unsupported / 0 grade failures, all styles. Full-view hashes deterministic W1/W8/W12, Clang = GCC (reviewer re-run). Evaluator 10, FI geometry 5. KNOWN: the FI helix renders as a spiral ribbon in open air around the pillar instead of a tunnel enclosed in rock; fix next round. Unreal edits uncompiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
1 parent
71d09a8c52
commit
cb9236c265
17 files changed
+922
-122
No files matched your search
@@ -249,8 +249,8 @@ namespace VoxelForgeCore
|
||||
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
|
||||
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
||||
// Version 9 indexes passage landings so non-monotone route tails stay carveable.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 9;
|
||||
// Version 10 adds per-strate passage styles and rough wall relief to resolved passages.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 10;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
@@ -396,7 +396,9 @@ namespace VoxelForgeCore
|
||||
// bounded lobed roots and arched spans without changing the feature-family schema.
|
||||
// Version 3 keeps the version-2 island shapes, removes the all-XY lower boundary seal,
|
||||
// and routes its upper seal through the shared strate-boundary primitive. Version 4
|
||||
// records the director-resolved strate layout width in the recipe.
|
||||
// records the director-resolved strate layout width. Version 5 adds one passage-rooted
|
||||
// island whose root reaches the owned bedrock and whose surface route ends at its mouth.
|
||||
// New production adapters opt in explicitly; the compatibility default remains version 4.
|
||||
std::uint32_t geometryVersion = 4;
|
||||
std::uint32_t unitsPerVoxel = static_cast<std::uint32_t>(WorldAssemblySubvoxelsPerVoxel);
|
||||
// Import scale is explicit in microns; the reduced ratio maps owner-authored lengths to Core voxels.
|
||||
@@ -434,6 +436,14 @@ namespace VoxelForgeCore
|
||||
std::int64_t boundarySealSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t bridgeHalfWidthSubvoxels = 3 * WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t bridgeThicknessSubvoxels = 4 * WorldAssemblySubvoxelsPerVoxel;
|
||||
// Version 5 anchor-island inputs are resolved from that strate's passage config.
|
||||
// The anchor is the last primary route island and must stay inside the passage disk.
|
||||
std::uint32_t anchorIslandOrdinal = 2;
|
||||
std::int64_t anchorPlacementRadiusSubvoxels = 0;
|
||||
std::int64_t anchorRootRadiusSubvoxels = 0;
|
||||
std::int64_t anchorIslandMinimumRadiusSubvoxels = 0;
|
||||
std::int64_t anchorRootBottomZSubvoxels = 0;
|
||||
std::int64_t anchorMouthOffsetSubvoxels = 0;
|
||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
@@ -660,6 +670,8 @@ namespace VoxelForgeCore
|
||||
{
|
||||
std::int32_t upperStrateIndex = -1;
|
||||
std::int32_t connections = 1;
|
||||
// EVoxelPassageStyle numeric values: Straight=0, Worm=1, Spiral=2, Cascading=3.
|
||||
std::uint32_t style = 1;
|
||||
std::int64_t mouthRadiusSubvoxels = 0;
|
||||
std::int64_t midRadiusSubvoxels = 0;
|
||||
std::int64_t reachMinimumSubvoxels = 0;
|
||||
@@ -667,6 +679,12 @@ namespace VoxelForgeCore
|
||||
std::int64_t distanceMinimumSubvoxels = 0;
|
||||
std::int64_t distanceMaximumSubvoxels = 0;
|
||||
std::int64_t wanderSubvoxels = 0;
|
||||
std::uint32_t segments = 20;
|
||||
std::int64_t verticalWobbleSubvoxels = 0;
|
||||
std::int64_t spiralRadiusSubvoxels = 0;
|
||||
std::int64_t spiralTurnsQ16 = 2ll << 16;
|
||||
std::uint32_t cascadeSteps = 4;
|
||||
std::int64_t cascadeLedgeSubvoxels = 0;
|
||||
};
|
||||
|
||||
struct VFCORE_API ResolvedLayoutSnapshotV3
|
||||
|
||||
@@ -188,6 +188,15 @@ namespace floating_island_detail
|
||||
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);
|
||||
@@ -247,7 +256,7 @@ namespace floating_island_detail
|
||||
|
||||
inline bool ValidateRecipe(const FloatingIslandRecipeV1& recipe, std::string& error)
|
||||
{
|
||||
const std::array<std::int64_t, 26> values = {
|
||||
const std::array<std::int64_t, 32> values = {
|
||||
recipe.layoutOriginXSubvoxels, recipe.layoutOriginYSubvoxels,
|
||||
recipe.layoutCenterTopZSubvoxels, recipe.layoutMinimumZSubvoxels,
|
||||
recipe.layoutMaximumZSubvoxels, recipe.layoutChunkWidthSubvoxels, recipe.islandDensityQ16,
|
||||
@@ -259,6 +268,10 @@ namespace floating_island_detail
|
||||
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};
|
||||
@@ -268,7 +281,7 @@ namespace floating_island_detail
|
||||
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
||||
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
||||
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
||||
if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 4
|
||||
if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 5
|
||||
|| (recipe.geometryVersion == 1
|
||||
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
||||
|| recipe.unitsPerVoxel != Q
|
||||
@@ -323,6 +336,21 @@ namespace floating_island_detail
|
||||
|| 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;
|
||||
}
|
||||
|
||||
@@ -725,6 +753,25 @@ namespace floating_island_detail
|
||||
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 && island.ordinal == recipe.anchorIslandOrdinal
|
||||
&& recipe.anchorRootBottomZSubvoxels < island.bottomZSubvoxels)
|
||||
{
|
||||
const double rootTop = static_cast<double>(island.bottomZSubvoxels);
|
||||
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;
|
||||
@@ -895,9 +942,15 @@ namespace floating_island_detail
|
||||
FloatingIslandAnchorV1& island)
|
||||
{
|
||||
const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels;
|
||||
const std::int64_t reach = island.radiusSubvoxels + recipe.contourWarpSubvoxels + pad;
|
||||
const bool isAnchor=recipe.geometryVersion>=5
|
||||
&&island.ordinal==recipe.anchorIslandOrdinal;
|
||||
const std::int64_t reach = std::max(island.radiusSubvoxels,
|
||||
isAnchor?recipe.anchorRootRadiusSubvoxels:std::int64_t{0})
|
||||
+ recipe.contourWarpSubvoxels + pad;
|
||||
island.influenceMinimumSubvoxels = {island.centerSubvoxels[0] - reach,
|
||||
island.centerSubvoxels[1] - reach, island.bottomZSubvoxels - pad};
|
||||
island.centerSubvoxels[1] - reach,
|
||||
(isAnchor?std::min(island.bottomZSubvoxels,recipe.anchorRootBottomZSubvoxels)
|
||||
:island.bottomZSubvoxels)-pad};
|
||||
island.influenceMaximumSubvoxels = {island.centerSubvoxels[0] + reach,
|
||||
island.centerSubvoxels[1] + reach, island.topZSubvoxels + pad};
|
||||
}
|
||||
@@ -942,12 +995,19 @@ namespace floating_island_detail
|
||||
|
||||
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 < recipe.minimumRadiusSubvoxels
|
||||
|| island.radiusSubvoxels > recipe.maximumRadiusSubvoxels
|
||||
|| 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
|
||||
@@ -964,12 +1024,16 @@ namespace floating_island_detail
|
||||
{ error = "floating-island anchor has invalid coordinates or an inverted bound"; return false; }
|
||||
}
|
||||
const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels;
|
||||
const std::int64_t reach = island.radiusSubvoxels + recipe.contourWarpSubvoxels + pad;
|
||||
const std::int64_t reach = std::max(island.radiusSubvoxels,
|
||||
isAnchor?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] > island.bottomZSubvoxels - pad
|
||||
|| island.influenceMinimumSubvoxels[2] > (isAnchor
|
||||
?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)
|
||||
@@ -978,6 +1042,19 @@ namespace floating_island_detail
|
||||
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;
|
||||
@@ -1239,6 +1316,22 @@ std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
||||
&& 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 5 is missing a passage-rooted anchor field";return std::nullopt;}
|
||||
}
|
||||
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)
|
||||
@@ -1391,6 +1484,13 @@ std::string FloatingIslandManifestV1::ToJson() const
|
||||
<< ",\"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;
|
||||
out << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels
|
||||
<< ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels
|
||||
<< ",\"roughness_q\":" << recipe.roughnessSubvoxels
|
||||
@@ -1552,6 +1652,9 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
||||
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,
|
||||
@@ -1638,14 +1741,31 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
||||
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(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 = floating_island_detail::FindCapsuleFootingTopZ(
|
||||
recipe, manifest.plan, x, y);
|
||||
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 = {x, y, *surface + capsuleHalf
|
||||
point.positionSubvoxels = {routeX, routeY, *surface + capsuleHalf
|
||||
+ recipe.routeConstraints.minimumClearanceSubvoxels
|
||||
+ (recipe.geometryVersion >= 2 ? floating_island_detail::Q / 8 : 0)};
|
||||
point.radiusSubvoxels = routeRadius;
|
||||
|
||||
@@ -594,12 +594,19 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
const std::int64_t inner=cfg?std::min(cfg->distanceMinimumSubvoxels,
|
||||
cfg->distanceMaximumSubvoxels):8*qPerMetre;
|
||||
const std::int64_t placementRadius=std::max<std::int64_t>(1,outer);
|
||||
const std::int64_t mouthRadius=std::max<std::int64_t>(static_cast<std::int64_t>(std::llround(1.25*qPerMetre)),
|
||||
// Leave room for the capsule and bounded wall relief at the floor/wall seam.
|
||||
const std::int64_t mouthRadius=std::max<std::int64_t>(static_cast<std::int64_t>(std::llround(1.5*qPerMetre)),
|
||||
cfg?cfg->mouthRadiusSubvoxels:0);
|
||||
const std::int64_t midRadius=std::max<std::int64_t>(mouthRadius,
|
||||
cfg?cfg->midRadiusSubvoxels:0);
|
||||
const std::int64_t reachMinimum=cfg?std::max<std::int64_t>(0,cfg->reachMinimumSubvoxels):8*qPerMetre;
|
||||
const std::int64_t reachMaximum=cfg?std::max<std::int64_t>(reachMinimum,cfg->reachMaximumSubvoxels):24*qPerMetre;
|
||||
const std::uint32_t style=cfg?std::min<std::uint32_t>(cfg->style,3u):1u;
|
||||
const std::uint32_t segments=cfg?std::clamp<std::uint32_t>(cfg->segments,16u,48u):20u;
|
||||
const std::int64_t wander=cfg?std::max<std::int64_t>(0,cfg->wanderSubvoxels):3*qPerMetre;
|
||||
const std::int64_t spiralRadius=cfg?std::max<std::int64_t>(0,cfg->spiralRadiusSubvoxels):6*qPerMetre;
|
||||
const std::int64_t spiralTurnsQ16=cfg?std::max<std::int64_t>(16384,cfg->spiralTurnsQ16):2ll<<16;
|
||||
const std::int64_t verticalWobble=cfg?std::max<std::int64_t>(0,cfg->verticalWobbleSubvoxels):0;
|
||||
const std::uint32_t connectionCount=cfg?static_cast<std::uint32_t>(std::clamp(cfg->connections,1,12)):1;
|
||||
for(std::uint32_t connection=0;connection<connectionCount;++connection)
|
||||
{
|
||||
@@ -612,6 +619,12 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
std::array<std::int64_t,3> upperMouth{},lowerMouth{};
|
||||
const bool hasUpperRoute=connection==0&&routeAnchor(upper,true,upperMouth);
|
||||
const bool hasLowerRoute=connection==0&&routeAnchor(lower,false,lowerMouth);
|
||||
const ResolvedLayoutAnchorV2* passageAnchor=nullptr;
|
||||
if(connection==0)
|
||||
for(const auto& anchor:upper.anchors)
|
||||
if(anchor.kind=="passage_anchor"&&anchor.values.size()>=7)
|
||||
{passageAnchor=&anchor;break;}
|
||||
const bool anchoredSpiral=style==2&&passageAnchor!=nullptr;
|
||||
if(!hasUpperRoute||!hasLowerRoute)
|
||||
{
|
||||
const auto xy=PassageRandomXY(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
||||
@@ -642,8 +655,22 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
const bool hasLowerTangent=hasLowerRoute
|
||||
&&routeTangent(lower,false,lowerTangent);
|
||||
|
||||
const std::int64_t ringRadius=std::max<std::int64_t>(1,
|
||||
const std::int64_t outerRingRadius=std::max<std::int64_t>(1,
|
||||
placementRadius-mouthRadius-qPerMetre/2);
|
||||
const std::uint32_t pathStyle=anchoredSpiral?1u:style;
|
||||
std::int64_t ringRadius=outerRingRadius;
|
||||
if(pathStyle==1u)
|
||||
ringRadius=std::min<std::int64_t>(outerRingRadius,
|
||||
std::max<std::int64_t>(mouthRadius+2*qPerMetre,placementRadius*5/8));
|
||||
else if(pathStyle==2u)
|
||||
ringRadius=std::clamp(spiralRadius,
|
||||
std::min(outerRingRadius,mouthRadius+qPerMetre/2),outerRingRadius);
|
||||
else if(pathStyle==3u)
|
||||
ringRadius=std::clamp(outerRingRadius
|
||||
-std::min<std::int64_t>(outerRingRadius/3,
|
||||
std::max<std::int64_t>(0,cfg?cfg->cascadeLedgeSubvoxels:0)
|
||||
*static_cast<std::int64_t>(cfg?cfg->cascadeSteps:4)),
|
||||
mouthRadius+2*qPerMetre,outerRingRadius);
|
||||
std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
|
||||
if(hasUpperRoute)
|
||||
@@ -662,28 +689,159 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{bestAlignment=alignment;phase=candidate;}
|
||||
}
|
||||
}
|
||||
const bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
||||
bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
||||
connection,0x44495245u)&1u)!=0;
|
||||
const int direction=clockwise?1:-1;
|
||||
const auto ringPoint=[&](std::uint32_t directionIndex)
|
||||
if(anchoredSpiral&&passageAnchor&&hasUpperTangent)
|
||||
{
|
||||
const double radialX=static_cast<double>(upperMouth[0]-passageAnchor->values[0]);
|
||||
const double radialY=static_cast<double>(upperMouth[1]-passageAnchor->values[1]);
|
||||
const double clockwiseTangentX=-radialY,clockwiseTangentY=radialX;
|
||||
clockwise=clockwiseTangentX*upperTangent[0]
|
||||
+clockwiseTangentY*upperTangent[1]>=0.0;
|
||||
}
|
||||
const int direction=clockwise?1:-1;
|
||||
constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree contract.
|
||||
const std::int64_t wormRadialWobble=std::min<std::int64_t>(
|
||||
std::max<std::int64_t>(0,wander/3),2*qPerMetre);
|
||||
const std::int64_t wormCenterWobble=std::min<std::int64_t>(
|
||||
std::max<std::int64_t>(0,wander/6),qPerMetre);
|
||||
const auto ringPoint=[&](std::uint32_t directionIndex,std::uint32_t loopIndex=0)
|
||||
{
|
||||
const double angle=static_cast<double>(directionIndex)*
|
||||
(3.14159265358979323846/8.0);
|
||||
double radius=static_cast<double>(ringRadius);
|
||||
double centerX=0.0,centerY=0.0;
|
||||
if(pathStyle==1u)
|
||||
{
|
||||
const double loop=static_cast<double>(loopIndex);
|
||||
const double phaseA=loop*0.6180339887498948+0.37;
|
||||
const double phaseB=loop*0.4142135623730950+1.11;
|
||||
radius+=static_cast<double>(wormRadialWobble)
|
||||
*std::sin(angle+phaseA);
|
||||
centerX=static_cast<double>(wormCenterWobble)*std::sin(phaseB);
|
||||
centerY=static_cast<double>(wormCenterWobble)*std::cos(phaseB);
|
||||
}
|
||||
else if(pathStyle==3u)
|
||||
{
|
||||
const std::uint32_t steps=std::max<std::uint32_t>(1,
|
||||
cfg?cfg->cascadeSteps:4u);
|
||||
const double ledge=std::min<double>(ringRadius/4,
|
||||
static_cast<double>(cfg?std::max<std::int64_t>(0,
|
||||
cfg->cascadeLedgeSubvoxels):0));
|
||||
radius-=ledge*(0.5-0.5*std::cos(angle*steps));
|
||||
}
|
||||
return std::array<std::int64_t,2>{
|
||||
static_cast<std::int64_t>(ringRadius)*directions[directionIndex][0]/65536,
|
||||
static_cast<std::int64_t>(ringRadius)*directions[directionIndex][1]/65536};
|
||||
static_cast<std::int64_t>(std::llround(centerX+radius*directions[directionIndex][0]/65536.0)),
|
||||
static_cast<std::int64_t>(std::llround(centerY+radius*directions[directionIndex][1]/65536.0))};
|
||||
};
|
||||
std::vector<std::array<std::int64_t,2>> xy;
|
||||
const auto pushXY=[&](std::array<std::int64_t,2> p)
|
||||
{if(xy.empty()||xy.back()!=p)xy.push_back(p);};
|
||||
const auto firstRing=ringPoint(phase);
|
||||
std::size_t helixEndIndex=0;
|
||||
std::int64_t helixBedrockZ=0;
|
||||
bool hasAnchorHelix=false;
|
||||
pushXY({upperMouth[0],upperMouth[1]});
|
||||
if(anchoredSpiral&&passageAnchor&&upper.bedrock.presentInCurrentField
|
||||
&&spiralRadius>0)
|
||||
{
|
||||
const std::int64_t anchorX=passageAnchor->values[0];
|
||||
const std::int64_t anchorY=passageAnchor->values[1];
|
||||
const std::int64_t rootRadius=passageAnchor->values[2];
|
||||
const std::int64_t rootBottom=passageAnchor->values[5];
|
||||
helixBedrockZ=rootBottom!=0?rootBottom:contentZQ(upper.bedrock.topMicrometres);
|
||||
const std::int64_t helixRadius=spiralRadius;
|
||||
if(rootRadius<helixRadius+midRadius+qPerMetre/4
|
||||
||std::hypot(static_cast<double>(upperMouth[0]-anchorX),
|
||||
static_cast<double>(upperMouth[1]-anchorY))
|
||||
>static_cast<double>(rootRadius+qPerMetre/2)
|
||||
||upperMouth[2]<=helixBedrockZ)
|
||||
{error=passage.stableId+" anchor root cannot contain its authored spiral mouth or bedrock descent";return false;}
|
||||
const double drop=static_cast<double>(upperMouth[2]-helixBedrockZ);
|
||||
const double pointsPerTurn=static_cast<double>(segments);
|
||||
const double polygonCircumference=2.0*3.14159265358979323846
|
||||
*static_cast<double>(helixRadius)
|
||||
*(std::sin(3.14159265358979323846/pointsPerTurn)
|
||||
/(3.14159265358979323846/pointsPerTurn));
|
||||
const double horizontalPerTurn=polygonCircumference
|
||||
*static_cast<double>(TargetGradeQ16)/65536.0;
|
||||
const std::uint32_t authoredTurns=static_cast<std::uint32_t>(std::max<std::int64_t>(1,
|
||||
(spiralTurnsQ16+65535)/65536));
|
||||
const std::uint32_t gradeTurns=static_cast<std::uint32_t>(std::max(1.0,
|
||||
std::ceil(drop/std::max(1.0,horizontalPerTurn))));
|
||||
// The first coil grows from the island mouth offset to the full authored
|
||||
// radius, so reserve one extra turn for that shorter opening revolution.
|
||||
const std::uint32_t turns=std::max(authoredTurns,gradeTurns+1);
|
||||
if(turns>512)
|
||||
{error=passage.stableId+" authored spiral radius needs more than 512 turns to meet walkable grade";return false;}
|
||||
const double startAngle=std::atan2(static_cast<double>(upperMouth[1]-anchorY),
|
||||
static_cast<double>(upperMouth[0]-anchorX));
|
||||
const std::uint32_t helixPoints=turns*segments;
|
||||
for(std::uint32_t point=1;point<=helixPoints;++point)
|
||||
{
|
||||
const double t=static_cast<double>(point)/helixPoints;
|
||||
const double angle=startAngle+static_cast<double>(clockwise?1:-1)
|
||||
*2.0*3.14159265358979323846*t*turns;
|
||||
const double radialT=std::min(1.0,static_cast<double>(point)/segments);
|
||||
const double mouthOffset=std::clamp(static_cast<double>(passageAnchor->values[3]),
|
||||
0.0,static_cast<double>(helixRadius));
|
||||
const double radial=mouthOffset+(static_cast<double>(helixRadius)-mouthOffset)
|
||||
*radialT;
|
||||
const std::array<std::int64_t,2> p{
|
||||
static_cast<std::int64_t>(std::llround(anchorX
|
||||
+radial*std::cos(angle))),
|
||||
static_cast<std::int64_t>(std::llround(anchorY
|
||||
+radial*std::sin(angle)))};
|
||||
pushXY(p);
|
||||
}
|
||||
helixEndIndex=xy.size()-1;
|
||||
hasAnchorHelix=helixEndIndex>0;
|
||||
}
|
||||
const std::array<std::int64_t,2> firstRing=ringPoint(phase);
|
||||
if(hasAnchorHelix&&pathStyle==1u)
|
||||
{
|
||||
// Let the anchored helix widen into the winding inter-strate route
|
||||
// over a full, smooth coil. A direct jump from the root radius to the
|
||||
// outer worm ring creates a sharp support seam inside the rock neck.
|
||||
const double anchorX=static_cast<double>(passageAnchor->values[0]);
|
||||
const double anchorY=static_cast<double>(passageAnchor->values[1]);
|
||||
const double startAngle=std::atan2(static_cast<double>(upperMouth[1])-anchorY,
|
||||
static_cast<double>(upperMouth[0])-anchorX);
|
||||
const double targetX=static_cast<double>(firstRing[0])-anchorX;
|
||||
const double targetY=static_cast<double>(firstRing[1])-anchorY;
|
||||
const double targetAngle=std::atan2(targetY,targetX);
|
||||
const double angleDelta=static_cast<double>(direction)*2.0
|
||||
*3.14159265358979323846
|
||||
+std::remainder(targetAngle-startAngle,2.0*3.14159265358979323846);
|
||||
const double startRadius=static_cast<double>(spiralRadius);
|
||||
const std::int64_t targetDx=firstRing[0]-passageAnchor->values[0];
|
||||
const std::int64_t targetDy=firstRing[1]-passageAnchor->values[1];
|
||||
const double targetRadius=static_cast<double>(PassageIsqrt(
|
||||
static_cast<std::uint64_t>(targetDx*targetDx+targetDy*targetDy)));
|
||||
const std::uint32_t transitionSegments=static_cast<std::uint32_t>(
|
||||
std::clamp(std::ceil(std::abs(angleDelta)*(startRadius+targetRadius)
|
||||
/(2.0*0.75*qPerMetre)),static_cast<double>(segments),256.0));
|
||||
for(std::uint32_t step=1;step<=transitionSegments;++step)
|
||||
{
|
||||
const double t=static_cast<double>(step)/transitionSegments;
|
||||
const double smooth=t*t*(3.0-2.0*t);
|
||||
const double radius=startRadius+(targetRadius-startRadius)*smooth;
|
||||
const double angle=startAngle+angleDelta*t;
|
||||
const std::array<std::int64_t,2> point=step==transitionSegments?firstRing:
|
||||
std::array<std::int64_t,2>{
|
||||
static_cast<std::int64_t>(std::llround(anchorX+radius*std::cos(angle))),
|
||||
static_cast<std::int64_t>(std::llround(anchorY+radius*std::sin(angle)))};
|
||||
pushXY(point);
|
||||
}
|
||||
}
|
||||
pushXY(firstRing);
|
||||
std::uint64_t totalHorizontal=0;std::uint32_t loops=0;
|
||||
const std::int64_t verticalDistance=std::abs(lowerMouth[2]-upperMouth[2]);
|
||||
constexpr std::int64_t TargetGradeQ16=12500; // leave margin below the 15-degree contract.
|
||||
const std::int64_t verticalDistance=hasAnchorHelix
|
||||
?std::abs(lowerMouth[2]-helixBedrockZ):std::abs(lowerMouth[2]-upperMouth[2]);
|
||||
auto measurePath=[&]()
|
||||
{
|
||||
std::uint64_t total=0;
|
||||
for(std::size_t i=1;i<xy.size();++i)
|
||||
const std::size_t start=hasAnchorHelix?helixEndIndex:0;
|
||||
for(std::size_t i=start+1;i<xy.size();++i)
|
||||
total+=distanceQ(xy[i][0]-xy[i-1][0],xy[i][1]-xy[i-1][1]);
|
||||
return total;
|
||||
};
|
||||
@@ -696,7 +854,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{
|
||||
int directionIndex=static_cast<int>(phase)+direction*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex)));
|
||||
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex),
|
||||
loops+(step==16?1u:0u)));
|
||||
}
|
||||
++loops;
|
||||
}
|
||||
@@ -710,7 +869,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{
|
||||
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
||||
(hashedLowerPhase+offset)%16);
|
||||
const auto terminal=ringPoint(candidate);
|
||||
const auto terminal=ringPoint(candidate,loops);
|
||||
const std::int64_t dx=lowerMouth[0]-terminal[0];
|
||||
const std::int64_t dy=lowerMouth[1]-terminal[1];
|
||||
const std::int64_t alignment=dx*lowerTangent[0]+dy*lowerTangent[1];
|
||||
@@ -728,7 +887,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{
|
||||
int directionIndex=static_cast<int>(phase)+direction*step;
|
||||
directionIndex=(directionIndex%16+16)%16;
|
||||
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex)));
|
||||
pushXY(ringPoint(static_cast<std::uint32_t>(directionIndex),loops));
|
||||
if(static_cast<std::uint32_t>(directionIndex)==lowerPhase)break;
|
||||
}
|
||||
pushXY(ringPoint(lowerPhase));
|
||||
@@ -775,16 +934,33 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
for(std::size_t i=1;i<xy.size();++i)
|
||||
cumulative[i]=cumulative[i-1]+distanceQ(xy[i][0]-xy[i-1][0],xy[i][1]-xy[i-1][1]);
|
||||
const std::uint64_t mouthHorizontal=cumulative[lowerMouthIndex];
|
||||
const std::uint64_t helixHorizontal=hasAnchorHelix
|
||||
?cumulative[helixEndIndex]:0;
|
||||
if(mouthHorizontal==0)
|
||||
{error=passage.stableId+" has no horizontal reach before its lower mouth";return false;}
|
||||
if(hasAnchorHelix&&helixHorizontal==0)
|
||||
{error=passage.stableId+" anchor spiral has no horizontal reach";return false;}
|
||||
passage.controlPoints.reserve(xy.size());
|
||||
for(std::size_t i=0;i<xy.size();++i)
|
||||
{
|
||||
const std::int64_t radius=(i<=1||i+1>=xy.size()-1)?mouthRadius:midRadius;
|
||||
const std::int64_t playerZ=i<=lowerMouthIndex
|
||||
?upperMouth[2]+PassageRoundRatio(lowerMouth[2]-upperMouth[2],
|
||||
static_cast<std::int64_t>(cumulative[i]),static_cast<std::int64_t>(mouthHorizontal))
|
||||
:lowerLandingPoints[i-lowerMouthIndex-1][2];
|
||||
const std::int64_t radius=(i<=1||i==lowerMouthIndex
|
||||
||i+1>=xy.size()-1)?mouthRadius:midRadius;
|
||||
std::int64_t playerZ=0;
|
||||
if(i>lowerMouthIndex)
|
||||
playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2];
|
||||
else if(hasAnchorHelix&&i<=helixEndIndex)
|
||||
playerZ=upperMouth[2]+PassageRoundRatio(helixBedrockZ-upperMouth[2],
|
||||
static_cast<std::int64_t>(cumulative[i]),
|
||||
static_cast<std::int64_t>(helixHorizontal));
|
||||
else
|
||||
{
|
||||
const std::uint64_t segmentStart=hasAnchorHelix?helixHorizontal:0;
|
||||
const std::int64_t startZ=hasAnchorHelix?helixBedrockZ:upperMouth[2];
|
||||
const std::uint64_t segmentLength=mouthHorizontal-segmentStart;
|
||||
playerZ=startZ+PassageRoundRatio(lowerMouth[2]-startZ,
|
||||
static_cast<std::int64_t>(cumulative[i]-segmentStart),
|
||||
static_cast<std::int64_t>(segmentLength));
|
||||
}
|
||||
ResolvedLayoutPassagePointV1 point;
|
||||
point.positionSubvoxels={xy[i][0],xy[i][1],playerZ+radius-playerCenterAboveFloorQ};
|
||||
point.radiusSubvoxels=radius;passage.controlPoints.push_back(point);
|
||||
|
||||
@@ -107,6 +107,66 @@ public:
|
||||
const double floorThicknessVoxels=qPerMetre/Q;
|
||||
const double playerCenterAboveFloorQ=1.13*qPerMetre;
|
||||
float composed=density;
|
||||
const auto composeRouteMouthSupport=[&](std::int32_t strateIndex,
|
||||
const std::array<std::int64_t,3>& mouth,bool departure)
|
||||
{
|
||||
const double mouthDx=qx-static_cast<double>(mouth[0]);
|
||||
const double mouthDy=qy-static_cast<double>(mouth[1]);
|
||||
const double routeSupportReach=13.0*qPerMetre;
|
||||
if(mouthDx*mouthDx+mouthDy*mouthDy>routeSupportReach*routeSupportReach)return;
|
||||
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[strateIndex](const auto& value)
|
||||
{return value.strateIndex==strateIndex;});
|
||||
if(stratum==ResolvedLayout->strates.end())return;
|
||||
std::array<const VoxelForgeCore::ResolvedLayoutAnchorV2*,5> route{};
|
||||
std::size_t routeCount=0;
|
||||
for(const auto& anchor:stratum->anchors)
|
||||
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||
{
|
||||
if(departure)
|
||||
{
|
||||
if(routeCount<route.size())route[routeCount++]=&anchor;
|
||||
else
|
||||
{
|
||||
std::move(route.begin()+1,route.end(),route.begin());
|
||||
route.back()=&anchor;
|
||||
}
|
||||
}
|
||||
else if(routeCount<route.size())route[routeCount++]=&anchor;
|
||||
}
|
||||
if(routeCount<2)return;
|
||||
const std::size_t lastSegment=routeCount-1;
|
||||
const double floorThickness=1000000.0/std::max<std::uint32_t>(1,
|
||||
ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
for(std::size_t index=0;index<lastSegment;++index)
|
||||
{
|
||||
const auto& a=*route[index];const auto& b=*route[index+1];
|
||||
const double dx=static_cast<double>(b.values[0]-a.values[0]);
|
||||
const double dy=static_cast<double>(b.values[1]-a.values[1]);
|
||||
const double lengthSq=dx*dx+dy*dy;
|
||||
const double t=lengthSq>0.0?std::clamp(((qx-a.values[0])*dx
|
||||
+(qy-a.values[1])*dy)/lengthSq,0.0,1.0):0.0;
|
||||
const double centerX=a.values[0]+t*dx,centerY=a.values[1]+t*dy;
|
||||
const double centerZ=a.values[2]+t*(static_cast<double>(b.values[2])-a.values[2]);
|
||||
const double routeRadius=a.values.size()>=4
|
||||
?a.values[3]+t*(static_cast<double>(b.values[3])-a.values[3]):0.0;
|
||||
const double halfWidth=std::max(0.75*qPerMetre,routeRadius);
|
||||
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||
if(horizontalX*horizontalX+horizontalY*horizontalY>halfWidth*halfWidth)continue;
|
||||
const double floorVoxels=(centerZ-1.20*qPerMetre)/Q;
|
||||
if(Z<floorVoxels-floorThickness||Z>floorVoxels+1.0/Q)continue;
|
||||
composed=std::min(composed,static_cast<float>(
|
||||
std::clamp(Z-floorVoxels,-floorThickness,0.0)));
|
||||
}
|
||||
};
|
||||
// Seed the route floor first. The bore carve below reopens any overlap at the
|
||||
// mouth, then applies its own lower tread, so neither shape blocks the capsule.
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
composeRouteMouthSupport(passage.upperStrateIndex,passage.upperMouthSubvoxels,true);
|
||||
// The lower mouth emerges into its native terrain. Keep the passage tread
|
||||
// support, but do not stamp short, flat route-anchor shelves over that cave mouth.
|
||||
}
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
if(passage.controlPoints.size()<2)continue;
|
||||
@@ -125,16 +185,25 @@ public:
|
||||
const std::ptrdiff_t lowerMouthIndex=std::clamp<std::ptrdiff_t>(
|
||||
static_cast<std::ptrdiff_t>(passage.lowerMouthControlIndex),1,lastIndex);
|
||||
const auto mainEnd=passage.controlPoints.begin()+lowerMouthIndex+1;
|
||||
const auto carveFound=std::lower_bound(passage.controlPoints.begin(),mainEnd,qz,
|
||||
// The tube radius is measured from its centerline, while floorProfile is
|
||||
// one player-center offset away. Search the full vertical radius of the SDF;
|
||||
// a narrow point window misses earlier turns of a low-pitch helix.
|
||||
const double carveSearchMargin=std::max(passage.mouthRadiusSubvoxels,
|
||||
passage.midRadiusSubvoxels)+0.25*qPerMetre;
|
||||
const auto carveHigh=std::lower_bound(passage.controlPoints.begin(),mainEnd,
|
||||
qz+carveSearchMargin,
|
||||
[&](const auto& point,double value){return floorProfile(point)>value;});
|
||||
const std::ptrdiff_t carveCenter=carveFound-passage.controlPoints.begin();
|
||||
const std::ptrdiff_t carveBegin=std::max<std::ptrdiff_t>(0,carveCenter-3);
|
||||
const std::ptrdiff_t carveEnd=std::min<std::ptrdiff_t>(lowerMouthIndex-1,carveCenter+2);
|
||||
const auto carveLow=std::lower_bound(passage.controlPoints.begin(),mainEnd,
|
||||
qz-carveSearchMargin,
|
||||
[&](const auto& point,double value){return floorProfile(point)>value;});
|
||||
const std::ptrdiff_t carveBegin=std::max<std::ptrdiff_t>(0,
|
||||
(carveHigh-passage.controlPoints.begin())-1);
|
||||
const std::ptrdiff_t carveEnd=std::min<std::ptrdiff_t>(lowerMouthIndex-1,
|
||||
(carveLow-passage.controlPoints.begin())+1);
|
||||
double bestSignedDistance=std::numeric_limits<double>::infinity();
|
||||
double bestT=0.0;
|
||||
std::ptrdiff_t bestIndex=0;
|
||||
bool hasSupportFloor=false;
|
||||
double bestSupportFloorVoxels=-std::numeric_limits<double>::infinity();
|
||||
double bestSupportDensity=0.0;
|
||||
const auto considerCarveSegment=[&](std::ptrdiff_t index)
|
||||
{
|
||||
@@ -164,65 +233,94 @@ public:
|
||||
for(std::ptrdiff_t index=lowerMouthIndex;index<lastIndex;++index)
|
||||
considerCarveSegment(index);
|
||||
if(!std::isfinite(bestSignedDistance))continue;
|
||||
// Positive density is air; the passage SDF is applied after both owner bands.
|
||||
composed=std::max(composed,static_cast<float>(-bestSignedDistance/Q));
|
||||
const auto& nearestA=passage.controlPoints[static_cast<std::size_t>(bestIndex)];
|
||||
const auto& nearestB=passage.controlPoints[static_cast<std::size_t>(bestIndex+1)];
|
||||
const double nearestRadius=nearestA.radiusSubvoxels+bestT*(
|
||||
static_cast<double>(nearestB.radiusSubvoxels)-nearestA.radiusSubvoxels);
|
||||
const double nearestPlayerCenterZ=nearestA.positionSubvoxels[2]+bestT*(
|
||||
const double nearestDx=static_cast<double>(nearestB.positionSubvoxels[0])
|
||||
-nearestA.positionSubvoxels[0];
|
||||
const double nearestDy=static_cast<double>(nearestB.positionSubvoxels[1])
|
||||
-nearestA.positionSubvoxels[1];
|
||||
const double nearestHorizontalSq=nearestDx*nearestDx+nearestDy*nearestDy;
|
||||
const double supportPathT=nearestHorizontalSq>0.0
|
||||
?std::clamp(((qx-nearestA.positionSubvoxels[0])*nearestDx
|
||||
+(qy-nearestA.positionSubvoxels[1])*nearestDy)/nearestHorizontalSq,0.0,1.0)
|
||||
:bestT;
|
||||
const double supportPathRadius=nearestA.radiusSubvoxels+supportPathT*(
|
||||
static_cast<double>(nearestB.radiusSubvoxels)-nearestA.radiusSubvoxels);
|
||||
const double supportPathPlayerCenterZ=nearestA.positionSubvoxels[2]+supportPathT*(
|
||||
static_cast<double>(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2])
|
||||
-nearestRadius+playerCenterAboveFloorQ;
|
||||
// Only compose a support shelf below the capsule feet. This keeps support
|
||||
// probes on the matching floor while capsule samples remain air at bends.
|
||||
if(qz<nearestPlayerCenterZ-0.90*qPerMetre)
|
||||
-supportPathRadius+playerCenterAboveFloorQ;
|
||||
const double nearestPlayerCenterZ=supportPathPlayerCenterZ;
|
||||
const double distanceToCenter=std::max(1.0e-9,bestSignedDistance+nearestRadius);
|
||||
const double verticalWallAlignment=distanceToCenter>1.0e-9
|
||||
?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*(
|
||||
static_cast<double>(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2])))
|
||||
/distanceToCenter,0.0,1.0):0.0;
|
||||
const float relief=VF_FieldSurfaceRelief(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),static_cast<std::uint32_t>(Owner?Owner->Seed:0));
|
||||
const double wallReliefWeight=1.0-0.80*verticalWallAlignment;
|
||||
const double roughSignedDistance=bestSignedDistance
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.24*qPerMetre*wallReliefWeight;
|
||||
// Positive density is air; rough rock is cut with full side/ceiling detail and
|
||||
// gentler relief on the gravity-facing wall so the supported tread stays open.
|
||||
composed=std::max(composed,static_cast<float>(-roughSignedDistance/Q));
|
||||
// At tight bends, keep the tread tied to the player-center profile selected
|
||||
// by horizontal path position so vertical probe offsets cannot shift the ramp.
|
||||
const bool fallbackSupport=qz>=nearestPlayerCenterZ-1.50*qPerMetre;
|
||||
const double supportThicknessVoxels=fallbackSupport
|
||||
?qPerMetre/Q:0.5*qPerMetre/Q;
|
||||
double bestSupportDistanceSquared=std::numeric_limits<double>::infinity();
|
||||
const auto considerSupportSegment=[&](std::ptrdiff_t index)
|
||||
{
|
||||
const double supportCenterQueryQ=qz+playerCenterAboveFloorQ;
|
||||
const auto supportFound=std::lower_bound(passage.controlPoints.begin(),
|
||||
mainEnd,supportCenterQueryQ,
|
||||
[&](const auto& point,double value){return floorProfile(point)>value;});
|
||||
const std::ptrdiff_t supportCenter=supportFound-passage.controlPoints.begin();
|
||||
const std::ptrdiff_t supportBegin=std::max<std::ptrdiff_t>(0,supportCenter-3);
|
||||
const std::ptrdiff_t supportEnd=std::min<std::ptrdiff_t>(lowerMouthIndex-1,supportCenter+2);
|
||||
const auto considerSupportSegment=[&](std::ptrdiff_t index)
|
||||
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
|
||||
const auto& segmentB=passage.controlPoints[static_cast<std::size_t>(index+1)];
|
||||
const double dx=static_cast<double>(segmentB.positionSubvoxels[0])-segmentA.positionSubvoxels[0];
|
||||
const double dy=static_cast<double>(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1];
|
||||
const double radiusA=segmentA.radiusSubvoxels;
|
||||
const double radiusB=segmentB.radiusSubvoxels;
|
||||
// Keep the tread within the probe band and below the capsule's
|
||||
// lowest sampled point.
|
||||
const double floorOffset=0.20;
|
||||
const double horizontalLengthSq=dx*dx+dy*dy;
|
||||
const double supportT=horizontalLengthSq>0.0
|
||||
?std::clamp(((qx-segmentA.positionSubvoxels[0])*dx
|
||||
+(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq,0.0,1.0)
|
||||
:bestT;
|
||||
const double centerX=segmentA.positionSubvoxels[0]+supportT*dx;
|
||||
const double centerY=segmentA.positionSubvoxels[1]+supportT*dy;
|
||||
const double centerZ=segmentA.positionSubvoxels[2]+supportT*(
|
||||
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
|
||||
const double radius=segmentA.radiusSubvoxels+supportT*(
|
||||
static_cast<double>(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels);
|
||||
const double supportPlayerCenterZ=centerZ-radius+playerCenterAboveFloorQ;
|
||||
if(fallbackSupport)
|
||||
{
|
||||
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
|
||||
const auto& segmentB=passage.controlPoints[static_cast<std::size_t>(index+1)];
|
||||
const double dx=static_cast<double>(segmentB.positionSubvoxels[0])-segmentA.positionSubvoxels[0];
|
||||
const double dy=static_cast<double>(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1];
|
||||
const double horizontalLengthSq=dx*dx+dy*dy;
|
||||
const double supportT=horizontalLengthSq>0.0
|
||||
?std::clamp(((qx-segmentA.positionSubvoxels[0])*dx
|
||||
+(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq,0.0,1.0)
|
||||
:bestT;
|
||||
const double supportCenterX=segmentA.positionSubvoxels[0]+supportT*dx;
|
||||
const double supportCenterY=segmentA.positionSubvoxels[1]+supportT*dy;
|
||||
const double supportCenterZ=segmentA.positionSubvoxels[2]+supportT*(
|
||||
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
|
||||
const double supportRadius=segmentA.radiusSubvoxels+supportT*(
|
||||
static_cast<double>(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels);
|
||||
// Leave a 12 cm settle gap below the capsule feet while staying inside
|
||||
// the route support probes' search depth.
|
||||
const double floorQ=supportCenterZ-supportRadius+0.12*qPerMetre;
|
||||
const double floorVoxels=floorQ/Q;
|
||||
const double horizontalX=qx-supportCenterX,horizontalY=qy-supportCenterY;
|
||||
const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX+horizontalY*horizontalY);
|
||||
const double supportHalfWidth=std::max(0.0,supportRadius-0.15*qPerMetre);
|
||||
if(horizontalDistance<=supportHalfWidth&&Z>=floorVoxels-floorThicknessVoxels
|
||||
&&Z<=floorVoxels+1.0/Q)
|
||||
{
|
||||
if(!hasSupportFloor||floorVoxels>bestSupportFloorVoxels)
|
||||
{
|
||||
bestSupportFloorVoxels=floorVoxels;hasSupportFloor=true;
|
||||
bestSupportDensity=std::clamp(Z-floorVoxels,-floorThicknessVoxels,0.0);
|
||||
}
|
||||
}
|
||||
};
|
||||
for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index)
|
||||
considerSupportSegment(index);
|
||||
for(std::ptrdiff_t index=lowerMouthIndex;index<lastIndex;++index)
|
||||
considerSupportSegment(index);
|
||||
}
|
||||
const double supportProfileError=std::abs(
|
||||
supportPlayerCenterZ-nearestPlayerCenterZ);
|
||||
if(supportProfileError>0.06*qPerMetre)return;
|
||||
}
|
||||
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||
const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX
|
||||
+horizontalY*horizontalY);
|
||||
const double floorQ=centerZ-radius+floorOffset*qPerMetre;
|
||||
const double floorVoxels=floorQ/Q;
|
||||
if(Z<floorVoxels-supportThicknessVoxels||Z>floorVoxels+1.0/Q)return;
|
||||
const double supportHalfWidth=fallbackSupport?radius
|
||||
:std::max(0.0,radius+2.0*qPerMetre);
|
||||
if(horizontalDistance>supportHalfWidth)return;
|
||||
const double supportDistanceSq=horizontalDistance*horizontalDistance
|
||||
+(Z-floorVoxels)*(Z-floorVoxels);
|
||||
if(supportDistanceSq>=bestSupportDistanceSquared)return;
|
||||
bestSupportDistanceSquared=supportDistanceSq;
|
||||
hasSupportFloor=true;
|
||||
bestSupportDensity=std::clamp(Z-floorVoxels,
|
||||
-supportThicknessVoxels,0.0);
|
||||
};
|
||||
const std::ptrdiff_t supportBegin=std::max<std::ptrdiff_t>(0,bestIndex-1);
|
||||
const std::ptrdiff_t supportEnd=std::min<std::ptrdiff_t>(lastIndex-1,bestIndex+1);
|
||||
for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index)
|
||||
considerSupportSegment(index);
|
||||
if(hasSupportFloor)
|
||||
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user