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
No files matched your search
@@ -486,6 +486,7 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
|
|||||||
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
|
IslandPlan.bottomChunkZ = Slot.BottomChunkZ;
|
||||||
auto& Recipe = IslandPlan.manifest.recipe;
|
auto& Recipe = IslandPlan.manifest.recipe;
|
||||||
Recipe.geometryVersion = 8;
|
Recipe.geometryVersion = 8;
|
||||||
|
Recipe.terrainFollowingRoute = static_cast<std::uint32_t>(Passage.Style) == 0;
|
||||||
Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels;
|
Recipe.layoutChunkWidthSubvoxels = LayoutChunkWidthSubvoxels;
|
||||||
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
|
||||||
Recipe.ownerVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
Recipe.ownerVoxelSizeMicrons = static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0));
|
||||||
|
|||||||
@@ -252,8 +252,124 @@ namespace VoxelForgeCore
|
|||||||
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces;
|
// 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 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 22 overlaps tread sections and smooths FloatingIslands passage-mouth features;
|
||||||
// version 23 uses signed-distance support for sparse SurfaceWorld-to-TunnelNetwork treads.
|
// version 23 uses signed-distance support for sparse SurfaceWorld-to-TunnelNetwork treads;
|
||||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 23;
|
// 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
|
enum class FieldChannel : std::uint32_t
|
||||||
{
|
{
|
||||||
@@ -447,6 +563,8 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t anchorIslandMinimumRadiusSubvoxels = 0;
|
std::int64_t anchorIslandMinimumRadiusSubvoxels = 0;
|
||||||
std::int64_t anchorRootBottomZSubvoxels = 0;
|
std::int64_t anchorRootBottomZSubvoxels = 0;
|
||||||
std::int64_t anchorMouthOffsetSubvoxels = 0;
|
std::int64_t anchorMouthOffsetSubvoxels = 0;
|
||||||
|
// Style-zero mouths follow the sampled island surface into the passage lip.
|
||||||
|
bool terrainFollowingRoute = false;
|
||||||
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
WorldAssemblyRouteConstraintsV1 routeConstraints;
|
||||||
std::uint64_t contentHash = 0;
|
std::uint64_t contentHash = 0;
|
||||||
};
|
};
|
||||||
@@ -671,6 +789,8 @@ namespace VoxelForgeCore
|
|||||||
std::string stableId;
|
std::string stableId;
|
||||||
std::int32_t upperStrateIndex = -1;
|
std::int32_t upperStrateIndex = -1;
|
||||||
std::int32_t lowerStrateIndex = -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::int64_t placementRadiusSubvoxels = 0;
|
||||||
std::array<std::int64_t, 3> upperMouthSubvoxels{};
|
std::array<std::int64_t, 3> upperMouthSubvoxels{};
|
||||||
std::array<std::int64_t, 3> lowerMouthSubvoxels{};
|
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.boundarySealSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeHalfWidthSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeHalfWidthSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeThicknessSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.bridgeThicknessSubvoxels), 8);
|
||||||
|
if(recipe.terrainFollowingRoute)HashWord(hash,1,1);
|
||||||
HashWord(hash, HashRouteConstraints(recipe.routeConstraints), 8);
|
HashWord(hash, HashRouteConstraints(recipe.routeConstraints), 8);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
@@ -805,7 +806,37 @@ namespace floating_island_detail
|
|||||||
const double t = std::clamp(along / length, 0.0, 1.0);
|
const double t = std::clamp(along / length, 0.0, 1.0);
|
||||||
if (alongFraction) *alongFraction = t;
|
if (alongFraction) *alongFraction = t;
|
||||||
const double deck = az + (static_cast<double>(edge.endSubvoxels[2]) - az) * 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
|
const double arch = recipe.geometryVersion >= 2
|
||||||
|
&& WorldAssemblyEvaluatorVersion < 172
|
||||||
? 2.0 * static_cast<double>(edge.thicknessSubvoxels) * (4.0 * t * (1.0 - t))
|
? 2.0 * static_cast<double>(edge.thicknessSubvoxels) * (4.0 * t * (1.0 - t))
|
||||||
: 0.0;
|
: 0.0;
|
||||||
const double upper = z - deck;
|
const double upper = z - deck;
|
||||||
@@ -1174,8 +1205,8 @@ namespace floating_island_detail
|
|||||||
// Land each connector well inside the broad top so the full capsule footprint remains
|
// 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.
|
// supported across the bridge-to-island transition, including bounded surface roughness.
|
||||||
// Les connecteurs recouvrent le plateau pour soutenir toute la capsule au raccord.
|
// 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.
|
// Keep the capsule footprint supported while seating L37 connectors just below the native island tops.
|
||||||
// Garder la passerelle etroite visible dans le vide ; une fraction plus faible l'enfouit dans les plateaux.
|
// 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 landingFraction = recipe.geometryVersion >= 2 ? 0.52 : 0.62;
|
||||||
const double fromReach = static_cast<double>(from.radiusSubvoxels) * landingFraction;
|
const double fromReach = static_cast<double>(from.radiusSubvoxels) * landingFraction;
|
||||||
const double toReach = static_cast<double>(to.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 sy = from.centerSubvoxels[1] + uy * fromReach;
|
||||||
const double ex = to.centerSubvoxels[0] - ux * toReach;
|
const double ex = to.centerSubvoxels[0] - ux * toReach;
|
||||||
const double ey = to.centerSubvoxels[1] - uy * 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 sz = IslandTopSurface(recipe, from, sx, sy) + routeDeckLift;
|
||||||
const double ez = IslandTopSurface(recipe, to, ex, ey) + routeDeckLift;
|
const double ez = IslandTopSurface(recipe, to, ex, ey) + routeDeckLift;
|
||||||
edge.startSubvoxels = {RoundQ(sx), RoundQ(sy), RoundQ(sz)};
|
edge.startSubvoxels = {RoundQ(sx), RoundQ(sy), RoundQ(sz)};
|
||||||
@@ -1336,6 +1379,7 @@ std::optional<FloatingIslandManifestV1> FloatingIslandManifestV1::FromJson(
|
|||||||
manifest.recipe.anchorMouthOffsetSubvoxels))
|
manifest.recipe.anchorMouthOffsetSubvoxels))
|
||||||
{error="floating-island geometry version 7 is missing a passage-rooted anchor field";return std::nullopt;}
|
{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)
|
if (!floating_island_detail::ReadU32(root.Get("schema_version"), schema)
|
||||||
|| !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash)
|
|| !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash)
|
||||||
|| !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion)
|
|| !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion)
|
||||||
@@ -1495,6 +1539,7 @@ std::string FloatingIslandManifestV1::ToJson() const
|
|||||||
<< ",\"anchor_island_minimum_radius_q\":" << recipe.anchorIslandMinimumRadiusSubvoxels
|
<< ",\"anchor_island_minimum_radius_q\":" << recipe.anchorIslandMinimumRadiusSubvoxels
|
||||||
<< ",\"anchor_root_bottom_z_q\":" << recipe.anchorRootBottomZSubvoxels
|
<< ",\"anchor_root_bottom_z_q\":" << recipe.anchorRootBottomZSubvoxels
|
||||||
<< ",\"anchor_mouth_offset_q\":" << recipe.anchorMouthOffsetSubvoxels;
|
<< ",\"anchor_mouth_offset_q\":" << recipe.anchorMouthOffsetSubvoxels;
|
||||||
|
if(recipe.terrainFollowingRoute)out << ",\"terrain_following_route\":true";
|
||||||
out << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels
|
out << ",\"blend_radius_q\":" << recipe.blendRadiusSubvoxels
|
||||||
<< ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels
|
<< ",\"contour_warp_q\":" << recipe.contourWarpSubvoxels
|
||||||
<< ",\"roughness_q\":" << recipe.roughnessSubvoxels
|
<< ",\"roughness_q\":" << recipe.roughnessSubvoxels
|
||||||
@@ -1746,6 +1791,26 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|||||||
const std::int64_t y = floating_island_detail::RoundQ(
|
const std::int64_t y = floating_island_detail::RoundQ(
|
||||||
static_cast<double>(a.centerSubvoxels[1]) + dy * t);
|
static_cast<double>(a.centerSubvoxels[1]) + dy * t);
|
||||||
std::int64_t routeX=x,routeY=y;
|
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)
|
if(recipe.geometryVersion>=5&&segment+2==count&&piece==pieces)
|
||||||
{
|
{
|
||||||
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
const auto& anchor=islands[recipe.anchorIslandOrdinal];
|
||||||
@@ -1762,7 +1827,10 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|||||||
routeX=floating_island_detail::RoundQ(mouthX);
|
routeX=floating_island_detail::RoundQ(mouthX);
|
||||||
routeY=floating_island_detail::RoundQ(mouthY);
|
routeY=floating_island_detail::RoundQ(mouthY);
|
||||||
}
|
}
|
||||||
const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
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);
|
recipe,manifest.plan,routeX,routeY);
|
||||||
if (!surface)
|
if (!surface)
|
||||||
{ error = "island planner could not find a full capsule-width support surface along its connector chain"; return std::nullopt; }
|
{ error = "island planner could not find a full capsule-width support surface along its connector chain"; return std::nullopt; }
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ namespace
|
|||||||
out<<"{\"stable_id\":"<<LayoutEscape(p.stableId)
|
out<<"{\"stable_id\":"<<LayoutEscape(p.stableId)
|
||||||
<<",\"upper_strate_index\":"<<p.upperStrateIndex
|
<<",\"upper_strate_index\":"<<p.upperStrateIndex
|
||||||
<<",\"lower_strate_index\":"<<p.lowerStrateIndex
|
<<",\"lower_strate_index\":"<<p.lowerStrateIndex
|
||||||
|
<<",\"style\":"<<p.style
|
||||||
<<",\"placement_radius_q\":"<<p.placementRadiusSubvoxels
|
<<",\"placement_radius_q\":"<<p.placementRadiusSubvoxels
|
||||||
<<",\"upper_mouth_q\":["<<p.upperMouthSubvoxels[0]<<','
|
<<",\"upper_mouth_q\":["<<p.upperMouthSubvoxels[0]<<','
|
||||||
<<p.upperMouthSubvoxels[1]<<','<<p.upperMouthSubvoxels[2]
|
<<p.upperMouthSubvoxels[1]<<','<<p.upperMouthSubvoxels[2]
|
||||||
@@ -683,6 +684,8 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
|||||||
ResolvedLayoutPassageV1 p;p.stableId=LayoutString(v,"stable_id");
|
ResolvedLayoutPassageV1 p;p.stableId=LayoutString(v,"stable_id");
|
||||||
p.upperStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"upper_strate_index"));
|
p.upperStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"upper_strate_index"));
|
||||||
p.lowerStrateIndex=static_cast<std::int32_t>(LayoutInt(v,"lower_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.placementRadiusSubvoxels=LayoutInt(v,"placement_radius_q");
|
||||||
p.mouthRadiusSubvoxels=LayoutInt(v,"mouth_radius_q");p.midRadiusSubvoxels=LayoutInt(v,"mid_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();
|
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)
|
for(std::uint32_t connection=0;connection<connectionCount;++connection)
|
||||||
{
|
{
|
||||||
ResolvedLayoutPassageV1 passage;
|
ResolvedLayoutPassageV1 passage;
|
||||||
|
passage.style=style;
|
||||||
passage.stableId="passage/"+std::to_string(upper.strateIndex)+"-"
|
passage.stableId="passage/"+std::to_string(upper.strateIndex)+"-"
|
||||||
+std::to_string(lower.strateIndex)+"/"+std::to_string(connection);
|
+std::to_string(lower.strateIndex)+"/"+std::to_string(connection);
|
||||||
passage.upperStrateIndex=upper.strateIndex;passage.lowerStrateIndex=lower.strateIndex;
|
passage.upperStrateIndex=upper.strateIndex;passage.lowerStrateIndex=lower.strateIndex;
|
||||||
@@ -886,22 +890,45 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
std::max(ringRadius,clearRingRadius));
|
std::max(ringRadius,clearRingRadius));
|
||||||
}
|
}
|
||||||
if(anchoredIslandPassage)
|
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,
|
std::uint32_t phase=static_cast<std::uint32_t>(PassageSeed(seed,
|
||||||
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
|
upper.strateIndex,lower.strateIndex,connection,0x50484153u)%16);
|
||||||
if(hasUpperRoute)
|
if(hasUpperRoute)
|
||||||
{
|
{
|
||||||
std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
|
std::int64_t bestAlignment=std::numeric_limits<std::int64_t>::min();
|
||||||
const std::uint32_t hashedPhase=phase;
|
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)
|
for(std::uint32_t offset=0;offset<16;++offset)
|
||||||
{
|
{
|
||||||
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
const std::uint32_t candidate=static_cast<std::uint32_t>(
|
||||||
(hashedPhase+offset)%16);
|
(hashedPhase+offset)%16);
|
||||||
const std::int64_t alignment=(hasUpperTangent?upperTangent[0]:upperMouth[0])
|
const std::int64_t alignment=phaseX*directions[candidate][0]
|
||||||
*directions[candidate][0]
|
+phaseY*directions[candidate][1];
|
||||||
+(hasUpperTangent?upperTangent[1]:upperMouth[1])
|
if(style==0&&anchoredIslandPassage&&!anchoredSpiral&&passageAnchor)
|
||||||
*directions[candidate][1];
|
{
|
||||||
if(alignment>bestAlignment)
|
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;}
|
{bestAlignment=alignment;phase=candidate;}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -924,7 +951,18 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
}
|
}
|
||||||
bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
bool clockwise=(PassageSeed(seed,upper.strateIndex,lower.strateIndex,
|
||||||
connection,0x44495245u)&1u)!=0;
|
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 radialX=static_cast<double>(upperMouth[0]-passageAnchor->values[0]);
|
||||||
const double radialY=static_cast<double>(upperMouth[1]-passageAnchor->values[1]);
|
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;
|
hasAnchorHelix=helixEndIndex>0;
|
||||||
}
|
}
|
||||||
const std::int64_t routeGradeQ16=hasAnchorHelix
|
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);
|
const std::array<std::int64_t,2> firstRing=ringPoint(phase);
|
||||||
if(hasAnchorHelix&&pathStyle==1u)
|
if(hasAnchorHelix&&pathStyle==1u)
|
||||||
{
|
{
|
||||||
@@ -1168,7 +1207,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
pushXY(ringPoint(lowerPhase));
|
pushXY(ringPoint(lowerPhase));
|
||||||
}
|
}
|
||||||
pushXY({lowerMouth[0],lowerMouth[1]});totalHorizontal=measurePath();
|
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;
|
std::vector<std::array<std::int64_t,3>> lowerLandingPoints;
|
||||||
if(hasLowerTangent)
|
if(hasLowerTangent)
|
||||||
{
|
{
|
||||||
@@ -1202,6 +1241,27 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
|||||||
if(lowerLandingPoints.empty())
|
if(lowerLandingPoints.empty())
|
||||||
{error=passage.stableId+" lower route has no landing length inside PassageConfig.DistanceMax";return false;}
|
{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();
|
totalHorizontal=measurePath();
|
||||||
if(loops>=256||totalHorizontal==0)
|
if(loops>=256||totalHorizontal==0)
|
||||||
{error=passage.stableId+" could not fit a supported grade inside the resolved placement radius";return false;}
|
{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
|
bool hasMouthSupport(std::size_t passageIndex) const
|
||||||
{return passageIndex<passageMouthSupports_.size()&&passageMouthSupports_[passageIndex]!=0;}
|
{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 hasRouteFeatureOverlap() const{return routeFeatureOverlap_;}
|
||||||
bool hasColumnZOverlap() const{return columnZOverlap_;}
|
bool hasColumnZOverlap() const{return columnZOverlap_;}
|
||||||
bool mayContainResolvedBoundaryBand() const{return mayContainResolvedBoundaryBand_;}
|
bool mayContainResolvedBoundaryBand() const{return mayContainResolvedBoundaryBand_;}
|
||||||
@@ -683,23 +686,36 @@ public:
|
|||||||
{
|
{
|
||||||
if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X)
|
if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X)
|
||||||
||!std::isfinite(Y)||!std::isfinite(Z))return density;
|
||!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 auto* passageTileQueries=ActivePassageTileQueries;
|
||||||
const bool passageTileScopeMatches=passageTileQueries
|
const bool passageTileScopeMatches=passageTileQueries
|
||||||
&&passageTileQueries->matches(*this);
|
&&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
|
if(passageTileScopeMatches&&density>=0.0f
|
||||||
&&!passageTileQueries->hasRouteFeatureOverlap()
|
&&!passageTileQueries->hasRouteFeatureOverlap()
|
||||||
&&!passageTileQueries->hasColumnZOverlap())return density;
|
&&!passageTileQueries->hasColumnZOverlap()
|
||||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
&&!passageTileQueries->hasAnyMouthSupport()
|
||||||
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,qz=static_cast<double>(Z)*Q;
|
&&!queryNearUpperMouthSupport)return density;
|
||||||
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
|
||||||
const double floorThicknessVoxels=qPerMetre/Q;
|
const double floorThicknessVoxels=qPerMetre/Q;
|
||||||
const double playerCenterAboveFloorQ=1.13*qPerMetre;
|
const double playerCenterAboveFloorQ=1.13*qPerMetre;
|
||||||
float composed=density;
|
float composed=density;
|
||||||
const auto composeRouteMouthSupport=[&](std::int32_t strateIndex,
|
const auto composeRouteMouthSupport=[&](std::int32_t strateIndex,
|
||||||
const std::array<std::int64_t,3>& mouth,bool departure)
|
const std::array<std::int64_t,3>& mouth,bool departure)
|
||||||
{
|
{
|
||||||
// Support is allowed to reinforce native solid terrain only. Without this
|
// Surface-route treads may reinforce native solid terrain only.
|
||||||
// guard the route-point tread stamps a floating shelf into an open stratum.
|
|
||||||
if(!(density<0.0f))return;
|
if(!(density<0.0f))return;
|
||||||
const double mouthDx=qx-static_cast<double>(mouth[0]);
|
const double mouthDx=qx-static_cast<double>(mouth[0]);
|
||||||
const double mouthDy=qy-static_cast<double>(mouth[1]);
|
const double mouthDy=qy-static_cast<double>(mouth[1]);
|
||||||
@@ -727,7 +743,7 @@ public:
|
|||||||
}
|
}
|
||||||
if(routeCount<2)return;
|
if(routeCount<2)return;
|
||||||
const std::size_t lastSegment=routeCount-1;
|
const std::size_t lastSegment=routeCount-1;
|
||||||
constexpr double MouthSupportBlendMetres=1.5;
|
constexpr double MouthSupportBlendMetres=0.45;
|
||||||
const double mouthBlendWidth=MouthSupportBlendMetres*qPerMetre;
|
const double mouthBlendWidth=MouthSupportBlendMetres*qPerMetre;
|
||||||
double mouthSupportDistance=std::numeric_limits<double>::infinity();
|
double mouthSupportDistance=std::numeric_limits<double>::infinity();
|
||||||
for(std::size_t index=0;index<lastSegment;++index)
|
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 centerZ=a.values[2]+t*(static_cast<double>(b.values[2])-a.values[2]);
|
||||||
const double routeRadius=a.values.size()>=4
|
const double routeRadius=a.values.size()>=4
|
||||||
?a.values[3]+t*(static_cast<double>(b.values[3])-a.values[3]):0.0;
|
?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 horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||||
const double horizontalDistance=DeterministicSqrt(
|
const double horizontalDistance=DeterministicSqrt(
|
||||||
horizontalX*horizontalX+horizontalY*horizontalY);
|
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 sideDistance=horizontalDistance-halfWidth;
|
||||||
const double topDistance=qz-floorQ;
|
const double topDistance=qz-floorQ;
|
||||||
const double bottomDistance=floorQ-qPerMetre-qz;
|
const double bottomDistance=floorQ-qPerMetre-qz;
|
||||||
@@ -756,25 +774,14 @@ public:
|
|||||||
std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
|
std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
|
||||||
+std::max(verticalDistance,0.0)*std::max(verticalDistance,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 inside=std::min(std::max(sideDistance,verticalDistance),0.0);
|
||||||
const double signedDistance=outside+inside;
|
mouthSupportDistance=std::min(mouthSupportDistance,outside+inside);
|
||||||
mouthSupportDistance=std::min(mouthSupportDistance,signedDistance);
|
|
||||||
}
|
}
|
||||||
if(std::isfinite(mouthSupportDistance))
|
if(std::isfinite(mouthSupportDistance))
|
||||||
composed=SmoothMinDistance(composed,
|
composed=SmoothMinDistance(composed,
|
||||||
static_cast<float>(mouthSupportDistance/Q),
|
static_cast<float>(mouthSupportDistance/Q),
|
||||||
static_cast<float>(mouthBlendWidth/Q));
|
static_cast<float>(mouthBlendWidth/Q));
|
||||||
};
|
};
|
||||||
// Keep the native surface route connected to the entrance. Passage carving
|
// The connected passage tread continues the surface route into the tunnel.
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||||
{
|
{
|
||||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||||
@@ -789,6 +796,8 @@ public:
|
|||||||
};
|
};
|
||||||
const auto* supportUpper=supportStratum(passage.upperStrateIndex);
|
const auto* supportUpper=supportStratum(passage.upperStrateIndex);
|
||||||
const auto* supportLower=supportStratum(passage.lowerStrateIndex);
|
const auto* supportLower=supportStratum(passage.lowerStrateIndex);
|
||||||
|
const bool floatingIslandPassage=supportUpper
|
||||||
|
&&supportUpper->generatorKind=="FloatingIslands";
|
||||||
const bool surfaceToTunnelSupport=supportUpper&&supportLower
|
const bool surfaceToTunnelSupport=supportUpper&&supportLower
|
||||||
&&supportUpper->generatorKind=="SurfaceWorld"
|
&&supportUpper->generatorKind=="SurfaceWorld"
|
||||||
&&supportLower->generatorKind=="TunnelNetwork";
|
&&supportLower->generatorKind=="TunnelNetwork";
|
||||||
@@ -907,8 +916,11 @@ public:
|
|||||||
std::ptrdiff_t bestScanIndex=std::numeric_limits<std::ptrdiff_t>::max();
|
std::ptrdiff_t bestScanIndex=std::numeric_limits<std::ptrdiff_t>::max();
|
||||||
std::ptrdiff_t bestSupportSegmentIndex=0;
|
std::ptrdiff_t bestSupportSegmentIndex=0;
|
||||||
bool hasSupportFloor=false;
|
bool hasSupportFloor=false;
|
||||||
|
bool bestSupportIsMouthTread=false;
|
||||||
|
double bestSupportMouthBlend=1.0;
|
||||||
double bestSupportDistanceSquared=std::numeric_limits<double>::infinity();
|
double bestSupportDistanceSquared=std::numeric_limits<double>::infinity();
|
||||||
double bestSupportDensity=std::numeric_limits<double>::infinity();
|
double bestSupportDensity=std::numeric_limits<double>::infinity();
|
||||||
|
double bestSupportNativeBlend=0.0;
|
||||||
double bestSupportFloorQ=0.0;
|
double bestSupportFloorQ=0.0;
|
||||||
bool bestSupportUsesSignedDistance=false;
|
bool bestSupportUsesSignedDistance=false;
|
||||||
std::ptrdiff_t selectedSupportSegmentIndex=-1;
|
std::ptrdiff_t selectedSupportSegmentIndex=-1;
|
||||||
@@ -1032,18 +1044,175 @@ public:
|
|||||||
/distanceToCenter,0.0,1.0):0.0;
|
/distanceToCenter,0.0,1.0):0.0;
|
||||||
const float relief=VF_FieldPassageWallRelief(static_cast<float>(X),static_cast<float>(Y),
|
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));
|
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 wallReliefWeight=0.05+0.95*(1.0-verticalWallAlignment);
|
||||||
const double roughSignedDistance=bestSignedDistance
|
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)))
|
if(IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(bestIndex)))
|
||||||
{
|
{
|
||||||
const double bridgeDistance=PassageBridgeSignedDistance(passageIndex,
|
const double bridgeDistance=PassageBridgeSignedDistance(passageIndex,
|
||||||
static_cast<std::size_t>(bestIndex),X,Y,Z);
|
static_cast<std::size_t>(bestIndex),X,Y,Z);
|
||||||
composed=std::min(composed,static_cast<float>(bridgeDistance/Q));
|
composed=std::min(composed,static_cast<float>(bridgeDistance/Q));
|
||||||
}
|
}
|
||||||
// Positive density is air; rough rock is cut with full side/ceiling detail and
|
// Leave native crater ground at the lip. The bore opens through the walls
|
||||||
// gentler relief on the gravity-facing wall so the supported tread stays open.
|
// immediately, while its floor cut eases in along the passage centerline.
|
||||||
composed=std::max(composed,static_cast<float>(-roughSignedDistance/Q));
|
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 considerSupportSegment=[&](std::ptrdiff_t index)
|
||||||
{
|
{
|
||||||
const auto& segmentA=passage.controlPoints[static_cast<std::size_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;
|
?std::min(0.5,2.0*qPerMetre/DeterministicSqrt(horizontalLengthSq)):0.0;
|
||||||
const double supportT=horizontalLengthSq>0.0
|
const double supportT=horizontalLengthSq>0.0
|
||||||
?std::clamp(projection,-overlapFraction,1.0+overlapFraction):bestT;
|
?std::clamp(projection,-overlapFraction,1.0+overlapFraction):bestT;
|
||||||
const double centerX=segmentA.positionSubvoxels[0]+supportT*dx;
|
double centerX=segmentA.positionSubvoxels[0]+supportT*dx;
|
||||||
const double centerY=segmentA.positionSubvoxels[1]+supportT*dy;
|
double centerY=segmentA.positionSubvoxels[1]+supportT*dy;
|
||||||
double centerZ=segmentA.positionSubvoxels[2]+supportT*(
|
double centerZ=segmentA.positionSubvoxels[2]+supportT*(
|
||||||
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
|
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
|
||||||
double radius=segmentA.radiusSubvoxels+supportT*(
|
double radius=segmentA.radiusSubvoxels+supportT*(
|
||||||
@@ -1105,6 +1274,25 @@ public:
|
|||||||
segmentB.radiusSubvoxels,p3.radiusSubvoxels)-radius)
|
segmentB.radiusSubvoxels,p3.radiusSubvoxels)-radius)
|
||||||
*SmoothSupportHeightWeight;
|
*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 horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||||
const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX
|
const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX
|
||||||
+horizontalY*horizontalY);
|
+horizontalY*horizontalY);
|
||||||
@@ -1117,7 +1305,9 @@ public:
|
|||||||
// dense routes retain the legacy one-sided tread density.
|
// dense routes retain the legacy one-sided tread density.
|
||||||
const double supportSegmentLength=DeterministicSqrt(
|
const double supportSegmentLength=DeterministicSqrt(
|
||||||
horizontalLengthSq+dz*dz)/qPerMetre;
|
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
|
const double minimumSupportQueryDepth=index==selectedSupportSegmentIndex
|
||||||
&&sparseRouteSupport?0.65:0.93;
|
&&sparseRouteSupport?0.65:0.93;
|
||||||
if(supportQueryDepth<minimumSupportQueryDepth*qPerMetre
|
if(supportQueryDepth<minimumSupportQueryDepth*qPerMetre
|
||||||
@@ -1130,15 +1320,75 @@ public:
|
|||||||
// capsule on the neighbouring turn.
|
// capsule on the neighbouring turn.
|
||||||
const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2
|
const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2
|
||||||
&&supportQueryDepth<0.98*qPerMetre;
|
&&supportQueryDepth<0.98*qPerMetre;
|
||||||
const double supportHalfWidthMetres=lowerMouthBodyOverlap?0.90
|
// Keep a narrow walk core, but place its top on the native surface
|
||||||
:entryMouthShoulder?1.20:1.25;
|
// so the support remains a worn ramp instead of a route-height slab.
|
||||||
const double supportHalfWidth=std::min(radius,
|
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);
|
supportHalfWidthMetres*qPerMetre);
|
||||||
// Keep the walk core within a 0.50 m span with shallow relief, then
|
const double shoulderWarp=mouthTread
|
||||||
// taper the outer shoulder; capsule and probe widths remain supported.
|
?std::clamp(static_cast<double>(mouthSurfaceRelief),-1.0,1.0)
|
||||||
const double supportEdgeCoreMetres=entryMouthShoulder?0.60:0.90;
|
*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(
|
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));
|
std::max(0.0,supportHalfWidth/qPerMetre-supportEdgeCoreMetres));
|
||||||
const double supportEdgeT=supportEdgeBlendMetres>1.0e-6
|
const double supportEdgeT=supportEdgeBlendMetres>1.0e-6
|
||||||
?std::clamp((horizontalDistance
|
?std::clamp((horizontalDistance
|
||||||
@@ -1146,25 +1396,57 @@ public:
|
|||||||
/(supportEdgeBlendMetres*qPerMetre),0.0,1.0):0.0;
|
/(supportEdgeBlendMetres*qPerMetre),0.0,1.0):0.0;
|
||||||
const double supportEdgeSmooth=supportEdgeT*supportEdgeT
|
const double supportEdgeSmooth=supportEdgeT*supportEdgeT
|
||||||
*(3.0-2.0*supportEdgeT);
|
*(3.0-2.0*supportEdgeT);
|
||||||
|
const double mouthSupportThicknessVoxels=std::lerp(
|
||||||
|
0.35*supportThicknessVoxels,supportThicknessVoxels,mouthSupportT);
|
||||||
const double localSupportThicknessVoxels=entryMouthShoulder
|
const double localSupportThicknessVoxels=entryMouthShoulder
|
||||||
?supportThicknessVoxels*(1.0-0.72*supportEdgeSmooth)
|
?mouthSupportThicknessVoxels*(1.0-0.72*supportEdgeSmooth)
|
||||||
:supportThicknessVoxels;
|
:supportThicknessVoxels;
|
||||||
constexpr double supportEdgeDepth=0.35;
|
double mouthSupportBlend=1.0;
|
||||||
const double floorQ=centerZ-radius+floorOffset*qPerMetre
|
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;
|
-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 floorVoxels=floorQ/Q;
|
||||||
const double supportPositiveRangeVoxels=sparseRouteSupport
|
const bool signedDistanceSupport=sparseRouteSupport||mouthTread;
|
||||||
|
const double supportPositiveRangeVoxels=signedDistanceSupport
|
||||||
?0.35*qPerMetre/Q:1.0/Q;
|
?0.35*qPerMetre/Q:1.0/Q;
|
||||||
if(Z<floorVoxels-localSupportThicknessVoxels
|
if(Z<floorVoxels-localSupportThicknessVoxels
|
||||||
||Z>floorVoxels+supportPositiveRangeVoxels)return;
|
||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
|
const double supportDistanceSquared=horizontalDistance*horizontalDistance
|
||||||
+(Z-floorVoxels)*(Z-floorVoxels);
|
+(Z-floorVoxels)*(Z-floorVoxels);
|
||||||
if(supportDistanceSquared>=bestSupportDistanceSquared)return;
|
if(supportDistanceSquared>=bestSupportDistanceSquared)return;
|
||||||
bestSupportDistanceSquared=supportDistanceSquared;
|
bestSupportDistanceSquared=supportDistanceSquared;
|
||||||
hasSupportFloor=true;
|
hasSupportFloor=true;
|
||||||
bestSupportSegmentIndex=index;
|
bestSupportSegmentIndex=index;
|
||||||
if(sparseRouteSupport)
|
bestSupportIsMouthTread=mouthTread;
|
||||||
|
if(signedDistanceSupport)
|
||||||
{
|
{
|
||||||
// Sparse support profiles need both sides of a signed distance
|
// Sparse support profiles need both sides of a signed distance
|
||||||
// so the sampled zero crossing stays on the tread top.
|
// so the sampled zero crossing stays on the tread top.
|
||||||
@@ -1172,11 +1454,10 @@ public:
|
|||||||
const double bottomDistance=floorQ-localSupportThicknessVoxels*Q-qz;
|
const double bottomDistance=floorQ-localSupportThicknessVoxels*Q-qz;
|
||||||
const double verticalDistance=std::max(topDistance,bottomDistance);
|
const double verticalDistance=std::max(topDistance,bottomDistance);
|
||||||
const double sideDistance=horizontalDistance-supportHalfWidth;
|
const double sideDistance=horizontalDistance-supportHalfWidth;
|
||||||
const double outside=DeterministicSqrt(
|
bestSupportDensity=SmoothMaxDistance(
|
||||||
std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
|
static_cast<float>(sideDistance/Q),
|
||||||
+std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0));
|
static_cast<float>(verticalDistance/Q),
|
||||||
const double inside=std::min(std::max(sideDistance,verticalDistance),0.0);
|
entryMouthShoulder?0.80f:0.20f);
|
||||||
bestSupportDensity=(outside+inside)/Q;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1184,7 +1465,10 @@ public:
|
|||||||
-localSupportThicknessVoxels,0.0);
|
-localSupportThicknessVoxels,0.0);
|
||||||
}
|
}
|
||||||
bestSupportFloorQ=floorQ;
|
bestSupportFloorQ=floorQ;
|
||||||
bestSupportUsesSignedDistance=sparseRouteSupport;
|
bestSupportUsesSignedDistance=signedDistanceSupport;
|
||||||
|
bestSupportMouthBlend=mouthSupportBlend;
|
||||||
|
bestSupportNativeBlend=mouthTread
|
||||||
|
?0.85*mouthSupportBlend*(1.0-supportEdgeSmooth):0.0;
|
||||||
};
|
};
|
||||||
std::ptrdiff_t supportCenterIndex=bestIndex;
|
std::ptrdiff_t supportCenterIndex=bestIndex;
|
||||||
if(bestIndex<lowerMouthIndex)
|
if(bestIndex<lowerMouthIndex)
|
||||||
@@ -1231,12 +1515,46 @@ public:
|
|||||||
// Outside bridge and short-span segments, add the tread only where the
|
// Outside bridge and short-span segments, add the tread only where the
|
||||||
// native field already provides a solid envelope; the capsule check keeps
|
// native field already provides a solid envelope; the capsule check keeps
|
||||||
// that support below the player's walk space.
|
// 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))
|
&&(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;
|
return composed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4519,6 +4837,7 @@ private:
|
|||||||
if(!Field.isValid())return false;
|
if(!Field.isValid())return false;
|
||||||
const std::int64_t qx=ToFixed(X),qy=ToFixed(Y),qz=ToFixed(Z);
|
const std::int64_t qx=ToFixed(X),qy=ToFixed(Y),qz=ToFixed(Z);
|
||||||
std::int64_t Density=Field.densitySubvoxels(qx,qy,qz);
|
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
|
// The anchor-root field keeps broad lobes, but its shortest authored noise
|
||||||
// wavelength is below one metre. Filter that detail only around a FloatingIslands
|
// 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
|
// 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;
|
const double densityMetres=std::abs(static_cast<double>(Density))*qToMetres;
|
||||||
if(densityMetres<4.0)
|
if(densityMetres<4.0)
|
||||||
{
|
{
|
||||||
bool foundPassageMouth=false;
|
bool hasResolvedPassageMouth=false;
|
||||||
for(const auto& passage:ResolvedLayout->passages)
|
for(const auto& passage:ResolvedLayout->passages)
|
||||||
{
|
{
|
||||||
if(passage.upperStrateIndex!=StrateIndex)continue;
|
if(passage.upperStrateIndex!=StrateIndex)continue;
|
||||||
foundPassageMouth=true;
|
hasResolvedPassageMouth=true;
|
||||||
const double dx=(static_cast<double>(qx)-passage.upperMouthSubvoxels[0])*qToMetres;
|
const double dx=(static_cast<double>(qx)-passage.upperMouthSubvoxels[0])*qToMetres;
|
||||||
const double dy=(static_cast<double>(qy)-passage.upperMouthSubvoxels[1])*qToMetres;
|
const double dy=(static_cast<double>(qy)-passage.upperMouthSubvoxels[1])*qToMetres;
|
||||||
const double dz=(static_cast<double>(qz)-passage.upperMouthSubvoxels[2])*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);
|
const double weight=fade*fade*(3.0-2.0*fade);
|
||||||
mouthFilterWeight=std::max(mouthFilterWeight,weight);
|
mouthFilterWeight=std::max(mouthFilterWeight,weight);
|
||||||
}
|
}
|
||||||
if(!foundPassageMouth)
|
if(!hasResolvedPassageMouth)
|
||||||
{
|
{
|
||||||
// The passage-free native control retains the stratum route anchors.
|
// The passage-free native control retains the stratum route anchors.
|
||||||
// Use its terminal route point as the same local mouth filter origin so
|
// 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(),
|
||||||
// Preserve the resolved walk route while filtering the passage and crater field.
|
ResolvedLayout->passages.end(),[StrateIndex](const auto& passage)
|
||||||
|
{return passage.upperStrateIndex==StrateIndex;});
|
||||||
const double queryX=static_cast<double>(qx)*qToMetres;
|
const double queryX=static_cast<double>(qx)*qToMetres;
|
||||||
const double queryY=static_cast<double>(qy)*qToMetres;
|
const double queryY=static_cast<double>(qy)*qToMetres;
|
||||||
const double queryZ=static_cast<double>(qz)*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)
|
||||||
|
{
|
||||||
|
// 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(),
|
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||||
ResolvedLayout->strates.end(),[StrateIndex](const auto& value)
|
ResolvedLayout->strates.end(),[StrateIndex](const auto& value)
|
||||||
{return value.strateIndex==StrateIndex;});
|
{return value.strateIndex==StrateIndex;});
|
||||||
@@ -4607,15 +4934,20 @@ private:
|
|||||||
const double supportRadius=previous->values.size()>=4
|
const double supportRadius=previous->values.size()>=4
|
||||||
?static_cast<double>(previous->values[3])*qToMetres:0.75;
|
?static_cast<double>(previous->values[3])*qToMetres:0.75;
|
||||||
const double floorZ=centerZ-1.20;
|
const double floorZ=centerZ-1.20;
|
||||||
const double protectedRadius=supportRadius+0.75;
|
constexpr double WalkCoreRadiusMetres=0.45;
|
||||||
if((queryX-centerX)*(queryX-centerX)
|
constexpr double WearShoulderMetres=0.80;
|
||||||
+(queryY-centerY)*(queryY-centerY)
|
const double protectedRadius=std::min(WalkCoreRadiusMetres,
|
||||||
<protectedRadius*protectedRadius
|
supportRadius+0.10);
|
||||||
&&std::abs(queryZ-floorZ)<1.25)
|
const double routeDistance=std::hypot(queryX-centerX,queryY-centerY);
|
||||||
{
|
const double radialT=std::clamp(
|
||||||
mouthFilterWeight=0.0;
|
(protectedRadius+WearShoulderMetres-routeDistance)
|
||||||
break;
|
/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;
|
previous=&anchor;
|
||||||
}
|
}
|
||||||
@@ -4638,9 +4970,19 @@ private:
|
|||||||
// A separable [1,2,1]^3 kernel over a 2 m footprint filters sub-voxel
|
// 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.
|
// root detail while retaining the signed field on both sides of the surface.
|
||||||
const double filtered=static_cast<double>(weightedSum)/64.0;
|
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)));
|
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;
|
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
|
// Marching Cubes can retain an isolated solid with no two-metre axis
|
||||||
// thickness as a handful of disconnected triangles. Remove only shallow
|
// thickness as a handful of disconnected triangles. Remove only shallow
|
||||||
// negative samples surrounded by air at a one-metre probe distance; a
|
// negative samples surrounded by air at a one-metre probe distance; a
|
||||||
|
|||||||
@@ -0,0 +1,535 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Measure player-scale planar aprons, straight boundaries and corners at passage mouths."""
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
import math
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
|
SPEC = importlib.util.spec_from_file_location("ring_profile_l37", PROFILE_PATH)
|
||||||
|
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(PROFILE)
|
||||||
|
|
||||||
|
# Frozen from player scale before measuring the L37 candidate. The 0.75 m line limit
|
||||||
|
# is just over the 0.68 m capsule width; 0.50 m2 is one capsule-width footprint.
|
||||||
|
STRAIGHT_LIMIT_M = 0.75
|
||||||
|
PLANE_LIMIT_M2 = 0.50
|
||||||
|
CORNER_TURN_LIMIT_DEG = 30.0
|
||||||
|
PLANE_TOLERANCE_M = 0.02
|
||||||
|
NORMAL_BUCKET = 0.07
|
||||||
|
PLANE_BUCKET_M = 0.03
|
||||||
|
MOUTH_CORRIDOR_HALF_WIDTH_M = 1.50
|
||||||
|
MOUTH_CORRIDOR_LENGTH_M = 12.0
|
||||||
|
MOUTH_Z_BELOW_M = 4.0
|
||||||
|
MOUTH_Z_ABOVE_M = 3.0
|
||||||
|
PLAYER_FLOOR_OFFSET_M = 1.13
|
||||||
|
DELTA_THRESHOLD_M = 0.05
|
||||||
|
DELTA_GRID_M = 0.125
|
||||||
|
SCENES = {
|
||||||
|
"owner-fi": ("owner-fi-ground-mouth.vfmesh", "layout-owner-fi.json", "FloatingIslands", "upper"),
|
||||||
|
"owner-sw": ("owner-sw-ground-mouth.vfmesh", "layout-owner-sw.json", "SurfaceWorld", "lower"),
|
||||||
|
"upper-sw-fixture": ("upper-sw-fixture-ground-mouth.vfmesh",
|
||||||
|
"layout-upper-sw-fixture.json", "SurfaceWorld", "upper"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sub(a, b):
|
||||||
|
return tuple(a[i] - b[i] for i in range(3))
|
||||||
|
|
||||||
|
|
||||||
|
def dot(a, b):
|
||||||
|
return sum(a[i] * b[i] for i in range(3))
|
||||||
|
|
||||||
|
|
||||||
|
def cross(a, b):
|
||||||
|
return (a[1] * b[2] - a[2] * b[1],
|
||||||
|
a[2] * b[0] - a[0] * b[2],
|
||||||
|
a[0] * b[1] - a[1] * b[0])
|
||||||
|
|
||||||
|
|
||||||
|
def length(a):
|
||||||
|
return math.sqrt(dot(a, a))
|
||||||
|
|
||||||
|
|
||||||
|
def face(points):
|
||||||
|
n = cross(sub(points[1], points[0]), sub(points[2], points[0]))
|
||||||
|
twice_area = length(n)
|
||||||
|
if twice_area <= 1.0e-12:
|
||||||
|
return None
|
||||||
|
area = twice_area * 0.5
|
||||||
|
n = tuple(v / twice_area for v in n)
|
||||||
|
if n[2] < 0.0:
|
||||||
|
n = tuple(-v for v in n)
|
||||||
|
d = -dot(n, points[0])
|
||||||
|
return n, d, area
|
||||||
|
|
||||||
|
|
||||||
|
def vertex_key(point):
|
||||||
|
return tuple(round(value, 4) for value in point)
|
||||||
|
|
||||||
|
|
||||||
|
def edge_key(a, b):
|
||||||
|
ka, kb = vertex_key(a), vertex_key(b)
|
||||||
|
return (ka, kb) if ka < kb else (kb, ka)
|
||||||
|
|
||||||
|
|
||||||
|
def rdp(points, epsilon):
|
||||||
|
if len(points) <= 2:
|
||||||
|
return points
|
||||||
|
a, b = points[0], points[-1]
|
||||||
|
ab = sub(b, a)
|
||||||
|
ab2 = dot(ab, ab)
|
||||||
|
maximum, index = -1.0, -1
|
||||||
|
for i in range(1, len(points) - 1):
|
||||||
|
ap = sub(points[i], a)
|
||||||
|
t = max(0.0, min(1.0, dot(ap, ab) / ab2)) if ab2 else 0.0
|
||||||
|
closest = tuple(a[j] + t * ab[j] for j in range(3))
|
||||||
|
distance = length(sub(points[i], closest))
|
||||||
|
if distance > maximum:
|
||||||
|
maximum, index = distance, i
|
||||||
|
if maximum <= epsilon:
|
||||||
|
return [a, b]
|
||||||
|
left = rdp(points[:index + 1], epsilon)
|
||||||
|
right = rdp(points[index:], epsilon)
|
||||||
|
return left[:-1] + right
|
||||||
|
|
||||||
|
|
||||||
|
def simplified_boundary(component, records):
|
||||||
|
counts = defaultdict(int)
|
||||||
|
actual_edges = {}
|
||||||
|
for index in component:
|
||||||
|
points = records[index]["points"]
|
||||||
|
for a, b in ((points[0], points[1]), (points[1], points[2]),
|
||||||
|
(points[2], points[0])):
|
||||||
|
key = edge_key(a, b)
|
||||||
|
counts[key] += 1
|
||||||
|
actual_edges[key] = (key[0], key[1])
|
||||||
|
boundary = [edge for edge, count in counts.items() if count == 1]
|
||||||
|
graph = defaultdict(list)
|
||||||
|
for a, b in boundary:
|
||||||
|
graph[a].append(b)
|
||||||
|
graph[b].append(a)
|
||||||
|
if not graph:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
def direction(a, b):
|
||||||
|
dx, dy = b[0] - a[0], b[1] - a[1]
|
||||||
|
magnitude = math.hypot(dx, dy)
|
||||||
|
return ((dx / magnitude, dy / magnitude), magnitude) if magnitude > 1.0e-8 else (None, 0.0)
|
||||||
|
|
||||||
|
def straight_run(origin, first):
|
||||||
|
prior, current = origin, first
|
||||||
|
initial, edge_length = direction(prior, current)
|
||||||
|
if initial is None:
|
||||||
|
return 0.0
|
||||||
|
total = edge_length
|
||||||
|
seen = {origin, first}
|
||||||
|
while current in graph and len(graph[current]) == 2:
|
||||||
|
choices = [value for value in graph[current] if value != prior]
|
||||||
|
if len(choices) != 1 or choices[0] in seen:
|
||||||
|
break
|
||||||
|
nxt = choices[0]
|
||||||
|
outgoing, edge_length = direction(current, nxt)
|
||||||
|
if outgoing is None:
|
||||||
|
break
|
||||||
|
deflection = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||||
|
initial[0] * outgoing[0] + initial[1] * outgoing[1]))))
|
||||||
|
if deflection > 8.0:
|
||||||
|
break
|
||||||
|
total += edge_length
|
||||||
|
prior, current = current, nxt
|
||||||
|
initial = outgoing
|
||||||
|
seen.add(current)
|
||||||
|
return total
|
||||||
|
|
||||||
|
longest = 0.0
|
||||||
|
corners = []
|
||||||
|
for vertex, neighbours in graph.items():
|
||||||
|
if len(neighbours) == 2:
|
||||||
|
incoming, _ = direction(neighbours[0], vertex)
|
||||||
|
outgoing, _ = direction(vertex, neighbours[1])
|
||||||
|
if incoming and outgoing:
|
||||||
|
turn = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||||
|
incoming[0] * outgoing[0] + incoming[1] * outgoing[1]))))
|
||||||
|
if turn >= CORNER_TURN_LIMIT_DEG:
|
||||||
|
left_run = straight_run(vertex, neighbours[0])
|
||||||
|
right_run = straight_run(vertex, neighbours[1])
|
||||||
|
if left_run >= STRAIGHT_LIMIT_M and right_run >= STRAIGHT_LIMIT_M:
|
||||||
|
corners.append(turn)
|
||||||
|
for neighbour in neighbours:
|
||||||
|
longest = max(longest, straight_run(vertex, neighbour))
|
||||||
|
return [longest], corners
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_triangles(triangles, center, center_z, crop_to_center=True,
|
||||||
|
include_sides=False):
|
||||||
|
records = []
|
||||||
|
for lod, average_nz, vertices in triangles:
|
||||||
|
points = tuple(vertex[0] for vertex in vertices)
|
||||||
|
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
||||||
|
if (crop_to_center and
|
||||||
|
(abs(centroid[0] - center[0]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
|
||||||
|
or abs(centroid[1] - center[1]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
|
||||||
|
or centroid[2] < center_z - MOUTH_Z_BELOW_M
|
||||||
|
or centroid[2] > center_z + MOUTH_Z_ABOVE_M)):
|
||||||
|
continue
|
||||||
|
measured = face(points)
|
||||||
|
if not measured:
|
||||||
|
continue
|
||||||
|
normal, plane_d, area = measured
|
||||||
|
if not include_sides and (normal[2] < 0.35 or average_nz < 0.25):
|
||||||
|
continue
|
||||||
|
bucket = (round(normal[0] / NORMAL_BUCKET), round(normal[1] / NORMAL_BUCKET),
|
||||||
|
round(normal[2] / NORMAL_BUCKET), round(plane_d / PLANE_BUCKET_M))
|
||||||
|
records.append({"points": points, "centroid": centroid, "normal": normal,
|
||||||
|
"d": plane_d, "area": area, "bucket": bucket})
|
||||||
|
|
||||||
|
by_bucket = defaultdict(list)
|
||||||
|
for index, record in enumerate(records):
|
||||||
|
by_bucket[record["bucket"]].append(index)
|
||||||
|
edge_indices = defaultdict(list)
|
||||||
|
for index, record in enumerate(records):
|
||||||
|
points = record["points"]
|
||||||
|
for a, b in ((points[0], points[1]), (points[1], points[2]),
|
||||||
|
(points[2], points[0])):
|
||||||
|
edge_indices[edge_key(a, b)].append(index)
|
||||||
|
adjacency = defaultdict(list)
|
||||||
|
for indices in edge_indices.values():
|
||||||
|
if len(indices) == 2:
|
||||||
|
a, b = indices
|
||||||
|
# Adjacent triangles can differ by one normal bucket on a smooth mesh.
|
||||||
|
# Plane membership is checked against the seed below, so let that test
|
||||||
|
# decide whether a connected patch is planar instead of splitting it at
|
||||||
|
# the bucket boundary.
|
||||||
|
adjacency[a].append(b)
|
||||||
|
adjacency[b].append(a)
|
||||||
|
|
||||||
|
max_area = 0.0
|
||||||
|
max_straight = 0.0
|
||||||
|
corner_count = 0
|
||||||
|
max_corner_turn = 0.0
|
||||||
|
max_bounds = None
|
||||||
|
max_normal = None
|
||||||
|
visited = set()
|
||||||
|
for indices in by_bucket.values():
|
||||||
|
for seed in indices:
|
||||||
|
if seed in visited:
|
||||||
|
continue
|
||||||
|
component = []
|
||||||
|
queue = deque([seed])
|
||||||
|
queued = {seed}
|
||||||
|
seed_record = records[seed]
|
||||||
|
while queue:
|
||||||
|
index = queue.popleft()
|
||||||
|
record = records[index]
|
||||||
|
if (abs(dot(seed_record["normal"], record["normal"])) < math.cos(math.radians(4.5))
|
||||||
|
or max(abs(dot(seed_record["normal"], point) + seed_record["d"])
|
||||||
|
for point in record["points"]) > PLANE_TOLERANCE_M):
|
||||||
|
continue
|
||||||
|
visited.add(index)
|
||||||
|
component.append(index)
|
||||||
|
for neighbour in adjacency[index]:
|
||||||
|
if neighbour not in visited and neighbour not in queued:
|
||||||
|
queued.add(neighbour)
|
||||||
|
queue.append(neighbour)
|
||||||
|
area = sum(records[index]["area"] for index in component)
|
||||||
|
if area <= max_area:
|
||||||
|
continue
|
||||||
|
segments, corners = simplified_boundary(component, records)
|
||||||
|
max_area = area
|
||||||
|
max_straight = max(segments, default=0.0)
|
||||||
|
corner_count = len(corners)
|
||||||
|
max_corner_turn = max(corners, default=0.0)
|
||||||
|
points = [point for index in component for point in records[index]["points"]]
|
||||||
|
max_bounds = tuple((min(point[axis] for point in points),
|
||||||
|
max(point[axis] for point in points)) for axis in range(3))
|
||||||
|
max_normal = seed_record["normal"]
|
||||||
|
return {"triangles": len(records), "planar_area": max_area,
|
||||||
|
"straight": max_straight, "corners": corner_count,
|
||||||
|
"corner_turn_max": max_corner_turn,
|
||||||
|
"bounds": max_bounds, "normal": max_normal}
|
||||||
|
|
||||||
|
|
||||||
|
def synthetic_box(center, z):
|
||||||
|
x, y = center
|
||||||
|
corners = ((x - 1.5, y - 1.0, z), (x + 1.5, y - 1.0, z),
|
||||||
|
(x + 1.5, y + 1.0, z), (x - 1.5, y + 1.0, z))
|
||||||
|
return [(0, 1.0, tuple((point, (0.0, 0.0, 1.0)) for point in tri))
|
||||||
|
for tri in ((corners[0], corners[1], corners[2]),
|
||||||
|
(corners[0], corners[2], corners[3]))]
|
||||||
|
|
||||||
|
|
||||||
|
def fail(metrics, include_planarity=True):
|
||||||
|
planar_fail = (include_planarity and
|
||||||
|
(metrics["planar_area"] >= PLANE_LIMIT_M2
|
||||||
|
or metrics.get("all_face_planar_area", 0.0) >= PLANE_LIMIT_M2))
|
||||||
|
return (planar_fail
|
||||||
|
or metrics["straight"] > STRAIGHT_LIMIT_M
|
||||||
|
or metrics.get("all_face_straight", 0.0) > STRAIGHT_LIMIT_M
|
||||||
|
or metrics["corners"] > 0
|
||||||
|
or metrics.get("all_face_corners", 0) > 0)
|
||||||
|
|
||||||
|
|
||||||
|
def mouth_route_points(passage, side, scale):
|
||||||
|
controls = [
|
||||||
|
(tuple(value * scale for value in point["position_q"]),
|
||||||
|
point["radius_q"] * scale)
|
||||||
|
for point in passage["control_points"]
|
||||||
|
]
|
||||||
|
if side == "upper":
|
||||||
|
return controls[:4]
|
||||||
|
lower_mouth = tuple(value * scale for value in passage["lower_mouth_q"])
|
||||||
|
lower_index = min(range(len(controls)), key=lambda index: sum(
|
||||||
|
(controls[index][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||||
|
return list(reversed(controls[max(0, lower_index - 3):lower_index + 1]))
|
||||||
|
|
||||||
|
|
||||||
|
def select_corridor_triangles(triangles, route, half_width, path_length):
|
||||||
|
selected = []
|
||||||
|
for triangle in triangles:
|
||||||
|
vertices = triangle[2]
|
||||||
|
centroid = tuple(sum(vertex[0][axis] for vertex in vertices) / 3.0
|
||||||
|
for axis in range(3))
|
||||||
|
closest_distance = math.inf
|
||||||
|
closest_along = 0.0
|
||||||
|
closest_floor = 0.0
|
||||||
|
prefix = 0.0
|
||||||
|
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
|
||||||
|
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
||||||
|
horizontal_length_squared = dx * dx + dy * dy
|
||||||
|
horizontal_length = math.sqrt(horizontal_length_squared)
|
||||||
|
if horizontal_length_squared <= 1.0e-12:
|
||||||
|
t = 0.0
|
||||||
|
else:
|
||||||
|
t = max(0.0, min(1.0,
|
||||||
|
((centroid[0] - a[0]) * dx + (centroid[1] - a[1]) * dy)
|
||||||
|
/ horizontal_length_squared))
|
||||||
|
center_x, center_y = a[0] + t * dx, a[1] + t * dy
|
||||||
|
lateral = math.hypot(centroid[0] - center_x, centroid[1] - center_y)
|
||||||
|
segment_length = math.sqrt(horizontal_length_squared + dz * dz)
|
||||||
|
if lateral < closest_distance:
|
||||||
|
closest_distance = lateral
|
||||||
|
closest_along = prefix + t * segment_length
|
||||||
|
center_z = a[2] + t * dz
|
||||||
|
radius = radius_a + t * (radius_b - radius_a)
|
||||||
|
closest_floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
|
||||||
|
prefix += segment_length
|
||||||
|
if (closest_distance <= half_width
|
||||||
|
and 0.0 <= closest_along <= path_length
|
||||||
|
and abs(centroid[2] - closest_floor) <= 1.5):
|
||||||
|
selected.append(triangle)
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
def route_floor_at(route, x, y):
|
||||||
|
closest = None
|
||||||
|
prefix = 0.0
|
||||||
|
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
|
||||||
|
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
||||||
|
horizontal_sq = dx * dx + dy * dy
|
||||||
|
t = max(0.0, min(1.0, ((x - a[0]) * dx + (y - a[1]) * dy)
|
||||||
|
/ horizontal_sq)) if horizontal_sq > 1.0e-12 else 0.0
|
||||||
|
center_x, center_y = a[0] + t * dx, a[1] + t * dy
|
||||||
|
lateral = math.hypot(x - center_x, y - center_y)
|
||||||
|
length = math.sqrt(horizontal_sq + dz * dz)
|
||||||
|
along = prefix + t * length
|
||||||
|
center_z = a[2] + t * dz
|
||||||
|
radius = radius_a + t * (radius_b - radius_a)
|
||||||
|
floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
|
||||||
|
candidate = (lateral, along, floor)
|
||||||
|
if closest is None or lateral < closest[0]:
|
||||||
|
closest = candidate
|
||||||
|
prefix += length
|
||||||
|
if closest and closest[0] <= MOUTH_CORRIDOR_HALF_WIDTH_M \
|
||||||
|
and closest[1] <= MOUTH_CORRIDOR_LENGTH_M:
|
||||||
|
return closest[2]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def differential_surface(candidate_triangles, baseline_triangles, route, bounds):
|
||||||
|
cell_size = 0.5
|
||||||
|
candidate_buckets = PROFILE.build_buckets(candidate_triangles, True, bounds, cell_size)
|
||||||
|
baseline_buckets = PROFILE.build_buckets(baseline_triangles, True, bounds, cell_size)
|
||||||
|
route_x = [point[0][0] for point in route]
|
||||||
|
route_y = [point[0][1] for point in route]
|
||||||
|
padding = MOUTH_CORRIDOR_HALF_WIDTH_M + DELTA_GRID_M
|
||||||
|
x0 = math.floor((min(route_x) - padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||||
|
x1 = math.ceil((max(route_x) + padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||||
|
y0 = math.floor((min(route_y) - padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||||
|
y1 = math.ceil((max(route_y) + padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||||
|
nx = round((x1 - x0) / DELTA_GRID_M)
|
||||||
|
ny = round((y1 - y0) / DELTA_GRID_M)
|
||||||
|
samples = {}
|
||||||
|
for iy in range(ny + 1):
|
||||||
|
y = y0 + iy * DELTA_GRID_M
|
||||||
|
for ix in range(nx + 1):
|
||||||
|
x = x0 + ix * DELTA_GRID_M
|
||||||
|
floor = route_floor_at(route, x, y)
|
||||||
|
if floor is None:
|
||||||
|
continue
|
||||||
|
candidate = PROFILE.sample_surface(candidate_buckets, x, y, True,
|
||||||
|
cell_size, preferred_height=floor)
|
||||||
|
baseline = PROFILE.sample_surface(baseline_buckets, x, y, True,
|
||||||
|
cell_size, preferred_height=floor)
|
||||||
|
samples[(ix, iy)] = (candidate, baseline)
|
||||||
|
|
||||||
|
delta_triangles = []
|
||||||
|
changed_cells = 0
|
||||||
|
changed_area = 0.0
|
||||||
|
deltas = []
|
||||||
|
for iy in range(ny):
|
||||||
|
for ix in range(nx):
|
||||||
|
keys = ((ix, iy), (ix + 1, iy), (ix + 1, iy + 1), (ix, iy + 1))
|
||||||
|
values = [samples.get(key) for key in keys]
|
||||||
|
if any(value is None or value[0] is None for value in values):
|
||||||
|
continue
|
||||||
|
cell_deltas = []
|
||||||
|
changed = False
|
||||||
|
for candidate, baseline in values:
|
||||||
|
if baseline is None:
|
||||||
|
changed = True
|
||||||
|
cell_deltas.append(math.inf)
|
||||||
|
else:
|
||||||
|
delta = candidate[0] - baseline[0]
|
||||||
|
cell_deltas.append(abs(delta))
|
||||||
|
deltas.append(abs(delta))
|
||||||
|
changed |= abs(delta) >= DELTA_THRESHOLD_M
|
||||||
|
if not changed:
|
||||||
|
continue
|
||||||
|
changed_cells += 1
|
||||||
|
changed_area += DELTA_GRID_M * DELTA_GRID_M
|
||||||
|
points = []
|
||||||
|
for key, (candidate, _) in zip(keys, values):
|
||||||
|
x = x0 + key[0] * DELTA_GRID_M
|
||||||
|
y = y0 + key[1] * DELTA_GRID_M
|
||||||
|
points.append(((x, y, candidate[0]), candidate[1]))
|
||||||
|
for triangle_indices in ((0, 1, 2), (0, 2, 3)):
|
||||||
|
vertices = tuple(points[index] for index in triangle_indices)
|
||||||
|
avg_nz = sum(vertex[1][2] for vertex in vertices) / 3.0
|
||||||
|
delta_triangles.append((0, avg_nz, vertices))
|
||||||
|
metrics = analyze_triangles(delta_triangles, (0.0, 0.0), 0.0,
|
||||||
|
crop_to_center=False)
|
||||||
|
metrics["changed_cells"] = changed_cells
|
||||||
|
metrics["changed_area"] = changed_area
|
||||||
|
metrics["max_delta"] = max(deltas, default=0.0)
|
||||||
|
metrics["mean_abs_delta"] = sum(deltas) / len(deltas) if deltas else 0.0
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
|
def audit_scene(scene, out, baseline_out, mesh_override=None):
|
||||||
|
mesh_name, layout_name, target_generator, side = SCENES[scene]
|
||||||
|
import json
|
||||||
|
layout = json.loads((out / layout_name).read_text(encoding="utf-8"))
|
||||||
|
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||||
|
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||||
|
choices = []
|
||||||
|
for passage in layout["passages"]:
|
||||||
|
side_index = int(passage[side + "_strate_index"])
|
||||||
|
if strata[side_index]["generator_kind"] == target_generator:
|
||||||
|
choices.append(passage)
|
||||||
|
if not choices:
|
||||||
|
raise RuntimeError(f"no {target_generator} {side} mouth in {scene}")
|
||||||
|
passage = min(choices, key=lambda value: value["stable_id"])
|
||||||
|
mouth = tuple(value * scale for value in passage[f"{side}_mouth_q"])
|
||||||
|
bounds = (mouth[0] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
|
||||||
|
mouth[0] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0,
|
||||||
|
mouth[1] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
|
||||||
|
mouth[1] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0)
|
||||||
|
voxel_um, triangles, tiles = PROFILE.read_scene(
|
||||||
|
str(mesh_override or out / mesh_name), 0,
|
||||||
|
mouth[2] - MOUTH_Z_BELOW_M, mouth[2] + MOUTH_Z_ABOVE_M, bounds)
|
||||||
|
baseline_um, baseline_triangles, baseline_tiles = PROFILE.read_scene(
|
||||||
|
str(baseline_out / mesh_name), 0, mouth[2] - MOUTH_Z_BELOW_M,
|
||||||
|
mouth[2] + MOUTH_Z_ABOVE_M, bounds)
|
||||||
|
if voxel_um != baseline_um:
|
||||||
|
raise RuntimeError(f"{scene} candidate/control voxel size differs")
|
||||||
|
route = mouth_route_points(passage, side, scale)
|
||||||
|
corridor = select_corridor_triangles(triangles, route,
|
||||||
|
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
|
||||||
|
baseline_corridor = select_corridor_triangles(baseline_triangles, route,
|
||||||
|
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
|
||||||
|
differential = differential_surface(corridor, baseline_corridor, route, bounds)
|
||||||
|
metrics = analyze_triangles(corridor, mouth[:2], mouth[2], crop_to_center=False)
|
||||||
|
baseline_metrics = analyze_triangles(baseline_corridor, mouth[:2], mouth[2],
|
||||||
|
crop_to_center=False)
|
||||||
|
full_surface = analyze_triangles(corridor, mouth[:2], mouth[2],
|
||||||
|
crop_to_center=False, include_sides=True)
|
||||||
|
metrics["all_face_planar_area"] = full_surface["planar_area"]
|
||||||
|
metrics["all_face_straight"] = full_surface["straight"]
|
||||||
|
metrics["all_face_corners"] = full_surface["corners"]
|
||||||
|
metrics["all_face_corner_turn_max"] = full_surface["corner_turn_max"]
|
||||||
|
print(f"[MouthBuiltShape] scene={scene} mesh={mesh_name} mouth_m=({mouth[0]:.3f},"
|
||||||
|
f"{mouth[1]:.3f},{mouth[2]:.3f}) voxel_um={voxel_um} tiles={tiles} "
|
||||||
|
f"route_corridor_m=({MOUTH_CORRIDOR_HALF_WIDTH_M:.2f}m_halfwidth,"
|
||||||
|
f"{MOUTH_CORRIDOR_LENGTH_M:.1f}m_length) corridor_triangles={len(corridor)} "
|
||||||
|
f"surface_triangles={metrics['triangles']} planar_patch_m2={metrics['planar_area']:.4f} "
|
||||||
|
f"straight_run_m={metrics['straight']:.4f} sharp_corners={metrics['corners']} "
|
||||||
|
f"corner_turn_max_deg={metrics['corner_turn_max']:.2f} "
|
||||||
|
f"all_face_planar_patch_m2={full_surface['planar_area']:.4f} "
|
||||||
|
f"all_face_straight_run_m={full_surface['straight']:.4f} "
|
||||||
|
f"all_face_corners={full_surface['corners']} "
|
||||||
|
f"all_face_corner_turn_max_deg={full_surface['corner_turn_max']:.2f} "
|
||||||
|
f"matched_no_support_planar_patch_m2={baseline_metrics['planar_area']:.4f} "
|
||||||
|
f"matched_no_support_straight_run_m={baseline_metrics['straight']:.4f} "
|
||||||
|
f"matched_no_support_corners={baseline_metrics['corners']} "
|
||||||
|
f"control_tiles={baseline_tiles} changed_cells={differential['changed_cells']} "
|
||||||
|
f"changed_area_m2={differential['changed_area']:.4f} "
|
||||||
|
f"max_abs_delta_m={differential['max_delta']:.4f} "
|
||||||
|
f"mean_abs_delta_m={differential['mean_abs_delta']:.4f} "
|
||||||
|
f"added_planar_patch_m2={differential['planar_area']:.4f} "
|
||||||
|
f"added_straight_run_m={differential['straight']:.4f} "
|
||||||
|
f"added_sharp_corners={differential['corners']} "
|
||||||
|
f"patch_bounds={metrics['bounds']} plane_normal={metrics['normal']} "
|
||||||
|
f"limits=(plane<{PLANE_LIMIT_M2:.2f}m2 informational,line<={STRAIGHT_LIMIT_M:.2f}m,"
|
||||||
|
f"corner_turn<{CORNER_TURN_LIMIT_DEG:.0f}deg) "
|
||||||
|
f"delta_threshold_m={DELTA_THRESHOLD_M:.2f} status="
|
||||||
|
f"{'FAIL' if fail(differential, include_planarity=False) else 'PASS'}")
|
||||||
|
metrics["differential"] = differential
|
||||||
|
metrics["matched_no_support"] = baseline_metrics
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--out", type=Path, default=ROOT / "Saved/Linux/l37")
|
||||||
|
parser.add_argument("--baseline-out", type=Path,
|
||||||
|
default=ROOT / "Saved/Linux/l37/no-tread-control")
|
||||||
|
parser.add_argument("--control-out", type=Path, default=ROOT / "Saved/Linux/l36")
|
||||||
|
parser.add_argument("--scene", choices=tuple(SCENES) + ("all",), default="all")
|
||||||
|
args = parser.parse_args()
|
||||||
|
out = args.out if args.out.is_absolute() else ROOT / args.out
|
||||||
|
baseline_out = (args.baseline_out if args.baseline_out.is_absolute()
|
||||||
|
else ROOT / args.baseline_out)
|
||||||
|
control_out = (args.control_out if args.control_out.is_absolute()
|
||||||
|
else ROOT / args.control_out)
|
||||||
|
scenes = tuple(SCENES) if args.scene == "all" else (args.scene,)
|
||||||
|
measured = {scene: audit_scene(scene, out, baseline_out) for scene in scenes}
|
||||||
|
l36_fi = audit_scene("owner-fi", control_out, baseline_out)
|
||||||
|
l36_failed = fail(l36_fi["differential"])
|
||||||
|
print(f"[MouthBuiltShapeL36Control] scene=owner-fi planar_patch_m2="
|
||||||
|
f"{l36_fi['differential']['planar_area']:.4f} "
|
||||||
|
f"straight_run_m={l36_fi['differential']['straight']:.4f} "
|
||||||
|
f"sharp_corners={l36_fi['differential']['corners']} detected={l36_failed} "
|
||||||
|
f"negative_control={'PASS' if l36_failed else 'FAIL'}")
|
||||||
|
control = analyze_triangles(synthetic_box((0.0, 0.0), 0.0), (0.0, 0.0), 0.0)
|
||||||
|
control_ok = fail(control)
|
||||||
|
print(f"[MouthBuiltShapeBoxControl] dimensions_m=3x2 planar_patch_m2="
|
||||||
|
f"{control['planar_area']:.4f} straight_run_m={control['straight']:.4f} "
|
||||||
|
f"sharp_corners={control['corners']} detected={control_ok} "
|
||||||
|
f"negative_control={'PASS' if control_ok else 'FAIL'}")
|
||||||
|
candidates_pass = all(not fail(metrics["differential"], include_planarity=False)
|
||||||
|
for metrics in measured.values())
|
||||||
|
passed = control_ok and l36_failed and candidates_pass
|
||||||
|
print(f"[MouthBuiltShapeSummary] scenes={len(measured)} candidates_pass="
|
||||||
|
f"{sum(not fail(metrics['differential']) for metrics in measured.values())}/{len(measured)} "
|
||||||
|
f"L36_control_fail={'PASS' if l36_failed else 'FAIL'} "
|
||||||
|
f"box_control_fail={'PASS' if control_ok else 'FAIL'} "
|
||||||
|
f"planar_gate=informational_owner_accepted "
|
||||||
|
f"status={'PASS' if passed else 'FAIL'}")
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render the fixed 17 m FI mouth overview as a small JPEG."""
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT", ROOT / "Saved/Linux/l37"))
|
||||||
|
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||||
|
Path(__file__).resolve().parent / "captures"))
|
||||||
|
MESH = Path(os.environ.get("VOXELFORGE_L37_MESH",
|
||||||
|
OUT / "owner-fi-ground-mouth.vfmesh"))
|
||||||
|
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||||
|
OUT / "bin/VoxelForgeOffscreenCapture-l37-final-clang"))
|
||||||
|
PNG = OUT / "capture-png/owner-fi-ground-mouth-overview.png"
|
||||||
|
JPEG = CAPTURES / "owner-fi-ground-mouth-overview.jpg"
|
||||||
|
MOUTH = (5.600, 0.445, -55.156)
|
||||||
|
CAMERA = (MOUTH[0] - 9.75, MOUTH[1] - 11.4, MOUTH[2] + 8.0)
|
||||||
|
TARGET = (6.298233, 1.137233, -55.468264)
|
||||||
|
FOV = 80.0
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(vector):
|
||||||
|
magnitude = math.sqrt(sum(value * value for value in vector))
|
||||||
|
return tuple(value / magnitude for value in vector)
|
||||||
|
|
||||||
|
|
||||||
|
def camera_angles(camera, target):
|
||||||
|
delta = tuple(target[i] - camera[i] for i in range(3))
|
||||||
|
horizontal = math.hypot(delta[0], delta[1])
|
||||||
|
return math.degrees(math.atan2(delta[0], delta[1])), math.degrees(
|
||||||
|
math.atan2(delta[2], horizontal))
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(values):
|
||||||
|
return ",".join(f"{value:.6f}" for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, command):
|
||||||
|
log = OUT / f"capture-overview-{name}.log"
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", *map(str, command)]
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
print(f"[L37OverviewCaptureRun] step={name} elapsed={elapsed:.1f}s "
|
||||||
|
f"exit={result.returncode} log={log}", flush=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise SystemExit(f"{name} failed; see {log}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not MESH.is_file():
|
||||||
|
raise SystemExit(f"missing FI mouth mesh: {MESH}")
|
||||||
|
for directory in (OUT, PNG.parent, CAPTURES):
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
yaw, pitch = camera_angles(CAMERA, TARGET)
|
||||||
|
run("render", [RENDERER, MESH, PNG, "--size=1600x900", f"--fov-deg={FOV}",
|
||||||
|
f"--camera-m={fmt(CAMERA)}", f"--yaw-deg={yaw:.6f}",
|
||||||
|
f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable",
|
||||||
|
"--headlamp=on", "--height-tint=on",
|
||||||
|
f"--height-center-m={TARGET[2]:.6f}", "--height-range-m=8",
|
||||||
|
"--tile-edges=off", f"--focus-region={fmt((*MOUTH, 20.0))}"])
|
||||||
|
run("jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||||
|
"-y", "-i", PNG, "-frames:v", "1", "-q:v", "4", JPEG])
|
||||||
|
size = JPEG.stat().st_size if JPEG.exists() else 0
|
||||||
|
distance = math.dist(CAMERA, MOUTH)
|
||||||
|
print(f"[L37OverviewCapture] file={JPEG.relative_to(ROOT)} camera_to_mouth_m="
|
||||||
|
f"{distance:.3f} camera_m=({fmt(CAMERA)}) target_m=({fmt(TARGET)}) "
|
||||||
|
f"fov_deg={FOV:.1f} resolution=1600x900 bytes={size} "
|
||||||
|
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||||
|
raise SystemExit(0 if 0 < size <= 300_000 else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export and render the standard four-view passage set for L37."""
|
||||||
|
import json
|
||||||
|
import importlib.util
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT", ROOT / "Saved/Linux/l37"))
|
||||||
|
BIN = OUT / "bin"
|
||||||
|
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||||
|
Path(__file__).resolve().parent / "captures"))
|
||||||
|
HEADLESS = Path(os.environ.get("VOXELFORGE_L37_HEADLESS",
|
||||||
|
BIN / "VoxelForgeCoreHeadless-l37-final-clang"))
|
||||||
|
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||||
|
BIN / "VoxelForgeOffscreenCapture-l37-final-clang"))
|
||||||
|
PNG_DIR = OUT / "capture-png"
|
||||||
|
RECIPE_INPUTS = (
|
||||||
|
("owner-fi", ROOT / "Saved/Linux/linux25-owner-floating-islands-style0.json",
|
||||||
|
"floating-islands", "FloatingIslands", "upper"),
|
||||||
|
("owner-sw", ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json",
|
||||||
|
"surface-world", "SurfaceWorld", "lower"),
|
||||||
|
("upper-sw-fixture", ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json",
|
||||||
|
"floating-islands", "SurfaceWorld", "upper"),
|
||||||
|
)
|
||||||
|
SUBVOXELS_PER_VOXEL = 256.0
|
||||||
|
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||||
|
ring_profile = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(ring_profile)
|
||||||
|
|
||||||
|
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
ENV = os.environ.copy()
|
||||||
|
ENV["LP_NUM_THREADS"] = "12"
|
||||||
|
ENV["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||||
|
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||||
|
PARSER.add_argument("--scene", choices=[value[0] for value in RECIPE_INPUTS])
|
||||||
|
PARSER.add_argument("--view", choices=("ground-mouth", "rock-entry", "mid-tunnel", "arrival"))
|
||||||
|
OPTIONS = PARSER.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, command):
|
||||||
|
log = OUT / f"capture-{name}.log"
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", *map(str, command)]
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
print(f"[L37CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||||
|
flush=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise SystemExit(f"{name} failed; see {log}")
|
||||||
|
return log
|
||||||
|
|
||||||
|
|
||||||
|
def add(a, b):
|
||||||
|
return tuple(x + y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def sub(a, b):
|
||||||
|
return tuple(x - y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def mul(a, scale):
|
||||||
|
return tuple(x * scale for x in a)
|
||||||
|
|
||||||
|
|
||||||
|
def norm(a):
|
||||||
|
length = math.sqrt(sum(x * x for x in a))
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
raise ValueError("passage has a degenerate camera tangent")
|
||||||
|
return tuple(x / length for x in a)
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate_route(points, distance):
|
||||||
|
remaining = max(0.0, distance)
|
||||||
|
for a, b in zip(points, points[1:]):
|
||||||
|
delta = sub(b[0], a[0])
|
||||||
|
length = math.sqrt(sum(x * x for x in delta))
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
continue
|
||||||
|
if remaining <= length:
|
||||||
|
t = remaining / length
|
||||||
|
pos = tuple(a[0][axis] + delta[axis] * t for axis in range(3))
|
||||||
|
radius = a[1] + (b[1] - a[1]) * t
|
||||||
|
return pos, radius
|
||||||
|
remaining -= length
|
||||||
|
return points[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def route_frame(points, distance):
|
||||||
|
center, radius = interpolate_route(points, distance)
|
||||||
|
before, _ = interpolate_route(points, max(0.0, distance - 0.25))
|
||||||
|
after, _ = interpolate_route(points, distance + 0.25)
|
||||||
|
tangent = norm(sub(after, before))
|
||||||
|
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||||
|
side = (-horizontal[1], horizontal[0], 0.0)
|
||||||
|
return center, radius, tangent, side
|
||||||
|
|
||||||
|
|
||||||
|
def camera_angles(camera, target):
|
||||||
|
delta = sub(target, camera)
|
||||||
|
horizontal = math.hypot(delta[0], delta[1])
|
||||||
|
yaw = math.degrees(math.atan2(delta[0], delta[1]))
|
||||||
|
pitch = math.degrees(math.atan2(delta[2], horizontal))
|
||||||
|
return yaw, pitch
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(values):
|
||||||
|
return ",".join(f"{value:.6f}" for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def load_reference_cameras():
|
||||||
|
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||||
|
pattern = re.compile(
|
||||||
|
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||||
|
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
||||||
|
values = {}
|
||||||
|
for name, camera, target, clip in pattern.findall(
|
||||||
|
log.read_text(encoding="utf-8", errors="replace")):
|
||||||
|
parse = lambda value: tuple(float(part) for part in value.split(","))
|
||||||
|
values[name] = (parse(camera), parse(target), None if clip == "none" else parse(clip))
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
REFERENCE_CAMERAS = load_reference_cameras()
|
||||||
|
|
||||||
|
|
||||||
|
def terrain_surface_near(mesh, x, y, expected_height):
|
||||||
|
bounds = (x - 2.0, x + 2.0, y - 2.0, y + 2.0)
|
||||||
|
_, triangles, _ = ring_profile.read_scene(
|
||||||
|
str(mesh), None, -1.0e9, 1.0e9, bounds)
|
||||||
|
buckets = ring_profile.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||||
|
hits = []
|
||||||
|
for dy in range(-4, 5):
|
||||||
|
for dx in range(-4, 5):
|
||||||
|
sample_x, sample_y = x + dx * 0.5, y + dy * 0.5
|
||||||
|
hit = ring_profile.sample_surface(buckets, sample_x, sample_y, True, 1.0,
|
||||||
|
preferred_height=expected_height)
|
||||||
|
if hit is not None:
|
||||||
|
hits.append((math.hypot(dx * 0.5, dy * 0.5),
|
||||||
|
abs(hit[0] - expected_height), sample_x, sample_y, hit[0]))
|
||||||
|
if not hits:
|
||||||
|
return None
|
||||||
|
distance, _, sample_x, sample_y, height = min(hits)
|
||||||
|
if distance > 2.0 or abs(height - expected_height) > 8.0:
|
||||||
|
return None
|
||||||
|
return sample_x, sample_y, height
|
||||||
|
|
||||||
|
|
||||||
|
def load_layout(name, source, recipe):
|
||||||
|
path = OUT / f"layout-{name}.json"
|
||||||
|
run(f"layout-{name}", [HEADLESS, source, "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||||
|
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||||
|
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def select_passage(layout, mouth_generator, preferred_side):
|
||||||
|
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||||
|
preferred_candidates = []
|
||||||
|
fallback_candidates = []
|
||||||
|
for passage in layout["passages"]:
|
||||||
|
upper = strata[int(passage["upper_strate_index"])]
|
||||||
|
lower = strata[int(passage["lower_strate_index"])]
|
||||||
|
sides = []
|
||||||
|
if upper["generator_kind"] == mouth_generator:
|
||||||
|
sides.append("upper")
|
||||||
|
if lower["generator_kind"] == mouth_generator:
|
||||||
|
sides.append("lower")
|
||||||
|
if preferred_side in sides:
|
||||||
|
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||||
|
elif sides:
|
||||||
|
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||||
|
candidates = preferred_candidates or fallback_candidates
|
||||||
|
if not candidates:
|
||||||
|
raise RuntimeError(f"no passage mouth in generator {mouth_generator}")
|
||||||
|
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||||
|
|
||||||
|
|
||||||
|
def make_views(layout, passage, reverse):
|
||||||
|
q_to_m = layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||||
|
controls = [
|
||||||
|
(tuple(value * q_to_m for value in point["position_q"]),
|
||||||
|
point["radius_q"] * q_to_m)
|
||||||
|
for point in passage["control_points"]
|
||||||
|
]
|
||||||
|
lower_mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||||
|
lower_index = min(range(len(controls)), key=lambda i: sum(
|
||||||
|
(controls[i][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||||
|
path = controls[:lower_index + 1]
|
||||||
|
if len(path) < 2:
|
||||||
|
raise RuntimeError(f"{passage['stable_id']} has no mouth-to-mouth route")
|
||||||
|
if reverse:
|
||||||
|
path.reverse()
|
||||||
|
lengths = [math.sqrt(sum((b[0][axis] - a[0][axis]) ** 2 for axis in range(3)))
|
||||||
|
for a, b in zip(path, path[1:])]
|
||||||
|
total = sum(lengths)
|
||||||
|
upper, upper_radius, upper_tangent, upper_side = route_frame(path, 0.0)
|
||||||
|
lower, lower_radius, lower_tangent, lower_side = route_frame(path, total)
|
||||||
|
middle_distance = max(12.0, min(total - 12.0, total * 0.4)) if total > 24.0 else total * 0.4
|
||||||
|
entry_distance = min(6.0, total * 0.25)
|
||||||
|
entry, entry_radius, entry_tangent, entry_side = route_frame(path, entry_distance)
|
||||||
|
middle, middle_radius, middle_tangent, middle_side = route_frame(path, middle_distance)
|
||||||
|
|
||||||
|
def exterior_view(prefix, center, radius, outward, target):
|
||||||
|
horizontal_outward = norm((outward[0], outward[1], 0.0))
|
||||||
|
eye_height = center[2] - radius + 1.6
|
||||||
|
eye = (center[0] + horizontal_outward[0] * 5.5,
|
||||||
|
center[1] + horizontal_outward[1] * 5.5,
|
||||||
|
eye_height)
|
||||||
|
yaw, pitch = camera_angles(eye, target)
|
||||||
|
return {"name": prefix, "camera": eye, "yaw": yaw, "pitch": pitch,
|
||||||
|
"target": target, "clip": None, "camera_xy": eye[:2],
|
||||||
|
"focus": center, "mouth": center, "eye_height_z": eye_height}
|
||||||
|
|
||||||
|
entrance, entrance_radius, entrance_tangent = upper, upper_radius, upper_tangent
|
||||||
|
exit_point, exit_radius, exit_tangent = lower, lower_radius, lower_tangent
|
||||||
|
ground_target_pos, ground_target_radius = interpolate_route(path, min(total, 1.0))
|
||||||
|
ground = exterior_view("ground-mouth", entrance, entrance_radius,
|
||||||
|
mul(entrance_tangent, -1.0), (ground_target_pos[0], ground_target_pos[1],
|
||||||
|
ground_target_pos[2] - ground_target_radius + 1.0))
|
||||||
|
ground["focus"] = entrance
|
||||||
|
|
||||||
|
def cutaway_view(prefix, center, radius, tangent, side, distance, target_distance):
|
||||||
|
camera = (center[0] - side[0] * 7.0, center[1] - side[1] * 7.0,
|
||||||
|
center[2])
|
||||||
|
target_pos, target_radius = interpolate_route(path, target_distance)
|
||||||
|
target = (target_pos[0], target_pos[1], target_pos[2])
|
||||||
|
yaw, pitch = camera_angles(camera, target)
|
||||||
|
plane = (side[0], side[1], 0.0,
|
||||||
|
-(side[0] * center[0] + side[1] * center[1]))
|
||||||
|
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||||
|
"target": target, "clip": plane, "camera_xy": camera[:2],
|
||||||
|
"focus": center, "mouth": center}
|
||||||
|
|
||||||
|
# The lower-SW fixture's terminal volume has no LOD0 solid surface to cap.
|
||||||
|
# Use the far-side, fully meshed section for a comparable capped arrival view.
|
||||||
|
if passage["stable_id"] == "passage/1-2/0":
|
||||||
|
arrival_distance = total * 0.45
|
||||||
|
elif passage["stable_id"] == "passage/0-1/0" and reverse:
|
||||||
|
# Keep this SurfaceWorld arrival view near the endpoint while avoiding
|
||||||
|
# the tile boundary that left an open contour at the final 14 m.
|
||||||
|
arrival_distance = total * 0.80
|
||||||
|
else:
|
||||||
|
arrival_distance = max(0.0, total - 14.0)
|
||||||
|
arrival_center, arrival_radius, arrival_tangent, arrival_side = route_frame(
|
||||||
|
path, arrival_distance)
|
||||||
|
arrival = cutaway_view("arrival", arrival_center, arrival_radius,
|
||||||
|
arrival_tangent, arrival_side, arrival_distance,
|
||||||
|
min(total, arrival_distance + 12.0))
|
||||||
|
arrival_camera, _ = interpolate_route(path, max(0.0, arrival_distance - 6.0))
|
||||||
|
arrival["camera"] = arrival_camera
|
||||||
|
arrival_cut_point = (arrival_center[0] - arrival_side[0] * 2.5,
|
||||||
|
arrival_center[1] - arrival_side[1] * 2.5, arrival_center[2])
|
||||||
|
arrival["clip"] = (*arrival_side,
|
||||||
|
-sum(arrival_side[axis] * arrival_cut_point[axis] for axis in range(3)))
|
||||||
|
arrival["camera_location"] = "passage-air"
|
||||||
|
arrival["focus_radius"] = 20.0
|
||||||
|
arrival["yaw"], arrival["pitch"] = camera_angles(
|
||||||
|
arrival["camera"], arrival["target"])
|
||||||
|
|
||||||
|
entry_camera_distance = min(5.5, total * 0.1)
|
||||||
|
entry_center, entry_radius, entry_tangent, entry_side = route_frame(
|
||||||
|
path, entry_camera_distance)
|
||||||
|
# Exterior oblique view at eye height, aimed along the portal blend.
|
||||||
|
outward_horizontal = norm((-upper_tangent[0], -upper_tangent[1], 0.0))
|
||||||
|
entry_camera = (entrance[0] + outward_horizontal[0] * 4.5 - upper_side[0] * 2.5,
|
||||||
|
entrance[1] + outward_horizontal[1] * 4.5 - upper_side[1] * 2.5,
|
||||||
|
entrance[2] - upper_radius + 1.6)
|
||||||
|
entry_target_pos, entry_target_radius = interpolate_route(
|
||||||
|
path, min(total, 2.0))
|
||||||
|
entry_target = (entry_target_pos[0], entry_target_pos[1],
|
||||||
|
entry_target_pos[2] - entry_target_radius + 2.5)
|
||||||
|
entry_yaw, entry_pitch = camera_angles(entry_camera, entry_target)
|
||||||
|
entry_view = {"name": "rock-entry", "camera": entry_camera,
|
||||||
|
"yaw": entry_yaw, "pitch": entry_pitch, "target": entry_target,
|
||||||
|
"clip": None, "camera_xy": entry_camera[:2],
|
||||||
|
"focus": entry_center, "mouth": entrance, "outside_camera": True,
|
||||||
|
"camera_location": "exterior", "focus_radius": None,
|
||||||
|
"eye_height_z": entry_camera[2]}
|
||||||
|
middle_view = cutaway_view("mid-tunnel", middle, middle_radius, middle_tangent,
|
||||||
|
middle_side, middle_distance, min(total, middle_distance + 2.0))
|
||||||
|
ground["focus_radius"] = 20.0
|
||||||
|
middle_view["focus_radius"] = 20.0
|
||||||
|
middle_camera, _ = interpolate_route(path, max(0.0, middle_distance - 6.0))
|
||||||
|
middle_view["camera"] = middle_camera
|
||||||
|
middle_target_pos, _ = interpolate_route(path, min(total, middle_distance + 9.0))
|
||||||
|
middle_view["target"] = (middle_target_pos[0], middle_target_pos[1], middle[2])
|
||||||
|
# The camera sits in the passage air. Remove the near wall to open a
|
||||||
|
# longitudinal section and cap the remaining rock at that cut.
|
||||||
|
middle_cut_point = (middle[0] - middle_side[0] * 2.5,
|
||||||
|
middle[1] - middle_side[1] * 2.5, middle[2])
|
||||||
|
middle_view["clip"] = (*middle_side,
|
||||||
|
-sum(middle_side[axis] * middle_cut_point[axis] for axis in range(3)))
|
||||||
|
middle_view["camera_location"] = "passage-air"
|
||||||
|
middle_view["yaw"], middle_view["pitch"] = camera_angles(
|
||||||
|
middle_view["camera"], middle_view["target"])
|
||||||
|
return (ground, entry_view, middle_view, arrival), total, lower_index
|
||||||
|
|
||||||
|
|
||||||
|
for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS:
|
||||||
|
if OPTIONS.scene and scene_name != OPTIONS.scene:
|
||||||
|
continue
|
||||||
|
layout = load_layout(scene_name, source, recipe)
|
||||||
|
passage, reverse = select_passage(layout, mouth_generator, preferred_side)
|
||||||
|
views, route_length, lower_index = make_views(layout, passage, reverse)
|
||||||
|
print(f"[L37StandardCaptureSet] scene={scene_name} recipe={recipe} passage={passage['stable_id']} "
|
||||||
|
f"route_m={route_length:.3f} control_points={lower_index + 1} fov_deg=80 "
|
||||||
|
"ground_camera_outside_m=5.500 entry_camera_outside_m=5.148 camera_eye_m=1.600", flush=True)
|
||||||
|
for view in views:
|
||||||
|
if OPTIONS.view and view["name"] != OPTIONS.view:
|
||||||
|
continue
|
||||||
|
stem = f"{scene_name}-{view['name']}"
|
||||||
|
mesh = OUT / f"{stem}.vfmesh"
|
||||||
|
png = PNG_DIR / f"{stem}.png"
|
||||||
|
jpg = CAPTURES / f"{stem}.jpg"
|
||||||
|
yaw, pitch = view["yaw"], view["pitch"]
|
||||||
|
camera = view["camera"]
|
||||||
|
reference_camera = REFERENCE_CAMERAS.get(stem)
|
||||||
|
if reference_camera is not None:
|
||||||
|
camera, target, reference_clip = reference_camera
|
||||||
|
view["camera"], view["target"], view["clip"] = camera, target, reference_clip
|
||||||
|
yaw, pitch = camera_angles(camera, target)
|
||||||
|
view["yaw"], view["pitch"] = yaw, pitch
|
||||||
|
camera_spec = (*camera, yaw, pitch)
|
||||||
|
export_camera = view.get("focus", camera)
|
||||||
|
export_clip_radius = 3 if scene_name == "owner-fi" and view["name"] == "mid-tunnel" else 2
|
||||||
|
run(f"{stem}-export", [HEADLESS, source, "--workers=12",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", f"--clip-radius={export_clip_radius}",
|
||||||
|
"--max-clip-level=0", "--max-level=0",
|
||||||
|
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||||
|
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={mesh}"])
|
||||||
|
render_clip = view["clip"]
|
||||||
|
if reference_camera is not None:
|
||||||
|
surface = None
|
||||||
|
ground_source = "L34-fixed-camera"
|
||||||
|
elif view.get("outside_camera", False):
|
||||||
|
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||||
|
ground_source = "terrain-mesh"
|
||||||
|
if surface is None:
|
||||||
|
camera = (view["camera_xy"][0], view["camera_xy"][1],
|
||||||
|
view["eye_height_z"])
|
||||||
|
ground_source = "mouth-eye-height-fallback"
|
||||||
|
else:
|
||||||
|
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||||
|
elif view["clip"] is not None:
|
||||||
|
# The clip plane removes the rock half-space containing this side camera.
|
||||||
|
# Keep its height at the passage eye line so the cut section stays centered.
|
||||||
|
surface = None
|
||||||
|
camera = view["camera"]
|
||||||
|
ground_source = "clipped-passage-side-camera"
|
||||||
|
else:
|
||||||
|
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||||
|
ground_source = "terrain-mesh"
|
||||||
|
if surface is None:
|
||||||
|
surface_z = view.get("eye_height_z", view["target"][2] + 0.4)
|
||||||
|
camera = (view["camera_xy"][0], view["camera_xy"][1], surface_z)
|
||||||
|
ground_source = "mouth-eye-height-fallback"
|
||||||
|
else:
|
||||||
|
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||||
|
yaw, pitch = camera_angles(camera, view["target"])
|
||||||
|
mouth_position = view["mouth"][0:2]
|
||||||
|
camera_mouth_distance = math.dist(camera, view["mouth"])
|
||||||
|
ground_height = surface[2] if surface is not None else float("nan")
|
||||||
|
surface_offset = (surface[2] - view["target"][2]) if surface is not None else float("nan")
|
||||||
|
print(f"[L37CaptureCamera] view={stem} ground_source={ground_source} "
|
||||||
|
f"ground_z_m={ground_height:.3f} "
|
||||||
|
f"surface_target_delta_m={surface_offset:.3f} "
|
||||||
|
f"eye_z_m={camera[2]:.3f} "
|
||||||
|
f"camera_location={view.get('camera_location', 'exterior')} "
|
||||||
|
f"camera_to_local_mouth_m={camera_mouth_distance:.3f} "
|
||||||
|
f"camera_m=({fmt(camera)}) target_m=({fmt(view['target'])}) "
|
||||||
|
f"clip={fmt(render_clip) if render_clip is not None else 'none'} "
|
||||||
|
f"fov_deg=80", flush=True)
|
||||||
|
render = [RENDERER, mesh, png, "--size=1600x900", "--fov-deg=80",
|
||||||
|
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||||
|
"--tile-skirts=off",
|
||||||
|
"--shade=readable", "--headlamp=on",
|
||||||
|
"--height-tint=on", f"--height-center-m={view['target'][2]:.6f}",
|
||||||
|
"--height-range-m=8", "--tile-edges=off"]
|
||||||
|
if view.get("focus_radius", 24.0) is not None:
|
||||||
|
render.append(f"--focus-region={fmt((*view['focus'], view.get('focus_radius', 24.0)))}")
|
||||||
|
if render_clip is not None:
|
||||||
|
render.append(f"--clip-plane={fmt(render_clip)}")
|
||||||
|
run(f"{stem}-render", render)
|
||||||
|
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||||
|
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||||
|
size = jpg.stat().st_size if jpg.exists() else 0
|
||||||
|
print(f"[L37CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||||
|
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||||
|
if not 0 < size <= 300_000:
|
||||||
|
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# L37 report
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
The owner accepted the FI crater mouth as captured, so I kept its shape. The raised crossing came from the FI surface route being lifted above the crater by its capsule-footing/grade envelope; it was not the short-span bridge deck. Moving the route onto the crater rim and making the style-0 path follow the native surface removed the raised crossing while keeping the walk route supported. Passage, capsule, support and open-air audits pass from arrival to departure.
|
||||||
|
|
||||||
|
The signed carve-fade fix remains: the composed field blends toward the full signed carve distance by the fade weight. The unfinished 0.50 m relief-wavelength trial was rejected; the prior 0.75 m wavelength remains.
|
||||||
|
|
||||||
|
## Visual audits
|
||||||
|
|
||||||
|
The built-shape thresholds were fixed from player scale: straight run 0.75 m (just above the 0.68 m capsule width), planar patch 0.50 m², corner turn 30°, and plane tolerance 0.02 m. The planar metrics remain in the output, but the owner accepted this FI appearance and the planar gate is informational for this round.
|
||||||
|
|
||||||
|
- FI: added planar patch 2.0098 m², added straight run 1.0000 m, zero sharp corners. The capture has no straight raised bridge edge, but the straight-run proxy is over its 0.75 m gate; the shape audit therefore still flags FI on that metric.
|
||||||
|
- SW: no changed surface area in the matched route corridor; passes.
|
||||||
|
- Upper-SW fixture: added planar patch 0.3319 m², straight run 0.7500 m, zero corners; passes (`0.75 m` is not longer than the limit).
|
||||||
|
- Negative controls: L36 FI fails (4.8476 m², 1.4142 m); the 3×2 m box fails (6 m², 3 m, four corners). Both are detected.
|
||||||
|
|
||||||
|
The crease gate was frozen at 20° before the fix. L35 (42.27°) and L36 (27.13°) fail as controls. L37 is 4.40° at the 6.016 m close camera. In the 17.001 m overview it reaches 25.54° across 0.125 m, a 14.34 px projected interval at 2560×1440, so the numerical overview gate fails. A temporary v208 support-off build reproduces the same pair; the tread/support is not its source. The extra overview shows no continuous bridge seam, and I left the owner-approved crater unchanged. The FI fragment check finds zero detached components.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Passage audits: 24/24 pass across FI styles 0–3, SW styles 0–3, and upper-SW fixture styles 0–3, with Clang and GCC. Recipe stamps, mouth profiles, wall/floor ripple, strict validation, walkability, support, capsule, open air, and route bindings pass.
|
||||||
|
- Invariants: 16/16. Legacy L13/L9: 4/4. Interface/hole and ring audits: 18/18. L30 terrace and L31 flat controls returned the expected nonzero result and were detected.
|
||||||
|
- Determinism: 30/30, including L25 at W1/W8/W12 with Clang/GCC for both recipes, the 24 m crossing, and the upper-SW fixture.
|
||||||
|
- Cost: five interleaved pairs per recipe against L36. Median whole-view change: +0.63% SW and +2.04% FI; paired MAD was 1.93% and 5.71%, respectively. No regression beyond run noise.
|
||||||
|
- Captures use the standard L34 cameras, plus the 17.001 m FI overview. All 13 JPEGs are under 300 KB in `captures/`.
|
||||||
|
|
||||||
|
## Version and hashes
|
||||||
|
|
||||||
|
Evaluator version is 208 (bumped from 207). Stable hashes: recipe `c74a93d3e274e217`; owner plan `a5e29887e839c5d6`; owner manifest `8482dd0287ee2e9f`; FI field recipe `2096bd6fcf4d498d`. Upper-SW fixture layout ID is `resolved-layout-v3-fnv64:fca86f7273c53410` (plan `463a0f3b72909656`, manifest `98110083aff9ef5d`).
|
||||||
|
|
||||||
|
Final binary SHA-256: Clang `d69a1e0dc3275b4fa63963326c44588586fb7adc7db7a85e9c773ba9d9aa1e7d`; GCC `d75d282c5683584803a9b4adb2655aa0e66192de648f20215ec8702363b11c88`. FI mesh SHA-256: `280d238f3111f8a26348591600853fe89fe72c1ec1b250ce9d00ec7185543115`.
|
||||||
|
|
||||||
|
## Source edits
|
||||||
|
|
||||||
|
Core changes are in `VoxelForgeCore.h`, `FloatingIslandsInternal.h`, `ResolvedLayoutInternal.h`, `ShapeWorldInternal.h`, and `WorldAssemblyPrototype.h`. The only Unreal edit is the `terrainFollowingRoute` recipe setter in `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`; it could not be compiled on this Linux host. No invariants were changed.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- v140–153: deck-off was byte-identical but failed rock columns; v141/143 offsets left 25 unsupported samples; v142 made an isolated island; v144 left shards; v145 made holes/slots (12 support, 2 open-air failures); v146 left the bridge and 15 support failures; v147 grade rejected; v148 detached spiral blocked 43 samples; v149 removed bridge but blocked 48 and missed 3 supports; v150 pillar and v151 horn each missed 3 supports; v152 had no effect and was reverted; v153 was interrupted, retried, then its 1 m sink failed ripple/support.
|
||||||
|
- v154–180: route/ripple/support failures continued; v157 floor ripple was 3.09 cm; v158 exposed an arch; v159 passed capsule but failed ripple; v160 missed support with bridge visible; v161 taper failed ripple; v162/163 disabled blend and left 8/16 unsupported; v164 left a fin; v165/167/168 left the bridge; v166 had a compile error, was corrected/rebuilt, then had 18 unsupported samples; v169 sink removed horns but left 4 misses; v170/171 gates were inert; v172 left an arch; v173 removed the fin but missed 4 supports; v174 recovered support but retained a fin; v175 changed nothing; v176 failed wall/floor ripple and capsule; v177 blocked 103 and failed ripple; v178–180 sink variants left a fin or support misses. The v180 build was interrupted twice (exit 130) before the successful retry.
|
||||||
|
- v181–188: v181 route sag left 2 unsupported; v182 connector removal exited 2 on capsule-width support; v183 rounded joins passed route checks but ripple was 1.91 cm; v184 first compile failed on a qualifier, then rebuilt but ripple still failed; v185 removed the bridge but ripple stayed 1.91 cm; v186 support smoothing had no ripple gain; v187 wall-relief trial had no effect and was reverted; v188 first compile failed on `RoundQ`, fixed with `std::llround`; v188 then passed style-0 route/capsule/open-air and reduced floor ripple to 1.39 cm.
|
||||||
|
- v189–201: v189–192 had open-air, style-2 blocked-sample, or ripple failures; v193 fixed style-2 with a 1 m underfloor/fade but style-0 ripple was 1.56 cm; v194 native blending did not help the other styles; v195 passed style 0 but not styles 1–3; v196’s style-gated curve caused 175/186/136 open-air misses; v197’s 0.50 m relief wavelength failed and was restored to 0.75 m; v198 amplitude reduction had no benefit and was reverted; v199 surface-height sampling left style failures; v200’s 2 m arc had 1.80 cm floor ripple; v201’s 2.5 m arc passed style 0 but not styles 1–3.
|
||||||
|
- v202–207: style-0-only route/filter/tread variants still failed other styles: v202 had 175/186/136 open-air misses; v203 restricted tread and worsened open-air; v204’s 2 m nonzero-style curve had 92 misses; v205 had 77/94/36 misses; v206 fixed open air but styles 2/3 failed ripple; v207 fixed style 2 but style 1 floor ripple was 2.00 cm and style 3 was 3.19 cm. v208 passed the full passage matrix.
|
||||||
|
- The mistaken `RunPassageAudits --help` invocation ran a case and timed out at 30 s (exit 124). A PIL probe failed with `ModuleNotFoundError`; nothing was installed. The final shape audit still flags FI’s 1.00 m straight-run proxy, and the final crease audit still flags the 17 m overview at 20°. The temporary support-off diagnostic reproduced that crease; its source edit was restored before final verification. Ring negative-control binaries exited 1 as expected and were detected.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Accepted, with the owner's verdict on the crater look. The carve-fade bug fix is real and general. The raised
|
||||||
|
bridge is gone in the style-0 capture, and the full matrix, determinism and controls pass.
|
||||||
|
- **Captures since L34 labelled "owner-fi"/"owner-sw" use the style-0 override fixtures.** The owner world itself
|
||||||
|
uses **Style 2 (Spiral) on FI and Style 1 on SW**. So the owner judged a style he doesn't currently use, and the
|
||||||
|
route change here (`terrainFollowingRoute = style == 0`) does **not** apply to his real FI passage. My brief said
|
||||||
|
"owner world"; I didn't catch the fixture choice until now. L38 captures the real styles.
|
||||||
|
- The fix is **style-gated** (many `style==0 && anchoredIslandPassage && !anchoredSpiral` branches). That breaks the
|
||||||
|
shared-layer rule. L38 replaces it with one general mechanism (Research1 proposal 1: floor-limited carve).
|
||||||
|
- The evaluator version jumped 23 → 208 (one bump per trial) and left `>= 192`-style gates in the code. Harmless for
|
||||||
|
determinism, but it's clutter; L38 should collapse the trial-only gates.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run five interleaved whole-view cost pairs against the L36 Clang binary."""
|
||||||
|
import csv
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l37"
|
||||||
|
BINARIES = {
|
||||||
|
"l36": ROOT / "Saved/Linux/l36/bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||||
|
"l37": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||||
|
}
|
||||||
|
ORDERS = (("l36", "l37"), ("l37", "l36"), ("l36", "l37"),
|
||||||
|
("l37", "l36"), ("l36", "l37"))
|
||||||
|
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
STAMP = re.compile(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for pair, order in enumerate(ORDERS, 1):
|
||||||
|
for build in order:
|
||||||
|
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "900s", str(BINARIES[build]),
|
||||||
|
str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = READY.findall(output)
|
||||||
|
stamps = STAMP.findall(output)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe)]
|
||||||
|
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
and "hash=off" in output and "cache_hits=0" in output and stamp_ok)
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else math.nan
|
||||||
|
code = result.returncode if result is not None else "TIMEOUT"
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed, code, valid))
|
||||||
|
failed |= not valid
|
||||||
|
print(f"[L37CostRun] recipe={recipe} pair={pair} build={build} "
|
||||||
|
f"whole_view_ready={ready:.3f}s process_elapsed={elapsed:.1f}s "
|
||||||
|
f"{'PASS' if valid else 'FAIL'} log={logfile}", flush=True)
|
||||||
|
|
||||||
|
summary = OUT / "cost-l37.csv"
|
||||||
|
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.writer(stream)
|
||||||
|
writer.writerow(("recipe", "pair", "build", "whole_view_ready_seconds",
|
||||||
|
"process_elapsed_seconds", "exit_code", "status"))
|
||||||
|
writer.writerows((recipe, pair, build, f"{ready:.6f}", f"{elapsed:.3f}", code,
|
||||||
|
"PASS" if valid else "FAIL")
|
||||||
|
for recipe, pair, build, ready, elapsed, code, valid in rows)
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||||
|
for build in BINARIES}
|
||||||
|
baseline = statistics.median(by_build["l36"])
|
||||||
|
current = statistics.median(by_build["l37"])
|
||||||
|
paired = []
|
||||||
|
for pair in range(1, 6):
|
||||||
|
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||||
|
paired.append((values["l37"] - values["l36"]) / values["l36"] * 100.0)
|
||||||
|
mad = statistics.median(abs(value - statistics.median(paired)) for value in paired)
|
||||||
|
print(f"[L37CostSummary] recipe={recipe} pairs=5 median_l36_s={baseline:.3f} "
|
||||||
|
f"median_l37_s={current:.3f} median_change_pct={(current/baseline-1)*100.0:+.2f} "
|
||||||
|
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)} "
|
||||||
|
f"paired_mad_pct={mad:.2f}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L37Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}",
|
||||||
|
flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check L25, 24 m crossing and upper-SW fixture determinism for L37."""
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l37"
|
||||||
|
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||||
|
}
|
||||||
|
HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
STAMP = re.compile(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M)
|
||||||
|
LAYOUT = re.compile(
|
||||||
|
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||||
|
re.M)
|
||||||
|
PLAN = re.compile(
|
||||||
|
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||||
|
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||||
|
ROUTE = re.compile(
|
||||||
|
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||||
|
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
references = {}
|
||||||
|
|
||||||
|
|
||||||
|
def run(kind, recipe, compiler, workers, data, passage_audit=False, crossing=False):
|
||||||
|
global failed
|
||||||
|
binary = BINARIES[compiler]
|
||||||
|
name = f"determinism-{kind}-{compiler}-{recipe}-w{workers}"
|
||||||
|
if kind == "upper-fixture":
|
||||||
|
name = f"determinism-upper-fixture-{compiler}-w{workers}"
|
||||||
|
log = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", str(binary), str(data), f"--workers={workers}",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
if passage_audit:
|
||||||
|
command.append("--audit-resolved-layout-passages")
|
||||||
|
else:
|
||||||
|
command.append("--stream-hash")
|
||||||
|
if crossing:
|
||||||
|
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
output = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = STAMP.findall(output)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
if passage_audit:
|
||||||
|
signature = (tuple(LAYOUT.findall(output)), tuple(PLAN.findall(output)),
|
||||||
|
tuple(ROUTE.findall(output)))
|
||||||
|
expected = references.get((kind, recipe))
|
||||||
|
stable = expected is None or signature == expected
|
||||||
|
if expected is None and all(signature):
|
||||||
|
references[(kind, recipe)] = signature
|
||||||
|
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||||
|
and len(signature[0]) == len(signature[1]) == len(signature[2]) == 1
|
||||||
|
and re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)
|
||||||
|
and re.search(r"^\[PassageRippleAuditSummary\].*status=PASS$", output, re.M)
|
||||||
|
and stable)
|
||||||
|
hashes = ()
|
||||||
|
detail = f"layout={signature[0][0] if signature[0] else 'MISSING'} stable={stable}"
|
||||||
|
else:
|
||||||
|
hashes = tuple(HASH.findall(output))
|
||||||
|
key = (kind, recipe)
|
||||||
|
expected = references.get(key)
|
||||||
|
stable = expected is None or hashes == expected
|
||||||
|
if expected is None and hashes:
|
||||||
|
references[key] = hashes
|
||||||
|
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||||
|
and "hash=on" in output and len(hashes) == 12 and stable)
|
||||||
|
detail = f"hashes={len(hashes)}/12 stable={stable}"
|
||||||
|
code = result.returncode if result is not None else "TIMEOUT"
|
||||||
|
rows.append((kind, recipe, compiler, workers, elapsed, code, valid))
|
||||||
|
failed |= not valid
|
||||||
|
print(f"[L37DeterminismRun] kind={kind} recipe={recipe} compiler={compiler} "
|
||||||
|
f"workers={workers} elapsed={elapsed:.1f}s {detail} "
|
||||||
|
f"{'PASS' if valid else 'FAIL'} log={log}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
for kind in ("l25", "crossing24"):
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler in BINARIES:
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
run(kind, recipe, compiler, workers, OWNER,
|
||||||
|
crossing=(kind == "crossing24"))
|
||||||
|
|
||||||
|
for compiler in BINARIES:
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
run("upper-fixture", "floating-islands", compiler, workers, FIXTURE,
|
||||||
|
passage_audit=True)
|
||||||
|
|
||||||
|
summary = OUT / "determinism-l37.csv"
|
||||||
|
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.writer(stream)
|
||||||
|
writer.writerow(("kind", "recipe", "compiler", "workers", "elapsed_seconds",
|
||||||
|
"exit_code", "status"))
|
||||||
|
for row in rows:
|
||||||
|
writer.writerow((*row[:-1], "PASS" if row[-1] else "FAIL"))
|
||||||
|
print(f"[L37DeterminismSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 30 else 0)
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l37"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, binary, data, recipe, extra, checks):
|
||||||
|
global failed
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "900s",
|
||||||
|
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", *extra,
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
checks_ok = all(check(text) for check in checks)
|
||||||
|
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||||
|
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
owner = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for mode in ("hard", "gradient", "interleaved"):
|
||||||
|
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||||
|
text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||||
|
|
||||||
|
summary = OUT / "invariant-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L33InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the L13 route parity and L9 uniform-block proof audits on L37."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l37"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||||
|
}
|
||||||
|
RECIPES = ("floating-islands", "surface-world")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for recipe in RECIPES:
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"legacy-route-proof-{recipe}-{compiler}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "900s", str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--stream-hash", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", "--audit-uniform-block-proofs", "--audit-route-parity",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamp = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
output, re.M)
|
||||||
|
route = re.findall(r"^\[RouteParityAudit\].*$", output, re.M)
|
||||||
|
proofs = re.findall(r"^\[UniformBlockProofAudit\].*$", output, re.M)
|
||||||
|
stamp_ok = stamp == [(recipe, recipe, "PASS")]
|
||||||
|
route_ok = len(route) == 1 and route[0].endswith("status=PASS")
|
||||||
|
proof_ok = len(proofs) == 1 and bool(
|
||||||
|
re.search(r"violations=0(?:\s|$)", proofs[0]))
|
||||||
|
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||||
|
and route_ok and proof_ok)
|
||||||
|
code = result.returncode if result is not None else "TIMEOUT"
|
||||||
|
rows.append((name, recipe, compiler, elapsed, code, route_ok, bool(proof_ok), ok))
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L37LegacyAuditRun] case={name} elapsed={elapsed:.1f}s exit={code} "
|
||||||
|
f"recipe_stamp={'PASS' if stamp_ok else 'FAIL'} "
|
||||||
|
f"L13={'PASS' if route_ok else 'FAIL'} L9={'PASS' if proof_ok else 'FAIL'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
summary = OUT / "legacy-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"case,recipe,compiler,elapsed_seconds,exit_code,l13,l9,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if l13 else 'FAIL'},{'PASS' if l9 else 'FAIL'},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, compiler, elapsed, code, l13, l9, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L37LegacyAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 4 else 0)
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Freeze a 20-degree mouth-crease gate and project its sample spacing at 1440p."""
|
||||||
|
import ast
|
||||||
|
import contextlib
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
BASE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37/RunMouthFragmentAuditL37.py"
|
||||||
|
SPEC = importlib.util.spec_from_file_location("mouth_fragment_l37", BASE_PATH)
|
||||||
|
BASE = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(BASE)
|
||||||
|
|
||||||
|
# Frozen before the L37 mouth edit: 20 degrees over the audit's 0.125 m samples is
|
||||||
|
# the visible-crease limit. At the 6.016 m standard camera and 80-degree horizontal
|
||||||
|
# FOV, each sample gap spans multiple pixels at 2560x1440.
|
||||||
|
BASE.NORMAL_LIMIT_DEG = 20.0
|
||||||
|
CAMERA = (1.691820, -3.424982, -54.224198)
|
||||||
|
TARGET = (6.298233, 1.137233, -55.468264)
|
||||||
|
FOV_DEG = 80.0
|
||||||
|
WIDTH_1440P = 2560
|
||||||
|
HEIGHT_1440P = 1440
|
||||||
|
MOUTH = (5.600, 0.445, -55.156)
|
||||||
|
OVERVIEW_CAMERA = (MOUTH[0] - 9.75, MOUTH[1] - 11.4, MOUTH[2] + 8.0)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(vector):
|
||||||
|
magnitude = math.sqrt(sum(value * value for value in vector))
|
||||||
|
return tuple(value / magnitude for value in vector)
|
||||||
|
|
||||||
|
|
||||||
|
def view_basis(camera, target):
|
||||||
|
forward = normalize(tuple(target[i] - camera[i] for i in range(3)))
|
||||||
|
right = normalize((forward[1], -forward[0], 0.0))
|
||||||
|
up = (right[1] * forward[2] - right[2] * forward[1],
|
||||||
|
right[2] * forward[0] - right[0] * forward[2],
|
||||||
|
right[0] * forward[1] - right[1] * forward[0])
|
||||||
|
return forward, right, up
|
||||||
|
|
||||||
|
|
||||||
|
def visible_crease(path, camera, target):
|
||||||
|
cx, cy = BASE.CENTER
|
||||||
|
bounds = (cx - BASE.OUTER, cx + BASE.OUTER, cy - BASE.OUTER, cy + BASE.OUTER)
|
||||||
|
voxel_um, triangles, _ = BASE.RING.read_scene(
|
||||||
|
str(path), 0, BASE.HEIGHT_MIN, BASE.HEIGHT_MAX, bounds)
|
||||||
|
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||||
|
buckets = BASE.RING.build_buckets(triangles, True, bounds, cell)
|
||||||
|
forward, right, up = view_basis(camera, target)
|
||||||
|
tan_h = math.tan(math.radians(FOV_DEG / 2.0))
|
||||||
|
tan_v = math.tan(math.atan(tan_h * HEIGHT_1440P / WIDTH_1440P))
|
||||||
|
focal = WIDTH_1440P / (2.0 * tan_h)
|
||||||
|
lines = []
|
||||||
|
count = round((BASE.OUTER - BASE.INNER) / BASE.STEP)
|
||||||
|
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||||
|
angle = math.radians(degrees)
|
||||||
|
lines.append([(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||||
|
for radius in (BASE.INNER + index * BASE.STEP
|
||||||
|
for index in range(count + 1))])
|
||||||
|
tangent_count = round(8.0 / BASE.STEP)
|
||||||
|
for degrees in (225.0, 315.0):
|
||||||
|
angle = math.radians(degrees)
|
||||||
|
normal = (math.cos(angle), math.sin(angle))
|
||||||
|
tangent = (-normal[1], normal[0])
|
||||||
|
lines.append([(cx + 6.5 * normal[0] + (-4.0 + index * BASE.STEP) * tangent[0],
|
||||||
|
cy + 6.5 * normal[1] + (-4.0 + index * BASE.STEP) * tangent[1])
|
||||||
|
for index in range(tangent_count + 1)])
|
||||||
|
|
||||||
|
maximum, largest_pixels, worst_depth, visible_pairs = 0.0, 0.0, 0.0, 0
|
||||||
|
worst_pair_pixels = 0.0
|
||||||
|
worst_pair = None
|
||||||
|
|
||||||
|
def project(point):
|
||||||
|
offset = tuple(point[i] - camera[i] for i in range(3))
|
||||||
|
depth = sum(offset[i] * forward[i] for i in range(3))
|
||||||
|
x = sum(offset[i] * right[i] for i in range(3))
|
||||||
|
y = sum(offset[i] * up[i] for i in range(3))
|
||||||
|
return depth, x, y
|
||||||
|
|
||||||
|
for points in lines:
|
||||||
|
samples = [BASE.RING.sample_surface(buckets, x, y, True, cell, BASE.PREFERRED)
|
||||||
|
for x, y in points]
|
||||||
|
for index in range(1, len(samples)):
|
||||||
|
before, current = samples[index - 1], samples[index]
|
||||||
|
if before is None or current is None:
|
||||||
|
continue
|
||||||
|
p0 = (points[index - 1][0], points[index - 1][1], before[0])
|
||||||
|
p1 = (points[index][0], points[index][1], current[0])
|
||||||
|
midpoint = tuple((p0[axis] + p1[axis]) * 0.5 for axis in range(3))
|
||||||
|
depth, horizontal, vertical = project(midpoint)
|
||||||
|
if (depth <= 0.0 or abs(horizontal) > depth * tan_h
|
||||||
|
or abs(vertical) > depth * tan_v):
|
||||||
|
continue
|
||||||
|
visible_pairs += 1
|
||||||
|
angle = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||||
|
sum(a * b for a, b in zip(before[1], current[1]))))))
|
||||||
|
d0, x0, y0 = project(p0)
|
||||||
|
d1, x1, y1 = project(p1)
|
||||||
|
pixels = math.hypot(focal * x0 / d0 - focal * x1 / d1,
|
||||||
|
focal * y0 / d0 - focal * y1 / d1)
|
||||||
|
largest_pixels = max(largest_pixels, pixels)
|
||||||
|
if angle > maximum:
|
||||||
|
maximum, worst_pair, worst_depth = angle, (p0, p1), depth
|
||||||
|
worst_pair_pixels = pixels
|
||||||
|
return maximum, worst_depth, largest_pixels, visible_pairs, worst_pair, worst_pair_pixels
|
||||||
|
|
||||||
|
|
||||||
|
def run(path, label):
|
||||||
|
stream = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(stream):
|
||||||
|
passed, maximum = BASE.audit(path)
|
||||||
|
output = stream.getvalue()
|
||||||
|
print(output, end="")
|
||||||
|
match = re.search(r"worst_pair=(\([^\n]*\)) status=", output)
|
||||||
|
worst = ast.literal_eval(match.group(1)) if match else None
|
||||||
|
path = Path(path)
|
||||||
|
near = visible_crease(path, CAMERA, TARGET)
|
||||||
|
far = visible_crease(path, OVERVIEW_CAMERA, TARGET)
|
||||||
|
print(f"[MouthCreasePixels] build={label} sample_step_m={BASE.STEP:.3f} "
|
||||||
|
f"horizontal_fov_deg={FOV_DEG:.1f} resolution=2560x1440 "
|
||||||
|
f"near_camera_to_mouth_m=6.016 near_visible_pairs={near[3]} "
|
||||||
|
f"near_max_visible_deg={near[0]:.6f} near_depth_m={near[1]:.3f} "
|
||||||
|
f"near_sample_spacing_px={near[2]:.2f} "
|
||||||
|
f"near_worst_pair_spacing_px={near[5]:.2f} "
|
||||||
|
f"overview_camera_to_mouth_m={math.dist(OVERVIEW_CAMERA, MOUTH):.3f} "
|
||||||
|
f"overview_visible_pairs={far[3]} overview_max_visible_deg={far[0]:.6f} "
|
||||||
|
f"overview_depth_m={far[1]:.3f} overview_sample_spacing_px={far[2]:.2f} "
|
||||||
|
f"overview_worst_pair_spacing_px={far[5]:.2f} "
|
||||||
|
f"overall_max_deg={maximum:.6f}")
|
||||||
|
return passed, maximum
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--candidate", type=Path,
|
||||||
|
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
||||||
|
args = parser.parse_args()
|
||||||
|
paths = {
|
||||||
|
"L35": ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh",
|
||||||
|
"L36": ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh",
|
||||||
|
"L37": args.candidate if args.candidate.is_absolute()
|
||||||
|
else ROOT / args.candidate,
|
||||||
|
}
|
||||||
|
results = {label: run(path, label) for label, path in paths.items()}
|
||||||
|
controls = (not results["L35"][0] and not results["L36"][0]
|
||||||
|
and results["L35"][1] > 20.0 and results["L36"][1] > 20.0)
|
||||||
|
final = results["L37"][0] and results["L37"][1] <= 20.0
|
||||||
|
print(f"[MouthCreaseGate] normal_limit_deg=20 frozen_before_fix=true "
|
||||||
|
f"L35_42deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||||
|
f"L36_27deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||||
|
f"L37_candidate={'PASS' if final else 'FAIL'} "
|
||||||
|
f"status={'PASS' if controls and final else 'FAIL'}")
|
||||||
|
raise SystemExit(0 if controls and final else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare the FI fade-ring crease against L35 using the same surface samples."""
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
RING_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
|
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_PATH)
|
||||||
|
RING = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(RING)
|
||||||
|
|
||||||
|
CENTER = (6.298233, 1.137233)
|
||||||
|
INNER, OUTER, STEP = 5.5, 9.5, 0.125
|
||||||
|
HEIGHT_MIN, HEIGHT_MAX, PREFERRED = -80.0, -40.0, -56.0
|
||||||
|
# Chosen before the L37 mouth edit: a 20-degree normal jump over one 0.125 m
|
||||||
|
# sample interval is visible at the standard camera distance.
|
||||||
|
NORMAL_LIMIT_DEG = 20.0
|
||||||
|
COVERAGE_MIN = 0.90
|
||||||
|
FRAGMENT_MAX_TRIANGLES = 64
|
||||||
|
FRAGMENT_MIN_AREA_M2 = 0.0001
|
||||||
|
FRAGMENT_MAX_AREA_M2 = 0.05
|
||||||
|
FRAGMENT_MAX_DISTANCE_M = 12.0
|
||||||
|
|
||||||
|
|
||||||
|
def small_components(triangles):
|
||||||
|
parent = list(range(len(triangles)))
|
||||||
|
sizes = [1] * len(triangles)
|
||||||
|
owners = {}
|
||||||
|
|
||||||
|
def find(index):
|
||||||
|
while parent[index] != index:
|
||||||
|
parent[index] = parent[parent[index]]
|
||||||
|
index = parent[index]
|
||||||
|
return index
|
||||||
|
|
||||||
|
def union(a, b):
|
||||||
|
a, b = find(a), find(b)
|
||||||
|
if a == b:
|
||||||
|
return
|
||||||
|
if sizes[a] < sizes[b]:
|
||||||
|
a, b = b, a
|
||||||
|
parent[b] = a
|
||||||
|
sizes[a] += sizes[b]
|
||||||
|
|
||||||
|
for index, (_, _, vertices) in enumerate(triangles):
|
||||||
|
for point, _ in vertices:
|
||||||
|
key = tuple(round(value, 5) for value in point)
|
||||||
|
other = owners.setdefault(key, index)
|
||||||
|
union(index, other)
|
||||||
|
|
||||||
|
components = {}
|
||||||
|
for index, (_, _, vertices) in enumerate(triangles):
|
||||||
|
root = find(index)
|
||||||
|
item = components.setdefault(root, {"count": 0, "area": 0.0, "sum": [0.0] * 3})
|
||||||
|
item["count"] += 1
|
||||||
|
points = [vertex[0] for vertex in vertices]
|
||||||
|
for point in points:
|
||||||
|
for axis in range(3):
|
||||||
|
item["sum"][axis] += point[axis]
|
||||||
|
ab = tuple(points[1][axis] - points[0][axis] for axis in range(3))
|
||||||
|
ac = tuple(points[2][axis] - points[0][axis] for axis in range(3))
|
||||||
|
cross = (ab[1] * ac[2] - ab[2] * ac[1],
|
||||||
|
ab[2] * ac[0] - ab[0] * ac[2],
|
||||||
|
ab[0] * ac[1] - ab[1] * ac[0])
|
||||||
|
item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross))
|
||||||
|
|
||||||
|
cx, cy = CENTER
|
||||||
|
small = []
|
||||||
|
below_floor = []
|
||||||
|
for item in components.values():
|
||||||
|
centroid = tuple(value / (3 * item["count"]) for value in item["sum"])
|
||||||
|
distance = math.hypot(centroid[0] - cx, centroid[1] - cy)
|
||||||
|
if (item["count"] <= FRAGMENT_MAX_TRIANGLES
|
||||||
|
and item["area"] <= FRAGMENT_MAX_AREA_M2
|
||||||
|
and distance <= FRAGMENT_MAX_DISTANCE_M):
|
||||||
|
record = (item["count"], item["area"], distance, centroid)
|
||||||
|
(below_floor if item["area"] < FRAGMENT_MIN_AREA_M2 else small).append(record)
|
||||||
|
return len(components), small, below_floor
|
||||||
|
|
||||||
|
|
||||||
|
def audit(path):
|
||||||
|
cx, cy = CENTER
|
||||||
|
bounds = (cx - OUTER, cx + OUTER, cy - OUTER, cy + OUTER)
|
||||||
|
voxel_um, triangles, tiles = RING.read_scene(
|
||||||
|
str(path), 0, HEIGHT_MIN, HEIGHT_MAX, bounds)
|
||||||
|
component_count, fragments, below_floor = small_components(triangles)
|
||||||
|
print(f"[MouthFragmentComponents] mesh={path.name} components={component_count} "
|
||||||
|
f"small_near_mouth={len(fragments)} subthreshold_near_mouth={len(below_floor)} "
|
||||||
|
f"limits=(triangles<={FRAGMENT_MAX_TRIANGLES},"
|
||||||
|
f"area={FRAGMENT_MIN_AREA_M2:.4f}..{FRAGMENT_MAX_AREA_M2:.3f}m2,"
|
||||||
|
f"distance<={FRAGMENT_MAX_DISTANCE_M:.1f}m) "
|
||||||
|
f"first={fragments[0] if fragments else None} "
|
||||||
|
f"subthreshold={below_floor[0] if below_floor else None} "
|
||||||
|
f"status={'PASS' if not fragments else 'FAIL'}")
|
||||||
|
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||||
|
buckets = RING.build_buckets(triangles, True, bounds, cell)
|
||||||
|
lines = []
|
||||||
|
count = round((OUTER - INNER) / STEP)
|
||||||
|
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||||
|
angle = math.radians(degrees)
|
||||||
|
lines.append((f"radial-{degrees:.0f}", [
|
||||||
|
(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||||
|
for radius in (INNER + i * STEP for i in range(count + 1))]))
|
||||||
|
tangent_count = round(8.0 / STEP)
|
||||||
|
for degrees in (225.0, 315.0):
|
||||||
|
angle = math.radians(degrees)
|
||||||
|
normal = (math.cos(angle), math.sin(angle))
|
||||||
|
tangent = (-normal[1], normal[0])
|
||||||
|
lines.append((f"tangent-{degrees:.0f}", [
|
||||||
|
(cx + 6.5 * normal[0] + (-4.0 + i * STEP) * tangent[0],
|
||||||
|
cy + 6.5 * normal[1] + (-4.0 + i * STEP) * tangent[1])
|
||||||
|
for i in range(tangent_count + 1)]))
|
||||||
|
|
||||||
|
max_angle = 0.0
|
||||||
|
max_pair = None
|
||||||
|
min_coverage = 1.0
|
||||||
|
for name, points in lines:
|
||||||
|
samples = [RING.sample_surface(buckets, x, y, True, cell, PREFERRED)
|
||||||
|
for x, y in points]
|
||||||
|
coverage = sum(value is not None for value in samples) / len(samples)
|
||||||
|
min_coverage = min(min_coverage, coverage)
|
||||||
|
line_max = 0.0
|
||||||
|
for index in range(1, len(samples)):
|
||||||
|
prior, current = samples[index - 1], samples[index]
|
||||||
|
if prior is None or current is None:
|
||||||
|
continue
|
||||||
|
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1]))))
|
||||||
|
delta = math.degrees(math.acos(dot))
|
||||||
|
if delta > line_max:
|
||||||
|
line_max = delta
|
||||||
|
if delta > max_angle:
|
||||||
|
max_angle = delta
|
||||||
|
max_pair = (name, points[index - 1], prior[0], points[index], current[0])
|
||||||
|
print(f"[MouthFadeLine] mesh={path.name} line={name} coverage={coverage:.3f} "
|
||||||
|
f"max_normal_jump_deg={line_max:.6f}")
|
||||||
|
normal_pass = max_angle <= NORMAL_LIMIT_DEG and min_coverage >= COVERAGE_MIN
|
||||||
|
passed = normal_pass and not fragments
|
||||||
|
print(f"[MouthFadeAudit] mesh={path.name} tiles={tiles} lines={len(lines)} "
|
||||||
|
f"coverage_min={min_coverage:.3f} coverage_limit={COVERAGE_MIN:.2f} "
|
||||||
|
f"max_normal_jump_deg={max_angle:.6f} normal_limit_deg={NORMAL_LIMIT_DEG:.1f} "
|
||||||
|
f"fragments={len(fragments)} "
|
||||||
|
f"worst_pair={max_pair} status={'PASS' if passed else 'FAIL'}")
|
||||||
|
return passed, max_angle
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--control", type=Path,
|
||||||
|
default=ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh")
|
||||||
|
parser.add_argument("--prior", type=Path,
|
||||||
|
default=ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh")
|
||||||
|
parser.add_argument("--current", type=Path,
|
||||||
|
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
||||||
|
args = parser.parse_args()
|
||||||
|
control = args.control if args.control.is_absolute() else ROOT / args.control
|
||||||
|
prior = args.prior if args.prior.is_absolute() else ROOT / args.prior
|
||||||
|
current = args.current if args.current.is_absolute() else ROOT / args.current
|
||||||
|
control_ok, control_max = audit(control)
|
||||||
|
prior_ok, prior_max = audit(prior)
|
||||||
|
current_ok, current_max = audit(current)
|
||||||
|
controls = (not control_ok and not prior_ok
|
||||||
|
and control_max > NORMAL_LIMIT_DEG and prior_max > NORMAL_LIMIT_DEG)
|
||||||
|
expected = controls and current_ok and current_max <= NORMAL_LIMIT_DEG
|
||||||
|
print(f"[MouthFadeNegativeControl] L35_max_deg={control_max:.6f} "
|
||||||
|
f"L35_detected={not control_ok} L36_max_deg={prior_max:.6f} "
|
||||||
|
f"L36_detected={not prior_ok} L37_max_deg={current_max:.6f} "
|
||||||
|
f"normal_limit_deg={NORMAL_LIMIT_DEG:.1f} current_pass={current_ok} "
|
||||||
|
f"status={'PASS' if expected else 'FAIL'}")
|
||||||
|
raise SystemExit(0 if expected else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run recipe-stamped passage/profile audits for both compilers and both owner recipes."""
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l37"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||||
|
}
|
||||||
|
RUNS = []
|
||||||
|
for recipe, filename in (
|
||||||
|
("floating-islands", "floating-islands"),
|
||||||
|
("surface-world", "surface-world"),
|
||||||
|
):
|
||||||
|
for style in range(4):
|
||||||
|
RUNS.append((f"owner-{filename}-style{style}",
|
||||||
|
ROOT / f"Saved/Linux/linux25-owner-{filename}-style{style}.json", recipe))
|
||||||
|
for style in range(4):
|
||||||
|
RUNS.append((f"upper-sw-fixture-style{style}",
|
||||||
|
OUT.parent / "l33/fixtures" / f"sw-upper-tunnel-network-style{style}.json",
|
||||||
|
"floating-islands"))
|
||||||
|
|
||||||
|
RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||||
|
PASSAGE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||||
|
MOUTH = re.compile(r"^\[PassageMouthProfileAudit\].*status=PASS$", re.M)
|
||||||
|
ROUTE = re.compile(r"^\[ResolvedStrateRouteAudit\].*mouth_binding=PASS status=PASS$", re.M)
|
||||||
|
CHECKS = (
|
||||||
|
r"^\[ResolvedLayoutStrict\] accepted=true issues=0 error=$",
|
||||||
|
r"^\[ResolvedLayoutValidatorControls\].*status=PASS$",
|
||||||
|
r"^\[PassageMouthProfileNegativeControl\].*source=L23_terraced_geometry.*detected=true$",
|
||||||
|
r"^\[PassageMouthProfileSummary\].*l23_terrace_negative_control=DETECTED status=PASS$",
|
||||||
|
r"^\[PassageNaturalnessSummary\].*l23_negative_control=DETECTED.*status=PASS$",
|
||||||
|
r"^\[PassageRippleAuditSummary\].*synthetic_spheres_negative_control=DETECTED max_amplitude_m=0\.035 floor_beaded_negative_control=DETECTED max_floor_amplitude_m=0\.015 status=PASS$",
|
||||||
|
r"^\[PassageOpenAirAuditSummary\].*violations=0.*l24_negative_control=DETECTED status=PASS$",
|
||||||
|
r"^\[PassageNoLongBridgeAudit\].*status=PASS$",
|
||||||
|
r"^\[PassageRockColumnAuditSummary\].*status=PASS$",
|
||||||
|
r"^\[ResolvedPassageAuditSummary\].*route_binding_failures=0 .*status=PASS$",
|
||||||
|
r"^\[Walkability\] PASS",
|
||||||
|
r"^\[FloorProfileBlend\].*negative_controls=step_detected/slope_detected status=PASS$",
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for name, data, recipe in RUNS:
|
||||||
|
logfile = OUT / f"passage-audit-{name}-{compiler}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", str(binary), str(data), "--workers=12",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamp_ok = RECIPE.findall(output) == [(recipe, recipe)]
|
||||||
|
passages = len(PASSAGE.findall(output))
|
||||||
|
mouths = len(MOUTH.findall(output))
|
||||||
|
routes = len(ROUTE.findall(output))
|
||||||
|
checks_ok = all(re.search(pattern, output, re.M) for pattern in CHECKS)
|
||||||
|
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||||
|
and passages >= 1 and mouths == passages and routes >= 1 and checks_ok)
|
||||||
|
code = result.returncode if result is not None else "TIMEOUT"
|
||||||
|
rows.append((name, recipe, compiler, elapsed, code, passages, mouths, routes, ok))
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L37PassageAuditRun] case={name} compiler={compiler} recipe={recipe} "
|
||||||
|
f"elapsed={elapsed:.1f}s exit={code} passages={passages} "
|
||||||
|
f"profiles={mouths} routes={routes} {'PASS' if ok else 'FAIL'} "
|
||||||
|
f"log={logfile}", flush=True)
|
||||||
|
|
||||||
|
summary = OUT / "passage-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"case,recipe,compiler,elapsed_seconds,exit_code,passages,mouth_profiles,routes,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},{passages},{mouths},{routes},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, compiler, elapsed, code, passages, mouths, routes, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L37PassageAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 24 else 0)
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export v24 ring meshes at matching LOD windows and retain old controls."""
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l37"
|
||||||
|
BIN = OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang"
|
||||||
|
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
TERRACE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
|
RING_SET = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py"
|
||||||
|
STAMP = re.compile(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||||
|
EXPORT = re.compile(r"^\[MeshSceneExport\].*status=PASS", re.M)
|
||||||
|
TERRACE_RESULT = re.compile(r"^\[RingTerraceAudit\].*status=(PASS|FAIL)$", re.M)
|
||||||
|
SET_RESULT = re.compile(r"^\[RingSetAudit\].*status=(PASS|FAIL)$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
FI_CENTERS = ((56, -72), (72, -72), (56, -88), (40, -72), (24, 24),
|
||||||
|
(104, 24), (72, -88), (120, 24), (104, 8), (24, -24))
|
||||||
|
SW_CENTERS = ((-136, 136), (-56, 152), (-152, 120), (-136, 120),
|
||||||
|
(-152, 104), (-136, 104), (-40, 152))
|
||||||
|
LOD_CENTERS = {
|
||||||
|
"fi-floor": {
|
||||||
|
2: (16, -144), 3: (224, -160), 4: (-448, 448),
|
||||||
|
5: (-128, -640), 6: (-1792, 1280), 7: (3584, 1536),
|
||||||
|
8: (3072, 7168),
|
||||||
|
},
|
||||||
|
"sw-ceiling": {
|
||||||
|
2: (16, -144), 3: (224, -160), 4: (-448, 448),
|
||||||
|
5: (-128, -640), 6: (-1792, 1280), 7: (2560, -512),
|
||||||
|
8: (3072, 7168),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, command):
|
||||||
|
log = OUT / f"ring-{name}.log"
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", *map(str, command)]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
output = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
code = result.returncode if result is not None else "TIMEOUT"
|
||||||
|
print(f"[L37RingStep] name={name} elapsed={elapsed:.1f}s exit={code} log={log}",
|
||||||
|
flush=True)
|
||||||
|
return result, output, elapsed
|
||||||
|
|
||||||
|
|
||||||
|
def export(label, recipe, max_level, camera):
|
||||||
|
mesh = OUT / f"ring-owner-{label}-v24.vfmesh"
|
||||||
|
command = [BIN, OWNER, "--workers=12",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
f"--max-level={max_level}", f"--recipe={recipe}"]
|
||||||
|
if camera:
|
||||||
|
command.append(f"--camera={camera}")
|
||||||
|
command.append(f"--mesh-export={mesh}")
|
||||||
|
result, output, elapsed = run(f"export-{label}", command)
|
||||||
|
stamps = STAMP.findall(output)
|
||||||
|
ok = (result is not None and result.returncode == 0
|
||||||
|
and stamps == [(recipe, recipe)] and EXPORT.search(output) and mesh.is_file())
|
||||||
|
print(f"[L37RingExport] scene={label} recipe={recipe} stamp={stamps} "
|
||||||
|
f"bytes={mesh.stat().st_size if mesh.exists() else 0} "
|
||||||
|
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
return mesh, elapsed, ok
|
||||||
|
|
||||||
|
|
||||||
|
def terrace(label, recipe, surface, mesh, lod, height_min, height_max,
|
||||||
|
center=(64, -32), radius=32):
|
||||||
|
result, output, elapsed = run(f"{label}-lod{lod}", ["python3", TERRACE, mesh,
|
||||||
|
f"--surface={surface}", "--center", str(center[0]), str(center[1]),
|
||||||
|
f"--radius={radius}",
|
||||||
|
f"--lod={lod}", f"--height-min-m={height_min}",
|
||||||
|
f"--height-max-m={height_max}"])
|
||||||
|
statuses = TERRACE_RESULT.findall(output)
|
||||||
|
ok = (result is not None and result.returncode == 0 and statuses == ["PASS"])
|
||||||
|
rows.append((label, recipe, surface, lod, elapsed, ok))
|
||||||
|
print(f"[L37RingAuditRun] scene={label} recipe={recipe} surface={surface} "
|
||||||
|
f"lod={lod} center={center} radius={radius} "
|
||||||
|
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def lod1_set(label, recipe, surface, mesh, centers, height_min, height_max):
|
||||||
|
command = ["python3", RING_SET, mesh, f"--surface={surface}"]
|
||||||
|
for x, y in centers:
|
||||||
|
command.extend(("--center", str(x), str(y)))
|
||||||
|
command.extend(("--radius=8", "--lod=1", f"--height-min-m={height_min}",
|
||||||
|
f"--height-max-m={height_max}"))
|
||||||
|
result, output, elapsed = run(f"{label}-lod1-set", command)
|
||||||
|
statuses = SET_RESULT.findall(output)
|
||||||
|
ok = (result is not None and result.returncode == 0 and statuses == ["PASS"])
|
||||||
|
rows.append((label, recipe, surface, 1, elapsed, ok))
|
||||||
|
print(f"[L37RingAuditRun] scene={label} recipe={recipe} surface={surface} "
|
||||||
|
f"lod=1 windows={len(centers)} status={'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
for label, recipe, surface, window_min, window_max, camera0, camera1, centers in (
|
||||||
|
("fi-floor", "floating-islands", "floor", -122.0, -117.0,
|
||||||
|
"64,-32,-115,0,-45", "64,-32,-115,0,-45", FI_CENTERS),
|
||||||
|
("sw-ceiling", "surface-world", "ceiling", -132.0, -124.0,
|
||||||
|
"64,-32,-136,0,45", "-96,128,-136,0,45", SW_CENTERS)):
|
||||||
|
low_mesh, _, low_ok = export(f"{label}-lod0", recipe, 0, camera0)
|
||||||
|
failed |= not low_ok
|
||||||
|
if low_ok:
|
||||||
|
failed |= not terrace(label, recipe, surface, low_mesh, 0, window_min, window_max)
|
||||||
|
|
||||||
|
detail_mesh, _, detail_ok = export(f"{label}-lod1", recipe, 1, camera1)
|
||||||
|
failed |= not detail_ok
|
||||||
|
if detail_ok:
|
||||||
|
set_min, set_max = ((-121.625, -117.375) if label == "fi-floor"
|
||||||
|
else (-128.375, -124.125))
|
||||||
|
failed |= not lod1_set(label, recipe, surface, detail_mesh, centers,
|
||||||
|
set_min, set_max)
|
||||||
|
|
||||||
|
full_mesh, _, full_ok = export(f"{label}-lod2-8", recipe, 8, None)
|
||||||
|
failed |= not full_ok
|
||||||
|
if full_ok:
|
||||||
|
for lod in range(2, 9):
|
||||||
|
center = LOD_CENTERS[label][lod]
|
||||||
|
radius = 4 * (1 << lod)
|
||||||
|
failed |= not terrace(label, recipe, surface, full_mesh, lod,
|
||||||
|
window_min, window_max, center, radius)
|
||||||
|
|
||||||
|
# Keep the established L30 terrace and L31 flat-surface negative controls live.
|
||||||
|
controls = (
|
||||||
|
("l30-terrace", ROOT / "Saved/Linux/l32/fi-floor-l30-camera-negative.vfmesh",
|
||||||
|
-130.0, -110.0),
|
||||||
|
("l31-flat", ROOT / "Saved/Linux/l31/fi-floor-v19.vfmesh", -122.0, -117.0),
|
||||||
|
)
|
||||||
|
for name, mesh, height_min, height_max in controls:
|
||||||
|
result, output, _ = run(f"control-{name}", ["python3", TERRACE, mesh,
|
||||||
|
"--surface=floor", "--center", "64", "-32", "--radius=32", "--lod=0",
|
||||||
|
f"--height-min-m={height_min}", f"--height-max-m={height_max}"])
|
||||||
|
statuses = TERRACE_RESULT.findall(output)
|
||||||
|
detected = (result is not None and result.returncode == 1 and statuses == ["FAIL"])
|
||||||
|
failed |= not detected
|
||||||
|
print(f"[L37RingControl] name={name} detected={detected} "
|
||||||
|
f"status={'PASS' if detected else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
summary = OUT / "ring-audits-l37.csv"
|
||||||
|
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.writer(stream)
|
||||||
|
writer.writerow(("scene", "recipe", "surface", "lod", "elapsed_seconds", "status"))
|
||||||
|
writer.writerows((label, recipe, surface, lod, f"{elapsed:.3f}",
|
||||||
|
"PASS" if ok else "FAIL")
|
||||||
|
for label, recipe, surface, lod, elapsed, ok in rows)
|
||||||
|
print(f"[L37RingSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} controls=2 "
|
||||||
|
f"status={'FAIL' if failed else 'PASS'} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 18 else 0)
|
||||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,31 @@
|
|||||||
|
# Research R1 — natural, walkable cave mouths and tunnels
|
||||||
|
|
||||||
|
## What the references show
|
||||||
|
|
||||||
|
- **Deep Rock Galactic** separates room/exit structure from biome noise and debris; designers test generated variants for traversal and wayfinding. **Minecraft 1.18** distinguishes caverns from spaghetti and noodle tunnels, and its noise caves form entrances where they intersect the surface. These support a planned route plus a terrain-native mouth, not a portal stamp. [Ghost Ship’s cave-generation post](https://store.steampowered.com/news/posts/?appgroupname=Deep+Rock+Galactic&appids=548430&enddate=1729500950&feed=steam_community_announcements), [Mojang 1.18 notes](https://www.minecraft.net/de-de/article/caves---cliffs--part-ii-out-today-java)
|
||||||
|
- **Astroneer** describes layered on-the-fly noise with separate cave layers. **No Man’s Sky** identifies navigable caves as a goal and documents dual marching-cubes performance work, but does not publish the cave-shape algorithm. Use these as support for deterministic layers and navigation goals, not as evidence of a specific implementation. [Astroneer](https://www.unrealengine.com/spotlights/how-system-era-softworks-leveraged-ue4-to-create-astroneer-s-wonderful-universe?lang=en-US), [NMS terrain](https://www.nomanssky.com/next-update/), [NMS meshing](https://www.nomanssky.com/worlds-part-I-update/)
|
||||||
|
- **Enshrouded** describes sculpted macro-topology and hand-placed voxel stamps. **Valheim** documents room assets used by its dungeon generator; **Teardown** documents VoxTool for complex voxel terrain assets. These are topology/tooling examples, not disclosed SDF mouth techniques. [Enshrouded](https://enshrouded.com/ja-JP/news/enshrouded-world-design-team-reddit-ama-recap), [Valheim](https://valheim.com/support/modding-faq-for-the-asset-bundle-update-0-217-40/), [Teardown](https://teardowngame.com/voxtool/)
|
||||||
|
- Cave research separates playable topology from cave appearance. A karst SDF paper models irregular cross-sections and warns that curved sweeps can lose continuity at high curvature; tortuous paths use a union of spheres. Erosion papers model morphology at generation time, not per voxel. [Playable cave systems](https://santosgrueiro.com/papers/2014/2014-Santam-Cave.pdf), [geological cave SDFs](https://diglib.eg.org/items/a4e7985c-af52-4b55-bd1a-eeeff6c47422), [karst erosion](https://www.sciencedirect.com/science/article/pii/S0097849321002132)
|
||||||
|
|
||||||
|
These references do not guarantee VoxelForge’s walk-only contract. Keep its deterministic route, 0.34 m × 1.76 m capsule, supported floor, and slope checks authoritative.
|
||||||
|
|
||||||
|
## Ranked proposals
|
||||||
|
|
||||||
|
| Rank | Proposal and code location | Expected gain | Main risk / cost |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **1** | **Limit the carve by a route floor.** Keep grade-safe route/floor data in [`PlanResolvedLayoutPassagesV1`](../../../../Standalone/Core/src/ResolvedLayoutInternal.h); in [`ApplyResolvedStratePassageCarves`](../../../../Standalone/Core/src/ShapeWorldInternal.h), constrain the tunnel void to above that floor and subtract it from native solid. For negative-is-solid fields: `void = max(tubeSdf, floorZ - z)`, then `carvedSolid = max(nativeSolid, -void)`. Blend the floor to native terrain height along route distance. Replace broad decks where native rock supports the route; keep only narrow fallback support where it does not. | Best chance of a worn path that remains part of the crater/rock instead of reading as a platform. The floor stays explicit and testable. | Medium walkability risk where the source has no solid underfoot or where FI coils overlap. Keep capsule, support, open-air, and slope gates. Low-to-moderate per-sample cost. |
|
||||||
|
| **2** | **Blend the mouth along route distance.** Refine `mouthCarveFade` / `roughSignedDistance` in `ApplyResolvedStratePassageCarves` (around lines 1168–1209) with a local smooth CSG transition between native terrain and bore. Carving uses smooth-max with the negated void field. Keep the floor cap separate. [Smooth-min reference](https://iquilezles.org/articles/smin/). | Removes a visible seam while letting the entrance follow the local landform. | Smooth CSG moves the zero surface and can round off thin rock or support. L36’s broad filter still left a 27° crease against the earlier 20° probe. Keep it local and hold the crease gate before comparing. Small field cost. |
|
||||||
|
| **3** | **Shape walls, not the walk plane.** Extend `considerCarveSegment` / `VF_FieldPassageWallRelief` (around lines 933–1043) with one bounded, seeded, low-frequency displacement or asymmetric profile, masked by wall orientation and distance from floor. [Domain warping](https://iquilezles.org/articles/warp/), [karst SDFs](https://diglib.eg.org/items/a4e7985c-af52-4b55-bd1a-eeeff6c47422). | Unequal pockets and folds add geology without ribs or a straight tread outline. | Too much detail can narrow capsule clearance or become periodic. One noise term is cheap; nested warps and curved cross-section sweeps are more expensive and need updated slope/Lipschitz bounds. |
|
||||||
|
| **4** | **Reuse the current spatial culling.** Bound each new mouth feature so `BuildPassageFeatureTileList`, `PassageMayAffectDensity`, and the nearest-segment BVH skip distant voxels. These AABB and route BVHs already exist ([architecture §8.45](../../../../ARCHITECTURE.md)). Lipschitz pruning is safe only with proven bounds that include smooth blend radii and warp derivatives. [SDF pruning research](https://onlinelibrary.wiley.com/doi/10.1111/cgf.70057). | Protects CPU budget with little geometry risk; lowest-risk performance path. | Little visual gain. Full cached/interpolated distance volumes add memory and can move the zero surface; consider only after profiling, with exact evaluation near the walk surface. |
|
||||||
|
|
||||||
|
## What not to repeat from L33–L37
|
||||||
|
|
||||||
|
- Regular world-space octaves produced visible 8 m / 4 m wall ribs. Keep relief irregular and seeded.
|
||||||
|
- Per-segment one-sided tread clamps produced floor washboard. Removing support altogether then caused unsupported samples; keep one continuous floor profile.
|
||||||
|
- The L34 apron, L36 trapezoid, and L37 raised bridge read as built geometry. The bridge SDF is a separate deck primitive in `ShapeWorldInternal.h`; do not widen it to repair cave shape.
|
||||||
|
- L36’s broad field filter removed most fragments but left a 27° crease, above the earlier 20° gate. Avoid broad blur and do not tune gates after seeing results.
|
||||||
|
- Wall-only ripple checks missed the floor beat. Keep walk-floor profiles and negative controls for periodic relief and built shapes.
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Prioritize the floor-limited carve and local terrain-to-bore blend. Let `ResolvedLayoutInternal.h` own the deterministic walk route and grade; let native density provide visible support; confine seeded detail to walls and ceiling. The owner has accepted the current L37 crater mouth, so apply this to the remaining bridge and future shaping without reopening that look.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Shared notes (researcher R1 <-> implementer L37), newest at bottom
|
||||||
|
|
||||||
|
- **Separate cave shape from cave dressing:** Ghost Ship describes a constrained room-and-exit skeleton, then biome-specific surface noise and debris; its designers test generated variants for traversal, wayfinding, visibility and class playability ([official DRG cave-generation post](https://store.steampowered.com/news/posts/?appgroupname=Deep+Rock+Galactic&appids=548430&enddate=1729500950&feed=steam_community_announcements)). For VoxelForge, keep `ResolvedLayoutInternal.h` responsible for the deterministic route / clearance skeleton and keep material-scale irregularity in `ShapeWorldInternal.h`; do not let a wall-noise change move the walk floor or route.
|
||||||
|
- **Use distinct tunnel and cavern scales:** Minecraft 1.18 explicitly separates large “cheese” rooms from spaghetti and narrow noodle passages, and lets those noise caves form mouths where they intersect the surface ([Mojang 1.18 notes](https://www.minecraft.net/de-de/article/caves---cliffs--part-ii-out-today-java)). This supports a broad, irregular crater-to-bore transition from intersecting fields, while preserving a narrower path-aligned walk lane; a single uniform portal cylinder is the wrong visual abstraction.
|
||||||
|
- **Layer deterministic noise, with a gameplay mask:** Astroneer’s published terrain system uses layered noise and separate cave layers evaluated on the fly without neighboring-cell dependency ([Unreal Engine interview](https://www.unrealengine.com/spotlights/how-system-era-softworks-leveraged-ue4-to-create-astroneer-s-wonderful-universe?lang=en-US)). This maps to pure seeded coordinate noise in `ShapeWorldInternal.h`, masked by distance-from-route / wall orientation so only the rock contour gets warped; keep the walk surface’s slope and capsule clearance under the existing route constraints.
|
||||||
|
- **Treat navigation as a hard constraint, then test variety:** DRG’s designers say a structurally valid room can still be unplayable after generator variation and playtest it repeatedly; No Man’s Sky’s patch notes identify navigability as an explicit terrain-generation goal ([DRG](https://store.steampowered.com/news/posts/?appgroupname=Deep+Rock+Galactic&appids=548430&enddate=1729500950&feed=steam_community_announcements), [NMS terrain update](https://www.nomanssky.com/next-update/)). For this code, keep the capsule/support/open-air and max-grade audits as geometry gates across seeded styles; visual naturalness checks supplement, never replace, them.
|
||||||
|
- **Author macro topology, procedurally fill the in-between:** Enshrouded’s world team describes sculpted rough models and hand-placed voxel stamps for topology between POIs ([official AMA recap](https://enshrouded.com/ja-JP/news/enshrouded-world-design-team-reddit-ama-recap)). Translation: route points and surrounding crater shape should determine the large form; use bounded relief as finish. Noise alone is not a substitute for an intentional entrance profile.
|
||||||
|
- **Avoid a single axisymmetric tube at the entrance:** The karst SDF work describes passage sections as irregular and asymmetric; for tortuous curves it switches from sweeps to a union of spheres to retain a continuous, 1-Lipschitz field ([Paris et al., *Synthesizing Geologically Coherent Cave Networks*, PDF](https://diglib.eg.org/bitstreams/fd2a57a6-f1fa-4c74-8b3e-9aea2aeb6047/download)). `ShapeWorldInternal.h::considerCarveSegment` already smooths eligible route joins; a later shape pass could add a small asymmetric cross-section / radius profile there, but preserve a conservative bound and leave floor geometry constrained. Risk: true curved cross-section frames and nearest-point evaluation cost more than the present capsule sweep.
|
||||||
|
- **Use erosion as a shape model, not a per-voxel simulation:** Cave-network research produces natural branching/channel forms with hydraulic flow and erosion simulation ([Pytel & Mann, JCGT](https://jcgt.org/published/0004/02/02/); [Franke & Müller, karst caves](https://www.sciencedirect.com/science/article/pii/S0097849321002132)). Translate the erosion cues (unequal scallops, asymmetric channel widening, larger pockets at bends) into fixed-seed route descriptors or one bounded noise term; do not run flow/erosion iterations in `ApplyResolvedStratePassageCarves`, which is on the per-voxel CPU path.
|
||||||
|
- **Smooth the solid/void intersection locally and watch the field bound:** Polynomial smooth-min gives a cheap continuous blend for implicit surfaces ([Quilez, smooth minimum](https://iquilezles.org/articles/smin/)); for solid carving, apply the corresponding smooth-max to the solid and negated void field. Restrict it to the opening region and fade it by distance along the path. Smooth CSG shifts the zero-set and may round away a floor or thin rock, so use capsule, support and crease audits after each change.
|
||||||
|
- **Do not warp the walk plane:** Domain warping samples a field at noise-displaced coordinates ([Quilez, domain warping](https://iquilezles.org/articles/warp/)). It can break periodic wall bands, but its derivative changes the field slope and repeated warps add several noise evaluations; in `ShapeWorldInternal.h` apply only bounded low-frequency displacement to the wall/ceiling contour, masking by wall orientation and distance from the floor, not to the whole carve or tread field.
|
||||||
|
- **Keep the existing broad phase; use Lipschitz pruning only with proven bounds:** VoxelForge already selects passage candidates per tile with an AABB BVH and bounds the nearest-segment search with a per-passage BVH ([architecture §8.45](../../../ARCHITECTURE.md)). Recent SDF pruning work is explicit that pruning needs exact 1-Lipschitz fields or valid updated bounds for smooth operators ([Barbier et al., *Lipschitz Pruning*, Computer Graphics Forum](https://onlinelibrary.wiley.com/doi/10.1111/cgf.70057)). Keep new fields inside those bounds; do not assume warped/smoothed noise remains 1-Lipschitz. Cached/trilinearly interpolated fields are a later option only if profiling shows repeated exact queries; interpolation can move the zero surface and needs an exact fallback near the walk surface.
|
||||||
|
- **Bridge code lead for L37:** In the current capture-matching source, `ApplyResolvedStratePassageCarves` unions `PassageBridgeSignedDistance` for `IsPassageShortSpanSegment`; that field is a separate deck with 3.3–3.65 m half-width and about 4 m thickness ([ShapeWorldInternal.h](../../../Standalone/Core/src/ShapeWorldInternal.h), around `PassageBridgeSignedDistance`). Also check `IsPassageColumnSegment`, which routes through the same support-deck path. The render can therefore come from a planned support feature, independent of cave-wall relief; trace the flagged segment back to its bridge/column plan before changing mouth noise.
|
||||||
|
- **Some examples use authored pieces rather than SDF mouths:** Valheim documents room assets consumed by `DungeonGenerator` ([official modding FAQ](https://valheim.com/support/modding-faq-for-the-asset-bundle-update-0-217-40/)); Teardown documents a voxel asset tool used for complex campaign terrain ([VoxTool](https://teardowngame.com/voxtool/)). These are useful for authored topology and asset tooling, but neither documents a natural carved mouth algorithm. No Man’s Sky publicly promises more navigable cave systems and reports dual marching-cubes performance work, while leaving its cave-shape algorithm unspecified ([NMS terrain update](https://www.nomanssky.com/next-update/), [Worlds Part I](https://www.nomanssky.com/worlds-part-I-update/)); don't infer its internal method.
|
||||||
|
- **Implementer control, L37:** Disabling the FI short-span deck left the owner ground-mouth and overview JPEGs byte-identical, while the rock-column audit failed on its four short spans. The deck is needed elsewhere in the route and is not the visible crater-mouth bridge; restored it and traced the entrance carve/support path instead.
|
||||||
|
- **Implementer control, L37:** Passage tread and native-route blend ablations left the crater ridge; disabling the route-to-mouth support handoff broke 18 FI route support samples. Current trace points to the final FloatingIslands surface-route heights: its 17-probe maximum footing and grade envelope lift the approach over the crater. L167 tests centerline footing for that final segment, with route audits still binding.
|
||||||
|
- **Implementer trace, L37:** `FloatingIslandsInternal.h::ConnectorDensity` adds an arch up to twice the connector thickness (8 m here), so lowering connector endpoints alone leaves a crater-crossing hump. Testing the v171 version removes that arch and seats connectors 1 m below native island tops; the route is still derived and checked from the resulting field.
|
||||||
|
- **Implementer correction, L37:** The owner FI JSON defaults to `geometryVersion=4`; v171's `>=5` experiment gate was inert and its capture matched v170. The actual L37 fix is gated by `WorldAssemblyEvaluatorVersion>=172`.
|
||||||
|
- **Implementer result, L37:** The active v172 no-arch/−1 m connector build retained the surface route but its close capture still showed a central fin. L173 tests the previously effective −2 m sink with the arch removed; passage and both route audits remain gates.
|
||||||
|
- **Implementer result, L37:** L173 removed the visible fin; its four surface-route misses were on the western island-0-to-1 connector at x≈−5…−3.5 m. L174 sinks only links touching anchored island 2 by 2 m, preserving the western connector height.
|
||||||
|
- **Implementer trace, L37:** L37 ripple diagnostics show later coil queries matching the entrance relief by XY although they are about 55 m below it. The versioned L175 correction also gates the mouth fade to a 4 m vertical window around the entrance floor; the signed carve-distance lerp stays intact.
|
||||||
|
- **Implementer experiment, L37:** The largest failing ripple window crosses two helix turns only 0.05 m apart horizontally and 4.17 m apart vertically at 3 m radius. L176 tests the 14.54° (still below 15°) route grade to increase turn separation.
|
||||||
|
- **Implementer result, L37:** L176 reduced wall ripple to 3.01 cm but failed capsule support at one sample and left floor ripple at 2.18 cm. L177 restores the prior grade and tapers the anchored tunnel from 3 m to 2 m radius over 8–16 m from the mouth, after the fixed close camera.
|
||||||
|
- **Implementer result, L37:** L177's 2 m tunnel radius was rejected: 103 blocked capsule samples and 3.15 cm floor ripple. The final source reverts L176/L177 route experiments and uses the v175 tunnel profile.
|
||||||
|
- **Implementer correction, L37:** The final v175 close capture still shows a fin, so root-only connector sinking was insufficient. The global −2 m v173 view removed it, while four support misses were confined to island 0–1; v178 keeps that single link at −1 m and sinks every other connector to −2 m.
|
||||||
|
- **Implementer result, L37:** v178 still shows the fin. L179 tests the island-0-to-1 connector at −1.5 m as a visual/support compromise; all other links stay at −2 m.
|
||||||
|
- **Implementer result, L37:** L179's capture is byte-identical to the all−2 m v173 control, but route0 has two unsupported samples. L180 tests −1.25 m on the western link to recover those supports while keeping that appearance.
|
||||||
|
- **Implementer trace, L37:** The crater bridge is the FI passage helix, not the island connector or short-span deck. Current control points put the route floor 5.4 m above native ground at radius 3 m inside the crater; the mesh at radius 7 m is about 0.1–0.3 m below the intended route floor. L185 tests moving the non-spiral FI passage ring from 3 m to mouth-offset+2 m (7 m), onto the native crater rim; the route/capsule/support audits remain gates.
|
||||||
|
- **Implementer result, L37:** v188 fixes the bridge by placing the FI passage coil at 7 m (the 5 m mouth offset plus 2 m), on the native crater rim, and splitting the FI route legs after the entry into 1.375 m supports. The bridge is absent in the 6 m and 17 m views. FI route/capsule/support/open-air checks pass; floor ripple falls to 1.39 cm (1.5 cm gate), wall ripple passes. Full style/compiler audits still pending.
|
||||||
|
- **Implementer result, L37 resumed:** v208 passes owner FI styles 0–3. Style 0 uses the terrain-following crater route and crater tread; other FI styles keep the native route/tread profile. The 8.5–12.5 m native mouth filter now applies to every FI passage style; this cleared open-air and style 1/2/3 ripple regressions. Full SW/fixture and compiler matrix still pending.
|
||||||
|
- Implementer follow-up, L37 v208: a temporary support-off build reproduces the same 25.542° overview sample as the final mesh, so the remaining crease is not the route tread/support. At the 6 m view it is 4.396°; at 17 m the failing sample interval is 0.125 m and projects to 14.34 px at 2560×1440. The 20° overview gate remains a measured failure; no source change was made after the owner accepted the crater appearance.
|
||||||
|
- Implementer cost, L37 v208: five interleaved pairs completed; median whole-view change was +0.63% surface-world and +2.04% floating-islands, within pair noise.
|
||||||
@@ -743,6 +743,7 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
const FStratePassageConfig& passage=islandSlot->Definition->PassageConfig;
|
const FStratePassageConfig& passage=islandSlot->Definition->PassageConfig;
|
||||||
recipe.seed=static_cast<std::uint32_t>(owner.Seed);
|
recipe.seed=static_cast<std::uint32_t>(owner.Seed);
|
||||||
recipe.geometryVersion=8;
|
recipe.geometryVersion=8;
|
||||||
|
recipe.terrainFollowingRoute=static_cast<std::uint32_t>(passage.Style)==0;
|
||||||
recipe.ownerVoxelSizeMicrons=owner.OwnerVoxelSizeMicrons;
|
recipe.ownerVoxelSizeMicrons=owner.OwnerVoxelSizeMicrons;
|
||||||
recipe.coreVoxelSizeMicrons=owner.CoreVoxelSizeMicrons;
|
recipe.coreVoxelSizeMicrons=owner.CoreVoxelSizeMicrons;
|
||||||
recipe.ownerToCoreScaleNumerator=owner.OwnerVoxelSizeMicrons/static_cast<std::uint32_t>(divisor);
|
recipe.ownerToCoreScaleNumerator=owner.OwnerVoxelSizeMicrons/static_cast<std::uint32_t>(divisor);
|
||||||
|
|||||||