Author SHA1 Message Date
Fr0zkaandClaude Opus 5.5 bf39712792 passages: smoothed open-air shell joins (L110, v257) - partial, not merged
Bands = end profiles of per-control-span shell capsules (~9.35 m pitch = span spacing).
Shell now smooth-unions adjacent spans over 6 m and reuses the interior chord distance; a 0.60 m
clearance fades in 5-20 m from each mouth. Close-ups: bands reduced but still visible on part of
the exposed shell. Cost vs L109: near-ready +8..+12% (FI 0.88 -> 1.00, TN 0.92 -> 1.01 s @W12).
Audits, Clang/GCC determinism, fast tier and Linux UE build pass. Kept on its own branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-29 16:07:52 +01:00
397 changed files with 7754 additions and 128 deletions

No files matched your search

@@ -469,7 +469,10 @@ namespace VoxelForgeCore
// Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB. // Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB.
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads. // Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
// Version 217 replaces passage supports with flat-floor hollow-shell geometry. // Version 217 replaces passage supports with flat-floor hollow-shell geometry.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 217; // Version 256 widens the smooth union across neighboring shell spans to
// remove their regularly spaced end-profile bands.
// Version 257 reuses passage chord distances for the shell to avoid duplicate curve work.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 257;
enum class FieldChannel : std::uint32_t enum class FieldChannel : std::uint32_t
{ {
+441 -122
View File
@@ -261,14 +261,17 @@ public:
struct FPassageSweepSample struct FPassageSweepSample
{ {
double signedDistance=std::numeric_limits<double>::infinity(); double signedDistance=std::numeric_limits<double>::infinity();
double shellSignedDistance=std::numeric_limits<double>::infinity();
double roundSignedDistance=std::numeric_limits<double>::infinity(); double roundSignedDistance=std::numeric_limits<double>::infinity();
double centerZSubvoxels=0.0,floorCenterZSubvoxels=0.0; double centerZSubvoxels=0.0,floorCenterZSubvoxels=0.0;
double radiusSubvoxels=0.0,floorDepthSubvoxels=0.0; double radiusSubvoxels=0.0,floorDepthSubvoxels=0.0;
double t=0.0; double t=0.0;
std::uint32_t segmentIndex=std::numeric_limits<std::uint32_t>::max(); std::uint32_t segmentIndex=std::numeric_limits<std::uint32_t>::max();
double shellSignedDistance=std::numeric_limits<double>::infinity();
std::uint32_t shellSegmentIndex=std::numeric_limits<std::uint32_t>::max();
double previousShellDistance=std::numeric_limits<double>::infinity();
double previousShellBlendWeight=0.0;
float wallRelief=0.0f; float wallRelief=0.0f;
bool valid=false,hasWallRelief=false; bool valid=false,shellValid=false,hasWallRelief=false;
}; };
struct FPassageTileSweepData struct FPassageTileSweepData
{ {
@@ -1195,13 +1198,16 @@ public:
||!std::isfinite(Y)||!std::isfinite(Z))return density; ||!std::isfinite(Y)||!std::isfinite(Z))return density;
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
constexpr double FloorShellThicknessMetres=0.75; constexpr double FloorShellThicknessMetres=0.75;
constexpr double WallShellThicknessMetres=0.75;
constexpr double ShellClearanceMetres=0.60;
constexpr double WallReliefMetres=0.21; constexpr double WallReliefMetres=0.21;
constexpr double MouthShellBlendMetres=5.0; constexpr double MouthShellBlendMetres=5.0;
const double qPerMetre=Q*1000000.0/ const double qPerMetre=Q*1000000.0/
std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres); std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q, const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,
qz=static_cast<double>(Z)*Q; qz=static_cast<double>(Z)*Q;
const double shellThickness=FloorShellThicknessMetres*qPerMetre; const double floorShellThicknessBase=FloorShellThicknessMetres*qPerMetre;
const double wallShellThickness=WallShellThicknessMetres*qPerMetre;
const auto* passageTileQueries=ActivePassageTileQueries; const auto* passageTileQueries=ActivePassageTileQueries;
const bool passageTileScopeMatches=passageTileQueries const bool passageTileScopeMatches=passageTileQueries
&&passageTileQueries->matches(*this); &&passageTileQueries->matches(*this);
@@ -1212,7 +1218,6 @@ public:
struct FNearestTubeSample struct FNearestTubeSample
{ {
double signedDistance=std::numeric_limits<double>::infinity(); double signedDistance=std::numeric_limits<double>::infinity();
double shellSignedDistance=std::numeric_limits<double>::infinity();
double roundSignedDistance=std::numeric_limits<double>::infinity(); double roundSignedDistance=std::numeric_limits<double>::infinity();
double centerZ=0.0,floorCenterZ=0.0,radius=0.0,floorDepth=0.0,t=0.0; double centerZ=0.0,floorCenterZ=0.0,radius=0.0,floorDepth=0.0,t=0.0;
std::size_t segmentIndex=0; std::size_t segmentIndex=0;
@@ -1232,7 +1237,7 @@ public:
continue; continue;
const auto& constants=PassageComposerConstants[passageIndex]; const auto& constants=PassageComposerConstants[passageIndex];
FNearestTubeSample nearest; FNearestTubeSample nearest,nearestShell;
const bool usePreparedSweep=passageTileScopeMatches const bool usePreparedSweep=passageTileScopeMatches
&&passageTileQueries->hasSweptTubeBase(); &&passageTileQueries->hasSweptTubeBase();
const auto* swept=usePreparedSweep const auto* swept=usePreparedSweep
@@ -1240,7 +1245,6 @@ public:
if(swept) if(swept)
{ {
nearest.signedDistance=swept->signedDistance; nearest.signedDistance=swept->signedDistance;
nearest.shellSignedDistance=swept->shellSignedDistance;
nearest.roundSignedDistance=swept->roundSignedDistance; nearest.roundSignedDistance=swept->roundSignedDistance;
nearest.segmentIndex=swept->segmentIndex; nearest.segmentIndex=swept->segmentIndex;
nearest.t=swept->t; nearest.t=swept->t;
@@ -1250,6 +1254,10 @@ public:
nearest.floorDepth=swept->floorDepthSubvoxels; nearest.floorDepth=swept->floorDepthSubvoxels;
nearest.valid=true; nearest.valid=true;
nearest.swept=swept; nearest.swept=swept;
nearestShell.signedDistance=swept->shellSignedDistance;
nearestShell.segmentIndex=swept->shellSegmentIndex;
nearestShell.valid=swept->shellValid;
nearestShell.swept=swept;
} }
else else
{ {
@@ -1265,7 +1273,17 @@ public:
const std::ptrdiff_t lastIndex=static_cast<std::ptrdiff_t>( const std::ptrdiff_t lastIndex=static_cast<std::ptrdiff_t>(
passage.controlPoints.size())-1; passage.controlPoints.size())-1;
constexpr double ShellJoinBlendMetres=6.00;
double bestDistance=std::numeric_limits<double>::infinity(); double bestDistance=std::numeric_limits<double>::infinity();
double bestShellSearchBound=std::numeric_limits<double>::infinity();
struct FShellCandidate
{
double distance=std::numeric_limits<double>::infinity();
std::size_t segmentIndex=0;
double blendWeight=1.0;
};
std::array<FShellCandidate,8> shellCandidates{};
std::size_t shellCandidateCount=0;
const auto visitSegment=[&](std::size_t segmentIndex,int) const auto visitSegment=[&](std::size_t segmentIndex,int)
{ {
if(passageProfile)++passageProfile->passageCarveSegmentVisits; if(passageProfile)++passageProfile->passageCarveSegmentVisits;
@@ -1275,13 +1293,12 @@ public:
+(qy-segment.a[1])*segment.delta[1] +(qy-segment.a[1])*segment.delta[1]
+(qz-segment.a[2])*segment.delta[2]; +(qz-segment.a[2])*segment.delta[2];
const double t=std::clamp(projection/segment.lengthSq,0.0,1.0); const double t=std::clamp(projection/segment.lengthSq,0.0,1.0);
double cx=segment.a[0]+t*segment.delta[0]; const double cx=segment.a[0]+t*segment.delta[0];
double cy=segment.a[1]+t*segment.delta[1]; const double cy=segment.a[1]+t*segment.delta[1];
double cz=segment.a[2]+t*segment.delta[2]; const double cz=segment.a[2]+t*segment.delta[2];
const double radius=segment.radiusA+t*segment.radiusDelta; const double radius=segment.radiusA+t*segment.radiusDelta;
const double ex=qx-cx,ey=qy-cy,ez=qz-cz; const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
const double roundDistance=DeterministicSqrt( const double roundDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius;
ex*ex+ey*ey+ez*ez)-radius;
const double floorT=segment.horizontalLengthSq>1.0e-12 const double floorT=segment.horizontalLengthSq>1.0e-12
?std::clamp(((qx-segment.a[0])*segment.delta[0] ?std::clamp(((qx-segment.a[0])*segment.delta[0]
+(qy-segment.a[1])*segment.delta[1]) +(qy-segment.a[1])*segment.delta[1])
@@ -1308,15 +1325,11 @@ public:
nearest.floorDepth=floorDepth; nearest.floorDepth=floorDepth;
nearest.valid=true; nearest.valid=true;
} }
}; };
VisitNearestPassageSegmentRange(passageIndex,0,lastIndex-1, VisitNearestPassageSegmentRange(passageIndex,0,lastIndex-1,
qx,qy,qz,0,bestDistance,visitSegment); qx,qy,qz,0,bestDistance,visitSegment);
// Shells form a union over nearby route segments, even when another
// segment supplies the nearest interior distance. The hierarchy bound
// is deliberately wider than the shell so its radius and corner blend
// cannot prune a segment that contributes a boundary shell.
double shellSearchBound=2.0*qPerMetre;
const auto visitShellSegment=[&](std::size_t segmentIndex,int) const auto visitShellSegment=[&](std::size_t segmentIndex,int)
{ {
const auto& segment=PassageSegmentData[passageIndex][segmentIndex]; const auto& segment=PassageSegmentData[passageIndex][segmentIndex];
@@ -1329,8 +1342,9 @@ public:
const double cy=segment.a[1]+t*segment.delta[1]; const double cy=segment.a[1]+t*segment.delta[1];
const double cz=segment.a[2]+t*segment.delta[2]; const double cz=segment.a[2]+t*segment.delta[2];
const double radius=segment.radiusA+t*segment.radiusDelta; const double radius=segment.radiusA+t*segment.radiusDelta;
const double ex=qx-cx,ey=qy-cy,ez=qz-cz; const double dx=qx-cx,dy=qy-cy,dz=qz-cz;
const double roundDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius; const double roundDistance=DeterministicSqrt(dx*dx+dy*dy+dz*dz)
-radius;
const double floorT=segment.horizontalLengthSq>1.0e-12 const double floorT=segment.horizontalLengthSq>1.0e-12
?std::clamp(((qx-segment.a[0])*segment.delta[0] ?std::clamp(((qx-segment.a[0])*segment.delta[0]
+(qy-segment.a[1])*segment.delta[1]) +(qy-segment.a[1])*segment.delta[1])
@@ -1341,13 +1355,14 @@ public:
const double floorCenterZ=PassageFlatFloorCenterZSubvoxels( const double floorCenterZ=PassageFlatFloorCenterZSubvoxels(
constants,segmentIndex,floorT,segment,floorDepth,qPerMetre); constants,segmentIndex,floorT,segment,floorDepth,qPerMetre);
const double flatDistance=PassageFlatBottomDistance( const double flatDistance=PassageFlatBottomDistance(
static_cast<float>(roundDistance),floorCenterZ,qz,floorDepth,qPerMetre); static_cast<float>(roundDistance),floorCenterZ,qz,floorDepth,
qPerMetre);
const double alongMetres=segment.authoredAlongMetres const double alongMetres=segment.authoredAlongMetres
+t*segment.authoredLengthMetres; +t*segment.authoredLengthMetres;
const double mouthDistanceMetres=std::max(0.0,std::min(alongMetres, const double mouthDistanceMetres=std::max(0.0,std::min(alongMetres,
constants.totalPathLengthMetres-alongMetres)); constants.totalPathLengthMetres-alongMetres));
const double shellFade=std::clamp(mouthDistanceMetres/ const double shellFade=std::clamp(mouthDistanceMetres
MouthShellBlendMetres,0.0,1.0); /MouthShellBlendMetres,0.0,1.0);
const double smoothShellFade=shellFade*shellFade const double smoothShellFade=shellFade*shellFade
*(3.0-2.0*shellFade); *(3.0-2.0*shellFade);
const double floorDistanceMetres=(floorCenterZ-floorDepth-qz) const double floorDistanceMetres=(floorCenterZ-floorDepth-qz)
@@ -1355,32 +1370,91 @@ public:
const double floorBlend=std::clamp(floorDistanceMetres/0.25,0.0,1.0); const double floorBlend=std::clamp(floorDistanceMetres/0.25,0.0,1.0);
const double smoothFloorBlend=floorBlend*floorBlend const double smoothFloorBlend=floorBlend*floorBlend
*(3.0-2.0*floorBlend); *(3.0-2.0*floorBlend);
const double floorAlongMetres=segment.authoredAlongMetres
+floorT*segment.authoredLengthMetres;
const double floorMouthDistanceMetres=std::max(0.0,
std::min(floorAlongMetres,constants.totalPathLengthMetres
-floorAlongMetres));
const double floorShellFade=std::clamp(floorMouthDistanceMetres
/MouthShellBlendMetres,0.0,1.0);
const double smoothFloorShellFade=floorShellFade*floorShellFade
*(3.0-2.0*floorShellFade);
const double mouthWallThickness=segmentIndex const double mouthWallThickness=segmentIndex
<constants.routeEndControlIndex <constants.routeEndControlIndex
?smoothShellFade*shellThickness:0.0; ?smoothShellFade*wallShellThickness:0.0;
double floorExtensionFade=1.0; double floorExtensionFade=1.0;
if(segmentIndex>=constants.routeEndControlIndex) if(segmentIndex>=constants.routeEndControlIndex)
{ {
const double pastMouthMetres=std::max(0.0,alongMetres const double pastMouthMetres=std::max(0.0,floorAlongMetres
-constants.totalPathLengthMetres); -constants.totalPathLengthMetres);
const double extensionFade=std::clamp(1.0 const double extensionFade=std::clamp(1.0
-pastMouthMetres/MouthShellBlendMetres,0.0,1.0); -pastMouthMetres/MouthShellBlendMetres,0.0,1.0);
floorExtensionFade=extensionFade*extensionFade floorExtensionFade=extensionFade*extensionFade
*(3.0-2.0*extensionFade); *(3.0-2.0*extensionFade);
} }
const double floorShellThickness=smoothFloorBlend*shellThickness const double floorShellThickness=smoothFloorBlend
*floorExtensionFade*smoothShellFade; *floorShellThicknessBase*floorExtensionFade
*smoothFloorShellFade;
const double shellClearanceT=std::clamp(
(mouthDistanceMetres-5.0)/15.0,0.0,1.0);
const double shellClearanceFade=shellClearanceT*shellClearanceT
*(3.0-2.0*shellClearanceT);
const double localThickness=std::max(mouthWallThickness, const double localThickness=std::max(mouthWallThickness,
floorShellThickness); floorShellThickness)+ShellClearanceMetres*qPerMetre
const double localShellDistance=std::max(-static_cast<double>(flatDistance), *shellClearanceFade;
static_cast<double>(flatDistance)-localThickness); const double joinBlendT=std::clamp(mouthDistanceMetres/15.0,
nearest.shellSignedDistance=std::min(nearest.shellSignedDistance, 0.0,1.0);
localShellDistance); const double joinBlendWeight=joinBlendT*joinBlendT
*(3.0-2.0*joinBlendT);
const double localOuterDistance=flatDistance-localThickness;
FShellCandidate candidate{localOuterDistance,segmentIndex,
joinBlendWeight};
std::size_t insertAt=0;
while(insertAt<shellCandidateCount
&&(shellCandidates[insertAt].distance<candidate.distance
||(shellCandidates[insertAt].distance==candidate.distance
&&shellCandidates[insertAt].segmentIndex
<candidate.segmentIndex)))++insertAt;
if(insertAt<shellCandidates.size())
{
const std::size_t newCount=std::min(shellCandidateCount+1,
shellCandidates.size());
for(std::size_t move=newCount-1;move>insertAt;--move)
shellCandidates[move]=shellCandidates[move-1];
shellCandidates[insertAt]=candidate;
shellCandidateCount=newCount;
}
bestShellSearchBound=std::min(bestShellSearchBound,
localOuterDistance+wallShellThickness
+ShellClearanceMetres*qPerMetre
+(0.25+ShellJoinBlendMetres)*qPerMetre);
}; };
VisitNearestPassageSegmentRange(passageIndex,0,lastIndex-1, VisitNearestPassageSegmentRange(passageIndex,0,lastIndex-1,
qx,qy,qz,0,shellSearchBound,visitShellSegment); qx,qy,qz,0,bestShellSearchBound,visitShellSegment);
if(shellCandidateCount>0)
{
nearestShell.signedDistance=shellCandidates[0].distance;
nearestShell.segmentIndex=shellCandidates[0].segmentIndex;
for(std::size_t left=0;left<shellCandidateCount;++left)
for(std::size_t right=left+1;right<shellCandidateCount;++right)
{
if(shellCandidates[right].segmentIndex
!=shellCandidates[left].segmentIndex+1)continue;
nearestShell.signedDistance=std::min(
nearestShell.signedDistance,PassageSmoothMinDistance(
shellCandidates[left].distance,
shellCandidates[right].distance,
ShellJoinBlendMetres*qPerMetre*std::min(
shellCandidates[left].blendWeight,
shellCandidates[right].blendWeight)));
} }
if(!nearest.valid)continue; nearestShell.valid=true;
}
}
if(!nearest.valid||!nearestShell.valid)continue;
nearestShell.signedDistance=std::max(
-nearest.signedDistance,nearestShell.signedDistance);
double distanceToCenter=std::max(1.0e-9, double distanceToCenter=std::max(1.0e-9,
nearest.roundSignedDistance+nearest.radius); nearest.roundSignedDistance+nearest.radius);
const double verticalWallAlignment=distanceToCenter>1.0e-9 const double verticalWallAlignment=distanceToCenter>1.0e-9
@@ -1402,43 +1476,9 @@ public:
const double shapedDistance=PassageFlatBottomDistance( const double shapedDistance=PassageFlatBottomDistance(
static_cast<float>(roughRoundDistance),nearest.floorCenterZ,qz, static_cast<float>(roughRoundDistance),nearest.floorCenterZ,qz,
nearest.floorDepth,qPerMetre); nearest.floorDepth,qPerMetre);
const auto& segment=PassageSegmentData[passageIndex][nearest.segmentIndex];
const double alongMetres=segment.authoredAlongMetres
+nearest.t*segment.authoredLengthMetres;
const double mouthDistanceMetres=std::max(0.0,std::min(alongMetres,
constants.totalPathLengthMetres-alongMetres));
const double shellFade=std::clamp(mouthDistanceMetres/
MouthShellBlendMetres,0.0,1.0);
const double smoothShellFade=shellFade*shellFade*(3.0-2.0*shellFade);
// Ease both wall and floor shell into the host ground over five metres.
// The floor shell remains below the floor plane throughout that blend.
const double floorDistanceMetres=(nearest.floorCenterZ
-nearest.floorDepth-qz)/qPerMetre;
const double floorBlend=std::clamp(floorDistanceMetres/0.25,0.0,1.0);
const double smoothFloorBlend=floorBlend*floorBlend
*(3.0-2.0*floorBlend);
const double mouthWallThickness=nearest.segmentIndex
<constants.routeEndControlIndex
?smoothShellFade*shellThickness:0.0;
double floorExtensionFade=1.0;
if(nearest.segmentIndex>=constants.routeEndControlIndex)
{
const double pastMouthMetres=std::max(0.0,alongMetres
-constants.totalPathLengthMetres);
const double extensionFade=std::clamp(1.0
-pastMouthMetres/MouthShellBlendMetres,0.0,1.0);
floorExtensionFade=extensionFade*extensionFade
*(3.0-2.0*extensionFade);
}
const double floorShellThickness=smoothFloorBlend*shellThickness
*floorExtensionFade*smoothShellFade;
const double localShellThickness=std::max(mouthWallThickness,
floorShellThickness);
unionDistance=std::min(unionDistance,static_cast<double>(shapedDistance)); unionDistance=std::min(unionDistance,static_cast<double>(shapedDistance));
const double localShellDistance=std::max(-static_cast<double>(shapedDistance),
static_cast<double>(shapedDistance)-localShellThickness);
unionShellDistance=std::min(unionShellDistance, unionShellDistance=std::min(unionShellDistance,
std::min(localShellDistance,nearest.shellSignedDistance)); nearestShell.signedDistance);
} }
if(!std::isfinite(unionDistance))return density; if(!std::isfinite(unionDistance))return density;
@@ -1592,7 +1632,7 @@ public:
const auto& p3=passage.controlPoints[ const auto& p3=passage.controlPoints[
std::min(segment+2,passage.controlPoints.size()-1)]; std::min(segment+2,passage.controlPoints.size()-1)];
const double radius=std::max(static_cast<double>(a.radiusSubvoxels), const double radius=std::max(static_cast<double>(a.radiusSubvoxels),
static_cast<double>(b.radiusSubvoxels))/Q+1.0; static_cast<double>(b.radiusSubvoxels))/Q+2.36;
double low[3]{},high[3]{}; double low[3]{},high[3]{};
for(int axis=0;axis<3;++axis) for(int axis=0;axis<3;++axis)
{ {
@@ -3371,6 +3411,8 @@ private:
struct FPassageSegmentData struct FPassageSegmentData
{ {
std::array<double,3> a{},b{},delta{}; std::array<double,3> a{},b{},delta{};
std::array<std::array<double,4>,3> shellCurveCoefficients{};
std::array<double,3> shellCurveMinimum{},shellCurveMaximum{};
std::array<std::int64_t,3> sweepMinimumSubvoxels{},sweepMaximumSubvoxels{}; std::array<std::int64_t,3> sweepMinimumSubvoxels{},sweepMaximumSubvoxels{};
double authoredCenterZA=0.0,authoredCenterZDelta=0.0; double authoredCenterZA=0.0,authoredCenterZDelta=0.0;
double authoredAlongMetres=0.0,authoredLengthMetres=0.0; double authoredAlongMetres=0.0,authoredLengthMetres=0.0;
@@ -3432,6 +3474,128 @@ private:
return SmoothMaxDistance(roundDistanceSubvoxels,floorDistance, return SmoothMaxDistance(roundDistanceSubvoxels,floorDistance,
static_cast<float>(CornerBlendMetres*qPerMetre)); static_cast<float>(CornerBlendMetres*qPerMetre));
} }
static double PassageSmoothMinDistance(double a,double b,double width)
{
if(!(width>0.0))return std::min(a,b);
const double h=std::max(width-std::abs(a-b),0.0)/width;
return std::min(a,b)-(h*h*h)*(width/6.0);
}
static double PassageCurveCoordinate(
const std::array<double,4>& coefficients,double t)
{
return ((coefficients[3]*t+coefficients[2])*t+coefficients[1])*t
+coefficients[0];
}
static double PassageCurveCoordinateExtended(
const std::array<double,4>& coefficients,double t)
{
if(t<0.0)return coefficients[0]+coefficients[1]*t;
if(t>1.0)
{
const double end=PassageCurveCoordinate(coefficients,1.0);
const double tangent=3.0*coefficients[3]+2.0*coefficients[2]
+coefficients[1];
return end+(t-1.0)*tangent;
}
return PassageCurveCoordinate(coefficients,t);
}
static void PassageCurveEvaluateExtended(
const std::array<double,4>& coefficients,double t,
double& coordinate,double& tangent,double& curvature)
{
if(t<0.0)
{
coordinate=coefficients[0]+coefficients[1]*t;
tangent=coefficients[1];
curvature=0.0;
}
else if(t>1.0)
{
const double end=PassageCurveCoordinate(coefficients,1.0);
tangent=3.0*coefficients[3]+2.0*coefficients[2]+coefficients[1];
coordinate=end+(t-1.0)*tangent;
curvature=0.0;
}
else
{
coordinate=PassageCurveCoordinate(coefficients,t);
tangent=(3.0*coefficients[3]*t+2.0*coefficients[2])*t
+coefficients[1];
curvature=6.0*coefficients[3]*t+2.0*coefficients[2];
}
}
static void PassageCurveAxisBounds(const std::array<double,4>& coefficients,
double& minimum,double& maximum,double tMinimum=0.0,double tMaximum=1.0)
{
minimum=std::min(PassageCurveCoordinate(coefficients,tMinimum),
PassageCurveCoordinate(coefficients,tMaximum));
maximum=std::max(PassageCurveCoordinate(coefficients,tMinimum),
PassageCurveCoordinate(coefficients,tMaximum));
const double a=3.0*coefficients[3],b=2.0*coefficients[2],c=coefficients[1];
const auto include=[&](double t)
{
if(t<=tMinimum||t>=tMaximum)return;
const double value=PassageCurveCoordinate(coefficients,t);
minimum=std::min(minimum,value);
maximum=std::max(maximum,value);
};
if(std::abs(a)<=1.0e-18)
{
if(std::abs(b)>1.0e-18)include(-c/b);
return;
}
const double discriminant=b*b-4.0*a*c;
if(discriminant<0.0)return;
const double root=DeterministicSqrt(discriminant);
include((-b-root)/(2.0*a));
include((-b+root)/(2.0*a));
}
static double PassageCatmullRoundDistanceSubvoxels(
const std::vector<FPassageSegmentData>& segments,std::size_t segmentIndex,
double qx,double qy,double qz,double extensionSubvoxels=0.0,
double* outT=nullptr)
{
if(segmentIndex>=segments.size())return std::numeric_limits<double>::infinity();
const auto& segment=segments[segmentIndex];
if(segment.lengthSq<=0.0)return std::numeric_limits<double>::infinity();
const double projection=(qx-segment.a[0])*segment.delta[0]
+(qy-segment.a[1])*segment.delta[1]
+(qz-segment.a[2])*segment.delta[2];
const double tExtension=std::max(0.0,extensionSubvoxels)
/std::max(1.0,segment.lengthSubvoxels);
const double tMinimum=-tExtension,tMaximum=1.0+tExtension;
double t=std::clamp(projection/segment.lengthSq,tMinimum,tMaximum);
for(int iteration=0;iteration<4;++iteration)
{
double firstDerivative=0.0,secondDerivative=0.0;
const double query[3]={qx,qy,qz};
for(std::size_t axis=0;axis<3;++axis)
{
const auto& c=segment.shellCurveCoefficients[axis];
double center=0.0,tangent=0.0,curvature=0.0;
PassageCurveEvaluateExtended(c,t,center,tangent,curvature);
const double difference=center-query[axis];
firstDerivative+=difference*tangent;
secondDerivative+=tangent*tangent+difference*curvature;
}
if(std::abs(secondDerivative)<=1.0e-12)break;
const double next=std::clamp(t-firstDerivative/secondDerivative,
tMinimum,tMaximum);
if(std::abs(next-t)<=1.0e-9){t=next;break;}
t=next;
}
double distanceSq=0.0;
const double query[3]={qx,qy,qz};
for(std::size_t axis=0;axis<3;++axis)
{
const auto& c=segment.shellCurveCoefficients[axis];
const double center=PassageCurveCoordinateExtended(c,t);
const double difference=query[axis]-center;
distanceSq+=difference*difference;
}
if(outT)*outT=t;
return DeterministicSqrt(distanceSq)-(segment.radiusA+t*segment.radiusDelta);
}
struct FPassageNearestNode struct FPassageNearestNode
{ {
std::array<double,3> minimum{},maximum{}; std::array<double,3> minimum{},maximum{};
@@ -4232,10 +4396,10 @@ private:
const auto& prepared=PassageSegmentData[passageIndex][segment]; const auto& prepared=PassageSegmentData[passageIndex][segment];
for(std::size_t axis=0;axis<3;++axis) for(std::size_t axis=0;axis<3;++axis)
{ {
node.minimum[axis]=std::min({node.minimum[axis], node.minimum[axis]=std::min(node.minimum[axis],
prepared.a[axis],prepared.b[axis]}); prepared.shellCurveMinimum[axis]);
node.maximum[axis]=std::max({node.maximum[axis], node.maximum[axis]=std::max(node.maximum[axis],
prepared.a[axis],prepared.b[axis]}); prepared.shellCurveMaximum[axis]);
} }
node.maximumRadius=std::max({node.maximumRadius, node.maximumRadius=std::max({node.maximumRadius,
static_cast<double>(a.radiusSubvoxels),static_cast<double>(b.radiusSubvoxels)}); static_cast<double>(a.radiusSubvoxels),static_cast<double>(b.radiusSubvoxels)});
@@ -4617,6 +4781,78 @@ private:
static_cast<double>(passage.controlPoints[pointIndex].positionSubvoxels[2]) static_cast<double>(passage.controlPoints[pointIndex].positionSubvoxels[2])
+(-routeDepth+localTubeDepth)*qPerMetre; +(-routeDepth+localTubeDepth)*qPerMetre;
} }
std::vector<std::array<double,3>> shellControlPoints(
segmentData.size()+1);
for(std::size_t pointIndex=0;pointIndex<shellControlPoints.size();++pointIndex)
{
for(std::size_t axis=0;axis<3;++axis)
shellControlPoints[pointIndex][axis]=axis==2
?effectiveCenterZSubvoxels[pointIndex]
:static_cast<double>(passage.controlPoints[pointIndex]
.positionSubvoxels[axis]);
}
const auto authoredShellControlPoints=shellControlPoints;
for(std::size_t pointIndex=1;pointIndex+1<shellControlPoints.size();
++pointIndex)
{
const double previousSpan=authoredPathDistanceMetres[pointIndex]
-authoredPathDistanceMetres[pointIndex-1];
const double nextSpan=authoredPathDistanceMetres[pointIndex+1]
-authoredPathDistanceMetres[pointIndex];
if(previousSpan>20.0||nextSpan>20.0)continue;
const double alongMetres=authoredPathDistanceMetres[pointIndex];
const double mouthDistance=std::max(0.0,std::min(alongMetres,
authoredTotalPathLengthMetres-alongMetres));
const double fadeT=std::clamp((mouthDistance-80.0)/40.0,0.0,1.0);
const double fade=fadeT*fadeT*(3.0-2.0*fadeT);
const double neighborWeight=0.10*fade;
for(std::size_t axis=0;axis<2;++axis)
shellControlPoints[pointIndex][axis]=
(1.0-2.0*neighborWeight)
*authoredShellControlPoints[pointIndex][axis]
+neighborWeight*authoredShellControlPoints[pointIndex-1][axis]
+neighborWeight*authoredShellControlPoints[pointIndex+1][axis];
}
std::vector<std::array<double,3>> shellSecondDerivatives(
shellControlPoints.size());
const std::size_t interiorPointCount=shellControlPoints.size()-2;
for(std::size_t axis=0;axis<3;++axis)
{
std::vector<double> diagonal(interiorPointCount);
std::vector<double> upper(interiorPointCount);
std::vector<double> lower(interiorPointCount);
std::vector<double> rhs(interiorPointCount);
for(std::size_t row=0;row<interiorPointCount;++row)
{
const std::size_t pointIndex=row+1;
const double previousSpan=authoredPathDistanceMetres[pointIndex]
-authoredPathDistanceMetres[pointIndex-1];
const double nextSpan=authoredPathDistanceMetres[pointIndex+1]
-authoredPathDistanceMetres[pointIndex];
lower[row]=previousSpan;
diagonal[row]=2.0*(previousSpan+nextSpan);
upper[row]=nextSpan;
rhs[row]=6.0*((shellControlPoints[pointIndex+1][axis]
-shellControlPoints[pointIndex][axis])/nextSpan
-(shellControlPoints[pointIndex][axis]
-shellControlPoints[pointIndex-1][axis])/previousSpan);
}
for(std::size_t row=1;row<interiorPointCount;++row)
{
const double factor=lower[row]/diagonal[row-1];
diagonal[row]-=factor*upper[row-1];
rhs[row]-=factor*rhs[row-1];
}
if(interiorPointCount>0)
{
shellSecondDerivatives[interiorPointCount][axis]=
rhs[interiorPointCount-1]/diagonal[interiorPointCount-1];
for(std::size_t row=interiorPointCount-1;row>0;--row)
shellSecondDerivatives[row][axis]=(rhs[row-1]
-upper[row-1]*shellSecondDerivatives[row+1][axis])
/diagonal[row-1];
}
}
for(std::size_t segmentIndex=0;segmentIndex<segmentData.size();++segmentIndex) for(std::size_t segmentIndex=0;segmentIndex<segmentData.size();++segmentIndex)
{ {
const auto& a=passage.controlPoints[segmentIndex]; const auto& a=passage.controlPoints[segmentIndex];
@@ -4649,6 +4885,44 @@ private:
data.horizontalLength=DeterministicSqrt(data.horizontalLengthSq); data.horizontalLength=DeterministicSqrt(data.horizontalLengthSq);
++preparedSegments; ++preparedSegments;
} }
for(std::size_t segmentIndex=0;segmentIndex<segmentData.size();++segmentIndex)
{
auto& data=segmentData[segmentIndex];
const bool hasLongNeighbor=(segmentIndex>0
&&segmentData[segmentIndex-1].authoredLengthMetres>20.0)
||(segmentIndex+1<segmentData.size()
&&segmentData[segmentIndex+1].authoredLengthMetres>20.0);
const bool useCurvedSpan=data.authoredLengthMetres<=20.0
&&!hasLongNeighbor;
for(std::size_t axis=0;axis<3;++axis)
{
if(!useCurvedSpan)
{
data.shellCurveCoefficients[axis]={data.a[axis],
data.delta[axis],0.0,0.0};
}
else
{
const double span=data.authoredLengthMetres;
const double p0=shellControlPoints[segmentIndex][axis];
const double p1=shellControlPoints[segmentIndex+1][axis];
const double m0=shellSecondDerivatives[segmentIndex][axis];
const double m1=shellSecondDerivatives[segmentIndex+1][axis];
data.shellCurveCoefficients[axis]={
p0,p1-p0-span*span*(2.0*m0+m1)/6.0,
span*span*m0/2.0,span*span*(m1-m0)/6.0};
}
PassageCurveAxisBounds(data.shellCurveCoefficients[axis],
data.shellCurveMinimum[axis],data.shellCurveMaximum[axis]);
// This tree also culls chord-based interior queries. Keep
// the authored chord inside broad-phase bounds after the
// shell-only centerline is smoothed.
data.shellCurveMinimum[axis]=std::min(data.shellCurveMinimum[axis],
std::min(data.a[axis],data.b[axis]));
data.shellCurveMaximum[axis]=std::max(data.shellCurveMaximum[axis],
std::max(data.a[axis],data.b[axis]));
}
}
constants.totalPathLengthMetres=authoredTotalPathLengthMetres; constants.totalPathLengthMetres=authoredTotalPathLengthMetres;
PassageSegmentOffsets[passageIndex+1]=preparedSegments; PassageSegmentOffsets[passageIndex+1]=preparedSegments;
} }
@@ -4675,7 +4949,7 @@ private:
PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0); PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0);
if(!ResolvedLayout||ResolvedLayout->passages.empty())return; if(!ResolvedLayout||ResolvedLayout->passages.empty())return;
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
constexpr double PassageShellMetres=0.75; constexpr double PassageShellMetres=2.35;
constexpr double ReliefMetres=0.21; constexpr double ReliefMetres=0.21;
constexpr double SweepReachMetres=4.0; constexpr double SweepReachMetres=4.0;
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size(); for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
@@ -4698,8 +4972,8 @@ private:
std::array<double,3> routeMinimum{},routeMaximum{}; std::array<double,3> routeMinimum{},routeMaximum{};
for(std::size_t axis=0;axis<3;++axis) for(std::size_t axis=0;axis<3;++axis)
{ {
routeMinimum[axis]=std::min(prepared.a[axis],prepared.b[axis])/Q; routeMinimum[axis]=prepared.shellCurveMinimum[axis]/Q;
routeMaximum[axis]=std::max(prepared.a[axis],prepared.b[axis])/Q; routeMaximum[axis]=prepared.shellCurveMaximum[axis]/Q;
} }
const double routeExtent=maximumRadius+PassageShellMetres const double routeExtent=maximumRadius+PassageShellMetres
+ReliefMetres+0.01; +ReliefMetres+0.01;
@@ -11286,6 +11560,11 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
{ {
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
constexpr double InfluenceMetres=4.0; constexpr double InfluenceMetres=4.0;
constexpr double FloorShellThicknessMetres=0.75;
constexpr double WallShellThicknessMetres=0.75;
constexpr double ShellClearanceMetres=0.60;
constexpr double MouthShellBlendMetres=5.0;
constexpr double ShellJoinBlendMetres=6.00;
if(!shape_||step<=0||cells<=0||!std::isfinite(passageCarveReachMetres_) if(!shape_||step<=0||cells<=0||!std::isfinite(passageCarveReachMetres_)
||std::abs(passageCarveReachMetres_-InfluenceMetres)>1.0e-9)return; ||std::abs(passageCarveReachMetres_-InfluenceMetres)>1.0e-9)return;
@@ -11306,8 +11585,6 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
const double qPerMetre=static_cast<double>(Q)*1000000.0/ const double qPerMetre=static_cast<double>(Q)*1000000.0/
std::max<std::uint32_t>(1,shape_->ResolvedLayout std::max<std::uint32_t>(1,shape_->ResolvedLayout
?shape_->ResolvedLayout->coreVoxelSizeMicrometres:250000); ?shape_->ResolvedLayout->coreVoxelSizeMicrometres:250000);
const double shellThickness=0.75*qPerMetre;
constexpr double MouthShellBlendMetres=5.0;
const double wallReliefShell=0.41*qPerMetre; const double wallReliefShell=0.41*qPerMetre;
const double floorDetailShell=1.0*qPerMetre; const double floorDetailShell=1.0*qPerMetre;
const bool profile=profileStats_!=nullptr; const bool profile=profileStats_!=nullptr;
@@ -11363,17 +11640,16 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
+static_cast<std::int64_t>(gz)*sweep->stepSubvoxels); +static_cast<std::int64_t>(gz)*sweep->stepSubvoxels);
const double projectionXY=(qx-segment.a[0])*segment.delta[0] const double projectionXY=(qx-segment.a[0])*segment.delta[0]
+(qy-segment.a[1])*segment.delta[1]; +(qy-segment.a[1])*segment.delta[1];
const double t=std::clamp((projectionXY const double chordT=std::clamp((projectionXY
+(qz-segment.a[2])*segment.delta[2])/segment.lengthSq,0.0,1.0); +(qz-segment.a[2])*segment.delta[2])/segment.lengthSq,0.0,1.0);
double cx=segment.a[0]+t*segment.delta[0]; const double cx=segment.a[0]+chordT*segment.delta[0];
double cy=segment.a[1]+t*segment.delta[1]; const double cy=segment.a[1]+chordT*segment.delta[1];
double cz=segment.a[2]+t*segment.delta[2]; const double cz=segment.a[2]+chordT*segment.delta[2];
const double radius=segment.radiusA+t*segment.radiusDelta; const double radius=segment.radiusA+chordT*segment.radiusDelta;
const double ex=qx-cx,ey=qy-cy,ez=qz-cz; const double dx=qx-cx,dy=qy-cy,dz=qz-cz;
const double signedDistance=FStandaloneShapeWorld::DeterministicSqrt( const double signedDistance=DeterministicSqrt(dx*dx+dy*dy+dz*dz)-radius;
ex*ex+ey*ey+ez*ez)-radius;
const double floorT=segment.horizontalLengthSq>1.0e-12 const double floorT=segment.horizontalLengthSq>1.0e-12
?std::clamp(projectionXY/segment.horizontalLengthSq,0.0,1.0):t; ?std::clamp(projectionXY/segment.horizontalLengthSq,0.0,1.0):chordT;
const double floorRadius=segment.radiusA+floorT*segment.radiusDelta; const double floorRadius=segment.radiusA+floorT*segment.radiusDelta;
const double floorDepth=FStandaloneShapeWorld::PassageFlatFloorDepthSubvoxels( const double floorDepth=FStandaloneShapeWorld::PassageFlatFloorDepthSubvoxels(
constants,candidate.segmentIndex,floorT,floorRadius,qPerMetre); constants,candidate.segmentIndex,floorT,floorRadius,qPerMetre);
@@ -11382,6 +11658,54 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
constants,candidate.segmentIndex,floorT,segment,floorDepth,qPerMetre); constants,candidate.segmentIndex,floorT,segment,floorDepth,qPerMetre);
const double flatDistance=FStandaloneShapeWorld::PassageFlatBottomDistance( const double flatDistance=FStandaloneShapeWorld::PassageFlatBottomDistance(
static_cast<float>(signedDistance),floorCenterZ,qz,floorDepth,qPerMetre); static_cast<float>(signedDistance),floorCenterZ,qz,floorDepth,qPerMetre);
const double shellAlongMetres=segment.authoredAlongMetres
+chordT*segment.authoredLengthMetres;
const double mouthDistanceMetres=std::max(0.0,std::min(shellAlongMetres,
constants.totalPathLengthMetres-shellAlongMetres));
const double shellFade=std::clamp(mouthDistanceMetres/
MouthShellBlendMetres,0.0,1.0);
const double smoothShellFade=shellFade*shellFade*(3.0-2.0*shellFade);
const double floorDistanceMetres=(floorCenterZ-floorDepth-qz)
/qPerMetre;
const double floorBlend=std::clamp(floorDistanceMetres/0.25,0.0,1.0);
const double smoothFloorBlend=floorBlend*floorBlend
*(3.0-2.0*floorBlend);
const double floorAlongMetres=segment.authoredAlongMetres
+floorT*segment.authoredLengthMetres;
const double floorMouthDistanceMetres=std::max(0.0,
std::min(floorAlongMetres,constants.totalPathLengthMetres
-floorAlongMetres));
const double floorShellFade=std::clamp(floorMouthDistanceMetres
/MouthShellBlendMetres,0.0,1.0);
const double smoothFloorShellFade=floorShellFade*floorShellFade
*(3.0-2.0*floorShellFade);
const double mouthWallThickness=candidate.segmentIndex
<constants.routeEndControlIndex
?smoothShellFade*WallShellThicknessMetres*qPerMetre:0.0;
double floorExtensionFade=1.0;
if(candidate.segmentIndex>=constants.routeEndControlIndex)
{
const double pastMouthMetres=std::max(0.0,floorAlongMetres
-constants.totalPathLengthMetres);
const double extensionFade=std::clamp(1.0
-pastMouthMetres/MouthShellBlendMetres,0.0,1.0);
floorExtensionFade=extensionFade*extensionFade
*(3.0-2.0*extensionFade);
}
const double floorShellThickness=smoothFloorBlend
*FloorShellThicknessMetres*qPerMetre*floorExtensionFade
*smoothFloorShellFade;
const double shellClearanceT=std::clamp(
(mouthDistanceMetres-5.0)/15.0,0.0,1.0);
const double shellClearanceFade=shellClearanceT*shellClearanceT
*(3.0-2.0*shellClearanceT);
const double shellThickness=std::max(mouthWallThickness,
floorShellThickness)+(ShellClearanceMetres*qPerMetre)
*shellClearanceFade;
const double joinBlendT=std::clamp(mouthDistanceMetres/15.0,0.0,1.0);
const double joinBlendWeight=joinBlendT*joinBlendT
*(3.0-2.0*joinBlendT);
const double localShellDistance=flatDistance-shellThickness;
if(profile)++profileStats_->passageSweepSegmentSamplePairs; if(profile)++profileStats_->passageSweepSegmentSamplePairs;
if(signedDistance>sweep->influenceSubvoxels) if(signedDistance>sweep->influenceSubvoxels)
{ {
@@ -11391,39 +11715,6 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
*static_cast<std::size_t>(sweep->dimension)+static_cast<std::size_t>(gy)) *static_cast<std::size_t>(sweep->dimension)+static_cast<std::size_t>(gy))
*static_cast<std::size_t>(sweep->dimension)+static_cast<std::size_t>(gx); *static_cast<std::size_t>(sweep->dimension)+static_cast<std::size_t>(gx);
auto& sample=passageSamples.samples[sampleIndex]; auto& sample=passageSamples.samples[sampleIndex];
const double alongMetres=segment.authoredAlongMetres
+t*segment.authoredLengthMetres;
const double mouthDistanceMetres=std::max(0.0,std::min(alongMetres,
constants.totalPathLengthMetres-alongMetres));
const double shellFade=std::clamp(mouthDistanceMetres/
MouthShellBlendMetres,0.0,1.0);
const double smoothShellFade=shellFade*shellFade
*(3.0-2.0*shellFade);
const double floorDistanceMetres=(floorCenterZ-floorDepth-qz)/qPerMetre;
const double floorBlend=std::clamp(floorDistanceMetres/0.25,0.0,1.0);
const double smoothFloorBlend=floorBlend*floorBlend
*(3.0-2.0*floorBlend);
const double mouthWallThickness=candidate.segmentIndex
<constants.routeEndControlIndex
?smoothShellFade*shellThickness:0.0;
double floorExtensionFade=1.0;
if(candidate.segmentIndex>=constants.routeEndControlIndex)
{
const double pastMouthMetres=std::max(0.0,alongMetres
-constants.totalPathLengthMetres);
const double extensionFade=std::clamp(1.0
-pastMouthMetres/MouthShellBlendMetres,0.0,1.0);
floorExtensionFade=extensionFade*extensionFade
*(3.0-2.0*extensionFade);
}
const double floorShellThickness=smoothFloorBlend*shellThickness
*floorExtensionFade*smoothShellFade;
const double localThickness=std::max(mouthWallThickness,
floorShellThickness);
const double localShellDistance=std::max(-static_cast<double>(flatDistance),
static_cast<double>(flatDistance)-localThickness);
sample.shellSignedDistance=std::min(sample.shellSignedDistance,
localShellDistance);
if(!sample.valid||flatDistance<sample.signedDistance if(!sample.valid||flatDistance<sample.signedDistance
||(flatDistance==sample.signedDistance ||(flatDistance==sample.signedDistance
&&candidate.segmentIndex<sample.segmentIndex)) &&candidate.segmentIndex<sample.segmentIndex))
@@ -11434,12 +11725,38 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
sample.floorCenterZSubvoxels=floorCenterZ; sample.floorCenterZSubvoxels=floorCenterZ;
sample.radiusSubvoxels=radius; sample.radiusSubvoxels=radius;
sample.floorDepthSubvoxels=floorDepth; sample.floorDepthSubvoxels=floorDepth;
sample.t=t; sample.t=chordT;
sample.segmentIndex=static_cast<std::uint32_t>(candidate.segmentIndex); sample.segmentIndex=static_cast<std::uint32_t>(candidate.segmentIndex);
sample.valid=true; sample.valid=true;
sample.hasWallRelief=false; sample.hasWallRelief=false;
if(profile)++profileStats_->passageSweepMinimumUpdates; if(profile)++profileStats_->passageSweepMinimumUpdates;
} }
if(!sample.shellValid)
{
sample.shellSignedDistance=localShellDistance;
sample.shellSegmentIndex=static_cast<std::uint32_t>(
candidate.segmentIndex);
sample.previousShellDistance=localShellDistance;
sample.previousShellBlendWeight=joinBlendWeight;
sample.shellValid=true;
}
else
{
if(candidate.segmentIndex==sample.shellSegmentIndex+1)
sample.shellSignedDistance=std::min(
sample.shellSignedDistance,
FStandaloneShapeWorld::PassageSmoothMinDistance(
sample.previousShellDistance,localShellDistance,
ShellJoinBlendMetres*qPerMetre*std::min(
sample.previousShellBlendWeight,
joinBlendWeight)));
sample.shellSignedDistance=std::min(sample.shellSignedDistance,
localShellDistance);
sample.shellSegmentIndex=static_cast<std::uint32_t>(
candidate.segmentIndex);
sample.previousShellDistance=localShellDistance;
sample.previousShellBlendWeight=joinBlendWeight;
}
} }
} }
} }
@@ -11448,8 +11765,6 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
for(auto& passageSamples:sweep->passages) for(auto& passageSamples:sweep->passages)
{ {
const std::size_t passageIndex=passageSamples.passageIndex; const std::size_t passageIndex=passageSamples.passageIndex;
const auto& constants=shape_->PassageComposerConstants[passageIndex];
const auto& segments=shape_->PassageSegmentData[passageIndex];
for(std::size_t sampleIndex=0;sampleIndex<passageSamples.samples.size();++sampleIndex) for(std::size_t sampleIndex=0;sampleIndex<passageSamples.samples.size();++sampleIndex)
{ {
auto& sample=passageSamples.samples[sampleIndex]; auto& sample=passageSamples.samples[sampleIndex];
@@ -11468,8 +11783,12 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
+static_cast<std::int64_t>(gy)*sweep->stepSubvoxels); +static_cast<std::int64_t>(gy)*sweep->stepSubvoxels);
const double qz=static_cast<double>(sweep->gridMinimumSubvoxels[2] const double qz=static_cast<double>(sweep->gridMinimumSubvoxels[2]
+static_cast<std::int64_t>(gz)*sweep->stepSubvoxels); +static_cast<std::int64_t>(gz)*sweep->stepSubvoxels);
const double wallReliefShell=0.41*qPerMetre;
const double radius=sample.radiusSubvoxels; const double radius=sample.radiusSubvoxels;
const double centerZ=sample.centerZSubvoxels; const double centerZ=sample.centerZSubvoxels;
if(sample.shellValid)
sample.shellSignedDistance=std::max(-sample.signedDistance,
sample.shellSignedDistance);
if(sample.signedDistance<=sweep->influenceSubvoxels) if(sample.signedDistance<=sweep->influenceSubvoxels)
{ {
anyPassageInfluence[sampleIndex]=1; anyPassageInfluence[sampleIndex]=1;
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Build review sheets for every L110 before/after passage capture."""
from __future__ import annotations
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
CAPTURES = Path(__file__).resolve().parent / "captures"
SHEETS = CAPTURES / "contact-sheets"
GROUPS = {
"openair-view-1": ("*-openair-*-view-1.png", "3x3"),
"openair-view-2": ("*-openair-*-view-2.png", "3x3"),
"openair-view-3": ("*-openair-*-view-3.png", "3x3"),
"openair-closeups": ("*-openair-*-closeup-*.png", "3x4"),
"mouths": ("*-*-*-*-mouth.png", "3x2"),
"stations": ("*-station-*.png", "3x3"),
}
def build(variant: str, group: str, pattern: str, layout: str) -> int:
files = sorted((CAPTURES / variant).glob(pattern))
if not files:
raise RuntimeError(f"no images matched {variant}/{pattern}")
columns, rows = (int(value) for value in layout.split("x"))
scale = "480:270"
output = SHEETS / f"{variant}-{group}.png"
command = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
"-f", "image2", "-pattern_type", "glob", "-framerate", "1",
"-i", str(CAPTURES / variant / pattern), "-vf",
f"scale={scale},tile={columns}x{rows}:nb_frames={columns*rows}:padding=6:margin=6",
"-frames:v", "1", str(output)]
result = subprocess.run(command, cwd=ROOT, check=False,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode:
raise RuntimeError(f"ffmpeg failed for {variant}/{group}: {result.stderr.strip()}")
return len(files)
def main() -> int:
SHEETS.mkdir(parents=True, exist_ok=True)
rows = []
for variant in ("before", "after"):
for group, (pattern, layout) in GROUPS.items():
count = build(variant, group, pattern, layout)
rows.append(f"- [{variant} {group}]({variant}-{group}.png) — {count} views")
(SHEETS / "INDEX.md").write_text("# L110 capture contact sheets\n\n"
+ "\n".join(rows) + "\n", encoding="utf-8")
print(f"contact sheets={len(rows)} captures={sum(
int(row.rsplit('— ', 1)[1].split()[0]) for row in rows)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,38 @@
# Linux 110 — open-air tunnel shell
Candidate evaluator v257, source stamp `04e9180521ab0963da5f1cabbf7c4646e32311edf1439c2dea514d3a61e7e464`.
## Cause and fix
The bands are the end profiles of per-control-span shell capsules. The affected 452.408 m open-air section touches 50 control spans: chord lengths are 8.520–10.076 m (median 9.353 m, mean 9.324 m). The visible band pitch follows that roughly 9.35 m span spacing. Radius delta is 0 across those spans, and the floor depth is constant, so radius/floor interpolation does not explain it. The 8 m tiles, 0.25 m voxels, and 0.75 m shell do not match the period. The pattern remains with tile skirts off; wall relief is unchanged. The chord/Catmull mismatch is not the periodic source.
The shell now smooth-unions adjacent spans over 6 m and reuses the chord distance already calculated for the passage interior. A 0.60 m shell clearance fades in from 5 m to 20 m from each mouth. Interior carving still takes precedence, and the shell is written only into host air. The repeated bands are removed from the exposed shell close-ups. Regular faceting on tunnel-facing interior walls remains visible in both L109 and v257 station captures.
## Captures
The [capture index](captures/INDEX.md) flags every **OPEN AIR** view. The [verification](captures/capture-verification.json) passes all 106 images: per before/after set, 6 mouths, 9 stations, 27 open-air views, and 11 open-air close-ups. All 22 close-ups have a measured 4.000 m standoff. Nine images in each set are marked low contrast, with visible geometry. Paired bytes changed in 24/27 open-air views, 10/11 close-ups, 4/6 mouths, and 1/9 stations.
The [contact sheets](captures/contact-sheets/INDEX.md) cover all views. The target 4 m close-ups are [before 1](captures/before/passage-0-1-0-openair-4-closeup-1.png), [after 1](captures/after/passage-0-1-0-openair-4-closeup-1.png), [before 2](captures/before/passage-0-1-0-openair-4-closeup-2.png), [after 2](captures/after/passage-0-1-0-openair-4-closeup-2.png), [before 3](captures/before/passage-0-1-0-openair-4-closeup-3.png), and [after 3](captures/after/passage-0-1-0-openair-4-closeup-3.png).
## Readiness
Five interleaved L80 pairs per cell; times are medians in seconds (`LOD0–2 near-ready / whole-view-ready`). All base/candidate tile counts matched.
| Strate | W8 near | W8 whole | W12 near | W12 whole |
|---|---:|---:|---:|---:|
| SW | 0.778 → 0.854 (+9.7%) | 1.767 → 1.829 (+3.5%) | 0.807 → 0.906 (+12.3%) | 1.722 → 1.843 (+7.0%) |
| FI | 0.937 → 1.010 (+7.8%) | 1.811 → 1.903 (+5.1%) | 0.883 → 0.995 (+12.6%) | 1.700 → 1.798 (+5.8%) |
| TN | 0.979 → 1.090 (+11.4%) | 1.806 → 1.932 (+7.0%) | 0.920 → 1.006 (+9.3%) | 1.709 → 1.790 (+4.7%) |
| VS | 0.797 → 0.871 (+9.2%) | 1.699 → 1.765 (+3.9%) | 0.758 → 0.835 (+10.2%) | 1.575 → 1.659 (+5.3%) |
## Checks
- Owner four-strata walk audit: all 3 interfaces pass; zero blocked, unsupported, or grade-failure samples. Shell audit: 89,365 floor, 8,423 wall, and 10,527 interior samples; zero violations; 22,755 open-air shell samples.
- Clang/GCC determinism: all 18 W1/W8/W12 hash runs match across owner4 Surface World, owner2 Surface World, and owner2 Floating Islands.
- Fast tier: PASS, 259 checks plus 5 diagnostic-only results; no failed gates.
- Linux VFHostEditor: PASS, 27 actions, 69.0 s, `-NoUBA -MaxParallelActions=4`. Unreal was run without `prlimit`.
- `git diff --check`: PASS. No run exceeded 30 minutes.
## Failed experiments
A clearance ramp delayed to 80–120 m left 6 owner2 wall samples uncovered. A 1.2 m mouth join produced 23 wall violations. Both were rejected; the final 6 m join with the 5–20 m clearance ramp passes owner2 and owner4 audits.
@@ -0,0 +1,550 @@
#!/usr/bin/env python3
"""Capture L109/L110 passage views, open-air shell close-ups, and image checks."""
from __future__ import annotations
import json
import math
import re
from array import array
import struct
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
RESEARCH = Path(__file__).resolve().parent
CAPTURES = RESEARCH / "captures"
SAVED = ROOT / "Saved/Linux/l110/captures"
DATA = ROOT / "Standalone/data/owner_world.json"
BINARIES = {
"before": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
"after": ROOT / "Saved/Linux/l110/bin-clang-v257/VoxelForgeCoreHeadless-l110-clang-v257",
}
LAYOUTS = {
"before": ROOT / "Saved/Linux/l109/audit-layout-final-stamp.json",
"after": ROOT / "Saved/Linux/l109/audit-layout-final-stamp.json",
}
AUDIT_LOG = ROOT / "Saved/Linux/l109/audit-owner4-final-stamp.log"
RENDERERS = {
"before": ROOT / "Saved/Linux/l110/bin-clang-v257/VoxelForgeOffscreenCapture-l110-clang-v257",
"after": ROOT / "Saved/Linux/l110/bin-clang-v257/VoxelForgeOffscreenCapture-l110-clang-v257",
}
SECTION = re.compile(
r"^\[PassageOpenAirSection\] id=(\S+) section=(\d+) start_m=([\d.]+) "
r"end_m=([\d.]+) samples=(\d+) midpoint_xyz_m=\(([^)]*)\) "
r"midpoint_radius_m=([\d.]+)", re.M)
DRAWN = re.compile(r"drawn_triangles=(\d+)")
QUALITY_METRICS = re.compile(r"stddev_luma=([\d.]+).*coverage_luma_gt12=([\d.]+)")
def vector(text: str) -> tuple[float, float, float]:
return tuple(float(value) for value in text.split(","))
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:
return (0.0, 1.0, 0.0)
return tuple(value / length for value in a)
def angles(camera, target):
delta = sub(target, camera)
horizontal = math.hypot(delta[0], delta[1])
return (math.degrees(math.atan2(delta[0], delta[1])),
math.degrees(math.atan2(delta[2], horizontal)))
def fmt(values) -> str:
return ",".join(f"{value:.6f}" for value in values)
def mesh_triangle_count(mesh: Path) -> int:
"""Read only the VFMSH headers and count indexed triangles."""
with mesh.open("rb") as stream:
magic = stream.read(8)
materials = magic == b"VFMSH002"
if magic not in (b"VFMSH001", b"VFMSH002"):
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
version, _voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
if version != (2 if materials else 1):
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
stream.seek(40, 1)
triangles = 0
for _ in range(tile_count):
stream.seek(16, 1)
surface_count, _skirts, vertex_count, index_count, mask_count = struct.unpack(
"<5I", stream.read(20))
triangles += index_count // 3
stream.seek(vertex_count * 24 + (10 * vertex_count if materials else 0)
+ index_count * 4 + mask_count, 1)
if stream.read(1):
raise RuntimeError(f"trailing data in mesh scene: {mesh}")
return triangles
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
def nearest_surface_anchor(mesh: Path, point, margin_m=200.0) -> dict:
"""Return a nearby VFMSH surface vertex and its normal."""
best_distance_sq = float("inf")
best_position = best_normal = None
with mesh.open("rb") as stream:
magic = stream.read(8)
materials = magic == b"VFMSH002"
if magic not in (b"VFMSH001", b"VFMSH002"):
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
if version != (2 if materials else 1):
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
stream.seek(40, 1)
for _ in range(tile_count):
key = struct.unpack("<4i", stream.read(16))
surface_count, _skirts, vertex_count, index_count, mask_count = struct.unpack(
"<5I", stream.read(20))
extent = (32 << key[3]) * voxel_um / 1_000_000.0
origin = (key[0] * extent, key[1] * extent, key[2] * extent)
if any(point[axis] < origin[axis] - margin_m or
point[axis] > origin[axis] + extent + margin_m for axis in range(3)):
stream.seek(vertex_count * 24 + (10 * vertex_count if materials else 0)
+ index_count * 4 + mask_count, 1)
continue
positions = array("f")
positions.frombytes(stream.read(vertex_count * 12))
normals = array("f")
normals.frombytes(stream.read(vertex_count * 12))
if materials:
stream.seek(vertex_count * 10, 1)
stream.seek(index_count * 4 + mask_count, 1)
if struct.pack("=I", 1) != struct.pack("<I", 1):
positions.byteswap()
normals.byteswap()
for index in range(min(surface_count, vertex_count)):
offset = index * 3
position = tuple(float(positions[offset + axis]) for axis in range(3))
distance_sq = dot(sub(position, point), sub(position, point))
if distance_sq >= best_distance_sq:
continue
raw_normal = tuple(float(normals[offset + axis]) for axis in range(3))
if dot(raw_normal, raw_normal) < 1.0e-10:
continue
best_distance_sq = distance_sq
best_position = position
best_normal = norm(raw_normal)
if best_position is None:
raise RuntimeError(f"no nearby surface vertices in mesh scene: {mesh}")
return {"position_m": best_position, "normal": best_normal,
"distance_m": math.sqrt(best_distance_sq)}
def sanitize(value: str) -> str:
return re.sub(r"[^A-Za-z0-9_-]+", "-", value).strip("-").replace("/", "-")
def route_for(layout: dict, passage: dict) -> list[tuple[tuple[float, float, float], float]]:
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
controls = [(tuple(value * scale for value in row["position_q"]),
row["radius_q"] * scale) for row in passage["control_points"]]
lower_q = passage["lower_mouth_q"]
player_center_above_floor_q = round(
1.13 * 256.0 * 1_000_000.0 / layout["core_voxel_size_um"])
lower_index = next((index for index, row in enumerate(passage["control_points"])
if row["position_q"][0] == lower_q[0]
and row["position_q"][1] == lower_q[1]
and row["position_q"][2] - row["radius_q"]
+ player_center_above_floor_q == lower_q[2]), None)
if lower_index is None:
raise RuntimeError(f"{passage['stable_id']}: lower mouth control is unbound")
result = controls[:lower_index + 1]
if len(result) < 2:
raise RuntimeError(f"{passage['stable_id']}: incomplete route controls")
return result
def route_lengths(route):
lengths = [math.dist(a[0], b[0]) for a, b in zip(route, route[1:])]
prefix = [0.0]
for length in lengths:
prefix.append(prefix[-1] + length)
return lengths, prefix
def at_distance(route, lengths, prefix, distance):
distance = max(0.0, min(distance, prefix[-1]))
for index, length in enumerate(lengths):
if distance <= prefix[index + 1] or index + 1 == len(lengths):
t = 0.0 if length <= 1.0e-9 else (distance - prefix[index]) / length
a, b = route[index], route[index + 1]
return (tuple(a[0][axis] + (b[0][axis] - a[0][axis]) * t
for axis in range(3)),
a[1] + (b[1] - a[1]) * t)
return route[-1]
def tangent_at(route, lengths, prefix, distance):
before = at_distance(route, lengths, prefix, max(0.0, distance - 0.25))[0]
after = at_distance(route, lengths, prefix, min(prefix[-1], distance + 0.25))[0]
return norm(sub(after, before))
def derived_route(route, passage, layout):
"""Apply L109's fixed-depth floor offset to raw layout controls."""
lengths, prefix = route_lengths(route)
total = prefix[-1]
body_radii = [radius for index, (_, radius) in enumerate(route)
if prefix[index] >= 10.0 and total - prefix[index] >= 10.0]
body_depth = max(0.5, min(body_radii or [radius for _, radius in route]) - 0.23)
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
upper_z = passage["upper_mouth_q"][2] * scale
lower_z = passage["lower_mouth_q"][2] * scale
upper_target = max(0.0, route[0][0][2] - upper_z + 1.13)
lower_target = max(0.0, route[-1][0][2] - lower_z + 1.13)
transformed = []
for index, (point, radius) in enumerate(route):
local_radius_depth = max(0.0, radius - 0.23)
mouth_distance = max(0.0, min(prefix[index], total - prefix[index]))
u = max(0.0, min(1.0, mouth_distance / 5.0))
ramp = u * u * (3.0 - 2.0 * u)
target = upper_target if prefix[index] <= total * 0.5 else lower_target
mouth_depth = max(0.0, min(radius, target))
route_depth = mouth_depth + (local_radius_depth - mouth_depth) * ramp
tube_depth = min(body_depth, local_radius_depth)
transformed.append(((point[0], point[1], point[2] - route_depth + tube_depth),
radius))
return transformed, body_depth
def make_views(layout: dict, audit_sections: list[dict], *, use_derived_shape: bool,
include_open_air: bool = True) -> list[dict]:
views = []
passages = sorted(layout["passages"], key=lambda item: item["stable_id"])
for passage in passages:
raw_route = route_for(layout, passage)
if use_derived_shape:
route, body_depth = derived_route(raw_route, passage, layout)
else:
route = raw_route
body_depth = max(point[1] for point in route)
def floor_depth(radius: float) -> float:
if not use_derived_shape:
return radius
return min(body_depth, max(0.0, radius - 0.23))
lengths, prefix = route_lengths(route)
total = prefix[-1]
passage_id = passage["stable_id"]
safe_id = sanitize(passage_id)
for end_name, distance, outward_sign in (
("upper-mouth", 0.0, -1.0), ("lower-mouth", total, 1.0)):
center, radius = at_distance(route, lengths, prefix, distance)
tangent = tangent_at(route, lengths, prefix, distance)
outward = mul(tangent, outward_sign)
floor_z = center[2] - floor_depth(radius)
eye = (center[0] + outward[0] * 4.5,
center[1] + outward[1] * 4.5,
floor_z + 1.55)
if end_name == "lower-mouth":
eye = add(eye, add(mul(outward, 5.0), (0.0, 0.0, 2.0)))
target = (center[0] - outward[0] * 2.0,
center[1] - outward[1] * 2.0,
floor_z + 1.45)
yaw, pitch = angles(eye, target)
views.append({"name": f"{safe_id}-{end_name}", "passage": passage_id,
"kind": "mouth", "station": end_name,
"camera": eye, "target": target, "yaw": yaw, "pitch": pitch,
"focus": center, "center_z": center[2]})
for fraction in (0.25, 0.50, 0.75):
distance = total * fraction
center, radius = at_distance(route, lengths, prefix, distance)
tangent = tangent_at(route, lengths, prefix, distance)
depth = floor_depth(radius)
floor_z = center[2] - depth
horizontal = norm((-tangent[1], tangent[0], 0.0))
eye = (center[0] + horizontal[0] * 0.75,
center[1] + horizontal[1] * 0.75, floor_z + 1.60)
target_center, target_radius = at_distance(route, lengths, prefix,
min(total, distance + 14.0))
target_depth = floor_depth(target_radius)
target = (target_center[0] + horizontal[0] * 2.35,
target_center[1] + horizontal[1] * 2.35,
target_center[2] - target_depth + 1.80)
yaw, pitch = angles(eye, target)
views.append({"name": f"{safe_id}-station-{int(fraction * 100)}",
"passage": passage_id, "kind": "station",
"station": f"{int(fraction * 100)}%", "camera": eye,
"target": target, "yaw": yaw, "pitch": pitch,
"focus": center, "center_z": center[2],
"route_distance_m": distance,
"route_length_m": total,
"requested_route_distance_m": distance})
if not include_open_air:
continue
for section in (item for item in audit_sections if item["passage"] == passage_id):
distance = 0.5 * (section["start_m"] + section["end_m"])
center, radius = (section["midpoint"], section["radius_m"])
tangent = tangent_at(route, lengths, prefix, distance)
side = norm((-tangent[1], tangent[0], 0.0))
depth = floor_depth(radius)
section_id = section["section"]
interior_eye = (center[0], center[1], center[2] - depth + 1.25)
interior_target = add(interior_eye, mul(tangent, 7.0))
side_eye = add(center, add(mul(side, radius + 5.0), (0.0, 0.0, 1.2)))
oblique_eye = add(center, add(mul(side, -(radius + 3.5)), (0.0, 0.0, 4.0)))
targets = (add(center, (0.0, 0.0, 0.1)),
add(center, mul(tangent, 2.0)), interior_target)
for view_index, (eye, target) in enumerate(zip(
(side_eye, oblique_eye, interior_eye), targets), start=1):
yaw, pitch = angles(eye, target)
views.append({"name": f"{safe_id}-openair-{section_id}-view-{view_index}",
"passage": passage_id, "kind": "open-air",
"station": f"section-{section_id}/view-{view_index}",
"camera": eye, "target": target, "yaw": yaw,
"pitch": pitch, "focus": center,
"center_z": center[2], "open_air_section": section_id,
"route_distance_m": distance,
"requested_route_distance_m": distance,
"route_length_m": total})
shell_radius = radius + 0.75
closeup_directions = [side]
if passage_id == "passage/0-1/0" and section_id == 4:
closeup_directions.extend([
norm(add(mul(side, -1.0), (0.0, 0.0, 0.65))),
norm(add(mul(side, 0.55), (0.0, 0.0, 0.84))),
])
for closeup_index, direction in enumerate(closeup_directions, start=1):
shell_target = add(center, mul(direction, shell_radius))
closeup_camera = add(shell_target, mul(direction, 4.0))
yaw, pitch = angles(closeup_camera, shell_target)
views.append({"name": f"{safe_id}-openair-{section_id}-closeup-"
f"{closeup_index}",
"passage": passage_id, "kind": "open-air-closeup",
"station": f"section-{section_id}/exterior-{closeup_index}",
"camera": closeup_camera, "target": shell_target,
"yaw": yaw, "pitch": pitch, "focus": center,
"center_z": center[2], "open_air_section": section_id,
"route_distance_m": distance,
"requested_route_distance_m": distance,
"route_length_m": total, "shell_standoff_m": 4.0})
return views
def run_process(name: str, command: list[str], log: Path, *, allow_failure=False) -> str:
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
"timeout", "1740s", *map(str, command)]
started = time.monotonic()
with log.open("w", encoding="utf-8") as stream:
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
stderr=subprocess.STDOUT, check=False, timeout=1740)
if result.returncode and not allow_failure:
raise RuntimeError(f"{name}: exit={result.returncode} log={log}")
print(f"[L110CaptureProcess] name={name} elapsed_s={time.monotonic()-started:.2f} "
f"exit={result.returncode} log={log.relative_to(ROOT)}", flush=True)
return log.read_text(encoding="utf-8", errors="replace")
def capture(variant: str, view: dict, layout: dict) -> dict:
variant_saved = SAVED / variant
variant_saved.mkdir(parents=True, exist_ok=True)
variant_capture = CAPTURES / variant
variant_capture.mkdir(parents=True, exist_ok=True)
scene = variant_saved / f"{view['name']}.vfmesh"
image = variant_capture / f"{view['name']}.png"
candidates = [(view["camera"], view["target"], view["focus"],
view.get("route_distance_m"), 0.0, "requested")]
if variant == "before" and view["kind"] != "mouth":
passage = next(item for item in layout["passages"]
if item["stable_id"] == view["passage"])
route = route_for(layout, passage)
lengths, prefix = route_lengths(route)
total = prefix[-1]
eye_offset = sub(view["camera"], view["focus"])
target_offset = sub(view["target"], view["focus"])
requested = view["route_distance_m"]
for delta in (5, -5, 10, -10, 20, -20, 35, -35, 50, -50, 75, -75,
100, -100, 150, -150, 200, -200):
distance = max(0.0, min(total, requested + delta))
if abs(distance - requested) < 0.01:
continue
center, _radius = at_distance(route, lengths, prefix, distance)
candidates.append((add(center, eye_offset), add(center, target_offset), center,
distance, distance - requested, "route-offset"))
attempt_errors = []
for attempt_index, (probe_camera, probe_target, probe_focus, route_distance,
route_adjustment, attempt_kind) in enumerate(candidates):
suffix = "" if attempt_index == 0 else f"-attempt-{attempt_index:02d}"
headless_log = variant_saved / f"{view['name']}{suffix}-mesh.log"
render_log = variant_saved / f"{view['name']}{suffix}-render.log"
quality_log = variant_saved / f"{view['name']}{suffix}-quality.log"
yaw, pitch = angles(probe_camera, probe_target)
camera = (*probe_camera, yaw, pitch)
export_command = [
str(BINARIES[variant]), str(DATA), "--world-assembler-prototype",
"--stream-benchmark", "--stream-pipeline", "--workers=12",
"--clip-radius=3", "--max-clip-level=0", "--max-level=0", "--force-generate",
"--recipe=surface-world", f"--camera={fmt(camera)}",
"--render-distance-chunks=1", f"--mesh-export={scene}",
]
export_output = run_process(f"{variant}-{view['name']}{suffix}-mesh", export_command,
headless_log)
if "status=PASS" not in export_output or not scene.is_file():
raise RuntimeError(f"{variant}-{view['name']}: missing successful scene export")
source_triangles = mesh_triangle_count(scene)
if source_triangles <= 0:
attempt_errors.append(f"{attempt_kind}@{route_distance}: empty mesh")
if variant == "after" or view["kind"] == "mouth":
break
continue
actual_camera, actual_target = probe_camera, probe_target
actual_focus = probe_focus
surface_distance = None
shell_standoff_actual = None
if attempt_kind == "route-offset" or view["kind"] == "open-air-closeup":
anchor_point = probe_focus if attempt_kind == "route-offset" else probe_target
anchor = nearest_surface_anchor(scene, anchor_point, margin_m=20.0)
surface_distance = anchor["distance_m"]
if view["kind"] == "open-air-closeup":
radial = norm(sub(anchor["position_m"], view["focus"]))
normal = anchor["normal"]
if dot(normal, radial) < 0.0:
normal = mul(normal, -1.0)
actual_camera = add(anchor["position_m"], mul(normal, 4.0))
actual_target = anchor["position_m"]
actual_focus = anchor["position_m"]
shell_standoff_actual = math.dist(actual_camera, actual_target)
else:
actual_camera = add(anchor["position_m"], mul(anchor["normal"], 12.0))
actual_target = anchor["position_m"]
actual_focus = anchor["position_m"]
actual_yaw, actual_pitch = angles(actual_camera, actual_target)
render_command = [
str(RENDERERS[variant]), str(scene), str(image), "--size=1920x1080",
f"--camera-m={fmt(actual_camera)}", f"--yaw-deg={actual_yaw:.6f}",
f"--pitch-deg={actual_pitch:.6f}", "--shade=readable", "--headlamp=on",
"--height-tint=on", f"--height-center-m={actual_focus[2]:.6f}",
"--height-range-m=32", "--tile-skirts=off",
]
render_output = run_process(f"{variant}-{view['name']}{suffix}-render", render_command,
render_log)
if "[OffscreenMeshDisplay] tile_skirts=off" not in render_output:
raise RuntimeError(f"{variant}-{view['name']}: skirt-free surface render missing")
drawn = DRAWN.search(render_output)
if drawn is None or int(drawn.group(1)) <= 0 or not image.is_file():
attempt_errors.append(f"{attempt_kind}@{route_distance}: no drawn geometry")
if variant == "after" or view["kind"] == "mouth":
break
continue
quality = run_process(f"{variant}-{view['name']}{suffix}-quality", [
"python3", "-B", str(ROOT / "Standalone/Tools/verify/capture_quality.py"),
str(image)], quality_log, allow_failure=True)
quality_status = "pass"
if "status=PASS" not in quality:
metrics = QUALITY_METRICS.search(quality)
coverage = float(metrics.group(2)) if metrics else 0.0
if coverage >= 0.001:
quality_status = "low_contrast_geometry_visible"
else:
attempt_errors.append(f"{attempt_kind}@{route_distance}: no visible pixel coverage")
if variant == "after" or view["kind"] == "mouth":
break
continue
row = {**view, "variant": variant, "capture": str(image.relative_to(RESEARCH)),
"scene": str(scene.relative_to(ROOT)), "drawn_triangles": int(drawn.group(1)),
"source_triangles": source_triangles, "verified": True,
"render_tile_skirts": "off",
"pixel_quality_status": quality_status,
"route_distance_actual_m": route_distance,
"route_station_adjustment_m": route_adjustment,
"fallback_kind": attempt_kind if attempt_kind != "requested" else None,
"fallback_surface_distance_m": surface_distance,
"shell_standoff_actual_m": shell_standoff_actual,
"camera_actual_m": actual_camera, "target_actual_m": actual_target,
"logs": [str(headless_log.relative_to(ROOT)),
str(render_log.relative_to(ROOT)),
str(quality_log.relative_to(ROOT))]}
print("[L110Capture] " + json.dumps(row, sort_keys=True), flush=True)
return row
raise RuntimeError(f"{variant}-{view['name']}: no verified image; "
+ "; ".join(attempt_errors))
def main() -> int:
if any(not path.is_file() for path in (*BINARIES.values(), *RENDERERS.values(),
*LAYOUTS.values(), AUDIT_LOG)):
raise SystemExit("missing L109 capture binary, layout, or audit log")
sections = []
for passage, section, start, end, samples, midpoint, radius in SECTION.findall(
AUDIT_LOG.read_text(encoding="utf-8", errors="replace")):
sections.append({"passage": passage, "section": int(section),
"start_m": float(start), "end_m": float(end),
"samples": int(samples), "midpoint": vector(midpoint),
"radius_m": float(radius)})
if not sections:
raise SystemExit("no open-air sections found in the final passage audit")
all_rows = []
for variant in ("before", "after"):
layout = json.loads(LAYOUTS[variant].read_text(encoding="utf-8"))
include_open_air = True
views = make_views(layout, sections, use_derived_shape=True,
include_open_air=include_open_air)
expected = 5 * len(layout["passages"])
if include_open_air:
expected += 4 * len(sections) + 2
if len(views) != expected:
raise RuntimeError(f"unexpected camera count: {len(views)} != {expected}")
print(f"[L110CapturePlan] variant={variant} passage_views={5*len(layout['passages'])} "
f"open_air_sections={len(sections) if include_open_air else 0} "
f"open_air_views={3*len(sections) if include_open_air else 0} "
f"exterior_closeups={len(sections)+2 if include_open_air else 0} "
f"total={len(views)}", flush=True)
for view in views:
all_rows.append(capture(variant, view, layout))
CAPTURES.mkdir(parents=True, exist_ok=True)
(CAPTURES / "capture-index.json").write_text(
json.dumps({"open_air_sections": sections, "captures": all_rows}, indent=2) + "\n",
encoding="utf-8")
lines = ["# L110 passage captures", "",
"Each PNG was rendered from its own exported mesh. The renderer reported visible "
"triangles with LOD seam skirts disabled for surface review, and each decoded "
"image met the pixel coverage floor. "
"`low_contrast_geometry_visible` marks images that met this floor below the "
"preferred contrast target.", ""]
for variant in ("before", "after"):
lines.extend([f"## {variant.title()}", ""])
for row in all_rows:
if row["variant"] != variant:
continue
flag = " **OPEN AIR**" if row["kind"].startswith("open-air") else ""
quality_note = (f"; {row['pixel_quality_status']}"
if row["pixel_quality_status"] != "pass" else "")
lines.append(f"- [{row['station']}]({variant}/{Path(row['capture']).name}) — "
f"{row['passage']}{flag}; {row['drawn_triangles']} visible triangles"
f"{quality_note}")
lines.append("")
(CAPTURES / "INDEX.md").write_text("\n".join(lines), encoding="utf-8")
failures = [row for row in all_rows if not row["verified"]]
print(f"[L110CaptureSummary] captures={len(all_rows)} verified={len(all_rows)-len(failures)} "
f"failed={len(failures)} open_air_sections={len(sections)}", flush=True)
return 1 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Verify Clang/GCC W1/W8/W12 hashes and pin the v257 shell recipes."""
from __future__ import annotations
import json
import re
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / "Saved/Linux/l110/hashes-v257"
DATASETS = {
"owner4": ROOT / "Standalone/data/owner_world.json",
"owner2": ROOT / "Standalone/data/regression/owner_world_2strate_l61.json",
}
BINARIES = {
"clang": ROOT / "Saved/Linux/l110/bin-clang-v257/VoxelForgeCoreHeadless-l110-clang-v257",
"gcc": ROOT / "Saved/Linux/l110/bin-gcc-v257/VoxelForgeCoreHeadless-l110-gcc-v257",
}
HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=(\S+)$", re.M)
def run(dataset: str, recipe: str, workers: int, compiler: str) -> dict:
OUT.mkdir(parents=True, exist_ok=True)
label = f"{dataset}-{recipe}-w{workers}-{compiler}"
log = OUT / f"{label}.log"
layout = OUT / f"{label}.layout.json"
command = [
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
"timeout", "1740s", str(BINARIES[compiler]), str(DATASETS[dataset]),
f"--workers={workers}", "--world-assembler-prototype", "--stream-benchmark",
"--stream-pipeline", "--force-generate", "--clip-radius=3",
"--max-clip-level=8", "--max-level=8", f"--recipe={recipe}",
"--stream-hash", f"--resolved-layout-output={layout}",
]
started = time.monotonic()
with log.open("w", encoding="utf-8") as stream:
result = subprocess.run(command, cwd=ROOT, stdout=stream,
stderr=subprocess.STDOUT, check=False, timeout=1740)
output = log.read_text(encoding="utf-8", errors="replace")
hashes = HASH.findall(output)
if result.returncode or len(hashes) != 12:
raise SystemExit(f"hash run failed or incomplete: {label}; exit={result.returncode} "
f"hashes={len(hashes)} log={log}")
stamp = STAMP.search(output)
row = {"dataset": dataset, "recipe": recipe, "workers": workers,
"compiler": compiler, "exit_code": result.returncode, "hashes": hashes,
"source_stamp": stamp.group(1) if stamp else None,
"elapsed_s": round(time.monotonic() - started, 3),
"log": str(log.relative_to(ROOT))}
print("[L110HashRun] " + json.dumps(row, sort_keys=True), flush=True)
return row
def main() -> int:
if any(not path.is_file() for path in (*DATASETS.values(), *BINARIES.values())):
raise SystemExit("missing L110 hash input")
rows = []
recipes = {"owner4": ("surface-world",),
"owner2": ("surface-world", "floating-islands")}
for dataset, dataset_recipes in recipes.items():
for recipe in dataset_recipes:
for workers in (1, 8, 12):
for compiler in ("clang", "gcc"):
rows.append(run(dataset, recipe, workers, compiler))
(OUT / "runs-partial.json").write_text(
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
groups = {}
for dataset, dataset_recipes in recipes.items():
for recipe in dataset_recipes:
key = f"{dataset}/{recipe}"
group = [row for row in rows if row["dataset"] == dataset
and row["recipe"] == recipe]
reference = group[0]["hashes"]
matches = all(row["hashes"] == reference for row in group)
groups[key] = {"all_worker_compiler_hashes_match": matches,
"source_stamps": sorted({row["source_stamp"] for row in group}),
"runs": [{"workers": row["workers"], "compiler": row["compiler"],
"log": row["log"]} for row in group]}
if not matches:
raise SystemExit(f"determinism mismatch: {key}")
if dataset == "owner2":
canonical = next(row for row in group if row["workers"] == 12
and row["compiler"] == "clang")
style = 1 if recipe == "surface-world" else 2
baseline = (ROOT / "Standalone/Tools/verify/baselines/owner-hash/canonical"
/ f"owner-hash-{recipe}-style{style}-v257.baseline")
baseline.write_text("\n".join(canonical["hashes"]) + "\n",
encoding="utf-8")
summary = {"runs": rows, "groups": groups,
"owner4_all_four_strata": "owner4/surface-world",
"owner2_canonical_pins": ["owner2/surface-world",
"owner2/floating-islands"]}
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Five interleaved L80 readiness pairs for L110 versus L109, at W8 and W12."""
from __future__ import annotations
import json
import re
import statistics
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
OUT = ROOT / "Saved/Linux/l110/readiness-v257"
DATA = ROOT / "Standalone/data/owner_world.json"
BINARIES = {
"base": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
"candidate": ROOT / "Saved/Linux/l110/bin-clang-v257/VoxelForgeCoreHeadless-l110-clang-v257",
}
CAMERAS = {
0: (8.0, 16.0, -372.2626953125),
1: (25.8369140625, 3.19921875, -737.0),
2: (19.166015625, -14.2001953125, -753.3740234375),
3: (-20.0625, -5.068359375, -900.353515625),
}
PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
NEAR = re.compile(
r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$",
re.M,
)
def run(strate: int, workers: int, variant: str, phase: str, replicate: int) -> dict:
OUT.mkdir(parents=True, exist_ok=True)
label = f"s{strate}-w{workers}-{variant}-{phase}{replicate}"
log = OUT / f"{label}.log"
camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0"
command = [
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
"timeout", "1740s", str(BINARIES[variant]), str(DATA),
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", "--max-level=8",
f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3",
]
start = time.monotonic()
with log.open("w", encoding="utf-8") as output:
result = subprocess.run(command, cwd=ROOT, stdout=output,
stderr=subprocess.STDOUT, check=False, timeout=1740)
content = log.read_text(encoding="utf-8", errors="replace")
pipeline_match = PIPELINE.search(content)
near_match = NEAR.search(content)
if result.returncode or pipeline_match is None or near_match is None:
raise SystemExit(f"readiness run failed or trace missing: {log}; exit={result.returncode}")
pipeline = dict(FIELDS.findall(pipeline_match.group(1)))
row = {
"strate": strate, "workers": workers, "variant": variant,
"phase": phase, "replicate": replicate, "exit_code": result.returncode,
"wall_s": round(time.monotonic() - start, 3),
"lod02_ready_s": float(near_match.group(3)),
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
"whole_tiles": int(pipeline["whole_tiles"].split("/")[0]),
"log": str(log.relative_to(ROOT)),
}
print("[L110TimingRun] " + json.dumps(row, sort_keys=True), flush=True)
return row
def main() -> int:
if not DATA.is_file() or any(not binary.is_file() for binary in BINARIES.values()):
raise SystemExit("missing data or timing binary")
rows = []
for strate in range(4):
for workers in (8, 12):
run(strate, workers, "base", "warmup", 0)
run(strate, workers, "candidate", "warmup", 0)
for replicate in range(1, 6):
order = ("base", "candidate") if replicate % 2 else ("candidate", "base")
for variant in order:
rows.append(run(strate, workers, variant, "measured", replicate))
(OUT / "runs-partial.json").write_text(
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
summary = []
for strate in range(4):
for workers in (8, 12):
item = {"strate": strate, "workers": workers, "pairs": 5}
for variant in ("base", "candidate"):
group = [row for row in rows if row["strate"] == strate
and row["workers"] == workers and row["variant"] == variant]
for key in ("lod02_ready_s", "whole_view_ready_s"):
values = [row[key] for row in group]
item[f"{variant}_{key}"] = values
item[f"{variant}_{key}_median"] = statistics.median(values)
for key in ("lod02_ready_s", "whole_view_ready_s"):
base = item[f"base_{key}_median"]
candidate = item[f"candidate_{key}_median"]
item[f"{key}_delta_pct"] = round(100 * (candidate / base - 1), 2)
summary.append(item)
(OUT / "summary.json").write_text(
json.dumps({"runs": rows, "summary": summary}, indent=2) + "\n",
encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Check the paired L109/L110 capture set and 4 m shell close-ups."""
from __future__ import annotations
import hashlib
import json
from collections import Counter
from pathlib import Path
import struct
ROOT = Path(__file__).resolve().parents[4]
CAPTURE_ROOT = Path(__file__).resolve().parent / "captures"
RESEARCH_ROOT = CAPTURE_ROOT.parent
INDEX = CAPTURE_ROOT / "capture-index.json"
OUTPUT = CAPTURE_ROOT / "capture-verification.json"
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def png_size(path: Path) -> tuple[int, int]:
with path.open("rb") as stream:
header = stream.read(24)
if len(header) != 24 or header[:8] != b"\x89PNG\r\n\x1a\n" or header[12:16] != b"IHDR":
raise RuntimeError(f"invalid PNG: {path}")
return struct.unpack(">II", header[16:24])
def main() -> int:
captures = json.loads(INDEX.read_text(encoding="utf-8"))["captures"]
counts = {variant: Counter(row["kind"] for row in captures
if row["variant"] == variant)
for variant in ("before", "after")}
by_variant = {variant: {row["name"]: row for row in captures
if row["variant"] == variant}
for variant in ("before", "after")}
errors: list[str] = []
for variant in ("before", "after"):
rows = [row for row in captures if row["variant"] == variant]
if len(rows) != 53:
errors.append(f"{variant}: expected 53 captures, got {len(rows)}")
for row in rows:
image = RESEARCH_ROOT / row["capture"]
if not row.get("verified") or row.get("render_tile_skirts") != "off":
errors.append(f"{variant}/{row['name']}: render verification flags failed")
if row.get("drawn_triangles", 0) <= 0 or not image.is_file():
errors.append(f"{variant}/{row['name']}: missing geometry or image")
elif png_size(image) != (1920, 1080):
errors.append(f"{variant}/{row['name']}: unexpected image size {png_size(image)}")
closeups = [row for row in rows if row["kind"] == "open-air-closeup"]
if len(closeups) != 11:
errors.append(f"{variant}: expected 11 close-ups, got {len(closeups)}")
for row in closeups:
distance = row.get("shell_standoff_actual_m")
if distance is None or not 3.0 <= distance <= 5.0:
errors.append(f"{variant}/{row['name']}: close-up standoff {distance}")
names_before, names_after = set(by_variant["before"]), set(by_variant["after"])
if names_before != names_after:
errors.append("before/after capture names do not match")
pair_counts = Counter()
for name in sorted(names_before & names_after):
left, right = by_variant["before"][name], by_variant["after"][name]
same = sha256(RESEARCH_ROOT / left["capture"]) == sha256(
RESEARCH_ROOT / right["capture"])
pair_counts[f"{left['kind']}_{'identical' if same else 'changed'}"] += 1
summary = {
"status": "PASS" if not errors else "FAIL",
"capture_count": len(captures),
"open_air_sections": len(json.loads(INDEX.read_text(encoding="utf-8"))[
"open_air_sections"]),
"per_variant_kind_counts": {variant: dict(sorted(counts[variant].items()))
for variant in counts},
"paired_image_bytes": dict(sorted(pair_counts.items())),
"errors": errors,
}
OUTPUT.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
print(json.dumps(summary, sort_keys=True))
return 0 if not errors else 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,115 @@
# L110 passage captures
Each PNG was rendered from its own exported mesh. The renderer reported visible triangles with LOD seam skirts disabled for surface review, and each decoded image met the pixel coverage floor. `low_contrast_geometry_visible` marks images that met this floor below the preferred contrast target.
## Before
- [upper-mouth](before/passage-0-1-0-upper-mouth.png) — passage/0-1/0; 245318 visible triangles
- [lower-mouth](before/passage-0-1-0-lower-mouth.png) — passage/0-1/0; 549656 visible triangles
- [25%](before/passage-0-1-0-station-25.png) — passage/0-1/0; 136516 visible triangles
- [50%](before/passage-0-1-0-station-50.png) — passage/0-1/0; 414142 visible triangles
- [75%](before/passage-0-1-0-station-75.png) — passage/0-1/0; 280006 visible triangles
- [section-1/view-1](before/passage-0-1-0-openair-1-view-1.png) — passage/0-1/0 **OPEN AIR**; 279562 visible triangles
- [section-1/view-2](before/passage-0-1-0-openair-1-view-2.png) — passage/0-1/0 **OPEN AIR**; 288373 visible triangles
- [section-1/view-3](before/passage-0-1-0-openair-1-view-3.png) — passage/0-1/0 **OPEN AIR**; 312807 visible triangles
- [section-1/exterior-1](before/passage-0-1-0-openair-1-closeup-1.png) — passage/0-1/0 **OPEN AIR**; 279562 visible triangles
- [section-2/view-1](before/passage-0-1-0-openair-2-view-1.png) — passage/0-1/0 **OPEN AIR**; 271894 visible triangles
- [section-2/view-2](before/passage-0-1-0-openair-2-view-2.png) — passage/0-1/0 **OPEN AIR**; 307275 visible triangles
- [section-2/view-3](before/passage-0-1-0-openair-2-view-3.png) — passage/0-1/0 **OPEN AIR**; 312807 visible triangles
- [section-2/exterior-1](before/passage-0-1-0-openair-2-closeup-1.png) — passage/0-1/0 **OPEN AIR**; 271894 visible triangles; low_contrast_geometry_visible
- [section-3/view-1](before/passage-0-1-0-openair-3-view-1.png) — passage/0-1/0 **OPEN AIR**; 252183 visible triangles
- [section-3/view-2](before/passage-0-1-0-openair-3-view-2.png) — passage/0-1/0 **OPEN AIR**; 299315 visible triangles; low_contrast_geometry_visible
- [section-3/view-3](before/passage-0-1-0-openair-3-view-3.png) — passage/0-1/0 **OPEN AIR**; 351475 visible triangles
- [section-3/exterior-1](before/passage-0-1-0-openair-3-closeup-1.png) — passage/0-1/0 **OPEN AIR**; 252183 visible triangles
- [section-4/view-1](before/passage-0-1-0-openair-4-view-1.png) — passage/0-1/0 **OPEN AIR**; 361215 visible triangles
- [section-4/view-2](before/passage-0-1-0-openair-4-view-2.png) — passage/0-1/0 **OPEN AIR**; 452228 visible triangles
- [section-4/view-3](before/passage-0-1-0-openair-4-view-3.png) — passage/0-1/0 **OPEN AIR**; 366459 visible triangles
- [section-4/exterior-1](before/passage-0-1-0-openair-4-closeup-1.png) — passage/0-1/0 **OPEN AIR**; 361215 visible triangles
- [section-4/exterior-2](before/passage-0-1-0-openair-4-closeup-2.png) — passage/0-1/0 **OPEN AIR**; 452228 visible triangles
- [section-4/exterior-3](before/passage-0-1-0-openair-4-closeup-3.png) — passage/0-1/0 **OPEN AIR**; 408374 visible triangles
- [upper-mouth](before/passage-1-2-0-upper-mouth.png) — passage/1-2/0; 565769 visible triangles; low_contrast_geometry_visible
- [lower-mouth](before/passage-1-2-0-lower-mouth.png) — passage/1-2/0; 505668 visible triangles
- [25%](before/passage-1-2-0-station-25.png) — passage/1-2/0; 655764 visible triangles
- [50%](before/passage-1-2-0-station-50.png) — passage/1-2/0; 590530 visible triangles
- [75%](before/passage-1-2-0-station-75.png) — passage/1-2/0; 607508 visible triangles
- [section-1/view-1](before/passage-1-2-0-openair-1-view-1.png) — passage/1-2/0 **OPEN AIR**; 624462 visible triangles
- [section-1/view-2](before/passage-1-2-0-openair-1-view-2.png) — passage/1-2/0 **OPEN AIR**; 582866 visible triangles
- [section-1/view-3](before/passage-1-2-0-openair-1-view-3.png) — passage/1-2/0 **OPEN AIR**; 611168 visible triangles
- [section-1/exterior-1](before/passage-1-2-0-openair-1-closeup-1.png) — passage/1-2/0 **OPEN AIR**; 624462 visible triangles
- [upper-mouth](before/passage-2-3-0-upper-mouth.png) — passage/2-3/0; 57772 visible triangles
- [lower-mouth](before/passage-2-3-0-lower-mouth.png) — passage/2-3/0; 291268 visible triangles; low_contrast_geometry_visible
- [25%](before/passage-2-3-0-station-25.png) — passage/2-3/0; 53210 visible triangles; low_contrast_geometry_visible
- [50%](before/passage-2-3-0-station-50.png) — passage/2-3/0; 454314 visible triangles
- [75%](before/passage-2-3-0-station-75.png) — passage/2-3/0; 475142 visible triangles
- [section-1/view-1](before/passage-2-3-0-openair-1-view-1.png) — passage/2-3/0 **OPEN AIR**; 103940 visible triangles
- [section-1/view-2](before/passage-2-3-0-openair-1-view-2.png) — passage/2-3/0 **OPEN AIR**; 79774 visible triangles
- [section-1/view-3](before/passage-2-3-0-openair-1-view-3.png) — passage/2-3/0 **OPEN AIR**; 94306 visible triangles; low_contrast_geometry_visible
- [section-1/exterior-1](before/passage-2-3-0-openair-1-closeup-1.png) — passage/2-3/0 **OPEN AIR**; 103940 visible triangles; low_contrast_geometry_visible
- [section-2/view-1](before/passage-2-3-0-openair-2-view-1.png) — passage/2-3/0 **OPEN AIR**; 475512 visible triangles
- [section-2/view-2](before/passage-2-3-0-openair-2-view-2.png) — passage/2-3/0 **OPEN AIR**; 477216 visible triangles
- [section-2/view-3](before/passage-2-3-0-openair-2-view-3.png) — passage/2-3/0 **OPEN AIR**; 477216 visible triangles
- [section-2/exterior-1](before/passage-2-3-0-openair-2-closeup-1.png) — passage/2-3/0 **OPEN AIR**; 475512 visible triangles
- [section-3/view-1](before/passage-2-3-0-openair-3-view-1.png) — passage/2-3/0 **OPEN AIR**; 484608 visible triangles
- [section-3/view-2](before/passage-2-3-0-openair-3-view-2.png) — passage/2-3/0 **OPEN AIR**; 475290 visible triangles
- [section-3/view-3](before/passage-2-3-0-openair-3-view-3.png) — passage/2-3/0 **OPEN AIR**; 488256 visible triangles; low_contrast_geometry_visible
- [section-3/exterior-1](before/passage-2-3-0-openair-3-closeup-1.png) — passage/2-3/0 **OPEN AIR**; 484608 visible triangles
- [section-4/view-1](before/passage-2-3-0-openair-4-view-1.png) — passage/2-3/0 **OPEN AIR**; 475298 visible triangles
- [section-4/view-2](before/passage-2-3-0-openair-4-view-2.png) — passage/2-3/0 **OPEN AIR**; 494744 visible triangles
- [section-4/view-3](before/passage-2-3-0-openair-4-view-3.png) — passage/2-3/0 **OPEN AIR**; 495418 visible triangles; low_contrast_geometry_visible
- [section-4/exterior-1](before/passage-2-3-0-openair-4-closeup-1.png) — passage/2-3/0 **OPEN AIR**; 475298 visible triangles
## After
- [upper-mouth](after/passage-0-1-0-upper-mouth.png) — passage/0-1/0; 244932 visible triangles
- [lower-mouth](after/passage-0-1-0-lower-mouth.png) — passage/0-1/0; 549634 visible triangles
- [25%](after/passage-0-1-0-station-25.png) — passage/0-1/0; 136516 visible triangles
- [50%](after/passage-0-1-0-station-50.png) — passage/0-1/0; 434960 visible triangles
- [75%](after/passage-0-1-0-station-75.png) — passage/0-1/0; 291514 visible triangles
- [section-1/view-1](after/passage-0-1-0-openair-1-view-1.png) — passage/0-1/0 **OPEN AIR**; 278806 visible triangles
- [section-1/view-2](after/passage-0-1-0-openair-1-view-2.png) — passage/0-1/0 **OPEN AIR**; 287745 visible triangles
- [section-1/view-3](after/passage-0-1-0-openair-1-view-3.png) — passage/0-1/0 **OPEN AIR**; 312051 visible triangles
- [section-1/exterior-1](after/passage-0-1-0-openair-1-closeup-1.png) — passage/0-1/0 **OPEN AIR**; 278806 visible triangles
- [section-2/view-1](after/passage-0-1-0-openair-2-view-1.png) — passage/0-1/0 **OPEN AIR**; 271138 visible triangles
- [section-2/view-2](after/passage-0-1-0-openair-2-view-2.png) — passage/0-1/0 **OPEN AIR**; 306519 visible triangles
- [section-2/view-3](after/passage-0-1-0-openair-2-view-3.png) — passage/0-1/0 **OPEN AIR**; 312051 visible triangles
- [section-2/exterior-1](after/passage-0-1-0-openair-2-closeup-1.png) — passage/0-1/0 **OPEN AIR**; 271138 visible triangles; low_contrast_geometry_visible
- [section-3/view-1](after/passage-0-1-0-openair-3-view-1.png) — passage/0-1/0 **OPEN AIR**; 251435 visible triangles
- [section-3/view-2](after/passage-0-1-0-openair-3-view-2.png) — passage/0-1/0 **OPEN AIR**; 298559 visible triangles; low_contrast_geometry_visible
- [section-3/view-3](after/passage-0-1-0-openair-3-view-3.png) — passage/0-1/0 **OPEN AIR**; 350719 visible triangles
- [section-3/exterior-1](after/passage-0-1-0-openair-3-closeup-1.png) — passage/0-1/0 **OPEN AIR**; 251435 visible triangles
- [section-4/view-1](after/passage-0-1-0-openair-4-view-1.png) — passage/0-1/0 **OPEN AIR**; 396359 visible triangles
- [section-4/view-2](after/passage-0-1-0-openair-4-view-2.png) — passage/0-1/0 **OPEN AIR**; 507164 visible triangles
- [section-4/view-3](after/passage-0-1-0-openair-4-view-3.png) — passage/0-1/0 **OPEN AIR**; 412746 visible triangles
- [section-4/exterior-1](after/passage-0-1-0-openair-4-closeup-1.png) — passage/0-1/0 **OPEN AIR**; 396359 visible triangles
- [section-4/exterior-2](after/passage-0-1-0-openair-4-closeup-2.png) — passage/0-1/0 **OPEN AIR**; 507164 visible triangles
- [section-4/exterior-3](after/passage-0-1-0-openair-4-closeup-3.png) — passage/0-1/0 **OPEN AIR**; 443786 visible triangles
- [upper-mouth](after/passage-1-2-0-upper-mouth.png) — passage/1-2/0; 565755 visible triangles; low_contrast_geometry_visible
- [lower-mouth](after/passage-1-2-0-lower-mouth.png) — passage/1-2/0; 505668 visible triangles
- [25%](after/passage-1-2-0-station-25.png) — passage/1-2/0; 655712 visible triangles
- [50%](after/passage-1-2-0-station-50.png) — passage/1-2/0; 590478 visible triangles
- [75%](after/passage-1-2-0-station-75.png) — passage/1-2/0; 607464 visible triangles
- [section-1/view-1](after/passage-1-2-0-openair-1-view-1.png) — passage/1-2/0 **OPEN AIR**; 624410 visible triangles
- [section-1/view-2](after/passage-1-2-0-openair-1-view-2.png) — passage/1-2/0 **OPEN AIR**; 582822 visible triangles
- [section-1/view-3](after/passage-1-2-0-openair-1-view-3.png) — passage/1-2/0 **OPEN AIR**; 611124 visible triangles
- [section-1/exterior-1](after/passage-1-2-0-openair-1-closeup-1.png) — passage/1-2/0 **OPEN AIR**; 624410 visible triangles
- [upper-mouth](after/passage-2-3-0-upper-mouth.png) — passage/2-3/0; 57772 visible triangles
- [lower-mouth](after/passage-2-3-0-lower-mouth.png) — passage/2-3/0; 291268 visible triangles; low_contrast_geometry_visible
- [25%](after/passage-2-3-0-station-25.png) — passage/2-3/0; 53210 visible triangles; low_contrast_geometry_visible
- [50%](after/passage-2-3-0-station-50.png) — passage/2-3/0; 454306 visible triangles
- [75%](after/passage-2-3-0-station-75.png) — passage/2-3/0; 475134 visible triangles
- [section-1/view-1](after/passage-2-3-0-openair-1-view-1.png) — passage/2-3/0 **OPEN AIR**; 103940 visible triangles
- [section-1/view-2](after/passage-2-3-0-openair-1-view-2.png) — passage/2-3/0 **OPEN AIR**; 79774 visible triangles
- [section-1/view-3](after/passage-2-3-0-openair-1-view-3.png) — passage/2-3/0 **OPEN AIR**; 94306 visible triangles; low_contrast_geometry_visible
- [section-1/exterior-1](after/passage-2-3-0-openair-1-closeup-1.png) — passage/2-3/0 **OPEN AIR**; 103940 visible triangles; low_contrast_geometry_visible
- [section-2/view-1](after/passage-2-3-0-openair-2-view-1.png) — passage/2-3/0 **OPEN AIR**; 475504 visible triangles
- [section-2/view-2](after/passage-2-3-0-openair-2-view-2.png) — passage/2-3/0 **OPEN AIR**; 477208 visible triangles
- [section-2/view-3](after/passage-2-3-0-openair-2-view-3.png) — passage/2-3/0 **OPEN AIR**; 477208 visible triangles
- [section-2/exterior-1](after/passage-2-3-0-openair-2-closeup-1.png) — passage/2-3/0 **OPEN AIR**; 475504 visible triangles
- [section-3/view-1](after/passage-2-3-0-openair-3-view-1.png) — passage/2-3/0 **OPEN AIR**; 484600 visible triangles
- [section-3/view-2](after/passage-2-3-0-openair-3-view-2.png) — passage/2-3/0 **OPEN AIR**; 475290 visible triangles
- [section-3/view-3](after/passage-2-3-0-openair-3-view-3.png) — passage/2-3/0 **OPEN AIR**; 488248 visible triangles; low_contrast_geometry_visible
- [section-3/exterior-1](after/passage-2-3-0-openair-3-closeup-1.png) — passage/2-3/0 **OPEN AIR**; 484600 visible triangles
- [section-4/view-1](after/passage-2-3-0-openair-4-view-1.png) — passage/2-3/0 **OPEN AIR**; 475290 visible triangles
- [section-4/view-2](after/passage-2-3-0-openair-4-view-2.png) — passage/2-3/0 **OPEN AIR**; 494736 visible triangles
- [section-4/view-3](after/passage-2-3-0-openair-4-view-3.png) — passage/2-3/0 **OPEN AIR**; 495410 visible triangles; low_contrast_geometry_visible
- [section-4/exterior-1](after/passage-2-3-0-openair-4-closeup-1.png) — passage/2-3/0 **OPEN AIR**; 475290 visible triangles
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Loaded 100 of 397 files, more files were not shown because too many files have changed in this diff. Show more