Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf39712792 |
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 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.
|
||||
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
|
||||
{
|
||||
|
||||
@@ -261,14 +261,17 @@ public:
|
||||
struct FPassageSweepSample
|
||||
{
|
||||
double signedDistance=std::numeric_limits<double>::infinity();
|
||||
double shellSignedDistance=std::numeric_limits<double>::infinity();
|
||||
double roundSignedDistance=std::numeric_limits<double>::infinity();
|
||||
double centerZSubvoxels=0.0,floorCenterZSubvoxels=0.0;
|
||||
double radiusSubvoxels=0.0,floorDepthSubvoxels=0.0;
|
||||
double t=0.0;
|
||||
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;
|
||||
bool valid=false,hasWallRelief=false;
|
||||
bool valid=false,shellValid=false,hasWallRelief=false;
|
||||
};
|
||||
struct FPassageTileSweepData
|
||||
{
|
||||
@@ -1195,13 +1198,16 @@ public:
|
||||
||!std::isfinite(Y)||!std::isfinite(Z))return density;
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
constexpr double FloorShellThicknessMetres=0.75;
|
||||
constexpr double WallShellThicknessMetres=0.75;
|
||||
constexpr double ShellClearanceMetres=0.60;
|
||||
constexpr double WallReliefMetres=0.21;
|
||||
constexpr double MouthShellBlendMetres=5.0;
|
||||
const double qPerMetre=Q*1000000.0/
|
||||
std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,
|
||||
qz=static_cast<double>(Z)*Q;
|
||||
const double shellThickness=FloorShellThicknessMetres*qPerMetre;
|
||||
const double floorShellThicknessBase=FloorShellThicknessMetres*qPerMetre;
|
||||
const double wallShellThickness=WallShellThicknessMetres*qPerMetre;
|
||||
const auto* passageTileQueries=ActivePassageTileQueries;
|
||||
const bool passageTileScopeMatches=passageTileQueries
|
||||
&&passageTileQueries->matches(*this);
|
||||
@@ -1212,7 +1218,6 @@ public:
|
||||
struct FNearestTubeSample
|
||||
{
|
||||
double signedDistance=std::numeric_limits<double>::infinity();
|
||||
double shellSignedDistance=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;
|
||||
std::size_t segmentIndex=0;
|
||||
@@ -1232,7 +1237,7 @@ public:
|
||||
continue;
|
||||
const auto& constants=PassageComposerConstants[passageIndex];
|
||||
|
||||
FNearestTubeSample nearest;
|
||||
FNearestTubeSample nearest,nearestShell;
|
||||
const bool usePreparedSweep=passageTileScopeMatches
|
||||
&&passageTileQueries->hasSweptTubeBase();
|
||||
const auto* swept=usePreparedSweep
|
||||
@@ -1240,7 +1245,6 @@ public:
|
||||
if(swept)
|
||||
{
|
||||
nearest.signedDistance=swept->signedDistance;
|
||||
nearest.shellSignedDistance=swept->shellSignedDistance;
|
||||
nearest.roundSignedDistance=swept->roundSignedDistance;
|
||||
nearest.segmentIndex=swept->segmentIndex;
|
||||
nearest.t=swept->t;
|
||||
@@ -1250,6 +1254,10 @@ public:
|
||||
nearest.floorDepth=swept->floorDepthSubvoxels;
|
||||
nearest.valid=true;
|
||||
nearest.swept=swept;
|
||||
nearestShell.signedDistance=swept->shellSignedDistance;
|
||||
nearestShell.segmentIndex=swept->shellSegmentIndex;
|
||||
nearestShell.valid=swept->shellValid;
|
||||
nearestShell.swept=swept;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1265,7 +1273,17 @@ public:
|
||||
|
||||
const std::ptrdiff_t lastIndex=static_cast<std::ptrdiff_t>(
|
||||
passage.controlPoints.size())-1;
|
||||
constexpr double ShellJoinBlendMetres=6.00;
|
||||
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)
|
||||
{
|
||||
if(passageProfile)++passageProfile->passageCarveSegmentVisits;
|
||||
@@ -1275,13 +1293,12 @@ public:
|
||||
+(qy-segment.a[1])*segment.delta[1]
|
||||
+(qz-segment.a[2])*segment.delta[2];
|
||||
const double t=std::clamp(projection/segment.lengthSq,0.0,1.0);
|
||||
double cx=segment.a[0]+t*segment.delta[0];
|
||||
double cy=segment.a[1]+t*segment.delta[1];
|
||||
double cz=segment.a[2]+t*segment.delta[2];
|
||||
const double cx=segment.a[0]+t*segment.delta[0];
|
||||
const double cy=segment.a[1]+t*segment.delta[1];
|
||||
const double cz=segment.a[2]+t*segment.delta[2];
|
||||
const double radius=segment.radiusA+t*segment.radiusDelta;
|
||||
const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
|
||||
const double roundDistance=DeterministicSqrt(
|
||||
ex*ex+ey*ey+ez*ez)-radius;
|
||||
const double roundDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius;
|
||||
const double floorT=segment.horizontalLengthSq>1.0e-12
|
||||
?std::clamp(((qx-segment.a[0])*segment.delta[0]
|
||||
+(qy-segment.a[1])*segment.delta[1])
|
||||
@@ -1308,15 +1325,11 @@ public:
|
||||
nearest.floorDepth=floorDepth;
|
||||
nearest.valid=true;
|
||||
}
|
||||
|
||||
};
|
||||
VisitNearestPassageSegmentRange(passageIndex,0,lastIndex-1,
|
||||
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& segment=PassageSegmentData[passageIndex][segmentIndex];
|
||||
@@ -1329,8 +1342,9 @@ public:
|
||||
const double cy=segment.a[1]+t*segment.delta[1];
|
||||
const double cz=segment.a[2]+t*segment.delta[2];
|
||||
const double radius=segment.radiusA+t*segment.radiusDelta;
|
||||
const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
|
||||
const double roundDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius;
|
||||
const double dx=qx-cx,dy=qy-cy,dz=qz-cz;
|
||||
const double roundDistance=DeterministicSqrt(dx*dx+dy*dy+dz*dz)
|
||||
-radius;
|
||||
const double floorT=segment.horizontalLengthSq>1.0e-12
|
||||
?std::clamp(((qx-segment.a[0])*segment.delta[0]
|
||||
+(qy-segment.a[1])*segment.delta[1])
|
||||
@@ -1341,13 +1355,14 @@ public:
|
||||
const double floorCenterZ=PassageFlatFloorCenterZSubvoxels(
|
||||
constants,segmentIndex,floorT,segment,floorDepth,qPerMetre);
|
||||
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
|
||||
+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 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)
|
||||
@@ -1355,32 +1370,91 @@ public:
|
||||
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=segmentIndex
|
||||
<constants.routeEndControlIndex
|
||||
?smoothShellFade*shellThickness:0.0;
|
||||
?smoothShellFade*wallShellThickness:0.0;
|
||||
double floorExtensionFade=1.0;
|
||||
if(segmentIndex>=constants.routeEndControlIndex)
|
||||
{
|
||||
const double pastMouthMetres=std::max(0.0,alongMetres
|
||||
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*shellThickness
|
||||
*floorExtensionFade*smoothShellFade;
|
||||
const double floorShellThickness=smoothFloorBlend
|
||||
*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,
|
||||
floorShellThickness);
|
||||
const double localShellDistance=std::max(-static_cast<double>(flatDistance),
|
||||
static_cast<double>(flatDistance)-localThickness);
|
||||
nearest.shellSignedDistance=std::min(nearest.shellSignedDistance,
|
||||
localShellDistance);
|
||||
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 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,
|
||||
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)));
|
||||
}
|
||||
nearestShell.valid=true;
|
||||
}
|
||||
|
||||
}
|
||||
if(!nearest.valid)continue;
|
||||
if(!nearest.valid||!nearestShell.valid)continue;
|
||||
nearestShell.signedDistance=std::max(
|
||||
-nearest.signedDistance,nearestShell.signedDistance);
|
||||
double distanceToCenter=std::max(1.0e-9,
|
||||
nearest.roundSignedDistance+nearest.radius);
|
||||
const double verticalWallAlignment=distanceToCenter>1.0e-9
|
||||
@@ -1402,43 +1476,9 @@ public:
|
||||
const double shapedDistance=PassageFlatBottomDistance(
|
||||
static_cast<float>(roughRoundDistance),nearest.floorCenterZ,qz,
|
||||
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));
|
||||
const double localShellDistance=std::max(-static_cast<double>(shapedDistance),
|
||||
static_cast<double>(shapedDistance)-localShellThickness);
|
||||
unionShellDistance=std::min(unionShellDistance,
|
||||
std::min(localShellDistance,nearest.shellSignedDistance));
|
||||
nearestShell.signedDistance);
|
||||
}
|
||||
|
||||
if(!std::isfinite(unionDistance))return density;
|
||||
@@ -1592,7 +1632,7 @@ public:
|
||||
const auto& p3=passage.controlPoints[
|
||||
std::min(segment+2,passage.controlPoints.size()-1)];
|
||||
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]{};
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
@@ -3371,6 +3411,8 @@ private:
|
||||
struct FPassageSegmentData
|
||||
{
|
||||
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{};
|
||||
double authoredCenterZA=0.0,authoredCenterZDelta=0.0;
|
||||
double authoredAlongMetres=0.0,authoredLengthMetres=0.0;
|
||||
@@ -3432,6 +3474,128 @@ private:
|
||||
return SmoothMaxDistance(roundDistanceSubvoxels,floorDistance,
|
||||
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
|
||||
{
|
||||
std::array<double,3> minimum{},maximum{};
|
||||
@@ -4232,10 +4396,10 @@ private:
|
||||
const auto& prepared=PassageSegmentData[passageIndex][segment];
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
node.minimum[axis]=std::min({node.minimum[axis],
|
||||
prepared.a[axis],prepared.b[axis]});
|
||||
node.maximum[axis]=std::max({node.maximum[axis],
|
||||
prepared.a[axis],prepared.b[axis]});
|
||||
node.minimum[axis]=std::min(node.minimum[axis],
|
||||
prepared.shellCurveMinimum[axis]);
|
||||
node.maximum[axis]=std::max(node.maximum[axis],
|
||||
prepared.shellCurveMaximum[axis]);
|
||||
}
|
||||
node.maximumRadius=std::max({node.maximumRadius,
|
||||
static_cast<double>(a.radiusSubvoxels),static_cast<double>(b.radiusSubvoxels)});
|
||||
@@ -4617,6 +4781,78 @@ private:
|
||||
static_cast<double>(passage.controlPoints[pointIndex].positionSubvoxels[2])
|
||||
+(-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)
|
||||
{
|
||||
const auto& a=passage.controlPoints[segmentIndex];
|
||||
@@ -4649,6 +4885,44 @@ private:
|
||||
data.horizontalLength=DeterministicSqrt(data.horizontalLengthSq);
|
||||
++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;
|
||||
PassageSegmentOffsets[passageIndex+1]=preparedSegments;
|
||||
}
|
||||
@@ -4675,7 +4949,7 @@ private:
|
||||
PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0);
|
||||
if(!ResolvedLayout||ResolvedLayout->passages.empty())return;
|
||||
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 SweepReachMetres=4.0;
|
||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
|
||||
@@ -4698,8 +4972,8 @@ private:
|
||||
std::array<double,3> routeMinimum{},routeMaximum{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
routeMinimum[axis]=std::min(prepared.a[axis],prepared.b[axis])/Q;
|
||||
routeMaximum[axis]=std::max(prepared.a[axis],prepared.b[axis])/Q;
|
||||
routeMinimum[axis]=prepared.shellCurveMinimum[axis]/Q;
|
||||
routeMaximum[axis]=prepared.shellCurveMaximum[axis]/Q;
|
||||
}
|
||||
const double routeExtent=maximumRadius+PassageShellMetres
|
||||
+ReliefMetres+0.01;
|
||||
@@ -11286,6 +11560,11 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||||
{
|
||||
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
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_)
|
||||
||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/
|
||||
std::max<std::uint32_t>(1,shape_->ResolvedLayout
|
||||
?shape_->ResolvedLayout->coreVoxelSizeMicrometres:250000);
|
||||
const double shellThickness=0.75*qPerMetre;
|
||||
constexpr double MouthShellBlendMetres=5.0;
|
||||
const double wallReliefShell=0.41*qPerMetre;
|
||||
const double floorDetailShell=1.0*qPerMetre;
|
||||
const bool profile=profileStats_!=nullptr;
|
||||
@@ -11363,17 +11640,16 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||||
+static_cast<std::int64_t>(gz)*sweep->stepSubvoxels);
|
||||
const double projectionXY=(qx-segment.a[0])*segment.delta[0]
|
||||
+(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);
|
||||
double cx=segment.a[0]+t*segment.delta[0];
|
||||
double cy=segment.a[1]+t*segment.delta[1];
|
||||
double cz=segment.a[2]+t*segment.delta[2];
|
||||
const double radius=segment.radiusA+t*segment.radiusDelta;
|
||||
const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
|
||||
const double signedDistance=FStandaloneShapeWorld::DeterministicSqrt(
|
||||
ex*ex+ey*ey+ez*ez)-radius;
|
||||
const double cx=segment.a[0]+chordT*segment.delta[0];
|
||||
const double cy=segment.a[1]+chordT*segment.delta[1];
|
||||
const double cz=segment.a[2]+chordT*segment.delta[2];
|
||||
const double radius=segment.radiusA+chordT*segment.radiusDelta;
|
||||
const double dx=qx-cx,dy=qy-cy,dz=qz-cz;
|
||||
const double signedDistance=DeterministicSqrt(dx*dx+dy*dy+dz*dz)-radius;
|
||||
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 floorDepth=FStandaloneShapeWorld::PassageFlatFloorDepthSubvoxels(
|
||||
constants,candidate.segmentIndex,floorT,floorRadius,qPerMetre);
|
||||
@@ -11382,6 +11658,54 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||||
constants,candidate.segmentIndex,floorT,segment,floorDepth,qPerMetre);
|
||||
const double flatDistance=FStandaloneShapeWorld::PassageFlatBottomDistance(
|
||||
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(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>(gx);
|
||||
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
|
||||
||(flatDistance==sample.signedDistance
|
||||
&&candidate.segmentIndex<sample.segmentIndex))
|
||||
@@ -11434,12 +11725,38 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||||
sample.floorCenterZSubvoxels=floorCenterZ;
|
||||
sample.radiusSubvoxels=radius;
|
||||
sample.floorDepthSubvoxels=floorDepth;
|
||||
sample.t=t;
|
||||
sample.t=chordT;
|
||||
sample.segmentIndex=static_cast<std::uint32_t>(candidate.segmentIndex);
|
||||
sample.valid=true;
|
||||
sample.hasWallRelief=false;
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
auto& sample=passageSamples.samples[sampleIndex];
|
||||
@@ -11468,8 +11783,12 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
|
||||
+static_cast<std::int64_t>(gy)*sweep->stepSubvoxels);
|
||||
const double qz=static_cast<double>(sweep->gridMinimumSubvoxels[2]
|
||||
+static_cast<std::int64_t>(gz)*sweep->stepSubvoxels);
|
||||
const double wallReliefShell=0.41*qPerMetre;
|
||||
const double radius=sample.radiusSubvoxels;
|
||||
const double centerZ=sample.centerZSubvoxels;
|
||||
if(sample.shellValid)
|
||||
sample.shellSignedDistance=std::max(-sample.signedDistance,
|
||||
sample.shellSignedDistance);
|
||||
if(sample.signedDistance<=sweep->influenceSubvoxels)
|
||||
{
|
||||
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
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 288 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 335 KiB |
|
After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 513 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 417 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 513 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 41 KiB |