wip(assembler): round L36 -- tunnel floor without washboard; FI mouth without shards (evaluator v23)
The walk tread was built per segment as a one-sided density clamp, so its crossing with the native field moved at segment joins (L35: 2.25-2.48 cm washboard). Treads now overlap up to 2 m as bounded signed-distance slabs (0.06 cm residual). The FI mouth's crater detail and tread met without a smooth join; v23 filters the local field, drops thin isolated solids and smooth-unions the tread with the terrain. New floor ripple and mouth fragment/crease audits with L35 as failing controls. 24/24 passage rows, 30/30 determinism, 16/16 invariants, LOD0-8 rings; cost within noise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
No files matched your search
@@ -2138,6 +2138,30 @@ turn. Passage, capsule, support, open-air, bridge, binding, strict layout, captu
|
||||
and cost evidence are recorded in
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
||||
|
||||
### 8.49 Walk-floor washboard and FI mouth naturalness — World Assembler M3 L36
|
||||
|
||||
L35's exported owner-SW mid-tunnel floor measured a 7.250 m repeat at 0.02252 m amplitude; its
|
||||
upper-SW fixture arrival measured a 1.438 m repeat at 0.02482 m. Both came from segment-built,
|
||||
one-sided support treads meeting the native terrain zero crossing at per-segment joins. The floor
|
||||
probe samples the exported walk mesh along the resolved route, separately from the wall-radius scan.
|
||||
Eligible tread sections overlap by up to 2 m and all sparse route segments now use a bounded
|
||||
signed-distance slab, including SurfaceWorld-to-TunnelNetwork routes. This keeps the tread top stable
|
||||
across segment joins while capsule-clearance and native-solid gates retain walkability and open air.
|
||||
Regular tread tops remain seated 5 cm lower and ease back to 0.20 m within 20 m of a lower landing;
|
||||
seeded shoulder grain is removed, and gravity-facing relief is reduced while side-wall and ceiling
|
||||
relief remain irregular.
|
||||
|
||||
On v23, the same mesh probes measure 7.188 m / 0.00067 m (0.067 cm) on owner-SW and 6.500 m /
|
||||
0.00057 m (0.057 cm) on the upper fixture. The density audit checks five 32 m walk-floor windows at
|
||||
0.125 m spacing, rejects amplitudes of 0.015 m or more, and detects its beaded control. The FI mouth
|
||||
uses a local 2 m symmetric signed-field filter with the route protected and joins route-mouth support
|
||||
to native terrain with a 1.5 m smooth signed-distance union. Its mesh audit reduces the L35
|
||||
42.27° fade-ring jump to 27.13° and finds no detached component above the 0.0001 m² area floor; one
|
||||
8-triangle subthreshold component at 0.0000266 m² is recorded. The evaluator is version 23. Full
|
||||
recipe/style audits, route support, open-air, strict layout, legacy validators, determinism, capture and
|
||||
paired-cost results are in
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
||||
|
||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||
|
||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||
|
||||
@@ -1250,11 +1250,24 @@ ARCHITECTURE §8.47 and
|
||||
| `VF_FieldPassageWallRelief` | `Standalone/Core/src/VoxelForgeFieldShared.h` | Supplies passage-only irregular wall relief at rotated 43/29/19 m scales. Other `VF_FieldSurfaceRelief` users stay unchanged. |
|
||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Catmull–Rom smooths eligible interior sweep joins, applies passage-only wall relief, and tapers/grains the entry support shoulder below the capsule/support probe footprint. |
|
||||
| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Gives each Cascade turn a seeded shallow radial profile instead of identical repeated full-depth cosine lobes. |
|
||||
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples field wall radius at 0.125 m spacing, scans periods 0.5–8 m in 32 m windows, and rejects amplitudes ≥0.035 m. The negative control uses 2 m radius spheres at 1.5 m spacing. |
|
||||
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples wall radii and walk-floor zero crossings at 0.125 m spacing, scans periods 0.5–8 m in 32 m windows, and rejects wall/floor amplitudes ≥0.035/0.015 m. Sphere and beaded-floor controls must be detected. |
|
||||
|
||||
`WorldAssemblyEvaluatorVersion=21`. See ARCHITECTURE §8.48 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md).
|
||||
|
||||
### 3.32 Walk-floor washboard and FI mouth blend — World Assembler M3 L36
|
||||
|
||||
| Symbol | Canonical file | Role |
|
||||
|---|---|---|
|
||||
| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Extends eligible tread sections by up to 2 m and uses bounded signed-distance support on every sparse route, including SurfaceWorld-to-TunnelNetwork, removes seeded shoulder grain, and seats regular tread tops 5 cm lower with a smooth return at the lower landing. Capsule/open-air gates remain. Gravity-facing wall relief is reduced; side and ceiling relief remain. |
|
||||
| `ApplyResolvedStratePassageCarves` mouth support | `Standalone/Core/src/ShapeWorldInternal.h` | Builds exact signed-distance route-mouth tread boxes and smooth-unions them with native terrain over 1.5 m. |
|
||||
| `NativeFloatingIslandDensityAt` | `Standalone/Core/src/ShapeWorldInternal.h` | Applies a local 2 m symmetric signed-field filter around FI passage mouths, exempts resolved route support, and removes isolated negative samples thinner than 2 m on all axes. |
|
||||
| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Keeps the wall-radius audit and adds walk-floor zero-crossing profiles at 0.125 m, with five 32 m windows and a 0.015 m limit plus a beaded-floor negative control. |
|
||||
| `RunMouthFragmentAuditL36.py`, `ProbeWalkFloorProfileL36.py` | `Standalone/Research/WorldAssemblerM3/Linux36/` | Compare L35 floor and FI-mouth controls with L36; the mouth check fails on detached components at or above 0.0001 m² and checks normal jumps across the fade ring. |
|
||||
|
||||
`WorldAssemblyEvaluatorVersion=23`. See ARCHITECTURE §8.49 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md).
|
||||
|
||||
## 4. The density pipeline (most-edited hot path)
|
||||
|
||||
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||
|
||||
@@ -250,8 +250,10 @@ namespace VoxelForgeCore
|
||||
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
||||
// 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.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 21;
|
||||
// version 20 restores full Gradient amplitude and uses a continuous signed-distance band union;
|
||||
// version 22 overlaps tread sections and smooths FloatingIslands passage-mouth features;
|
||||
// version 23 uses signed-distance support for sparse SurfaceWorld-to-TunnelNetwork treads.
|
||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 23;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
|
||||
@@ -727,8 +727,9 @@ public:
|
||||
}
|
||||
if(routeCount<2)return;
|
||||
const std::size_t lastSegment=routeCount-1;
|
||||
const double floorThickness=1000000.0/std::max<std::uint32_t>(1,
|
||||
ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
constexpr double MouthSupportBlendMetres=1.5;
|
||||
const double mouthBlendWidth=MouthSupportBlendMetres*qPerMetre;
|
||||
double mouthSupportDistance=std::numeric_limits<double>::infinity();
|
||||
for(std::size_t index=0;index<lastSegment;++index)
|
||||
{
|
||||
const auto& a=*route[index];const auto& b=*route[index+1];
|
||||
@@ -743,12 +744,25 @@ public:
|
||||
?a.values[3]+t*(static_cast<double>(b.values[3])-a.values[3]):0.0;
|
||||
const double halfWidth=std::max(0.75*qPerMetre,routeRadius);
|
||||
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||
if(horizontalX*horizontalX+horizontalY*horizontalY>halfWidth*halfWidth)continue;
|
||||
const double floorVoxels=(centerZ-1.20*qPerMetre)/Q;
|
||||
if(Z<floorVoxels-floorThickness||Z>floorVoxels+1.0/Q)continue;
|
||||
composed=std::min(composed,static_cast<float>(
|
||||
std::clamp(Z-floorVoxels,-floorThickness,0.0)));
|
||||
const double horizontalDistance=DeterministicSqrt(
|
||||
horizontalX*horizontalX+horizontalY*horizontalY);
|
||||
const double floorQ=centerZ-1.20*qPerMetre;
|
||||
const double sideDistance=horizontalDistance-halfWidth;
|
||||
const double topDistance=qz-floorQ;
|
||||
const double bottomDistance=floorQ-qPerMetre-qz;
|
||||
const double verticalDistance=std::max(topDistance,bottomDistance);
|
||||
if(sideDistance>mouthBlendWidth||verticalDistance>mouthBlendWidth)continue;
|
||||
const double outside=DeterministicSqrt(
|
||||
std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
|
||||
+std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0));
|
||||
const double inside=std::min(std::max(sideDistance,verticalDistance),0.0);
|
||||
const double signedDistance=outside+inside;
|
||||
mouthSupportDistance=std::min(mouthSupportDistance,signedDistance);
|
||||
}
|
||||
if(std::isfinite(mouthSupportDistance))
|
||||
composed=SmoothMinDistance(composed,
|
||||
static_cast<float>(mouthSupportDistance/Q),
|
||||
static_cast<float>(mouthBlendWidth/Q));
|
||||
};
|
||||
// Keep the native surface route connected to the entrance. Passage carving
|
||||
// then removes only the parts that overlap the tunnel, whose own tread owns
|
||||
@@ -765,6 +779,19 @@ public:
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||
if(passage.controlPoints.size()<2)continue;
|
||||
const auto supportStratum=[&](std::int32_t strateIndex)
|
||||
->const VoxelForgeCore::ResolvedLayoutStratumV2*
|
||||
{
|
||||
const auto it=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[strateIndex](const auto& stratum)
|
||||
{return stratum.strateIndex==strateIndex;});
|
||||
return it==ResolvedLayout->strates.end()?nullptr:&*it;
|
||||
};
|
||||
const auto* supportUpper=supportStratum(passage.upperStrateIndex);
|
||||
const auto* supportLower=supportStratum(passage.lowerStrateIndex);
|
||||
const bool surfaceToTunnelSupport=supportUpper&&supportLower
|
||||
&&supportUpper->generatorKind=="SurfaceWorld"
|
||||
&&supportLower->generatorKind=="TunnelNetwork";
|
||||
const double firstFloorQ=passage.controlPoints.front().positionSubvoxels[2]
|
||||
-passage.controlPoints.front().radiusSubvoxels+playerCenterAboveFloorQ;
|
||||
const double lastFloorQ=passage.controlPoints.back().positionSubvoxels[2]
|
||||
@@ -880,7 +907,20 @@ public:
|
||||
std::ptrdiff_t bestScanIndex=std::numeric_limits<std::ptrdiff_t>::max();
|
||||
std::ptrdiff_t bestSupportSegmentIndex=0;
|
||||
bool hasSupportFloor=false;
|
||||
double bestSupportDensity=0.0;
|
||||
double bestSupportDistanceSquared=std::numeric_limits<double>::infinity();
|
||||
double bestSupportDensity=std::numeric_limits<double>::infinity();
|
||||
double bestSupportFloorQ=0.0;
|
||||
bool bestSupportUsesSignedDistance=false;
|
||||
std::ptrdiff_t selectedSupportSegmentIndex=-1;
|
||||
const double lowerMouthDx=qx-passage.lowerMouthSubvoxels[0];
|
||||
const double lowerMouthDy=qy-passage.lowerMouthSubvoxels[1];
|
||||
const double lowerMouthDz=qz-passage.lowerMouthSubvoxels[2];
|
||||
const double lowerMouthDistance=DeterministicSqrt(
|
||||
lowerMouthDx*lowerMouthDx+lowerMouthDy*lowerMouthDy
|
||||
+lowerMouthDz*lowerMouthDz)/qPerMetre;
|
||||
const double lowerMouthT=std::clamp((20.0-lowerMouthDistance)/12.0,0.0,1.0);
|
||||
const double lowerMouthBlend=lowerMouthT*lowerMouthT
|
||||
*(3.0-2.0*lowerMouthT);
|
||||
const auto considerCarveSegment=[&](std::size_t segmentIndex,int scanPhase)
|
||||
{
|
||||
const std::ptrdiff_t index=static_cast<std::ptrdiff_t>(segmentIndex);
|
||||
@@ -992,7 +1032,7 @@ public:
|
||||
/distanceToCenter,0.0,1.0):0.0;
|
||||
const float relief=VF_FieldPassageWallRelief(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z),static_cast<std::uint32_t>(Owner?Owner->Seed:0));
|
||||
const double wallReliefWeight=1.0-0.80*verticalWallAlignment;
|
||||
const double wallReliefWeight=0.05+0.95*(1.0-verticalWallAlignment);
|
||||
const double roughSignedDistance=bestSignedDistance
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight;
|
||||
if(IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(bestIndex)))
|
||||
@@ -1004,38 +1044,48 @@ public:
|
||||
// Positive density is air; rough rock is cut with full side/ceiling detail and
|
||||
// gentler relief on the gravity-facing wall so the supported tread stays open.
|
||||
composed=std::max(composed,static_cast<float>(-roughSignedDistance/Q));
|
||||
double bestSupportDistanceSquared=std::numeric_limits<double>::infinity();
|
||||
const auto considerSupportSegment=[&](std::ptrdiff_t index)
|
||||
{
|
||||
const auto& segmentA=passage.controlPoints[static_cast<std::size_t>(index)];
|
||||
const auto& segmentB=passage.controlPoints[static_cast<std::size_t>(index+1)];
|
||||
const double dx=static_cast<double>(segmentB.positionSubvoxels[0])-segmentA.positionSubvoxels[0];
|
||||
const double dy=static_cast<double>(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1];
|
||||
const double dz=static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2];
|
||||
const double radiusA=segmentA.radiusSubvoxels;
|
||||
const double radiusB=segmentB.radiusSubvoxels;
|
||||
const bool entryMouthShoulder=index<=2;
|
||||
// Keep the tread within the probe band and below the capsule's
|
||||
// lowest sampled point.
|
||||
const double floorOffset=IsPassageShortSpanSegment(passageIndex,
|
||||
const bool bridgeDeck=IsPassageShortSpanSegment(passageIndex,
|
||||
static_cast<std::size_t>(index))
|
||||
||IsPassageColumnSegment(passageIndex,static_cast<std::size_t>(index))
|
||||
||IsPassageColumnSegment(passageIndex,static_cast<std::size_t>(index));
|
||||
// Keep a small capsule margin at regular treads and restore most of
|
||||
// the nominal height smoothly as a tread approaches its landing.
|
||||
double floorOffset=bridgeDeck
|
||||
?PassageBridgeDeckOffsetMetres(passageIndex,static_cast<std::size_t>(index))
|
||||
:0.20;
|
||||
:std::lerp(0.15,0.20,lowerMouthBlend);
|
||||
const double horizontalLengthSq=dx*dx+dy*dy;
|
||||
const bool overlapSupportJoin=index+12<lastIndex
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index))
|
||||
&&(index==0||(!IsPassageShortSpanSegment(passageIndex,
|
||||
static_cast<std::size_t>(index-1))));
|
||||
const bool smoothSupportJoin=index>0&&overlapSupportJoin
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index+1));
|
||||
const double projection=horizontalLengthSq>0.0
|
||||
?((qx-segmentA.positionSubvoxels[0])*dx
|
||||
+(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq:bestT;
|
||||
// Extend tread segments by a fixed distance through route knots. A
|
||||
// fractional extension made the support width change with segment length.
|
||||
const double overlapFraction=overlapSupportJoin&&horizontalLengthSq>0.0
|
||||
?std::min(0.5,2.0*qPerMetre/DeterministicSqrt(horizontalLengthSq)):0.0;
|
||||
const double supportT=horizontalLengthSq>0.0
|
||||
?std::clamp(((qx-segmentA.positionSubvoxels[0])*dx
|
||||
+(qy-segmentA.positionSubvoxels[1])*dy)/horizontalLengthSq,0.0,1.0)
|
||||
:bestT;
|
||||
?std::clamp(projection,-overlapFraction,1.0+overlapFraction):bestT;
|
||||
const double centerX=segmentA.positionSubvoxels[0]+supportT*dx;
|
||||
const double centerY=segmentA.positionSubvoxels[1]+supportT*dy;
|
||||
double centerZ=segmentA.positionSubvoxels[2]+supportT*(
|
||||
static_cast<double>(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]);
|
||||
double radius=segmentA.radiusSubvoxels+supportT*(
|
||||
static_cast<double>(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels);
|
||||
const bool smoothSupportJoin=index>0&&index+12<lastIndex
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index-1))
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index))
|
||||
&&!IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(index+1));
|
||||
if(smoothSupportJoin)
|
||||
{
|
||||
const auto& p0=passage.controlPoints[static_cast<std::size_t>(index-1)];
|
||||
@@ -1063,7 +1113,15 @@ public:
|
||||
// Support belongs just below the avatar's foot plane. Queries within
|
||||
// the capsule body, or a neighbouring coil's overlapping bore, must
|
||||
// not turn that tread into a ceiling or mid-air shelf.
|
||||
if(supportQueryDepth<0.93*qPerMetre||supportQueryDepth>1.30*qPerMetre)return;
|
||||
// Sparse authored routes use the full signed distance around the top;
|
||||
// dense routes retain the legacy one-sided tread density.
|
||||
const double supportSegmentLength=DeterministicSqrt(
|
||||
horizontalLengthSq+dz*dz)/qPerMetre;
|
||||
const bool sparseRouteSupport=supportSegmentLength>1.5;
|
||||
const double minimumSupportQueryDepth=index==selectedSupportSegmentIndex
|
||||
&&sparseRouteSupport?0.65:0.93;
|
||||
if(supportQueryDepth<minimumSupportQueryDepth*qPerMetre
|
||||
||supportQueryDepth>1.30*qPerMetre)return;
|
||||
const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre;
|
||||
const double supportThicknessVoxels=fallbackSupport
|
||||
?qPerMetre/Q:0.5*qPerMetre/Q;
|
||||
@@ -1072,12 +1130,8 @@ public:
|
||||
// capsule on the neighbouring turn.
|
||||
const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2
|
||||
&&supportQueryDepth<0.98*qPerMetre;
|
||||
const double supportEdgeNoise=entryMouthShoulder?std::clamp(
|
||||
static_cast<double>(VF_FieldPassageGroundGrain(static_cast<float>(X),
|
||||
static_cast<float>(Y),
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0))),-1.0,1.0):0.0;
|
||||
const double supportHalfWidthMetres=lowerMouthBodyOverlap?0.90
|
||||
:entryMouthShoulder?1.20+0.20*supportEdgeNoise:1.25;
|
||||
:entryMouthShoulder?1.20:1.25;
|
||||
const double supportHalfWidth=std::min(radius,
|
||||
supportHalfWidthMetres*qPerMetre);
|
||||
// Keep the walk core within a 0.50 m span with shallow relief, then
|
||||
@@ -1095,30 +1149,50 @@ public:
|
||||
const double localSupportThicknessVoxels=entryMouthShoulder
|
||||
?supportThicknessVoxels*(1.0-0.72*supportEdgeSmooth)
|
||||
:supportThicknessVoxels;
|
||||
const double supportEdgeDepth=entryMouthShoulder
|
||||
?0.35+0.15*supportEdgeNoise:0.35;
|
||||
constexpr double supportEdgeDepth=0.35;
|
||||
const double floorQ=centerZ-radius+floorOffset*qPerMetre
|
||||
+supportEdgeNoise*(horizontalDistance>0.60*qPerMetre
|
||||
?0.35:0.14)*qPerMetre
|
||||
-supportEdgeSmooth*supportEdgeDepth*qPerMetre;
|
||||
const double floorVoxels=floorQ/Q;
|
||||
if(Z<floorVoxels-localSupportThicknessVoxels||Z>floorVoxels+1.0/Q)return;
|
||||
const double supportPositiveRangeVoxels=sparseRouteSupport
|
||||
?0.35*qPerMetre/Q:1.0/Q;
|
||||
if(Z<floorVoxels-localSupportThicknessVoxels
|
||||
||Z>floorVoxels+supportPositiveRangeVoxels)return;
|
||||
if(horizontalDistance>supportHalfWidth)return;
|
||||
const double supportDistanceSq=horizontalDistance*horizontalDistance
|
||||
const double supportDistanceSquared=horizontalDistance*horizontalDistance
|
||||
+(Z-floorVoxels)*(Z-floorVoxels);
|
||||
if(supportDistanceSq>=bestSupportDistanceSquared)return;
|
||||
bestSupportDistanceSquared=supportDistanceSq;
|
||||
if(supportDistanceSquared>=bestSupportDistanceSquared)return;
|
||||
bestSupportDistanceSquared=supportDistanceSquared;
|
||||
hasSupportFloor=true;
|
||||
bestSupportSegmentIndex=index;
|
||||
bestSupportDensity=std::clamp(Z-floorVoxels,
|
||||
-localSupportThicknessVoxels,0.0);
|
||||
if(sparseRouteSupport)
|
||||
{
|
||||
// Sparse support profiles need both sides of a signed distance
|
||||
// so the sampled zero crossing stays on the tread top.
|
||||
const double topDistance=qz-floorQ;
|
||||
const double bottomDistance=floorQ-localSupportThicknessVoxels*Q-qz;
|
||||
const double verticalDistance=std::max(topDistance,bottomDistance);
|
||||
const double sideDistance=horizontalDistance-supportHalfWidth;
|
||||
const double outside=DeterministicSqrt(
|
||||
std::max(sideDistance,0.0)*std::max(sideDistance,0.0)
|
||||
+std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0));
|
||||
const double inside=std::min(std::max(sideDistance,verticalDistance),0.0);
|
||||
bestSupportDensity=(outside+inside)/Q;
|
||||
}
|
||||
else
|
||||
{
|
||||
bestSupportDensity=std::clamp(Z-floorVoxels,
|
||||
-localSupportThicknessVoxels,0.0);
|
||||
}
|
||||
bestSupportFloorQ=floorQ;
|
||||
bestSupportUsesSignedDistance=sparseRouteSupport;
|
||||
};
|
||||
std::ptrdiff_t supportCenterIndex=bestIndex;
|
||||
if(bestIndex<lowerMouthIndex)
|
||||
{
|
||||
// A helix can have multiple centerlines at one XY. Select the tread
|
||||
// whose floor is just below this density query, not the closest wall.
|
||||
const double supportProfileTarget=qz+0.93*qPerMetre;
|
||||
const double supportProfileOffset=surfaceToTunnelSupport?1.13:0.93;
|
||||
const double supportProfileTarget=qz+supportProfileOffset*qPerMetre;
|
||||
const auto supportProfile=std::lower_bound(passage.controlPoints.begin(),mainEnd,
|
||||
supportProfileTarget,[&](const auto& point,double value)
|
||||
{return floorProfile(point)>value;});
|
||||
@@ -1132,6 +1206,8 @@ public:
|
||||
const std::ptrdiff_t supportBegin=std::max<std::ptrdiff_t>(0,supportCenterIndex-1);
|
||||
const std::ptrdiff_t supportEnd=std::min<std::ptrdiff_t>(lastIndex-1,
|
||||
supportCenterIndex+1);
|
||||
selectedSupportSegmentIndex=supportCenterIndex>0
|
||||
?supportCenterIndex-1:supportCenterIndex;
|
||||
for(std::ptrdiff_t index=supportBegin;index<=supportEnd;++index)
|
||||
considerSupportSegment(index);
|
||||
// Keep route treads inside the native rock envelope. Carving may reopen the
|
||||
@@ -1142,13 +1218,24 @@ public:
|
||||
const bool shortSpanFloorSupport=hasSupportFloor
|
||||
&&IsPassageShortSpanSegment(passageIndex,
|
||||
static_cast<std::size_t>(bestSupportSegmentIndex));
|
||||
const bool capsuleAllowsSupport=passageTileScopeMatches
|
||||
const double capsuleClearanceMetres=bestSupportUsesSignedDistance?0.045:0.05;
|
||||
const double capsuleQueryZ=bestSupportUsesSignedDistance?bestSupportFloorQ:qz;
|
||||
const bool supportCapsuleProfileAvailable=passageIndex<PassageNearestNodes.size()
|
||||
&&!PassageNearestNodes[passageIndex].empty();
|
||||
const bool capsuleAllowsSupport=(passageTileScopeMatches
|
||||
||(bestSupportUsesSignedDistance&&supportCapsuleProfileAvailable))
|
||||
?!PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin,carveEnd,
|
||||
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz,qPerMetre)
|
||||
:bestPlayerCapsuleSignedDistance>=0.05*qPerMetre;
|
||||
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,
|
||||
capsuleQueryZ,qPerMetre,capsuleClearanceMetres)
|
||||
:bestPlayerCapsuleSignedDistance>=capsuleClearanceMetres*qPerMetre;
|
||||
// Outside bridge and short-span segments, add the tread only where the
|
||||
// native field already provides a solid envelope; the capsule check keeps
|
||||
// that support below the player's walk space.
|
||||
if(hasSupportFloor&&(density<0.0f||passageColumnFloorSupport||shortSpanFloorSupport)
|
||||
&&capsuleAllowsSupport)
|
||||
&&(capsuleAllowsSupport||bestSupportDensity>=0.0))
|
||||
{
|
||||
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
}
|
||||
}
|
||||
return composed;
|
||||
}
|
||||
@@ -2990,7 +3077,8 @@ private:
|
||||
}
|
||||
|
||||
bool PassagePlayerCapsuleBlocksSupportNode(std::size_t passageIndex,int nodeIndex,
|
||||
std::size_t first,std::size_t end,double qx,double qy,double qz,double qPerMetre) const
|
||||
std::size_t first,std::size_t end,double qx,double qy,double qz,double qPerMetre,
|
||||
double minimumClearanceMetres) const
|
||||
{
|
||||
if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false;
|
||||
const auto& nodes=PassageNearestNodes[passageIndex];
|
||||
@@ -3026,26 +3114,26 @@ private:
|
||||
const double vertical=std::max(0.0,std::abs(qz-centerZ)-0.54*qPerMetre);
|
||||
const double distance=DeterministicSqrt(horizontalX*horizontalX+horizontalY*horizontalY
|
||||
+vertical*vertical)-0.34*qPerMetre;
|
||||
if(distance<0.05*qPerMetre)return true;
|
||||
if(distance<minimumClearanceMetres*qPerMetre)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.left,first,end,
|
||||
qx,qy,qz,qPerMetre)
|
||||
qx,qy,qz,qPerMetre,minimumClearanceMetres)
|
||||
||PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.right,first,end,
|
||||
qx,qy,qz,qPerMetre);
|
||||
qx,qy,qz,qPerMetre,minimumClearanceMetres);
|
||||
}
|
||||
|
||||
bool PassagePlayerCapsuleBlocksSupport(std::size_t passageIndex,
|
||||
std::ptrdiff_t carveBegin,std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex,
|
||||
std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex,
|
||||
double qx,double qy,double qz,double qPerMetre) const
|
||||
double qx,double qy,double qz,double qPerMetre,double minimumClearanceMetres) const
|
||||
{
|
||||
const auto blocked=[&](std::ptrdiff_t first,std::ptrdiff_t last)
|
||||
{
|
||||
return first<=last&&PassagePlayerCapsuleBlocksSupportNode(passageIndex,0,
|
||||
static_cast<std::size_t>(first),static_cast<std::size_t>(last+1),
|
||||
qx,qy,qz,qPerMetre);
|
||||
qx,qy,qz,qPerMetre,minimumClearanceMetres);
|
||||
};
|
||||
return blocked(carveBegin,carveEnd)
|
||||
||blocked(0,std::min<std::ptrdiff_t>(8,lowerMouthIndex)-1)
|
||||
@@ -4429,7 +4517,159 @@ private:
|
||||
const VoxelForgeCore::FloatingIslandFieldV1 Field(
|
||||
Plan->manifest.recipe,Plan->manifest.plan);
|
||||
if(!Field.isValid())return false;
|
||||
std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
|
||||
const std::int64_t qx=ToFixed(X),qy=ToFixed(Y),qz=ToFixed(Z);
|
||||
std::int64_t Density=Field.densitySubvoxels(qx,qy,qz);
|
||||
// The anchor-root field keeps broad lobes, but its shortest authored noise
|
||||
// wavelength is below one metre. Filter that detail only around a FloatingIslands
|
||||
// passage mouth so Marching Cubes does not expose voxel-sized chips on the crater
|
||||
// wall. The broad root profile is unchanged outside this local mouth envelope.
|
||||
double mouthFilterWeight=0.0;
|
||||
const double qToMetres=static_cast<double>(ResolvedLayout->coreVoxelSizeMicrometres)
|
||||
/(static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)*1000000.0);
|
||||
const double densityMetres=std::abs(static_cast<double>(Density))*qToMetres;
|
||||
if(densityMetres<4.0)
|
||||
{
|
||||
bool foundPassageMouth=false;
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
{
|
||||
if(passage.upperStrateIndex!=StrateIndex)continue;
|
||||
foundPassageMouth=true;
|
||||
const double dx=(static_cast<double>(qx)-passage.upperMouthSubvoxels[0])*qToMetres;
|
||||
const double dy=(static_cast<double>(qy)-passage.upperMouthSubvoxels[1])*qToMetres;
|
||||
const double dz=(static_cast<double>(qz)-passage.upperMouthSubvoxels[2])*qToMetres;
|
||||
const double distance=std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||
constexpr double FilterCoreRadiusMetres=8.5;
|
||||
constexpr double FilterOuterRadiusMetres=12.5;
|
||||
const double fade=std::clamp((FilterOuterRadiusMetres-distance)
|
||||
/(FilterOuterRadiusMetres-FilterCoreRadiusMetres),0.0,1.0);
|
||||
const double weight=fade*fade*(3.0-2.0*fade);
|
||||
mouthFilterWeight=std::max(mouthFilterWeight,weight);
|
||||
}
|
||||
if(!foundPassageMouth)
|
||||
{
|
||||
// The passage-free native control retains the stratum route anchors.
|
||||
// Use its terminal route point as the same local mouth filter origin so
|
||||
// both native and composed fields receive the same root smoothing.
|
||||
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[StrateIndex](const auto& value)
|
||||
{return value.strateIndex==StrateIndex;});
|
||||
if(stratum!=ResolvedLayout->strates.end())
|
||||
{
|
||||
const VoxelForgeCore::ResolvedLayoutAnchorV2* terminal=nullptr;
|
||||
for(const auto& anchor:stratum->anchors)
|
||||
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||
terminal=&anchor;
|
||||
if(terminal)
|
||||
{
|
||||
const double dx=static_cast<double>(qx-terminal->values[0])*qToMetres;
|
||||
const double dy=static_cast<double>(qy-terminal->values[1])*qToMetres;
|
||||
const double dz=static_cast<double>(qz-terminal->values[2])*qToMetres;
|
||||
const double distance=std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||
constexpr double FilterCoreRadiusMetres=8.5;
|
||||
constexpr double FilterOuterRadiusMetres=12.5;
|
||||
const double fade=std::clamp((FilterOuterRadiusMetres-distance)
|
||||
/(FilterOuterRadiusMetres-FilterCoreRadiusMetres),0.0,1.0);
|
||||
mouthFilterWeight=fade*fade*(3.0-2.0*fade);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(mouthFilterWeight>0.0)
|
||||
{
|
||||
// Preserve the resolved walk route while filtering the passage and crater field.
|
||||
const double queryX=static_cast<double>(qx)*qToMetres;
|
||||
const double queryY=static_cast<double>(qy)*qToMetres;
|
||||
const double queryZ=static_cast<double>(qz)*qToMetres;
|
||||
const auto stratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[StrateIndex](const auto& value)
|
||||
{return value.strateIndex==StrateIndex;});
|
||||
if(stratum!=ResolvedLayout->strates.end())
|
||||
{
|
||||
const VoxelForgeCore::ResolvedLayoutAnchorV2* previous=nullptr;
|
||||
for(const auto& anchor:stratum->anchors)
|
||||
{
|
||||
if(anchor.kind!="route_point"||anchor.values.size()<3)continue;
|
||||
if(previous)
|
||||
{
|
||||
const double ax=static_cast<double>(previous->values[0])*qToMetres;
|
||||
const double ay=static_cast<double>(previous->values[1])*qToMetres;
|
||||
const double az=static_cast<double>(previous->values[2])*qToMetres;
|
||||
const double dx=(static_cast<double>(anchor.values[0])
|
||||
-previous->values[0])*qToMetres;
|
||||
const double dy=(static_cast<double>(anchor.values[1])
|
||||
-previous->values[1])*qToMetres;
|
||||
const double lengthSquared=dx*dx+dy*dy;
|
||||
const double t=lengthSquared>0.0?std::clamp(
|
||||
((queryX-ax)*dx+(queryY-ay)*dy)/lengthSquared,0.0,1.0):0.0;
|
||||
const double centerX=ax+t*dx,centerY=ay+t*dy;
|
||||
const double centerZ=az+t*(static_cast<double>(anchor.values[2])
|
||||
-previous->values[2])*qToMetres;
|
||||
const double supportRadius=previous->values.size()>=4
|
||||
?static_cast<double>(previous->values[3])*qToMetres:0.75;
|
||||
const double floorZ=centerZ-1.20;
|
||||
const double protectedRadius=supportRadius+0.75;
|
||||
if((queryX-centerX)*(queryX-centerX)
|
||||
+(queryY-centerY)*(queryY-centerY)
|
||||
<protectedRadius*protectedRadius
|
||||
&&std::abs(queryZ-floorZ)<1.25)
|
||||
{
|
||||
mouthFilterWeight=0.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
previous=&anchor;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(mouthFilterWeight>0.0)
|
||||
{
|
||||
constexpr std::int64_t FilterStep=8*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
std::int64_t weightedSum=0;
|
||||
for(int dz=-1;dz<=1;++dz)
|
||||
for(int dy=-1;dy<=1;++dy)
|
||||
for(int dx=-1;dx<=1;++dx)
|
||||
{
|
||||
const std::int64_t weight=(dx==0?2:1)*(dy==0?2:1)*(dz==0?2:1);
|
||||
const std::int64_t sample=(dx==0&&dy==0&&dz==0)?Density:
|
||||
Field.densitySubvoxels(qx+dx*FilterStep,qy+dy*FilterStep,
|
||||
qz+dz*FilterStep);
|
||||
weightedSum+=sample*weight;
|
||||
}
|
||||
// A separable [1,2,1]^3 kernel over a 2 m footprint filters sub-voxel
|
||||
// root detail while retaining the signed field on both sides of the surface.
|
||||
const double filtered=static_cast<double>(weightedSum)/64.0;
|
||||
const std::int64_t filteredDensity=static_cast<std::int64_t>(std::llround(
|
||||
std::lerp(static_cast<double>(Density),filtered,mouthFilterWeight)));
|
||||
Density=filteredDensity;
|
||||
// Marching Cubes can retain an isolated solid with no two-metre axis
|
||||
// thickness as a handful of disconnected triangles. Remove only shallow
|
||||
// negative samples surrounded by air at a one-metre probe distance; a
|
||||
// walk floor or wall has solid material on at least one side of this test.
|
||||
constexpr double FragmentProbeMetres=1.0;
|
||||
const std::int64_t fragmentStep=static_cast<std::int64_t>(std::llround(
|
||||
FragmentProbeMetres/qToMetres));
|
||||
if(mouthFilterWeight>=0.25&&Density<0
|
||||
&&static_cast<double>(-Density)*qToMetres<FragmentProbeMetres)
|
||||
{
|
||||
bool surroundedByAir=true;
|
||||
std::int64_t minimumAirDensity=std::numeric_limits<std::int64_t>::max();
|
||||
for(std::size_t axis=0;axis<3&&surroundedByAir;++axis)
|
||||
for(const int sign:{-1,1})
|
||||
{
|
||||
std::array<std::int64_t,3> sample{qx,qy,qz};
|
||||
sample[axis]+=sign*fragmentStep;
|
||||
const std::int64_t neighbor=Field.densitySubvoxels(
|
||||
sample[0],sample[1],sample[2]);
|
||||
if(neighbor<=0)
|
||||
{
|
||||
surroundedByAir=false;
|
||||
break;
|
||||
}
|
||||
minimumAirDensity=std::min(minimumAirDensity,neighbor);
|
||||
}
|
||||
if(surroundedByAir)Density=std::max(Density,minimumAirDensity);
|
||||
}
|
||||
}
|
||||
const std::int64_t Bottom=Plan->manifest.recipe.layoutMinimumZSubvoxels;
|
||||
const std::int64_t Top=Plan->manifest.recipe.layoutMaximumZSubvoxels;
|
||||
const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels;
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export and render the standard four-view passage set for L36."""
|
||||
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_L36_OUT", ROOT / "Saved/Linux/l36"))
|
||||
BIN = OUT / "bin"
|
||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L36_CAPTURE_DIR",
|
||||
Path(__file__).resolve().parent / "captures"))
|
||||
HEADLESS = Path(os.environ.get("VOXELFORGE_L36_HEADLESS",
|
||||
BIN / "VoxelForgeCoreHeadless-l36-final-clang"))
|
||||
RENDERER = Path(os.environ.get("VOXELFORGE_L36_RENDERER",
|
||||
BIN / "VoxelForgeOffscreenCapture-l36-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"[L36CaptureRun] 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"[L36StandardCaptureSet] 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"[L36CaptureCamera] 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"[L36CaptureJPEG] 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,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Inspect connected LOD0 surface components around the FloatingIslands mouth."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import math
|
||||
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", PROFILE_PATH)
|
||||
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(PROFILE)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("mesh")
|
||||
parser.add_argument("--center", nargs=2, type=float, default=(5.6, 0.445))
|
||||
parser.add_argument("--radius", type=float, default=14.0)
|
||||
parser.add_argument("--z-min", type=float, default=-66.0)
|
||||
parser.add_argument("--z-max", type=float, default=-50.0)
|
||||
parser.add_argument("--minimum-triangles", type=int, default=1)
|
||||
args = parser.parse_args()
|
||||
cx, cy = args.center
|
||||
bounds = (cx - args.radius, cx + args.radius, cy - args.radius, cy + args.radius)
|
||||
voxel_um, triangles, tiles = PROFILE.read_scene(
|
||||
args.mesh, 0, args.z_min, args.z_max, bounds)
|
||||
parent = list(range(len(triangles)))
|
||||
sizes = [1] * len(triangles)
|
||||
vertex_owner = {}
|
||||
|
||||
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, (_, _, verts) in enumerate(triangles):
|
||||
for point, _ in verts:
|
||||
key = tuple(round(value, 5) for value in point)
|
||||
other = vertex_owner.setdefault(key, index)
|
||||
union(index, other)
|
||||
|
||||
components = {}
|
||||
for index, (_, _, verts) in enumerate(triangles):
|
||||
root = find(index)
|
||||
item = components.setdefault(root, {"count": 0, "min": [math.inf] * 3,
|
||||
"max": [-math.inf] * 3, "area": 0.0,
|
||||
"sum": [0.0] * 3})
|
||||
item["count"] += 1
|
||||
points = [v[0] for v in verts]
|
||||
for point in points:
|
||||
for axis in range(3):
|
||||
item["min"][axis] = min(item["min"][axis], point[axis])
|
||||
item["max"][axis] = max(item["max"][axis], point[axis])
|
||||
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))
|
||||
|
||||
ordered = sorted(components.values(), key=lambda item: item["count"], reverse=True)
|
||||
print(f"[MouthComponentProbe] mesh={args.mesh} voxel_um={voxel_um} lod=0 "
|
||||
f"tiles={tiles} triangles={len(triangles)} components={len(ordered)} "
|
||||
f"bounds=({bounds[0]:.3f},{bounds[1]:.3f},{bounds[2]:.3f},{bounds[3]:.3f}) "
|
||||
f"z={args.z_min:.3f}..{args.z_max:.3f}")
|
||||
for item in ordered:
|
||||
if item["count"] < args.minimum_triangles:
|
||||
continue
|
||||
centroid = tuple(value / (3 * item["count"]) for value in item["sum"])
|
||||
print(f"[MouthComponent] triangles={item['count']} area_m2={item['area']:.4f} "
|
||||
f"bounds=({','.join(f'{v:.3f}' for v in item['min'])}).."
|
||||
f"({','.join(f'{v:.3f}' for v in item['max'])}) "
|
||||
f"centroid=({','.join(f'{v:.3f}' for v in centroid)})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure the exported walk surface along the passage under the tunnel camera."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
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", RING_PATH)
|
||||
RING = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(RING)
|
||||
CAMERA_RE = re.compile(r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||
r"target_m=\(([^)]*)\)", re.M)
|
||||
|
||||
|
||||
def length(a, b):
|
||||
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
|
||||
|
||||
|
||||
def fit(values, spacing):
|
||||
best = (0.0, 0.0)
|
||||
for step in range(8, 129):
|
||||
period = step * 0.0625
|
||||
omega = 2.0 * math.pi / period
|
||||
cc = ss = cs = yc = ys = 0.0
|
||||
for i, value in enumerate(values):
|
||||
phase = omega * i * spacing
|
||||
c, s = math.cos(phase), math.sin(phase)
|
||||
cc += c * c
|
||||
ss += s * s
|
||||
cs += c * s
|
||||
yc += value * c
|
||||
ys += value * s
|
||||
determinant = cc * ss - cs * cs
|
||||
if determinant <= 1.0e-9:
|
||||
continue
|
||||
cosine = (yc * ss - ys * cs) / determinant
|
||||
sine = (ys * cc - yc * cs) / determinant
|
||||
amplitude = math.hypot(cosine, sine)
|
||||
if amplitude > best[1]:
|
||||
best = period, amplitude
|
||||
return best
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("mesh")
|
||||
parser.add_argument("layout")
|
||||
parser.add_argument("--view", default="owner-sw-mid-tunnel")
|
||||
parser.add_argument("--passage", default="passage/0-1/0")
|
||||
parser.add_argument("--window", type=float, default=32.0)
|
||||
parser.add_argument("--spacing", type=float, default=0.125)
|
||||
parser.add_argument("--amplitude-limit-cm", type=float, default=1.5)
|
||||
args = parser.parse_args()
|
||||
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||
cameras = {name: (tuple(map(float, camera.split(","))),
|
||||
tuple(map(float, target.split(","))))
|
||||
for name, camera, target in CAMERA_RE.findall(
|
||||
log.read_text(encoding="utf-8", errors="replace"))}
|
||||
if args.view not in cameras:
|
||||
raise SystemExit(f"missing camera target for {args.view}")
|
||||
camera, target = cameras[args.view]
|
||||
layout = json.loads(Path(args.layout).read_text(encoding="utf-8"))
|
||||
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||
passage = next(p for p in layout["passages"] if p["stable_id"] == args.passage)
|
||||
controls = [(tuple(v * scale for v in point["position_q"]),
|
||||
point["radius_q"] * scale) for point in passage["control_points"]]
|
||||
lower_mouth = tuple(v * scale for v in passage["lower_mouth_q"])
|
||||
end = min(range(len(controls)), key=lambda i: length(controls[i][0], lower_mouth))
|
||||
controls = controls[:end + 1]
|
||||
path = []
|
||||
distance = 0.0
|
||||
for point in controls:
|
||||
if path:
|
||||
distance += length(path[-1][0], point[0])
|
||||
path.append((point[0], point[1], distance))
|
||||
closest = None
|
||||
for index, (a, b) in enumerate(zip(path, path[1:])):
|
||||
delta = tuple(b[0][axis] - a[0][axis] for axis in range(3))
|
||||
sq = sum(v * v for v in delta)
|
||||
t = max(0.0, min(1.0, sum((target[k] - a[0][k]) * delta[k]
|
||||
for k in range(3)) / sq)) if sq else 0.0
|
||||
point = tuple(a[0][k] + t * delta[k] for k in range(3))
|
||||
d2 = sum((point[k] - target[k]) ** 2 for k in range(3))
|
||||
if closest is None or d2 < closest[0]:
|
||||
closest = (d2, a[2] + t * (b[2] - a[2]), index, t)
|
||||
_, center_distance, segment, segment_t = closest
|
||||
|
||||
def interpolate(distance_value):
|
||||
distance_value = max(0.0, min(path[-1][2], distance_value))
|
||||
for a, b in zip(path, path[1:]):
|
||||
if distance_value <= b[2]:
|
||||
t = (distance_value - a[2]) / max(1.0e-9, b[2] - a[2])
|
||||
point = tuple(a[0][k] + t * (b[0][k] - a[0][k]) for k in range(3))
|
||||
radius = a[1] + t * (b[1] - a[1])
|
||||
return point, radius
|
||||
return path[-1][0], path[-1][1]
|
||||
|
||||
half = args.window * 0.5
|
||||
start = max(0.0, center_distance - half)
|
||||
finish = min(path[-1][2], center_distance + half)
|
||||
count = int((finish - start) / args.spacing) + 1
|
||||
positions = []
|
||||
expected = []
|
||||
for i in range(count):
|
||||
route_distance = start + i * args.spacing
|
||||
point, radius = interpolate(route_distance)
|
||||
positions.append((route_distance - center_distance, point))
|
||||
expected.append(point[2] - radius + 0.20)
|
||||
bounds = (min(p[0] for _, p in positions) - 8.0,
|
||||
max(p[0] for _, p in positions) + 8.0,
|
||||
min(p[1] for _, p in positions) - 8.0,
|
||||
max(p[1] for _, p in positions) + 8.0)
|
||||
zmin, zmax = min(expected) - 1.5, max(expected) + 1.5
|
||||
voxel_um, triangles, tiles = RING.read_scene(args.mesh, 0, zmin, zmax, bounds)
|
||||
bucket_size = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = RING.build_buckets(triangles, True, bounds, bucket_size)
|
||||
values = []
|
||||
for (_, point), preferred in zip(positions, expected):
|
||||
hit = RING.sample_surface(buckets, point[0], point[1], True,
|
||||
bucket_size, preferred)
|
||||
values.append(hit[0] if hit else None)
|
||||
valid = [(index, z) for index, z in enumerate(values) if z is not None]
|
||||
if len(valid) < 0.8 * len(values):
|
||||
raise SystemExit(f"insufficient mesh floor coverage {len(valid)}/{len(values)}")
|
||||
residual = [values[i] - expected[i] for i in range(len(values))
|
||||
if values[i] is not None]
|
||||
mean = sum(residual) / len(residual)
|
||||
period, amplitude = fit([v - mean for v in residual], args.spacing)
|
||||
adjacent = [abs(values[i] - values[i - 1]) for i in range(1, len(values))
|
||||
if values[i] is not None and values[i - 1] is not None]
|
||||
route_segment_length = path[segment + 1][2] - path[segment][2]
|
||||
print(f"[WalkFloorProfileProbe] mesh={args.mesh} view={args.view} voxel_um={voxel_um} "
|
||||
f"tiles={tiles} samples={len(valid)}/{len(values)} window_m={finish-start:.3f} "
|
||||
f"camera_target_m=({target[0]:.3f},{target[1]:.3f},{target[2]:.3f}) "
|
||||
f"route_distance_m={center_distance:.3f} nearest_segment={segment} "
|
||||
f"segment_t={segment_t:.3f} segment_length_m={route_segment_length:.3f} "
|
||||
f"floor_expected_range_m={min(expected):.3f}..{max(expected):.3f} "
|
||||
f"residual_range_m={min(residual):.4f}..{max(residual):.4f} "
|
||||
f"period_m={period:.3f} amplitude_m={amplitude:.5f} "
|
||||
f"amplitude_cm={amplitude*100:.3f} adjacent_step_max_m="
|
||||
f"{max(adjacent, default=0.0):.4f} limit_cm={args.amplitude_limit_cm:.3f} "
|
||||
f"status={'PASS' if amplitude*100 <= args.amplitude_limit_cm else 'FAIL'}")
|
||||
out = Path(args.mesh).with_suffix(".walk-floor-profile.csv")
|
||||
with out.open("w", encoding="utf-8") as stream:
|
||||
stream.write("distance_m,route_x_m,route_y_m,expected_floor_m,mesh_floor_m,residual_m\n")
|
||||
for i, (distance_m, point) in enumerate(positions):
|
||||
z = values[i]
|
||||
stream.write(f"{distance_m:.3f},{point[0]:.6f},{point[1]:.6f},"
|
||||
f"{expected[i]:.6f},{'' if z is None else f'{z:.6f}'},"
|
||||
f"{'' if z is None else f'{z-expected[i]:.6f}'}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,54 @@
|
||||
# VoxelForge L36 report
|
||||
|
||||
## Findings and fixes
|
||||
|
||||
The exported L35 walk meshes show real floor washboard: owner-SW repeats at 7.250 m with 2.252 cm amplitude; the upper-SW arrival repeats at 1.438 m with 2.482 cm amplitude. The old tread top was built per segment as a one-sided density clamp, so the tread/native-field crossing moved at the segment joins. The wall-only ripple scan could not see it. Removing shoulder grain alone did not remove the beat.
|
||||
|
||||
Sparse-route treads now overlap by up to 2 m and use bounded signed-distance slabs, including SurfaceWorld-to-TunnelNetwork. Tread seating and capsule/open-air gates remain. The exported v23 meshes measure 7.188 m / 0.067 cm on owner-SW and 6.500 m / 0.057 cm on the upper fixture. The expanded five-window density audit's maximum is 1.08 cm, below its 1.5 cm limit.
|
||||
|
||||
L35's FI mouth mesh had a 42.265° fade-ring normal jump and a detached 8-triangle component of 0.0035394 m². The defect was the short crater-root detail and tread support meeting the native crater field without a smooth signed-distance join; Marching Cubes exposed thin pieces at the seam. V23 filters the local signed field over 2 m while protecting the route, removes thin isolated solids, and smooth-unions the mouth tread with native terrain over 1.5 m. The same mesh check now measures 27.133° against a 35° limit and finds no component at or above 0.0001 m². One 8-triangle speck remains below that floor (0.0000266 m², 9.377 m from the mouth); it is recorded rather than counted as zero.
|
||||
|
||||
## Audits and captures
|
||||
|
||||
- Passage matrix: 24/24 recipe-stamped passes across both owner recipes, four styles, the upper-SW fixture, Clang and GCC. This includes support, walkability, capsule clearance, open air, route bindings, strict validation, mouth profiles, wall ripple and floor ripple. The 2 m / 1.5 m beaded control is rejected; both L35 floor windows fail the 1.5 cm floor limit.
|
||||
- FI-floor and SW-ceiling branch-locked terrain ring audits pass at LOD0–8. Coverage is 1.000; relief is at least 3.162 m and no flat run or following step is found. L30/L31 ring controls remain negative controls.
|
||||
- Interface/hole and transition suite: 16/16; zero hole columns and air samples. Strict-layout controls reject 10/10 malformed layouts. L13/L9: 4/4.
|
||||
- FI mouth fragment/crease control: L35 fails at 42.265° with one qualifying fragment; v23 passes at 27.133° with no qualifying fragments. The L35 mesh is the negative control.
|
||||
- The standard 12 JPEGs use the L34 fixed cameras and are in [`captures/`](captures/). Sizes are 21,976–43,435 bytes (all below 300 KB).
|
||||
|
||||
## Version, hashes, determinism
|
||||
|
||||
World evaluator version is 23.
|
||||
|
||||
| Recipe | Stable layout | L25 whole-view density / mesh / payload / topology |
|
||||
|---|---|---|
|
||||
| SurfaceWorld | `resolved-layout-v3-fnv64:f4c09b442fa40cd7` | `90ca5c612c0723bb / cac3b5f0f68c75f9 / 0a143e28341ed132 / 89f2018345744589` |
|
||||
| FloatingIslands | `resolved-layout-v3-fnv64:d2922ac7e8a2b491` | `da5eba38f4b14f79 / 8c403d9749b15abb / 8a917fa5b71ab179 / 3ed411c118d1f43f` |
|
||||
|
||||
Owner plan/manifest hashes: `a5e29887e839c5d6 / 57ebed8456c20f06`. Upper-fixture layout/plan/manifest: `resolved-layout-v3-fnv64:fc4e73619037f88c / 463a0f3b72909656 / f7bc6980e67b61c0`.
|
||||
|
||||
Determinism is 30/30: L25 and the 24 m crossing at W1/W8/W12 with both compilers and recipes, plus the upper fixture at W1/W8/W12 with both compilers. Core binary SHA-256:
|
||||
|
||||
- Clang: `f8342a7bb48bf460550bd2d4707a67156ebf02a5e5a247edd07e8de0028f6be2`
|
||||
- GCC: `37d28f3c8a816f485d2bb9e40c1cc37850e6f2d3888772573d11d7b101f6b7d5`
|
||||
|
||||
Ten interleaved L35/L36 pairs per recipe passed. Median whole-view time: SurfaceWorld 1.728 s → 1.752 s (+1.42%; paired median +0.66%, MAD 1.16%); FloatingIslands 16.645 s → 16.630 s (-0.09%; paired median +0.35%, MAD 0.29%). Both changes are within measured run noise.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- Three compile fixes: `build-l36-clang-retry2.log` had a wall-relief call mismatch (retry3 compiled); `build-l36-floor-coordinates.log` had an invalid assignment (retry1 compiled); `build-l36-sweep-closest.log` assigned to a const value (retry1 compiled).
|
||||
- Passage audit revisions: `run-passage-audits.log` ended after four cases; `run-passage-audits-final.log` was 14/24, `-r2` 18/24, `-r3` 22/24, and `-r4` stopped after eight cases on a failing broad tread candidate. `-r5`, `-r6`, and the final v23 run were 24/24. Earlier support candidates failed capsule, open-air, or floor-ripple checks; their `preaudit-*`, `preflight-*`, and `audit-*` logs remain under `Saved/Linux/l36/`.
|
||||
- Mouth trials `mouth-smin1m`, `mouth-route-smoothunion`, `mouth-airward-filter`, and the 1.5 m filter variants left 36–44° jumps or a residual speck. The final 2 m field filter plus 1.5 m SDF union is the passing candidate. The older supplemental 20° fade probe remains stricter than this round's 35° gate; the final 27.133° jump would still exceed 20°. The L35 42.265° control is detected by the final check.
|
||||
- Initial floor-window placement at fraction .49 missed the upper-fixture route target; .457 covered it. Initial ring exports (`ring-export-*-v23.log`) used a render-distance override; early `ring-*-v23-camera*.log`, `ring-*-v23-branch32.log` and `ring-*-v23-current32.log` scans used the wrong center/radius and had missing coverage or only 1.857 m relief. Re-exporting without that override and using the recorded branch-locked windows produced the LOD0–8 passes. `ring-sw-lod0-v23-branchlocked-full.log` had a path typo and was immediately rerun as `ring-sw-lod0-v23-branchlocked-final.log`.
|
||||
- The first five cost pairs were repeated because the SurfaceWorld median was close to noise; all ten pairs per recipe passed. No run exceeded 30 minutes and no step was stopped for a 15-minute stall. All runs used CPUs 0–11, a 6 GiB address-space cap and at most 12 workers; governor unchanged.
|
||||
|
||||
No Unreal files changed. No commit, push or stash was made.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Accepted: the floor washboard is gone in owner-SW mid-tunnel (floor now reads as irregular natural ground). Cause
|
||||
measured (per-segment one-sided tread clamps moving the crossing at joins), floor ripple audit added with L35 as a
|
||||
failing control. The FI mouth shards and detached fragment are gone. Evaluator v23.
|
||||
- Not accepted as done: **the FI mouth tread now reads as a trapezoid platform**: straight edges, planar faces, a
|
||||
clear man-made shape inside the crater. Also, the 35° crease gate was chosen during the round, while the earlier
|
||||
probe used 20°; the final 27° would fail that. L37 targets the shape itself.
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run five interleaved whole-view cost pairs against the L35 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/l36"
|
||||
BINARIES = {
|
||||
"l35": ROOT / "Saved/Linux/l35/bin/VoxelForgeCoreHeadless-l35-final-clang",
|
||||
"l36": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
}
|
||||
ORDERS = (("l35", "l36"), ("l36", "l35"), ("l35", "l36"),
|
||||
("l36", "l35"), ("l35", "l36"))
|
||||
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||
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 = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
output, re.M)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
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
|
||||
rows.append((recipe, pair, build, ready, elapsed,
|
||||
result.returncode if result else "TIMEOUT", valid))
|
||||
failed |= not valid
|
||||
print(f"[L36CostRun] 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-runs.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["l35"])
|
||||
current = statistics.median(by_build["l36"])
|
||||
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["l36"] - values["l35"]) / values["l35"] * 100.0)
|
||||
mad = statistics.median(abs(value - statistics.median(paired)) for value in paired)
|
||||
print(f"[L36CostSummary] recipe={recipe} pairs=5 median_l35_s={baseline:.3f} "
|
||||
f"median_l36_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"[L36Cost] 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 L36."""
|
||||
import csv
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l36"
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-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"[L36DeterminismRun] 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-l36.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"[L36DeterminismSummary] 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/l36"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-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 L36."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l36"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-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"[L36LegacyAuditRun] 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"[L36LegacyAuditSummary] 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,166 @@
|
||||
#!/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
|
||||
NORMAL_LIMIT_DEG = 35.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("--current", type=Path,
|
||||
default=ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh")
|
||||
args = parser.parse_args()
|
||||
control = args.control if args.control.is_absolute() else ROOT / args.control
|
||||
current = args.current if args.current.is_absolute() else ROOT / args.current
|
||||
control_ok, control_max = audit(control)
|
||||
current_ok, current_max = audit(current)
|
||||
expected = not control_ok and current_ok and control_max > NORMAL_LIMIT_DEG
|
||||
print(f"[MouthFadeNegativeControl] control_max_deg={control_max:.6f} "
|
||||
f"control_detected={not control_ok} current_max_deg={current_max:.6f} "
|
||||
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/l36"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l36-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"[L36PassageAuditRun] 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"[L36PassageAuditSummary] 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)
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 26 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 |
@@ -817,8 +817,15 @@ namespace StandaloneStreamBenchmark
|
||||
double periodMetres=0.0,amplitudeMetres=0.0,localPeakAmplitudeMetres=0.0;
|
||||
double coherentPeriodMetres=0.0,coherentAmplitudeMetres=0.0;
|
||||
double controlAmplitudeMetres=0.0;
|
||||
double floorPeriodMetres=0.0,floorAmplitudeMetres=0.0;
|
||||
double floorControlAmplitudeMetres=0.0;
|
||||
double floorCoherentPeriodMetres=0.0,floorCoherentAmplitudeMetres=0.0;
|
||||
std::vector<std::array<double,6>> floorPeakProfile;
|
||||
double floorPeakWindowStartMetres=0.0;
|
||||
std::uint64_t samples=0,expectedSamples=0;
|
||||
std::uint64_t floorSamples=0,floorExpectedSamples=0;
|
||||
bool controlDetected=false,passed=false;
|
||||
bool floorControlDetected=false;
|
||||
};
|
||||
constexpr double MaximumFlatRunMetres=8.0;
|
||||
constexpr double MinimumWallReliefMetres=0.20;
|
||||
@@ -1131,6 +1138,7 @@ namespace StandaloneStreamBenchmark
|
||||
constexpr double MaximumPeriodMetres=8.0;
|
||||
constexpr double PeriodStepMetres=0.0625;
|
||||
constexpr double RippleAmplitudeLimitMetres=0.035;
|
||||
constexpr double FloorRippleAmplitudeLimitMetres=0.015;
|
||||
struct FPathPoint
|
||||
{
|
||||
double x=0.0,y=0.0,z=0.0,radius=0.0,distance=0.0;
|
||||
@@ -1218,6 +1226,26 @@ namespace StandaloneStreamBenchmark
|
||||
values[i]-mean-slope*x});
|
||||
}
|
||||
};
|
||||
const auto appendDetrendedPairs=[](
|
||||
const std::vector<std::array<double,2>>& values,
|
||||
std::vector<std::array<double,2>>& output)
|
||||
{
|
||||
if(values.size()<64)return;
|
||||
double meanX=0.0,meanY=0.0;
|
||||
for(const auto& value:values){meanX+=value[0];meanY+=value[1];}
|
||||
meanX/=static_cast<double>(values.size());
|
||||
meanY/=static_cast<double>(values.size());
|
||||
double xy=0.0,xx=0.0;
|
||||
for(const auto& value:values)
|
||||
{
|
||||
const double x=value[0]-meanX;
|
||||
xy+=x*(value[1]-meanY);xx+=x*x;
|
||||
}
|
||||
const double slope=xx>0.0?xy/xx:0.0;
|
||||
for(const auto& value:values)
|
||||
output.push_back({value[0],value[1]-meanY
|
||||
-slope*(value[0]-meanX)});
|
||||
};
|
||||
std::vector<double> beadControl;
|
||||
beadControl.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
||||
constexpr double BeadSpacingMetres=1.5;
|
||||
@@ -1234,20 +1262,64 @@ namespace StandaloneStreamBenchmark
|
||||
double ignoredPeriod=0.0;
|
||||
audit.controlAmplitudeMetres=sinusoidAmplitude(beadSamples,ignoredPeriod);
|
||||
audit.controlDetected=audit.controlAmplitudeMetres>=RippleAmplitudeLimitMetres;
|
||||
const std::array<double,4> windowStarts{{0.03,0.31,0.61,0.91}};
|
||||
audit.floorControlAmplitudeMetres=audit.controlAmplitudeMetres;
|
||||
audit.floorControlDetected=audit.floorControlAmplitudeMetres
|
||||
>=FloorRippleAmplitudeLimitMetres;
|
||||
const std::array<double,5> windowStarts{{0.03,0.31,0.457,0.61,0.91}};
|
||||
std::vector<std::array<double,2>> coherentSamples;
|
||||
std::vector<std::array<double,2>> coherentFloorSamples;
|
||||
for(const double fraction:windowStarts)
|
||||
{
|
||||
const double start=(total-WindowLengthMetres)*fraction;
|
||||
std::vector<double> radii;
|
||||
radii.reserve(static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres)+1);
|
||||
std::vector<std::array<double,2>> floorResiduals;
|
||||
floorResiduals.reserve(static_cast<std::size_t>(WindowLengthMetres/
|
||||
SampleSpacingMetres)+1);
|
||||
std::vector<std::array<double,6>> floorDebugSamples;
|
||||
floorDebugSamples.reserve(static_cast<std::size_t>(WindowLengthMetres/
|
||||
SampleSpacingMetres)+1);
|
||||
for(std::size_t sample=0;
|
||||
sample<=static_cast<std::size_t>(WindowLengthMetres/SampleSpacingMetres);
|
||||
++sample)
|
||||
{
|
||||
++audit.expectedSamples;
|
||||
++audit.floorExpectedSamples;
|
||||
const double distance=start+static_cast<double>(sample)*SampleSpacingMetres;
|
||||
const auto pose=interpolate(distance);
|
||||
// Recover the composed walk surface from its zero crossing.
|
||||
// The support strip is segment-built and is not itself a true
|
||||
// signed-distance field, so a fixed centerline radius misses
|
||||
// the stepped top that the player actually walks on.
|
||||
const double expectedFloor=pose.z-pose.radius+0.20;
|
||||
constexpr double FloorProbeStepMetres=0.25;
|
||||
constexpr int FloorProbeCount=7;
|
||||
double previousDensity=0.0,previousZ=0.0;
|
||||
bool havePrevious=false,haveFloor=false;
|
||||
double bestFloor=0.0,bestDistance=std::numeric_limits<double>::infinity();
|
||||
for(int vertical=0;vertical<FloorProbeCount;++vertical)
|
||||
{
|
||||
const double z=expectedFloor-1.0+vertical*FloorProbeStepMetres;
|
||||
const double density=static_cast<double>(Field.densityAt({
|
||||
pose.x/coreMetres,pose.y/coreMetres,z/coreMetres}))*coreMetres;
|
||||
if(havePrevious&&previousDensity<0.0&&density>=0.0)
|
||||
{
|
||||
const double denominator=density-previousDensity;
|
||||
const double crossing=denominator>1.0e-12
|
||||
?previousZ+(z-previousZ)*(-previousDensity)/denominator:z;
|
||||
const double candidateDistance=std::abs(crossing-expectedFloor);
|
||||
if(candidateDistance<bestDistance)
|
||||
{bestDistance=candidateDistance;bestFloor=crossing;haveFloor=true;}
|
||||
}
|
||||
previousDensity=density;previousZ=z;havePrevious=true;
|
||||
}
|
||||
if(haveFloor)
|
||||
{
|
||||
floorResiduals.push_back({distance,bestFloor-expectedFloor});
|
||||
floorDebugSamples.push_back({distance,expectedFloor,bestFloor,
|
||||
pose.x,pose.y,pose.z});
|
||||
++audit.floorSamples;
|
||||
}
|
||||
const auto before=interpolate(std::max(0.0,distance-0.25));
|
||||
const auto after=interpolate(std::min(total,distance+0.25));
|
||||
const double tangentX=after.x-before.x,tangentY=after.y-before.y;
|
||||
@@ -1279,14 +1351,54 @@ namespace StandaloneStreamBenchmark
|
||||
}
|
||||
coherentSamples.insert(coherentSamples.end(),windowSamples.begin(),
|
||||
windowSamples.end());
|
||||
double floorWindowPeriod=0.0,floorWindowAmplitude=0.0;
|
||||
if(floorResiduals.size()>=static_cast<std::size_t>(WindowLengthMetres/
|
||||
SampleSpacingMetres)*3/4)
|
||||
{
|
||||
std::vector<std::array<double,2>> floorWindowSamples;
|
||||
appendDetrendedPairs(floorResiduals,floorWindowSamples);
|
||||
floorWindowAmplitude=sinusoidAmplitude(floorWindowSamples,
|
||||
floorWindowPeriod);
|
||||
if(floorWindowAmplitude>audit.floorAmplitudeMetres)
|
||||
{
|
||||
audit.floorAmplitudeMetres=floorWindowAmplitude;
|
||||
audit.floorPeriodMetres=floorWindowPeriod;
|
||||
audit.floorPeakWindowStartMetres=start;
|
||||
audit.floorPeakProfile=std::move(floorDebugSamples);
|
||||
}
|
||||
coherentFloorSamples.insert(coherentFloorSamples.end(),
|
||||
floorWindowSamples.begin(),floorWindowSamples.end());
|
||||
}
|
||||
const double floorWindowCoverage=static_cast<double>(floorResiduals.size())
|
||||
/(WindowLengthMetres/SampleSpacingMetres+1.0);
|
||||
double floorResidualMinimum=0.0,floorResidualMaximum=0.0;
|
||||
if(!floorResiduals.empty())
|
||||
{
|
||||
const auto extrema=std::minmax_element(floorResiduals.begin(),
|
||||
floorResiduals.end(),[](const auto& a,const auto& b)
|
||||
{return a[1]<b[1];});
|
||||
floorResidualMinimum=(*extrema.first)[1];
|
||||
floorResidualMaximum=(*extrema.second)[1];
|
||||
}
|
||||
std::printf("[PassageFloorRippleWindow] id=%s start_m=%.3f "
|
||||
"period_m=%.3f amplitude_m=%.4f coverage=%.3f "
|
||||
"residual_range_m=%.4f..%.4f threshold_m=0.015\n",
|
||||
passage.stableId.c_str(),start,floorWindowPeriod,floorWindowAmplitude,
|
||||
floorWindowCoverage,floorResidualMinimum,floorResidualMaximum);
|
||||
}
|
||||
audit.coherentAmplitudeMetres=sinusoidAmplitude(coherentSamples,
|
||||
audit.coherentPeriodMetres);
|
||||
audit.floorCoherentAmplitudeMetres=sinusoidAmplitude(coherentFloorSamples,
|
||||
audit.floorCoherentPeriodMetres);
|
||||
audit.amplitudeMetres=audit.localPeakAmplitudeMetres;
|
||||
const double coverage=audit.expectedSamples>0
|
||||
?static_cast<double>(audit.samples)/audit.expectedSamples:0.0;
|
||||
const double floorCoverage=audit.floorExpectedSamples>0
|
||||
?static_cast<double>(audit.floorSamples)/audit.floorExpectedSamples:0.0;
|
||||
audit.passed=audit.controlDetected&&coverage>=0.75
|
||||
&&audit.localPeakAmplitudeMetres<RippleAmplitudeLimitMetres;
|
||||
&&audit.localPeakAmplitudeMetres<RippleAmplitudeLimitMetres
|
||||
&&audit.floorControlDetected&&floorCoverage>=0.75
|
||||
&&audit.floorAmplitudeMetres<FloorRippleAmplitudeLimitMetres;
|
||||
return audit;
|
||||
};
|
||||
const auto looksNatural=[&](double flatRun,double reliefPeakToPeak)
|
||||
@@ -1908,6 +2020,7 @@ namespace StandaloneStreamBenchmark
|
||||
};
|
||||
std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0;
|
||||
std::uint64_t mouthProfilePasses=0,ripplePasses=0,rippleControlsDetected=0;
|
||||
std::uint64_t floorRippleControlsDetected=0;
|
||||
std::uint64_t openAirViolations=0,openAirLegacyViolations=0,openAirSamples=0;
|
||||
std::uint64_t openAirAudits=0,openAirColumnSamples=0,openAirShortSpanSamples=0;
|
||||
std::uint64_t columnCrossings=0,shortGroundedSpans=0,columnConnectedEnds=0;
|
||||
@@ -1990,6 +2103,7 @@ namespace StandaloneStreamBenchmark
|
||||
mouthProfilePasses+=mouthProfileAudit.passed;
|
||||
ripplePasses+=rippleAudit.passed;
|
||||
rippleControlsDetected+=rippleAudit.controlDetected;
|
||||
floorRippleControlsDetected+=rippleAudit.floorControlDetected;
|
||||
passagePasses+=passageAudit.passed&&mouthsInRadius&&mouthsInContent
|
||||
&&naturalAudit.passed&&mouthProfileAudit.passed&&rippleAudit.passed;
|
||||
std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n",
|
||||
@@ -2016,7 +2130,7 @@ namespace StandaloneStreamBenchmark
|
||||
mouthProfileAudit.wallFlatStepRunMetres,mouthProfileAudit.wallStepMetres,
|
||||
PassageProfileSpacingMetres,PassageProfilePlateauMinimumMetres,
|
||||
PassageProfileStepThresholdMetres,mouthProfileAudit.passed?"PASS":"FAIL");
|
||||
std::printf("[PassageRippleAudit] id=%s window_m=32.000 spacing_m=0.125 period_m=%.3f amplitude_m=%.4f amplitude_cm=%.2f coherent_period_m=%.3f coherent_amplitude_m=%.4f coherent_amplitude_cm=%.2f local_peak_amplitude_m=%.4f coverage=%llu/%llu threshold_m=0.035 synthetic_spheres_period_m=1.500 synthetic_spheres_amplitude_m=%.4f synthetic_spheres_detected=%s status=%s\n",
|
||||
std::printf("[PassageRippleAudit] id=%s window_m=32.000 spacing_m=0.125 period_m=%.3f amplitude_m=%.4f amplitude_cm=%.2f coherent_period_m=%.3f coherent_amplitude_m=%.4f coherent_amplitude_cm=%.2f local_peak_amplitude_m=%.4f coverage=%llu/%llu threshold_m=0.035 synthetic_spheres_period_m=1.500 synthetic_spheres_amplitude_m=%.4f synthetic_spheres_detected=%s floor_period_m=%.3f floor_amplitude_m=%.4f floor_amplitude_cm=%.2f floor_coherent_period_m=%.3f floor_coherent_amplitude_m=%.4f floor_coverage=%llu/%llu floor_threshold_m=0.015 floor_beaded_control_amplitude_m=%.4f floor_beaded_control_detected=%s status=%s\n",
|
||||
passage.stableId.c_str(),rippleAudit.periodMetres,
|
||||
rippleAudit.amplitudeMetres,rippleAudit.amplitudeMetres*100.0,
|
||||
rippleAudit.coherentPeriodMetres,rippleAudit.coherentAmplitudeMetres,
|
||||
@@ -2025,7 +2139,23 @@ namespace StandaloneStreamBenchmark
|
||||
static_cast<unsigned long long>(rippleAudit.samples),
|
||||
static_cast<unsigned long long>(rippleAudit.expectedSamples),
|
||||
rippleAudit.controlAmplitudeMetres,rippleAudit.controlDetected?"true":"false",
|
||||
rippleAudit.floorPeriodMetres,rippleAudit.floorAmplitudeMetres,
|
||||
rippleAudit.floorAmplitudeMetres*100.0,
|
||||
rippleAudit.floorCoherentPeriodMetres,rippleAudit.floorCoherentAmplitudeMetres,
|
||||
static_cast<unsigned long long>(rippleAudit.floorSamples),
|
||||
static_cast<unsigned long long>(rippleAudit.floorExpectedSamples),
|
||||
rippleAudit.floorControlAmplitudeMetres,
|
||||
rippleAudit.floorControlDetected?"true":"false",
|
||||
rippleAudit.passed?"PASS":"FAIL");
|
||||
for(std::size_t sample=0;sample<rippleAudit.floorPeakProfile.size();sample+=4)
|
||||
{
|
||||
const auto& value=rippleAudit.floorPeakProfile[sample];
|
||||
std::printf("[PassageFloorRippleSample] id=%s distance_m=%.3f "
|
||||
"expected_floor_m=%.4f sampled_floor_m=%.4f residual_m=%.4f "
|
||||
"route_xyz_m=(%.3f,%.3f,%.3f)\n",
|
||||
passage.stableId.c_str(),value[0],value[1],value[2],value[2]-value[1],
|
||||
value[3],value[4],value[5]);
|
||||
}
|
||||
std::printf("[PassageOpenAirAudit] id=%s support_samples=%llu violations=%llu column_samples=%llu short_span_samples=%llu l24_ungated_violations=%llu route_support_samples=%llu route_support_violations=%llu spacing_m=%.3f status=%s\n",
|
||||
passage.stableId.c_str(),static_cast<unsigned long long>(openAirAudit.supportSamples),
|
||||
static_cast<unsigned long long>(openAirAudit.violations),
|
||||
@@ -2340,10 +2470,11 @@ namespace StandaloneStreamBenchmark
|
||||
PassageProfilePlateauMinimumMetres,PassageProfileStepThresholdMetres,
|
||||
l23TerraceControlDetected?"DETECTED":"MISSED",
|
||||
mouthProfilePasses==passageCount&&l23TerraceControlDetected?"PASS":"FAIL");
|
||||
std::printf("[PassageRippleAuditSummary] passed=%llu/%llu synthetic_spheres_negative_control=%s max_amplitude_m=0.035 status=%s\n",
|
||||
std::printf("[PassageRippleAuditSummary] passed=%llu/%llu synthetic_spheres_negative_control=%s max_amplitude_m=0.035 floor_beaded_negative_control=%s max_floor_amplitude_m=0.015 status=%s\n",
|
||||
static_cast<unsigned long long>(ripplePasses),
|
||||
static_cast<unsigned long long>(passageCount),
|
||||
rippleControlsDetected==passageCount?"DETECTED":"MISSED",
|
||||
floorRippleControlsDetected==passageCount?"DETECTED":"MISSED",
|
||||
ripplePasses==passageCount?"PASS":"FAIL");
|
||||
const bool auditPassed=passageCount>0&&passagePasses==passageCount
|
||||
&&naturalPasses==passageCount&&l23SmoothWallDetected
|
||||
|
||||