wip(assembler): round L37 -- FI mouth carve-fade fix, no raised bridge (style 0); research R1
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
This commit is contained in:
1 parent
8be1c4d9ef
commit
3e443bf81c
33 files changed
+2767
-86
No files matched your search
@@ -252,8 +252,124 @@ namespace VoxelForgeCore
|
||||
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces;
|
||||
// version 20 restores full Gradient amplitude and uses a continuous signed-distance band union;
|
||||
// version 22 overlaps tread sections and smooths FloatingIslands passage-mouth features;
|
||||
// version 23 uses signed-distance support for sparse SurfaceWorld-to-TunnelNetwork treads.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 23;
|
||||
// version 23 uses signed-distance support for sparse SurfaceWorld-to-TunnelNetwork treads;
|
||||
// version 24 narrows passage-mouth support and filters native ground detail;
|
||||
// version 25 joins open-mouth treads to native terrain relief before flattening inward;
|
||||
// version 26 tapers mouth width and bounds the support to the native terrain surface;
|
||||
// version 27 keeps the native blend through the entrance ramp and narrows its filter;
|
||||
// version 28 tapers the open end to a worn path and blends it into walkable support;
|
||||
// version 29 curves the supported and carved path through both mouth shoulders;
|
||||
// version 30 narrows the flat core and blends support through a noisy natural shoulder;
|
||||
// version 31 adds shared, capsule-safe field relief to both open-mouth walking surfaces;
|
||||
// version 32 lowers the mouth core into the bore and removes thin filtered-field remnants;
|
||||
// version 33 fades relief before small detached field remnants;
|
||||
// version 34 keeps the open-mouth tread within native terrain curvature and noise;
|
||||
// version 35 follows the native heightfield farther into the mouth ramp;
|
||||
// version 36 protects the walking line with a gradual filter fade;
|
||||
// version 37 lengthens that fade and restores the last capsule margin;
|
||||
// version 38 keeps native ground at the lip and blends the tread in below it;
|
||||
// version 39 bends the mouth tread across the native field and restores the open bore;
|
||||
// version 40 uses broad route-only filtering and smooth, low-frequency tread relief;
|
||||
// version 41 rounds the mouth tread and removes filter route masks;
|
||||
// version 42 clips the mouth bore at the native walking surface;
|
||||
// version 43 localizes the mouth field filter without masking the surface route;
|
||||
// version 44 widens the natural shoulder blend and follows lower-frequency terrain;
|
||||
// version 45 tests the passage tread independently of route-anchor support;
|
||||
// version 46 fades the native filter broadly along the authored surface path;
|
||||
// version 47 carries the full winding route with a narrow native-height tread;
|
||||
// version 48 fades route support out before the surface mouth;
|
||||
// version 49 keeps the mouth filtered and restores detail on the outer route;
|
||||
// version 50 limits route-field restoration to 65% of the convolution weight;
|
||||
// version 51 narrows the route fade to 8 m horizontal and 6 m vertical;
|
||||
// version 52 evaluates mouth support in positive-density route tiles;
|
||||
// version 53 fades the supported passage tread in from native ground at both mouths;
|
||||
// version 54 tapers mouth-support width and depth;
|
||||
// version 55 blends that tread from native ground and applies route support after carving;
|
||||
// version 56 seats route support at the audited floor;
|
||||
// version 57's raised lip overfilled the mouth;
|
||||
// version 58 restores the native-height lip and explicitly covers route-support queries;
|
||||
// version 59 fixes route-support distance units and lengthens the worn-mouth taper;
|
||||
// version 60 brings the narrow terrain-following route tread in before the outer mouth lip;
|
||||
// version 61 finishes that tread by five metres and preserves its exact walking boundary;
|
||||
// version 62 fades the route tread along the authored path and seats it below the capsule;
|
||||
// version 63 only adds that tread through native-air gaps and fades it over two path metres;
|
||||
// version 64 rounds the path-to-ground join while keeping its top below the capsule;
|
||||
// version 65 tests rounded tread cross-sections and softer native-height following;
|
||||
// version 66 fades the mouth carve and support together so native crater ground forms the lip;
|
||||
// version 67 follows the native route height more closely and completes the entrance fade sooner;
|
||||
// version 68 narrows the flat walk core and eases a shallow worn ramp into the cave floor;
|
||||
// version 69 lets capsule-cleared mouth treads bridge positive-density floor gaps;
|
||||
// version 70 lowers the mouth tread enough to clear the capsule's toe margin;
|
||||
// version 71 keeps its rounded top inside the route's shallow support-probe band;
|
||||
// version 72 adds mouth support only below its signed floor, avoiding capsule-space fill;
|
||||
// version 73 carries the terrain-following route tread to the actual passage mouth;
|
||||
// version 74 seats that short connection in the audited shallow support band;
|
||||
// version 75 keeps the rounded path edge outside the capsule's shallow floor probes;
|
||||
// version 76 carries the endpoint tread through the passage's first support-probe depth;
|
||||
// version 77 joins the mouth floor to native ground through a noisy walk ramp;
|
||||
// version 78 rounds the ramp shoulder and clears its initial capsule envelope;
|
||||
// version 79 tested the unbuilt native-ground mouth;
|
||||
// version 80 narrows the walk tread;
|
||||
// version 81 adds bounded path relief and a wavering, worn shoulder;
|
||||
// version 82 lowers the relief into the tread to keep its capsule clearance;
|
||||
// version 83 increases the natural edge warp and recessed surface variation;
|
||||
// version 84 moves that variation to the actual mouth and restores support width;
|
||||
// version 85 keeps the established lower-mouth capsule profile while shaping entry mouths;
|
||||
// version 86 restricts route reinforcement to native solid and recesses the worn top;
|
||||
// version 87 restores its pre-carve route placement.
|
||||
// version 88 removes the independent mouth cap; the native crater surface forms the entry ramp.
|
||||
// version 89 removes the one-sided entry tread band as well.
|
||||
// version 90 lengthens the entrance floor-carve fade to retain native ground beneath the route.
|
||||
// version 91 lets a narrow mouth support follow the full native surface relief.
|
||||
// version 92 derives mouth floor alignment from the entrance path for first-segment samples too.
|
||||
// version 93 blends the carve against native density so a zero fade really preserves ground.
|
||||
// version 94 applies the mouth floor fade only beneath the walking surface.
|
||||
// version 95 seats retained native ground just below the capsule's foot clearance.
|
||||
// version 96 limits that preservation to the shallow support band, away from inner coils.
|
||||
// version 97 extends the native support band down to the sampled rock envelope.
|
||||
// version 98 raises the support band back to the route floor while keeping it below the capsule.
|
||||
// version 99 restores native-solid route support after the passage carve;
|
||||
// version 100 restores a shallow, noise-shaped entrance tread under its capsule gate;
|
||||
// version 101 preserves the native FI field around the resolved walking line;
|
||||
// version 102 tapers the mouth filter from its 1.5 m core to the same 12.5 m edge;
|
||||
// version 103 blends route protection over a metre-scale shoulder;
|
||||
// version 104 composes overlapping route masks once, by maximum influence;
|
||||
// version 105 widens the route blend to soften the native/filter seam;
|
||||
// version 106 preserves raw negative rock under the route without unfiltering its crown;
|
||||
// version 107 softens that restore and broadens the narrow upper-coil walk core;
|
||||
// version 108 deepens relief and lowers route restoration to soften the remaining crease;
|
||||
// version 109 blended 60% of the two-metre mouth field convolution;
|
||||
// version 110 restores the full filtered contour and widens the upper walk core;
|
||||
// version 111 permits the capsule-cleared signed tread to bridge the upper mouth gap;
|
||||
// version 112 adds a tenth-metre margin to the rounded upper tread edge.
|
||||
// version 139 keeps native ground only in a capsule-scale mouth lane and carries
|
||||
// the seeded terrain contour through the full upper-mouth support;
|
||||
// versions 140-145 tested short-span decks, mouth fade length, route phase and tread SDF;
|
||||
// versions 146-148 tested a sunk chord, near-side phase and all-style FI spiral.
|
||||
// version 149 routes non-spiral FI mouths into a tight near-side ring.
|
||||
// version 150 widened that ring but left a central pillar.
|
||||
// version 151 keeps the ring half a metre outside the passage bore.
|
||||
// version 152 fills capsule-clear positive-density gaps under the narrow mouth tread.
|
||||
// version 153 tested a one-metre entry-turn sink; it left the raised route visible and rippled the floor.
|
||||
// version 154 tightens the FI ring and uses a steeper, still walkable descent to separate hidden coils.
|
||||
// version 155's tighter coil removed the fin but made sharp fragments and route collisions.
|
||||
// version 156 keeps the 3 m ring, overlaps the bore by 0.25 m, and seats the native tread below the capsule.
|
||||
// version 157 joins the near-side FI ring at a tangent so its entrance does not kink across the crater.
|
||||
// version 158 tested a descending entry coil; its exposed canopy is rejected.
|
||||
// version 159 keeps the tangent and uses a steeper sub-15-degree FI descent to separate its coils.
|
||||
// version 160 removed the mouth walk lane; it left the bridge and lost one support sample.
|
||||
// version 161 tapered the anchored entry radius to 3 m; a fin remained at the crater lip.
|
||||
// version 162 disabled native-tread restoration alone; the carve fade still preserved the ridge.
|
||||
// version 163 disabled both passage restorations; it removed the path support.
|
||||
// version 164 narrowed the first two inner FI route points; it enlarged the lip fin.
|
||||
// version 165 disabled native-rock blending around the FI stratum route; the ridge remained.
|
||||
// version 166 disabled the FI mouth support handoff; route support failed on the island approach.
|
||||
// version 167 tested centerline footing on the FI approach; its bridge remained and floor ripple rose.
|
||||
// version 168 removed the connector's 1 m lift; the crater crossing remained exposed.
|
||||
// version 169 sank every connector 2 m; it left four FI route support probes low.
|
||||
// version 170 sinks only connectors touching the anchored root by 1 m.
|
||||
// Version 208 keeps the crater tread scoped to style-zero FI mouths.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 208;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
@@ -447,6 +563,8 @@ namespace VoxelForgeCore
|
||||
std::int64_t anchorIslandMinimumRadiusSubvoxels = 0;
|
||||
std::int64_t anchorRootBottomZSubvoxels = 0;
|
||||
std::int64_t anchorMouthOffsetSubvoxels = 0;
|
||||
// Style-zero mouths follow the sampled island surface into the passage lip.
|
||||
bool terrainFollowingRoute = false;
|
||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
@@ -671,6 +789,8 @@ namespace VoxelForgeCore
|
||||
std::string stableId;
|
||||
std::int32_t upperStrateIndex = -1;
|
||||
std::int32_t lowerStrateIndex = -1;
|
||||
// EVoxelPassageStyle numeric value used by style-specific passage support.
|
||||
std::uint32_t style = 1;
|
||||
std::int64_t placementRadiusSubvoxels = 0;
|
||||
std::array<std::int64_t, 3> upperMouthSubvoxels{};
|
||||
std::array<std::int64_t, 3> lowerMouthSubvoxels{};
|
||||
|
||||
@@ -203,6 +203,7 @@ namespace floating_island_detail
|
||||
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;
|
||||
}
|
||||
@@ -805,9 +806,39 @@ namespace floating_island_detail
|
||||
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
|
||||
? 2.0 * static_cast<double>(edge.thicknessSubvoxels) * (4.0 * t * (1.0 - t))
|
||||
: 0.0;
|
||||
&& 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);
|
||||
@@ -1174,8 +1205,8 @@ namespace floating_island_detail
|
||||
// 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 narrow span exposed to open sky; a smaller fraction buries most of it in the island tops.
|
||||
// Garder la passerelle etroite visible dans le vide ; une fraction plus faible l'enfouit dans les plateaux.
|
||||
// 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;
|
||||
@@ -1183,7 +1214,19 @@ namespace floating_island_detail
|
||||
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 double routeDeckLift = recipe.geometryVersion >= 2 ? 1.0 * Q : 0.0;
|
||||
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)};
|
||||
@@ -1336,6 +1379,7 @@ std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
||||
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)
|
||||
@@ -1495,6 +1539,7 @@ std::string FloatingIslandManifestV1::ToJson() const
|
||||
<< ",\"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
|
||||
@@ -1746,6 +1791,26 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
||||
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];
|
||||
@@ -1762,8 +1827,11 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
||||
routeX=floating_island_detail::RoundQ(mouthX);
|
||||
routeY=floating_island_detail::RoundQ(mouthY);
|
||||
}
|
||||
const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
||||
recipe, manifest.plan, routeX, routeY);
|
||||
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;
|
||||
|
||||
@@ -221,6 +221,7 @@ namespace
|
||||
out<<"{\"stable_id\":"<<LayoutEscape(p.stableId)
|
||||
<<",\"upper_strate_index\":"<<p.upperStrateIndex
|
||||
<<",\"lower_strate_index\":"<<p.lowerStrateIndex
|
||||
<<",\"style\":"<<p.style
|
||||
<<",\"placement_radius_q\":"<<p.placementRadiusSubvoxels
|
||||
<<",\"upper_mouth_q\":["<<p.upperMouthSubvoxels[0]<<','
|
||||
<<p.upperMouthSubvoxels[1]<<','<<p.upperMouthSubvoxels[2]
|
||||
@@ -683,6 +684,8 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
ResolvedLayoutPassageV1 p;p.stableId=LayoutString(v,"stable_id");
|
||||
p.upperStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"upper_strate_index"));
|
||||
p.lowerStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"lower_strate_index"));
|
||||
if(v.Get("style").IsNumber())
|
||||
p.style=static_cast<std::uint32_t>(LayoutInt(v,"style"));
|
||||
p.placementRadiusSubvoxels=LayoutInt(v,"placement_radius_q");
|
||||
p.mouthRadiusSubvoxels=LayoutInt(v,"mouth_radius_q");p.midRadiusSubvoxels=LayoutInt(v,"mid_radius_q");
|
||||
const auto& upper=v.Get("upper_mouth_q").AsArray();
|
||||
@@ -808,6 +811,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
for(std::uint32_t connection=0;connection<connectionCount;++connection)
|
||||
{
|
||||
ResolvedLayoutPassageV1 passage;
|
||||
passage.style=style;
|
||||
passage.stableId="passage/"+std::to_string(upper.strateIndex)+"-"
|
||||
+std::to_string(lower.strateIndex)+"/"+std::to_string(connection);
|
||||
passage.upperStrateIndex=upper.strateIndex;passage.lowerStrateIndex=lower.strateIndex;
|
||||
@@ -886,22 +890,45 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
std::max(ringRadius,clearRingRadius));
|
||||
}
|
||||
if(anchoredIslandPassage)
|
||||
ringRadius=std::min(ringRadius,mouthRadius+qPerMetre/2);
|
||||
{
|
||||
const std::int64_t craterRimRouteRadius=VoxelForgeCore::WorldAssemblyEvaluatorVersion>=185
|
||||
&&style==0&&!anchoredSpiral&&passageAnchor&&passageAnchor->values.size()>3
|
||||
?std::max<std::int64_t>(midRadius,
|
||||
passageAnchor->values[3]+2*qPerMetre)
|
||||
:mouthRadius+qPerMetre/2;
|
||||
ringRadius=std::min(ringRadius,anchoredSpiral
|
||||
?mouthRadius+qPerMetre/2:craterRimRouteRadius);
|
||||
}
|
||||
std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
|
||||
if(hasUpperRoute)
|
||||
{
|
||||
std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
|
||||
const std::uint32_t hashedPhase=phase;
|
||||
const std::int64_t phaseX=style==0&&anchoredIslandPassage
|
||||
&&!anchoredSpiral&&passageAnchor
|
||||
?upperMouth[0]-passageAnchor->values[0]
|
||||
:(hasUpperTangent?upperTangent[0]:upperMouth[0]);
|
||||
const std::int64_t phaseY=style==0&&anchoredIslandPassage
|
||||
&&!anchoredSpiral&&passageAnchor
|
||||
?upperMouth[1]-passageAnchor->values[1]
|
||||
:(hasUpperTangent?upperTangent[1]:upperMouth[1]);
|
||||
for(std::uint32_t offset=0;offset<16;++offset)
|
||||
{
|
||||
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
||||
(hashedPhase+offset)%16);
|
||||
const std::int64_t alignment=(hasUpperTangent?upperTangent[0]:upperMouth[0])
|
||||
*directions[candidate][0]
|
||||
+(hasUpperTangent?upperTangent[1]:upperMouth[1])
|
||||
*directions[candidate][1];
|
||||
if(alignment>bestAlignment)
|
||||
const std::int64_t alignment=phaseX*directions[candidate][0]
|
||||
+phaseY*directions[candidate][1];
|
||||
if(style==0&&anchoredIslandPassage&&!anchoredSpiral&&passageAnchor)
|
||||
{
|
||||
const std::int64_t targetProjection=ringRadius*65536;
|
||||
if(alignment<=0)continue;
|
||||
const std::int64_t tangentError=std::abs(alignment-targetProjection);
|
||||
if(tangentError<bestAlignment
|
||||
||bestAlignment==std::numeric_limits<std::int64_t>::min())
|
||||
{bestAlignment=tangentError;phase=candidate;}
|
||||
}
|
||||
else if(alignment>bestAlignment)
|
||||
{bestAlignment=alignment;phase=candidate;}
|
||||
}
|
||||
}
|
||||
@@ -924,7 +951,18 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
}
|
||||
bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
||||
connection,0x44495245u)&1u)!=0;
|
||||
if(anchoredSpiral&&passageAnchor&&hasUpperTangent)
|
||||
if(style==0&&anchoredIslandPassage&&!anchoredSpiral&&passageAnchor)
|
||||
{
|
||||
const auto& ringDirection=directions[phase];
|
||||
const double radiusX=static_cast<double>(ringRadius)*ringDirection[0]/65536.0;
|
||||
const double radiusY=static_cast<double>(ringRadius)*ringDirection[1]/65536.0;
|
||||
const double anchorX=static_cast<double>(passageAnchor->values[0]);
|
||||
const double anchorY=static_cast<double>(passageAnchor->values[1]);
|
||||
const double connectorX=anchorX+radiusX-upperMouth[0];
|
||||
const double connectorY=anchorY+radiusY-upperMouth[1];
|
||||
clockwise=connectorX*(-radiusY)+connectorY*radiusX>=0.0;
|
||||
}
|
||||
else 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]);
|
||||
@@ -1068,7 +1106,8 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
hasAnchorHelix=helixEndIndex>0;
|
||||
}
|
||||
const std::int64_t routeGradeQ16=hasAnchorHelix
|
||||
&&lower.generatorKind=="SurfaceWorld"?8000:TargetGradeQ16;
|
||||
&&lower.generatorKind=="SurfaceWorld"?8000
|
||||
:style==0&&anchoredIslandPassage&&!anchoredSpiral?15500:TargetGradeQ16;
|
||||
const std::array<std::int64_t,2> firstRing=ringPoint(phase);
|
||||
if(hasAnchorHelix&&pathStyle==1u)
|
||||
{
|
||||
@@ -1168,7 +1207,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
pushXY(ringPoint(lowerPhase));
|
||||
}
|
||||
pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath();
|
||||
const std::size_t lowerMouthIndex=xy.size()-1;
|
||||
std::size_t lowerMouthIndex=xy.size()-1;
|
||||
std::vector<std::array<std::int64_t,3>> lowerLandingPoints;
|
||||
if(hasLowerTangent)
|
||||
{
|
||||
@@ -1202,6 +1241,27 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
if(lowerLandingPoints.empty())
|
||||
{error=passage.stableId+" lower route has no landing length inside PassageConfig.DistanceMax";return false;}
|
||||
}
|
||||
if(VoxelForgeCore::WorldAssemblyEvaluatorVersion>=188
|
||||
&&style==0&&anchoredIslandPassage&&!anchoredSpiral&&xy.size()>4)
|
||||
{
|
||||
std::vector<std::array<std::int64_t,2>> refined;
|
||||
refined.reserve(xy.size()*2);
|
||||
for(std::size_t index=0;index+1<xy.size();++index)
|
||||
{
|
||||
refined.push_back(xy[index]);
|
||||
if(index<3)continue;
|
||||
refined.push_back({
|
||||
static_cast<std::int64_t>(std::llround(0.5*(
|
||||
static_cast<double>(xy[index][0])
|
||||
+static_cast<double>(xy[index+1][0])))),
|
||||
static_cast<std::int64_t>(std::llround(0.5*(
|
||||
static_cast<double>(xy[index][1])
|
||||
+static_cast<double>(xy[index+1][1]))))});
|
||||
}
|
||||
refined.push_back(xy.back());
|
||||
if(lowerMouthIndex>3)lowerMouthIndex+=lowerMouthIndex-3;
|
||||
xy.swap(refined);
|
||||
}
|
||||
totalHorizontal=measurePath();
|
||||
if(loops>=256||totalHorizontal==0)
|
||||
{error=passage.stableId+" could not fit a supported grade inside the resolved placement radius";return false;}
|
||||
|
||||
@@ -94,6 +94,9 @@ public:
|
||||
}
|
||||
bool hasMouthSupport(std::size_t passageIndex) const
|
||||
{return passageIndex<passageMouthSupports_.size()&&passageMouthSupports_[passageIndex]!=0;}
|
||||
bool hasAnyMouthSupport() const
|
||||
{return std::any_of(passageMouthSupports_.begin(),passageMouthSupports_.end(),
|
||||
[](std::uint8_t value){return value!=0;});}
|
||||
bool hasRouteFeatureOverlap() const{return routeFeatureOverlap_;}
|
||||
bool hasColumnZOverlap() const{return columnZOverlap_;}
|
||||
bool mayContainResolvedBoundaryBand() const{return mayContainResolvedBoundaryBand_;}
|
||||
@@ -683,23 +686,36 @@ public:
|
||||
{
|
||||
if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X)
|
||||
||!std::isfinite(Y)||!std::isfinite(Z))return density;
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
const double qPerMetre=Q*1000000.0/
|
||||
std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,
|
||||
qz=static_cast<double>(Z)*Q;
|
||||
const auto* passageTileQueries=ActivePassageTileQueries;
|
||||
const bool passageTileScopeMatches=passageTileQueries
|
||||
&&passageTileQueries->matches(*this);
|
||||
bool queryNearUpperMouthSupport=false;
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
const double dx=qx-static_cast<double>(passage.upperMouthSubvoxels[0]);
|
||||
const double dy=qy-static_cast<double>(passage.upperMouthSubvoxels[1]);
|
||||
const double dz=qz-static_cast<double>(passage.upperMouthSubvoxels[2]);
|
||||
const double reach=15.0*qPerMetre;
|
||||
if(dx*dx+dy*dy<=reach*reach&&std::abs(dz)<=8.0*qPerMetre)
|
||||
{queryNearUpperMouthSupport=true;break;}
|
||||
}
|
||||
if(passageTileScopeMatches&&density>=0.0f
|
||||
&&!passageTileQueries->hasRouteFeatureOverlap()
|
||||
&&!passageTileQueries->hasColumnZOverlap())return density;
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,qz=static_cast<double>(Z)*Q;
|
||||
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
&&!passageTileQueries->hasColumnZOverlap()
|
||||
&&!passageTileQueries->hasAnyMouthSupport()
|
||||
&&!queryNearUpperMouthSupport)return density;
|
||||
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)
|
||||
{
|
||||
// Support is allowed to reinforce native solid terrain only. Without this
|
||||
// guard the route-point tread stamps a floating shelf into an open stratum.
|
||||
// Surface-route treads may reinforce native solid terrain only.
|
||||
if(!(density<0.0f))return;
|
||||
const double mouthDx=qx-static_cast<double>(mouth[0]);
|
||||
const double mouthDy=qy-static_cast<double>(mouth[1]);
|
||||
@@ -727,7 +743,7 @@ public:
|
||||
}
|
||||
if(routeCount<2)return;
|
||||
const std::size_t lastSegment=routeCount-1;
|
||||
constexpr double MouthSupportBlendMetres=1.5;
|
||||
constexpr double MouthSupportBlendMetres=0.45;
|
||||
const double mouthBlendWidth=MouthSupportBlendMetres*qPerMetre;
|
||||
double mouthSupportDistance=std::numeric_limits<double>::infinity();
|
||||
for(std::size_t index=0;index<lastSegment;++index)
|
||||
@@ -742,11 +758,13 @@ public:
|
||||
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 halfWidth=std::clamp(routeRadius,
|
||||
0.42*qPerMetre,0.56*qPerMetre);
|
||||
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||
const double horizontalDistance=DeterministicSqrt(
|
||||
horizontalX*horizontalX+horizontalY*horizontalY);
|
||||
const double floorQ=centerZ-1.20*qPerMetre;
|
||||
const double floorQ=centerZ-(VoxelForgeCore::WorldAssemblyEvaluatorVersion>=184
|
||||
?playerCenterAboveFloorQ+0.25*qPerMetre:1.20*qPerMetre);
|
||||
const double sideDistance=horizontalDistance-halfWidth;
|
||||
const double topDistance=qz-floorQ;
|
||||
const double bottomDistance=floorQ-qPerMetre-qz;
|
||||
@@ -756,25 +774,14 @@ public:
|
||||
std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
|
||||
+std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0));
|
||||
const double inside=std::min(std::max(sideDistance,verticalDistance),0.0);
|
||||
const double signedDistance=outside+inside;
|
||||
mouthSupportDistance=std::min(mouthSupportDistance,signedDistance);
|
||||
mouthSupportDistance=std::min(mouthSupportDistance,outside+inside);
|
||||
}
|
||||
if(std::isfinite(mouthSupportDistance))
|
||||
composed=SmoothMinDistance(composed,
|
||||
static_cast<float>(mouthSupportDistance/Q),
|
||||
static_cast<float>(mouthBlendWidth/Q));
|
||||
};
|
||||
// Keep the native surface route connected to the entrance. Passage carving
|
||||
// then removes only the parts that overlap the tunnel, whose own tread owns
|
||||
// the interior ramp.
|
||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
const bool tileHasSupport=passageTileScopeMatches;
|
||||
if(!tileHasSupport||ActivePassageTileQueries->hasMouthSupport(passageIndex))
|
||||
composeRouteMouthSupport(passage.upperStrateIndex,
|
||||
passage.upperMouthSubvoxels,true);
|
||||
}
|
||||
// The connected passage tread continues the surface route into the tunnel.
|
||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
@@ -789,6 +796,8 @@ public:
|
||||
};
|
||||
const auto* supportUpper=supportStratum(passage.upperStrateIndex);
|
||||
const auto* supportLower=supportStratum(passage.lowerStrateIndex);
|
||||
const bool floatingIslandPassage=supportUpper
|
||||
&&supportUpper->generatorKind=="FloatingIslands";
|
||||
const bool surfaceToTunnelSupport=supportUpper&&supportLower
|
||||
&&supportUpper->generatorKind=="SurfaceWorld"
|
||||
&&supportLower->generatorKind=="TunnelNetwork";
|
||||
@@ -907,8 +916,11 @@ public:
|
||||
std::ptrdiff_t bestScanIndex=std::numeric_limits<std::ptrdiff_t>::max();
|
||||
std::ptrdiff_t bestSupportSegmentIndex=0;
|
||||
bool hasSupportFloor=false;
|
||||
bool bestSupportIsMouthTread=false;
|
||||
double bestSupportMouthBlend=1.0;
|
||||
double bestSupportDistanceSquared=std::numeric_limits<double>::infinity();
|
||||
double bestSupportDensity=std::numeric_limits<double>::infinity();
|
||||
double bestSupportNativeBlend=0.0;
|
||||
double bestSupportFloorQ=0.0;
|
||||
bool bestSupportUsesSignedDistance=false;
|
||||
std::ptrdiff_t selectedSupportSegmentIndex=-1;
|
||||
@@ -1032,18 +1044,175 @@ public:
|
||||
/distanceToCenter,0.0,1.0):0.0;
|
||||
const float relief=VF_FieldPassageWallRelief(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),static_cast<std::uint32_t>(Owner?Owner->Seed:0));
|
||||
const float mouthSurfaceRelief=VF_FieldSurfaceRelief(static_cast<float>(X)*1.5f,
|
||||
static_cast<float>(Y)*1.5f,static_cast<float>(Z)*1.5f,
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u);
|
||||
const float mouthTerrainRelief=VF_FieldSurfaceRelief(static_cast<float>(X)*1.5f,
|
||||
static_cast<float>(Y)*1.5f,static_cast<float>(Z)*1.5f,
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u);
|
||||
const float mouthTreadFieldRelief=VF_FieldSurfaceRelief(static_cast<float>(X)*0.75f,
|
||||
static_cast<float>(Y)*0.75f,static_cast<float>(Z)*0.75f,
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x54485244u);
|
||||
const auto routeSegmentLengthMetres=[&](std::ptrdiff_t routeIndex)
|
||||
{
|
||||
const auto& before=passage.controlPoints[static_cast<std::size_t>(routeIndex)];
|
||||
const auto& after=passage.controlPoints[static_cast<std::size_t>(routeIndex+1)];
|
||||
const double dx=static_cast<double>(after.positionSubvoxels[0])
|
||||
-before.positionSubvoxels[0];
|
||||
const double dy=static_cast<double>(after.positionSubvoxels[1])
|
||||
-before.positionSubvoxels[1];
|
||||
const double dz=static_cast<double>(after.positionSubvoxels[2])
|
||||
-before.positionSubvoxels[2];
|
||||
return DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
||||
};
|
||||
double distanceFromOpenMouth=std::numeric_limits<double>::infinity();
|
||||
if(bestIndex<=2)
|
||||
{
|
||||
distanceFromOpenMouth=bestT*routeSegmentLengthMetres(bestIndex);
|
||||
for(std::ptrdiff_t routeIndex=0;routeIndex<bestIndex;++routeIndex)
|
||||
distanceFromOpenMouth+=routeSegmentLengthMetres(routeIndex);
|
||||
}
|
||||
else if(bestIndex>=lowerMouthIndex-2&&bestIndex<=lowerMouthIndex+2)
|
||||
{
|
||||
if(bestIndex<lowerMouthIndex)
|
||||
{
|
||||
distanceFromOpenMouth=(1.0-bestT)*routeSegmentLengthMetres(bestIndex);
|
||||
for(std::ptrdiff_t routeIndex=bestIndex+1;
|
||||
routeIndex<lowerMouthIndex;++routeIndex)
|
||||
distanceFromOpenMouth+=routeSegmentLengthMetres(routeIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
distanceFromOpenMouth=bestT*routeSegmentLengthMetres(bestIndex);
|
||||
for(std::ptrdiff_t routeIndex=lowerMouthIndex;routeIndex<bestIndex;++routeIndex)
|
||||
distanceFromOpenMouth+=routeSegmentLengthMetres(routeIndex);
|
||||
}
|
||||
}
|
||||
double mouthEntryFloorAlignment=0.0;
|
||||
double mouthEntryPlanDistance=std::numeric_limits<double>::infinity();
|
||||
double mouthEntryFloorQ=0.0;
|
||||
bool upperMouthFloorFade=false;
|
||||
{
|
||||
// The large mouth bore overlaps several inner coils. Its global
|
||||
// nearest SDF can belong to a later turn even at the lip, so use the
|
||||
// projected entrance path to recover the local floor orientation.
|
||||
double prefixMetres=0.0;
|
||||
double nearestEntryPlanDistance=std::numeric_limits<double>::infinity();
|
||||
double projectedEntryDistance=std::numeric_limits<double>::infinity();
|
||||
for(std::ptrdiff_t routeIndex=0;
|
||||
routeIndex<std::min<std::ptrdiff_t>(3,lowerMouthIndex);++routeIndex)
|
||||
{
|
||||
const auto& before=passage.controlPoints[static_cast<std::size_t>(routeIndex)];
|
||||
const auto& after=passage.controlPoints[static_cast<std::size_t>(routeIndex+1)];
|
||||
const double ax=static_cast<double>(before.positionSubvoxels[0]);
|
||||
const double ay=static_cast<double>(before.positionSubvoxels[1]);
|
||||
const double az=static_cast<double>(before.positionSubvoxels[2]);
|
||||
const double dx=static_cast<double>(after.positionSubvoxels[0])-ax;
|
||||
const double dy=static_cast<double>(after.positionSubvoxels[1])-ay;
|
||||
const double dz=static_cast<double>(after.positionSubvoxels[2])-az;
|
||||
const double horizontalLengthSq=dx*dx+dy*dy;
|
||||
const double t=horizontalLengthSq>0.0?std::clamp(
|
||||
((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0;
|
||||
double projectedX=ax+t*dx,projectedY=ay+t*dy;
|
||||
const double horizontalLength=DeterministicSqrt(horizontalLengthSq);
|
||||
if(horizontalLength>1.0e-6)
|
||||
{
|
||||
const double pathRelief=std::clamp(static_cast<double>(
|
||||
VF_FieldSurfaceRelief(static_cast<float>(projectedX/qPerMetre*1.5),
|
||||
static_cast<float>(projectedY/qPerMetre*1.5),
|
||||
static_cast<float>((az+t*dz)/qPerMetre*1.5),
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u)),
|
||||
-1.0,1.0);
|
||||
const double lateralOffset=pathRelief*0.18*qPerMetre;
|
||||
projectedX-=dy/horizontalLength*lateralOffset;
|
||||
projectedY+=dx/horizontalLength*lateralOffset;
|
||||
}
|
||||
const double planDx=qx-projectedX,planDy=qy-projectedY;
|
||||
const double planDistance=DeterministicSqrt(planDx*planDx+planDy*planDy);
|
||||
if(planDistance<nearestEntryPlanDistance)
|
||||
{
|
||||
nearestEntryPlanDistance=planDistance;
|
||||
const double centerZ=az+t*dz;
|
||||
const double radius=before.radiusSubvoxels+t*(
|
||||
static_cast<double>(after.radiusSubvoxels)-before.radiusSubvoxels);
|
||||
mouthEntryFloorQ=centerZ-radius+0.20*qPerMetre;
|
||||
const double centerDistance=DeterministicSqrt(planDx*planDx
|
||||
+planDy*planDy+(qz-centerZ)*(qz-centerZ));
|
||||
mouthEntryFloorAlignment=centerDistance>1.0e-9
|
||||
?std::clamp(-(qz-centerZ)/centerDistance,0.0,1.0):0.0;
|
||||
projectedEntryDistance=prefixMetres
|
||||
+t*routeSegmentLengthMetres(routeIndex);
|
||||
}
|
||||
prefixMetres+=routeSegmentLengthMetres(routeIndex);
|
||||
}
|
||||
if(nearestEntryPlanDistance<=2.0*qPerMetre
|
||||
&&projectedEntryDistance<=8.0
|
||||
&&std::abs(qz-mouthEntryFloorQ)<=4.0*qPerMetre)
|
||||
{
|
||||
distanceFromOpenMouth=projectedEntryDistance;
|
||||
mouthEntryPlanDistance=nearestEntryPlanDistance/qPerMetre;
|
||||
upperMouthFloorFade=true;
|
||||
}
|
||||
}
|
||||
double mouthFloorBlend=0.0;
|
||||
if(std::isfinite(distanceFromOpenMouth))
|
||||
{
|
||||
const double fade=std::clamp(distanceFromOpenMouth/11.0,0.0,1.0);
|
||||
const double smooth=fade*fade*(3.0-2.0*fade);
|
||||
mouthFloorBlend=1.0-smooth;
|
||||
}
|
||||
const double floorReliefT=std::clamp((verticalWallAlignment-0.55)/0.45,0.0,1.0);
|
||||
const double floorReliefWeight=floorReliefT*floorReliefT
|
||||
*(3.0-2.0*floorReliefT);
|
||||
const double floorReliefOffset=std::clamp(
|
||||
static_cast<double>(mouthTerrainRelief),-1.0,1.0)
|
||||
*0.20*qPerMetre*mouthFloorBlend*floorReliefWeight
|
||||
*(upperMouthFloorFade?1.0:0.0);
|
||||
const double wallReliefWeight=0.05+0.95*(1.0-verticalWallAlignment);
|
||||
const double roughSignedDistance=bestSignedDistance
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight;
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight
|
||||
+floorReliefOffset;
|
||||
if(IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(bestIndex)))
|
||||
{
|
||||
const double bridgeDistance=PassageBridgeSignedDistance(passageIndex,
|
||||
static_cast<std::size_t>(bestIndex),X,Y,Z);
|
||||
composed=std::min(composed,static_cast<float>(bridgeDistance/Q));
|
||||
}
|
||||
// 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));
|
||||
// Leave native crater ground at the lip. The bore opens through the walls
|
||||
// immediately, while its floor cut eases in along the passage centerline.
|
||||
double mouthCarveFade=1.0;
|
||||
if(upperMouthFloorFade)
|
||||
{
|
||||
constexpr double mouthCarveFadeMetres=8.0;
|
||||
mouthCarveFade=std::clamp(distanceFromOpenMouth/mouthCarveFadeMetres,
|
||||
0.0,1.0);
|
||||
mouthCarveFade=mouthCarveFade*mouthCarveFade*(3.0-2.0*mouthCarveFade);
|
||||
}
|
||||
const double floorAlignment=std::max(verticalWallAlignment,
|
||||
mouthEntryFloorAlignment)*std::max(verticalWallAlignment,
|
||||
mouthEntryFloorAlignment);
|
||||
double floorBelowWeight=0.0;
|
||||
if(upperMouthFloorFade)
|
||||
{
|
||||
const double belowFloor=(mouthEntryFloorQ-qz)/qPerMetre;
|
||||
const double supportRamp=std::clamp(belowFloor/0.15,0.0,1.0);
|
||||
const double supportFade=std::clamp((1.0-belowFloor)/0.35,0.0,1.0);
|
||||
const double rampSmooth=supportRamp*supportRamp*(3.0-2.0*supportRamp);
|
||||
const double fadeSmooth=supportFade*supportFade*(3.0-2.0*supportFade);
|
||||
floorBelowWeight=rampSmooth*fadeSmooth;
|
||||
}
|
||||
// Preserve native terrain only along a capsule-scale worn lane. The
|
||||
// former bore-wide alignment feather left a straight platform-width
|
||||
// bridge across the FI mouth.
|
||||
double mouthWalkLaneT=std::clamp((1.05-mouthEntryPlanDistance)/0.60,0.0,1.0);
|
||||
mouthWalkLaneT=mouthWalkLaneT*mouthWalkLaneT*(3.0-2.0*mouthWalkLaneT);
|
||||
const double mouthWalkLaneWeight=upperMouthFloorFade?mouthWalkLaneT:0.0;
|
||||
const double floorCarveWeight=1.0-(1.0-mouthCarveFade)
|
||||
*floorAlignment*floorAlignment*floorBelowWeight*mouthWalkLaneWeight;
|
||||
const float fullyCarvedDensity=std::max(composed,
|
||||
static_cast<float>(-roughSignedDistance/Q));
|
||||
composed=std::lerp(composed,fullyCarvedDensity,
|
||||
static_cast<float>(floorCarveWeight));
|
||||
const auto considerSupportSegment=[&](std::ptrdiff_t index)
|
||||
{
|
||||
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
|
||||
@@ -1080,8 +1249,8 @@ public:
|
||||
?std::min(0.5,2.0*qPerMetre/DeterministicSqrt(horizontalLengthSq)):0.0;
|
||||
const double supportT=horizontalLengthSq>0.0
|
||||
?std::clamp(projection,-overlapFraction,1.0+overlapFraction):bestT;
|
||||
const double centerX=segmentA.positionSubvoxels[0]+supportT*dx;
|
||||
const double centerY=segmentA.positionSubvoxels[1]+supportT*dy;
|
||||
double centerX=segmentA.positionSubvoxels[0]+supportT*dx;
|
||||
double centerY=segmentA.positionSubvoxels[1]+supportT*dy;
|
||||
double centerZ=segmentA.positionSubvoxels[2]+supportT*(
|
||||
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
|
||||
double radius=segmentA.radiusSubvoxels+supportT*(
|
||||
@@ -1105,6 +1274,25 @@ public:
|
||||
segmentB.radiusSubvoxels,p3.radiusSubvoxels)-radius)
|
||||
*SmoothSupportHeightWeight;
|
||||
}
|
||||
double treadRelief=0.0;
|
||||
if(entryMouthShoulder
|
||||
||(index>=lowerMouthIndex-2&&index<=lowerMouthIndex+2))
|
||||
{
|
||||
const double horizontalLength=DeterministicSqrt(horizontalLengthSq);
|
||||
if(horizontalLength>1.0e-6)
|
||||
{
|
||||
const double pathRelief=std::clamp(static_cast<double>(
|
||||
VF_FieldSurfaceRelief(static_cast<float>(centerX/qPerMetre*1.5),
|
||||
static_cast<float>(centerY/qPerMetre*1.5),
|
||||
static_cast<float>(centerZ/qPerMetre*1.5),
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x4D4F5554u)),
|
||||
-1.0,1.0);
|
||||
treadRelief=pathRelief;
|
||||
const double lateralOffset=pathRelief*0.18*qPerMetre;
|
||||
centerX-=dy/horizontalLength*lateralOffset;
|
||||
centerY+=dx/horizontalLength*lateralOffset;
|
||||
}
|
||||
}
|
||||
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||
const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX
|
||||
+horizontalY*horizontalY);
|
||||
@@ -1117,7 +1305,9 @@ public:
|
||||
// dense routes retain the legacy one-sided tread density.
|
||||
const double supportSegmentLength=DeterministicSqrt(
|
||||
horizontalLengthSq+dz*dz)/qPerMetre;
|
||||
const bool sparseRouteSupport=supportSegmentLength>1.5;
|
||||
const double sparseRouteThreshold=VoxelForgeCore::WorldAssemblyEvaluatorVersion>=188
|
||||
&&floatingIslandPassage&&passage.style==0?1.0:1.5;
|
||||
const bool sparseRouteSupport=supportSegmentLength>sparseRouteThreshold;
|
||||
const double minimumSupportQueryDepth=index==selectedSupportSegmentIndex
|
||||
&&sparseRouteSupport?0.65:0.93;
|
||||
if(supportQueryDepth<minimumSupportQueryDepth*qPerMetre
|
||||
@@ -1130,15 +1320,75 @@ public:
|
||||
// capsule on the neighbouring turn.
|
||||
const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2
|
||||
&&supportQueryDepth<0.98*qPerMetre;
|
||||
const double supportHalfWidthMetres=lowerMouthBodyOverlap?0.90
|
||||
:entryMouthShoulder?1.20:1.25;
|
||||
const double supportHalfWidth=std::min(radius,
|
||||
// Keep a narrow walk core, but place its top on the native surface
|
||||
// so the support remains a worn ramp instead of a route-height slab.
|
||||
const bool mouthTread=entryMouthShoulder
|
||||
&&(!floatingIslandPassage||passage.style==0);
|
||||
double distanceFromMouthMetres=std::numeric_limits<double>::infinity();
|
||||
if(entryMouthShoulder)
|
||||
{
|
||||
double prefixMetres=0.0;
|
||||
for(std::ptrdiff_t routeIndex=0;routeIndex<index;++routeIndex)
|
||||
{
|
||||
const auto& before=passage.controlPoints[
|
||||
static_cast<std::size_t>(routeIndex)];
|
||||
const auto& after=passage.controlPoints[
|
||||
static_cast<std::size_t>(routeIndex+1)];
|
||||
const double dx=static_cast<double>(after.positionSubvoxels[0])
|
||||
-before.positionSubvoxels[0];
|
||||
const double dy=static_cast<double>(after.positionSubvoxels[1])
|
||||
-before.positionSubvoxels[1];
|
||||
const double dz=static_cast<double>(after.positionSubvoxels[2])
|
||||
-before.positionSubvoxels[2];
|
||||
prefixMetres+=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
||||
}
|
||||
distanceFromMouthMetres=std::max(0.0,
|
||||
prefixMetres+supportT*supportSegmentLength);
|
||||
}
|
||||
else if(lowerMouthBodyOverlap)
|
||||
{
|
||||
double suffixMetres=0.0;
|
||||
for(std::ptrdiff_t routeIndex=index+1;
|
||||
routeIndex<lowerMouthIndex;++routeIndex)
|
||||
{
|
||||
const auto& before=passage.controlPoints[
|
||||
static_cast<std::size_t>(routeIndex)];
|
||||
const auto& after=passage.controlPoints[
|
||||
static_cast<std::size_t>(routeIndex+1)];
|
||||
const double dx=static_cast<double>(after.positionSubvoxels[0])
|
||||
-before.positionSubvoxels[0];
|
||||
const double dy=static_cast<double>(after.positionSubvoxels[1])
|
||||
-before.positionSubvoxels[1];
|
||||
const double dz=static_cast<double>(after.positionSubvoxels[2])
|
||||
-before.positionSubvoxels[2];
|
||||
suffixMetres+=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
||||
}
|
||||
distanceFromMouthMetres=std::max(0.0,
|
||||
suffixMetres+(1.0-supportT)*supportSegmentLength);
|
||||
}
|
||||
double mouthReliefT=std::clamp(distanceFromMouthMetres/8.0,0.0,1.0);
|
||||
mouthReliefT=mouthReliefT*mouthReliefT*(3.0-2.0*mouthReliefT);
|
||||
double mouthSupportT=std::clamp((distanceFromMouthMetres-0.25)/4.0,0.0,1.0);
|
||||
mouthSupportT=mouthSupportT*mouthSupportT*(3.0-2.0*mouthSupportT);
|
||||
const double mouthSupportWidth=std::lerp(0.82,1.00,mouthSupportT);
|
||||
const double supportHalfWidthMetres=mouthTread?mouthSupportWidth
|
||||
:lowerMouthBodyOverlap?0.90:1.25;
|
||||
double supportHalfWidth=std::min(radius,
|
||||
supportHalfWidthMetres*qPerMetre);
|
||||
// Keep the walk core within a 0.50 m span with shallow relief, then
|
||||
// taper the outer shoulder; capsule and probe widths remain supported.
|
||||
const double supportEdgeCoreMetres=entryMouthShoulder?0.60:0.90;
|
||||
const double shoulderWarp=mouthTread
|
||||
?std::clamp(static_cast<double>(mouthSurfaceRelief),-1.0,1.0)
|
||||
*0.12*qPerMetre:0.0;
|
||||
supportHalfWidth=mouthTread
|
||||
?std::clamp(supportHalfWidth+shoulderWarp,
|
||||
std::lerp(0.52,0.62,mouthSupportT)*qPerMetre,
|
||||
std::lerp(0.92,1.10,mouthSupportT)*qPerMetre)
|
||||
:std::max(0.0,supportHalfWidth+shoulderWarp);
|
||||
// Keep the walk core within the player footprint, then ease the outer
|
||||
// edge into native ground over a long enough shoulder to avoid a berm.
|
||||
const double supportEdgeCoreMetres=mouthTread?0.08
|
||||
:entryMouthShoulder?0.60:0.90;
|
||||
const double supportEdgeBlendMetres=std::min(
|
||||
entryMouthShoulder?0.85:0.35,
|
||||
mouthTread?1.40:entryMouthShoulder?0.85:0.35,
|
||||
std::max(0.0,supportHalfWidth/qPerMetre-supportEdgeCoreMetres));
|
||||
const double supportEdgeT=supportEdgeBlendMetres>1.0e-6
|
||||
?std::clamp((horizontalDistance
|
||||
@@ -1146,25 +1396,57 @@ public:
|
||||
/(supportEdgeBlendMetres*qPerMetre),0.0,1.0):0.0;
|
||||
const double supportEdgeSmooth=supportEdgeT*supportEdgeT
|
||||
*(3.0-2.0*supportEdgeT);
|
||||
const double mouthSupportThicknessVoxels=std::lerp(
|
||||
0.35*supportThicknessVoxels,supportThicknessVoxels,mouthSupportT);
|
||||
const double localSupportThicknessVoxels=entryMouthShoulder
|
||||
?supportThicknessVoxels*(1.0-0.72*supportEdgeSmooth)
|
||||
?mouthSupportThicknessVoxels*(1.0-0.72*supportEdgeSmooth)
|
||||
:supportThicknessVoxels;
|
||||
constexpr double supportEdgeDepth=0.35;
|
||||
const double floorQ=centerZ-radius+floorOffset*qPerMetre
|
||||
double mouthSupportBlend=1.0;
|
||||
if(mouthTread&&std::isfinite(distanceFromMouthMetres))
|
||||
{
|
||||
const double mouthBlendT=std::clamp(
|
||||
distanceFromMouthMetres/0.75,0.0,1.0);
|
||||
mouthSupportBlend=0.75+0.25*mouthBlendT*mouthBlendT
|
||||
*(3.0-2.0*mouthBlendT);
|
||||
}
|
||||
const double supportEdgeDepth=mouthTread?0.25:0.35;
|
||||
const double mouthFloorOffset=mouthTread
|
||||
?std::lerp(0.15,0.19,lowerMouthBlend):floorOffset;
|
||||
const double baseFloorQ=centerZ-radius+mouthFloorOffset*qPerMetre
|
||||
-supportEdgeSmooth*supportEdgeDepth*qPerMetre;
|
||||
const double nativeSurfaceQ=static_cast<double>(qz)
|
||||
-static_cast<double>(density)*Q;
|
||||
// Keep support near the route floor; visible mouth preservation below
|
||||
// blends the source field directly instead of extending this SDF top.
|
||||
const double nativeFieldOffset=std::clamp(nativeSurfaceQ-baseFloorQ,
|
||||
-0.80*qPerMetre,0.80*qPerMetre);
|
||||
const double nativeSurfaceBlend=mouthTread?1.0:1.0-mouthReliefT;
|
||||
const double nativeTreadOffset=std::clamp(
|
||||
nativeFieldOffset*nativeSurfaceBlend+floorReliefOffset
|
||||
-std::abs(treadRelief)*0.08*qPerMetre
|
||||
*(1.0-0.25*mouthReliefT),
|
||||
-0.80*qPerMetre,0.80*qPerMetre);
|
||||
const double floorQ=baseFloorQ
|
||||
+(mouthTread?std::clamp(nativeTreadOffset
|
||||
+std::clamp(static_cast<double>(mouthTreadFieldRelief),-1.0,1.0)
|
||||
*0.40*qPerMetre,-0.80*qPerMetre,0.0):0.0);
|
||||
const double floorVoxels=floorQ/Q;
|
||||
const double supportPositiveRangeVoxels=sparseRouteSupport
|
||||
const bool signedDistanceSupport=sparseRouteSupport||mouthTread;
|
||||
const double supportPositiveRangeVoxels=signedDistanceSupport
|
||||
?0.35*qPerMetre/Q:1.0/Q;
|
||||
if(Z<floorVoxels-localSupportThicknessVoxels
|
||||
||Z>floorVoxels+supportPositiveRangeVoxels)return;
|
||||
if(horizontalDistance>supportHalfWidth)return;
|
||||
const double supportQueryHalfWidth=supportHalfWidth
|
||||
+(mouthTread?0.45*qPerMetre:0.0);
|
||||
if(horizontalDistance>supportQueryHalfWidth)return;
|
||||
const double supportDistanceSquared=horizontalDistance*horizontalDistance
|
||||
+(Z-floorVoxels)*(Z-floorVoxels);
|
||||
if(supportDistanceSquared>=bestSupportDistanceSquared)return;
|
||||
bestSupportDistanceSquared=supportDistanceSquared;
|
||||
hasSupportFloor=true;
|
||||
bestSupportSegmentIndex=index;
|
||||
if(sparseRouteSupport)
|
||||
bestSupportIsMouthTread=mouthTread;
|
||||
if(signedDistanceSupport)
|
||||
{
|
||||
// Sparse support profiles need both sides of a signed distance
|
||||
// so the sampled zero crossing stays on the tread top.
|
||||
@@ -1172,11 +1454,10 @@ public:
|
||||
const double bottomDistance=floorQ-localSupportThicknessVoxels*Q-qz;
|
||||
const double verticalDistance=std::max(topDistance,bottomDistance);
|
||||
const double sideDistance=horizontalDistance-supportHalfWidth;
|
||||
const double outside=DeterministicSqrt(
|
||||
std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
|
||||
+std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0));
|
||||
const double inside=std::min(std::max(sideDistance,verticalDistance),0.0);
|
||||
bestSupportDensity=(outside+inside)/Q;
|
||||
bestSupportDensity=SmoothMaxDistance(
|
||||
static_cast<float>(sideDistance/Q),
|
||||
static_cast<float>(verticalDistance/Q),
|
||||
entryMouthShoulder?0.80f:0.20f);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1184,7 +1465,10 @@ public:
|
||||
-localSupportThicknessVoxels,0.0);
|
||||
}
|
||||
bestSupportFloorQ=floorQ;
|
||||
bestSupportUsesSignedDistance=sparseRouteSupport;
|
||||
bestSupportUsesSignedDistance=signedDistanceSupport;
|
||||
bestSupportMouthBlend=mouthSupportBlend;
|
||||
bestSupportNativeBlend=mouthTread
|
||||
?0.85*mouthSupportBlend*(1.0-supportEdgeSmooth):0.0;
|
||||
};
|
||||
std::ptrdiff_t supportCenterIndex=bestIndex;
|
||||
if(bestIndex<lowerMouthIndex)
|
||||
@@ -1231,12 +1515,46 @@ public:
|
||||
// Outside bridge and short-span segments, add the tread only where the
|
||||
// native field already provides a solid envelope; the capsule check keeps
|
||||
// that support below the player's walk space.
|
||||
if(hasSupportFloor&&(density<0.0f||passageColumnFloorSupport||shortSpanFloorSupport)
|
||||
if(hasSupportFloor&&(density<0.0f||passageColumnFloorSupport
|
||||
||shortSpanFloorSupport)
|
||||
&&(capsuleAllowsSupport||bestSupportDensity>=0.0))
|
||||
{
|
||||
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
if(bestSupportIsMouthTread)
|
||||
{
|
||||
// Preserve the owner's native ground through the shallow mouth
|
||||
// carve. Blending back the native field keeps its crater curvature
|
||||
// and noise. The capsule test keeps neighbouring coils open.
|
||||
const bool nativeRestoreCapsuleClear=density<0.0f
|
||||
&&bestSupportNativeBlend>0.0&&capsuleAllowsSupport
|
||||
&&!PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin,
|
||||
carveEnd,lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,
|
||||
lastIndex,qx,qy,qz,qPerMetre,capsuleClearanceMetres);
|
||||
if(nativeRestoreCapsuleClear)
|
||||
{
|
||||
const double nativeSurfaceDetail=std::clamp(
|
||||
static_cast<double>(mouthTerrainRelief),-1.0,1.0)
|
||||
*0.18*qPerMetre/Q;
|
||||
const float nativeGroundWithRelief=static_cast<float>(density
|
||||
-nativeSurfaceDetail);
|
||||
composed=std::lerp(composed,nativeGroundWithRelief,
|
||||
static_cast<float>(bestSupportNativeBlend));
|
||||
}
|
||||
}
|
||||
else composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
}
|
||||
}
|
||||
// Restore the surface route where the passage cut crossed otherwise solid
|
||||
// native ground. The support stays native-solid-only, so it cannot float.
|
||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
if(passage.controlPoints.size()<2)continue;
|
||||
const bool tileHasSupport=passageTileScopeMatches;
|
||||
if(!tileHasSupport
|
||||
||ActivePassageTileQueries->hasMouthSupport(passageIndex)))
|
||||
composeRouteMouthSupport(passage.upperStrateIndex,
|
||||
passage.upperMouthSubvoxels,true);
|
||||
}
|
||||
return composed;
|
||||
}
|
||||
|
||||
@@ -4519,6 +4837,7 @@ private:
|
||||
if(!Field.isValid())return false;
|
||||
const std::int64_t qx=ToFixed(X),qy=ToFixed(Y),qz=ToFixed(Z);
|
||||
std::int64_t Density=Field.densitySubvoxels(qx,qy,qz);
|
||||
const std::int64_t NativeDensity=Density;
|
||||
// The anchor-root field keeps broad lobes, but its shortest authored noise
|
||||
// wavelength is below one metre. Filter that detail only around a FloatingIslands
|
||||
// passage mouth so Marching Cubes does not expose voxel-sized chips on the crater
|
||||
@@ -4529,11 +4848,11 @@ private:
|
||||
const double densityMetres=std::abs(static_cast<double>(Density))*qToMetres;
|
||||
if(densityMetres<4.0)
|
||||
{
|
||||
bool foundPassageMouth=false;
|
||||
bool hasResolvedPassageMouth=false;
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
if(passage.upperStrateIndex!=StrateIndex)continue;
|
||||
foundPassageMouth=true;
|
||||
hasResolvedPassageMouth=true;
|
||||
const double dx=(static_cast<double>(qx)-passage.upperMouthSubvoxels[0])*qToMetres;
|
||||
const double dy=(static_cast<double>(qy)-passage.upperMouthSubvoxels[1])*qToMetres;
|
||||
const double dz=(static_cast<double>(qz)-passage.upperMouthSubvoxels[2])*qToMetres;
|
||||
@@ -4545,7 +4864,7 @@ private:
|
||||
const double weight=fade*fade*(3.0-2.0*fade);
|
||||
mouthFilterWeight=std::max(mouthFilterWeight,weight);
|
||||
}
|
||||
if(!foundPassageMouth)
|
||||
if(!hasResolvedPassageMouth)
|
||||
{
|
||||
// The passage-free native control retains the stratum route anchors.
|
||||
// Use its terminal route point as the same local mouth filter origin so
|
||||
@@ -4574,12 +4893,20 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
if(mouthFilterWeight>0.0)
|
||||
double routeNativeBlend=0.0;
|
||||
const auto mouthPassage=std::find_if(ResolvedLayout->passages.begin(),
|
||||
ResolvedLayout->passages.end(),[StrateIndex](const auto& passage)
|
||||
{return passage.upperStrateIndex==StrateIndex;});
|
||||
const double queryX=static_cast<double>(qx)*qToMetres;
|
||||
const double queryY=static_cast<double>(qy)*qToMetres;
|
||||
const double queryZ=static_cast<double>(qz)*qToMetres;
|
||||
const bool outsideCraterMouth=mouthPassage==ResolvedLayout->passages.end()
|
||||
||std::hypot(queryX-mouthPassage->upperMouthSubvoxels[0]*qToMetres,
|
||||
queryY-mouthPassage->upperMouthSubvoxels[1]*qToMetres)>5.5;
|
||||
if(mouthFilterWeight>0.0&&outsideCraterMouth)
|
||||
{
|
||||
// Preserve the resolved walk route while filtering the passage and crater field.
|
||||
const double queryX=static_cast<double>(qx)*qToMetres;
|
||||
const double queryY=static_cast<double>(qy)*qToMetres;
|
||||
const double queryZ=static_cast<double>(qz)*qToMetres;
|
||||
// Retain the native route only on the outer approach. The crater-mouth
|
||||
// interior stays with the filtered ground field and cannot become a bridge.
|
||||
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[StrateIndex](const auto& value)
|
||||
{return value.strateIndex==StrateIndex;});
|
||||
@@ -4607,15 +4934,20 @@ private:
|
||||
const double supportRadius=previous->values.size()>=4
|
||||
?static_cast<double>(previous->values[3])*qToMetres:0.75;
|
||||
const double floorZ=centerZ-1.20;
|
||||
const double protectedRadius=supportRadius+0.75;
|
||||
if((queryX-centerX)*(queryX-centerX)
|
||||
+(queryY-centerY)*(queryY-centerY)
|
||||
<protectedRadius*protectedRadius
|
||||
&&std::abs(queryZ-floorZ)<1.25)
|
||||
{
|
||||
mouthFilterWeight=0.0;
|
||||
break;
|
||||
}
|
||||
constexpr double WalkCoreRadiusMetres=0.45;
|
||||
constexpr double WearShoulderMetres=0.80;
|
||||
const double protectedRadius=std::min(WalkCoreRadiusMetres,
|
||||
supportRadius+0.10);
|
||||
const double routeDistance=std::hypot(queryX-centerX,queryY-centerY);
|
||||
const double radialT=std::clamp(
|
||||
(protectedRadius+WearShoulderMetres-routeDistance)
|
||||
/WearShoulderMetres,0.0,1.0);
|
||||
const double verticalT=std::clamp(
|
||||
(3.25-std::abs(queryZ-floorZ))/2.0,0.0,1.0);
|
||||
const double radialSmooth=radialT*radialT*(3.0-2.0*radialT);
|
||||
const double verticalSmooth=verticalT*verticalT*(3.0-2.0*verticalT);
|
||||
routeNativeBlend=std::max(routeNativeBlend,
|
||||
radialSmooth*verticalSmooth);
|
||||
}
|
||||
previous=&anchor;
|
||||
}
|
||||
@@ -4638,9 +4970,19 @@ private:
|
||||
// A separable [1,2,1]^3 kernel over a 2 m footprint filters sub-voxel
|
||||
// root detail while retaining the signed field on both sides of the surface.
|
||||
const double filtered=static_cast<double>(weightedSum)/64.0;
|
||||
const std::int64_t filteredDensity=static_cast<std::int64_t>(std::llround(
|
||||
std::int64_t filteredDensity=static_cast<std::int64_t>(std::llround(
|
||||
std::lerp(static_cast<double>(Density),filtered,mouthFilterWeight)));
|
||||
// Filtering detail at an open mouth may soften the surface, but it cannot
|
||||
// turn native air into a new solid shelf over the crater.
|
||||
if(NativeDensity>=0&&filteredDensity<0)filteredDensity=0;
|
||||
Density=filteredDensity;
|
||||
if(routeNativeBlend>0.0&&NativeDensity<0)
|
||||
{
|
||||
const double routePreserved=std::lerp(static_cast<double>(Density),
|
||||
static_cast<double>(std::min(Density,NativeDensity)),
|
||||
routeNativeBlend*0.60);
|
||||
Density=static_cast<std::int64_t>(std::llround(routePreserved));
|
||||
}
|
||||
// Marching Cubes can retain an isolated solid with no two-metre axis
|
||||
// thickness as a handful of disconnected triangles. Remove only shallow
|
||||
// negative samples surrounded by air at a one-metre probe distance; a
|
||||
|
||||
Reference in new issue
Block a user