proto: continuous swept road deck; coarse-LOD proxies keep thin pillars intact (L143)
Road: one swept-deck stamp on a shared cubic-Hermite centerline (stamp vocabulary V5), 376.8 m, max 8.5 deg, walk audit clean, short-road control fails as expected. Pillars: regular fillPillar stamps get deterministic 24-sided capped-cylinder proxies once the LOD step exceeds their radius or half-height (applies to all worlds). Owner hashes unchanged (v227); five-fixture Clang/GCC determinism; fast tier green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
No files matched your search
@@ -892,6 +892,7 @@ namespace VoxelForgeCore
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV4 = 4;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV5 = 5;
|
||||
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
|
||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||
@@ -918,6 +919,9 @@ namespace VoxelForgeCore
|
||||
std::array<std::int64_t, 3> anchorPositionMicrometres{};
|
||||
// Present only for fillSweptDeckSegment; defines its directed centerline.
|
||||
std::array<std::array<std::int64_t, 3>, 2> sweepEndpointsMicrometres{};
|
||||
// V5 permits one continuous swept deck to carry its complete sampled centerline.
|
||||
// Empty retains the V4 single-segment interpretation.
|
||||
std::vector<std::array<std::int64_t, 3>> sweepPathMicrometres;
|
||||
std::int64_t placementRadiusMicrometres = 0;
|
||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||
std::int64_t smoothWidthMicrometres = 0;
|
||||
@@ -938,6 +942,7 @@ namespace VoxelForgeCore
|
||||
ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid;
|
||||
std::array<std::int64_t, 3> positionMicrometres{};
|
||||
std::array<std::array<std::int64_t, 3>, 2> sweepEndpointsMicrometres{};
|
||||
std::vector<std::array<std::int64_t, 3>> sweepPathMicrometres;
|
||||
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||
|
||||
@@ -599,6 +599,7 @@ namespace
|
||||
<<",\"irregularity_seed\":"<<stamp.irregularitySeed
|
||||
<<",\"yaw_quarter_turns\":"<<static_cast<unsigned>(stamp.yawQuarterTurns);
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
out<<",\"sweep_endpoints_um\":[["
|
||||
<<stamp.sweepEndpointsMicrometres[0][0]<<','
|
||||
<<stamp.sweepEndpointsMicrometres[0][1]<<','
|
||||
@@ -606,6 +607,19 @@ namespace
|
||||
<<stamp.sweepEndpointsMicrometres[1][0]<<','
|
||||
<<stamp.sweepEndpointsMicrometres[1][1]<<','
|
||||
<<stamp.sweepEndpointsMicrometres[1][2]<<"]]";
|
||||
if(!stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
out<<",\"sweep_path_um\":[";
|
||||
for(std::size_t pointIndex=0;pointIndex<stamp.sweepPathMicrometres.size();
|
||||
++pointIndex)
|
||||
{
|
||||
if(pointIndex)out<<',';
|
||||
const auto& point=stamp.sweepPathMicrometres[pointIndex];
|
||||
out<<'['<<point[0]<<','<<point[1]<<','<<point[2]<<']';
|
||||
}
|
||||
out<<']';
|
||||
}
|
||||
}
|
||||
if(s.stampVocabularyVersion>=ResolvedLayoutStampVocabularyVersionV3)
|
||||
out<<",\"rounding_radius_um\":"<<stamp.roundingRadiusMicrometres
|
||||
<<",\"minimum_spacing_um\":"<<stamp.minimumSpacingMicrometres
|
||||
@@ -1143,8 +1157,9 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
else if(stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV1
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV2
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV3
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV4)
|
||||
add("stamp_vocabulary_version","resolved stamps require supported vocabulary version 1, 2, 3 or 4");
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV4
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV5)
|
||||
add("stamp_vocabulary_version","resolved stamps require supported vocabulary version 1 through 5");
|
||||
std::vector<std::string> stampIds;
|
||||
for(const auto& stamp:stamps)
|
||||
{
|
||||
@@ -1184,9 +1199,30 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
-stamp.sweepEndpointsMicrometres[0][axis];
|
||||
lengthSquared+=delta*delta;
|
||||
}
|
||||
if(!(lengthSquared>0.0)
|
||||
||std::sqrt(lengthSquared)>2.0*stamp.halfExtentsMicrometres[0]+1.0)
|
||||
if(stamp.sweepPathMicrometres.empty()&&(!(lengthSquared>0.0)
|
||||
||std::sqrt(lengthSquared)>2.0*stamp.halfExtentsMicrometres[0]+1.0))
|
||||
add("stamp_sweep",stamp.stableId+" has invalid endpoints or longitudinal extent");
|
||||
if(!stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
bool validPath=stampVocabularyVersion>=ResolvedLayoutStampVocabularyVersionV5
|
||||
&&stamp.sweepPathMicrometres.size()>=2
|
||||
&&stamp.sweepPathMicrometres.size()<=1025
|
||||
&&stamp.sweepPathMicrometres.front()==stamp.sweepEndpointsMicrometres[0]
|
||||
&&stamp.sweepPathMicrometres.back()==stamp.sweepEndpointsMicrometres[1];
|
||||
for(std::size_t pointIndex=0;pointIndex<stamp.sweepPathMicrometres.size();
|
||||
++pointIndex)
|
||||
{
|
||||
const auto& point=stamp.sweepPathMicrometres[pointIndex];
|
||||
for(const auto coordinate:point)
|
||||
validPath=validPath&&coordinate>=-1000000000000ll
|
||||
&&coordinate<=1000000000000ll;
|
||||
if(pointIndex>0)
|
||||
validPath=validPath
|
||||
&&stamp.sweepPathMicrometres[pointIndex-1][0]<point[0];
|
||||
}
|
||||
if(!validPath)add("stamp_sweep_path",stamp.stableId
|
||||
+" has an invalid V5 path or non-increasing longitudinal axis");
|
||||
}
|
||||
}
|
||||
if(motif)
|
||||
{
|
||||
@@ -1215,7 +1251,8 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
const std::int64_t pad=motif
|
||||
?(stamp.smoothWidthMicrometres+5)/6+stamp.irregularityAmplitudeMicrometres
|
||||
:stamp.smoothWidthMicrometres+stamp.irregularityAmplitudeMicrometres;
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(const auto extent:stamp.halfExtentsMicrometres)
|
||||
@@ -1235,6 +1272,29 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
add("stamp_bounds",stamp.stableId+" conservative sweep bounds do not match its parameters");
|
||||
}
|
||||
}
|
||||
else if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(std::size_t axis=1;axis<3;++axis)
|
||||
{
|
||||
const double expanded=static_cast<double>(stamp.halfExtentsMicrometres[axis]+pad);
|
||||
radiusSquared+=expanded*expanded;
|
||||
}
|
||||
const auto radius=static_cast<std::int64_t>(std::ceil(std::sqrt(radiusSquared)));
|
||||
std::array<std::int64_t,3> minimum=stamp.sweepPathMicrometres.front();
|
||||
std::array<std::int64_t,3> maximum=minimum;
|
||||
for(const auto& point:stamp.sweepPathMicrometres)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
minimum[axis]=std::min(minimum[axis],point[axis]);
|
||||
maximum[axis]=std::max(maximum[axis],point[axis]);
|
||||
}
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(stamp.boundsMinimumMicrometres[axis]!=minimum[axis]-radius
|
||||
||stamp.boundsMaximumMicrometres[axis]!=maximum[axis]+radius)
|
||||
add("stamp_bounds",stamp.stableId
|
||||
+" conservative continuous-sweep bounds do not match its path");
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::array<std::int64_t,3> extents={
|
||||
@@ -1556,6 +1616,14 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
stamp.sweepEndpointsMicrometres[endpoint][axis]=values[axis].Integer();
|
||||
}
|
||||
for(const auto& value:v.Get("sweep_path_um").AsArray())
|
||||
{
|
||||
std::array<std::int64_t,3> point{};
|
||||
const auto& values=value.AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
point[axis]=values[axis].Integer();
|
||||
stamp.sweepPathMicrometres.push_back(point);
|
||||
}
|
||||
readTriplet("bounds_min_um",stamp.boundsMinimumMicrometres);
|
||||
readTriplet("bounds_max_um",stamp.boundsMaximumMicrometres);
|
||||
readTriplet("half_extents_um",stamp.halfExtentsMicrometres);
|
||||
@@ -1678,10 +1746,15 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
std::sort(ordered.begin(),ordered.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
ResolvedLayoutSnapshotV3 candidate=layout;
|
||||
const bool hasContinuousSweptDeck=std::any_of(ordered.begin(),ordered.end(),[](const auto& request)
|
||||
{return request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&!request.sweepPathMicrometres.empty();});
|
||||
const bool hasSweptDeck=std::any_of(ordered.begin(),ordered.end(),[](const auto& request)
|
||||
{return request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment;});
|
||||
candidate.stampVocabularyVersion=hasSweptDeck
|
||||
?ResolvedLayoutStampVocabularyVersionV4:ResolvedLayoutStampVocabularyVersionV1;
|
||||
candidate.stampVocabularyVersion=hasContinuousSweptDeck
|
||||
?ResolvedLayoutStampVocabularyVersionV5
|
||||
:hasSweptDeck?ResolvedLayoutStampVocabularyVersionV4
|
||||
:ResolvedLayoutStampVocabularyVersionV1;
|
||||
candidate.stamps.clear();candidate.stamps.reserve(ordered.size());
|
||||
for(std::size_t requestIndex=0;requestIndex<ordered.size();++requestIndex)
|
||||
{
|
||||
@@ -1712,7 +1785,8 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
for(const auto coordinate:request.anchorPositionMicrometres)
|
||||
if(coordinate<-1000000000000ll||coordinate>1000000000000ll)
|
||||
{error="stamp request "+request.stableId+" has an unbounded anchor";return false;}
|
||||
if(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
if(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&request.sweepPathMicrometres.empty())
|
||||
{
|
||||
double lengthSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
@@ -1729,6 +1803,25 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
||std::sqrt(lengthSquared)>2.0*request.halfExtentsMicrometres[0]+1.0)
|
||||
{error="stamp request "+request.stableId+" has invalid swept-segment extents";return false;}
|
||||
}
|
||||
if(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&!request.sweepPathMicrometres.empty())
|
||||
{
|
||||
if(request.sweepPathMicrometres.size()<2
|
||||
||request.sweepPathMicrometres.size()>1025
|
||||
||request.sweepPathMicrometres.front()!=request.sweepEndpointsMicrometres[0]
|
||||
||request.sweepPathMicrometres.back()!=request.sweepEndpointsMicrometres[1])
|
||||
{error="stamp request "+request.stableId+" has an invalid continuous deck path";return false;}
|
||||
for(std::size_t pointIndex=0;pointIndex<request.sweepPathMicrometres.size();
|
||||
++pointIndex)
|
||||
{
|
||||
const auto& point=request.sweepPathMicrometres[pointIndex];
|
||||
for(const auto coordinate:point)
|
||||
if(coordinate<-1000000000000ll||coordinate>1000000000000ll)
|
||||
{error="stamp request "+request.stableId+" has an unbounded path point";return false;}
|
||||
if(pointIndex>0&&request.sweepPathMicrometres[pointIndex-1][0]>=point[0])
|
||||
{error="stamp request "+request.stableId+" path X coordinates must increase strictly";return false;}
|
||||
}
|
||||
}
|
||||
|
||||
// The shared JSON value type stores numbers as IEEE-754 doubles; keep the
|
||||
// serialized seed in its exact integer range so the canonical round trip holds.
|
||||
@@ -1748,6 +1841,7 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
ResolvedLayoutStampV1 stamp;stamp.stableId=request.stableId;
|
||||
stamp.strateIndex=request.strateIndex;stamp.kind=request.kind;
|
||||
stamp.sweepEndpointsMicrometres=request.sweepEndpointsMicrometres;
|
||||
stamp.sweepPathMicrometres=request.sweepPathMicrometres;
|
||||
stamp.halfExtentsMicrometres=request.halfExtentsMicrometres;
|
||||
stamp.smoothWidthMicrometres=request.smoothWidthMicrometres;
|
||||
stamp.irregularityAmplitudeMicrometres=request.irregularityAmplitudeMicrometres;
|
||||
@@ -1769,7 +1863,8 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
stamp.positionMicrometres[axis]=request.anchorPositionMicrometres[axis]+offset[axis];
|
||||
const std::int64_t pad=stamp.smoothWidthMicrometres
|
||||
+stamp.irregularityAmplitudeMicrometres;
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(const auto extent:stamp.halfExtentsMicrometres)
|
||||
@@ -1788,6 +1883,30 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
stamp.boundsMaximumMicrometres[axis]=maximum+radius;
|
||||
}
|
||||
}
|
||||
else if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(std::size_t axis=1;axis<3;++axis)
|
||||
{
|
||||
const double expanded=static_cast<double>(stamp.halfExtentsMicrometres[axis]+pad);
|
||||
radiusSquared+=expanded*expanded;
|
||||
}
|
||||
const std::int64_t radius=static_cast<std::int64_t>(
|
||||
std::ceil(std::sqrt(radiusSquared)));
|
||||
std::array<std::int64_t,3> minimum=stamp.sweepPathMicrometres.front();
|
||||
std::array<std::int64_t,3> maximum=minimum;
|
||||
for(const auto& point:stamp.sweepPathMicrometres)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
minimum[axis]=std::min(minimum[axis],point[axis]);
|
||||
maximum[axis]=std::max(maximum[axis],point[axis]);
|
||||
}
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
stamp.boundsMinimumMicrometres[axis]=minimum[axis]-radius;
|
||||
stamp.boundsMaximumMicrometres[axis]=maximum[axis]+radius;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::array<std::int64_t,3> extents={
|
||||
|
||||
@@ -3536,6 +3536,7 @@ private:
|
||||
const VoxelForgeCore::ResolvedLayoutStampV1* record=nullptr;
|
||||
std::array<float,3> positionVoxels{},halfExtentsVoxels{};
|
||||
std::array<std::array<float,3>,2> sweepEndpointsVoxels{};
|
||||
std::vector<std::array<float,3>> sweepPathVoxels;
|
||||
float smoothWidthVoxels=0.0f;
|
||||
float irregularityAmplitudeVoxels=0.0f;
|
||||
float irregularityWavelengthVoxels=0.0f;
|
||||
@@ -3710,6 +3711,15 @@ private:
|
||||
static_cast<double>(stamp.sweepEndpointsMicrometres[endpoint][axis])
|
||||
/Owner->CoreVoxelSizeMicrons);
|
||||
}
|
||||
runtime.sweepPathVoxels.reserve(stamp.sweepPathMicrometres.size());
|
||||
for(const auto& pointMicrometres:stamp.sweepPathMicrometres)
|
||||
{
|
||||
std::array<float,3> point{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
point[axis]=static_cast<float>(static_cast<double>(pointMicrometres[axis])
|
||||
/Owner->CoreVoxelSizeMicrons);
|
||||
runtime.sweepPathVoxels.push_back(point);
|
||||
}
|
||||
const std::size_t xAxis=stamp.yawQuarterTurns%2?1:0;
|
||||
const std::size_t yAxis=stamp.yawQuarterTurns%2?0:1;
|
||||
runtime.halfExtentsVoxels={
|
||||
@@ -5169,6 +5179,20 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
static std::int32_t& CurrentStampSamplingStep() noexcept
|
||||
{
|
||||
static thread_local std::int32_t Step=0;
|
||||
return Step;
|
||||
}
|
||||
struct FScopedStampSamplingStep
|
||||
{
|
||||
explicit FScopedStampSamplingStep(std::int32_t Step)
|
||||
:Previous(CurrentStampSamplingStep())
|
||||
{CurrentStampSamplingStep()=std::max(0,Step);}
|
||||
~FScopedStampSamplingStep(){CurrentStampSamplingStep()=Previous;}
|
||||
std::int32_t Previous=0;
|
||||
};
|
||||
|
||||
bool GetNativeSurfaceHeightsAt(float X,float Y,int32 ChunkZ,
|
||||
float& OutGroundZ,float& OutCeilingZ) const
|
||||
{
|
||||
@@ -7123,6 +7147,111 @@ private:
|
||||
const float top=std::max(StampEllipsoidSdf(x,y,z,halfSize),-z);
|
||||
return SmoothMinDistance(lower,top,round*2.0f);
|
||||
}
|
||||
static float StampContinuousSweptDeckSdf(const FStampRuntime& stamp,
|
||||
float x,float y,float z)
|
||||
{
|
||||
const auto& path=stamp.sweepPathVoxels;
|
||||
if(path.size()<2)return std::numeric_limits<float>::infinity();
|
||||
const float halfWidth=stamp.halfExtentsVoxels[1];
|
||||
const float halfThickness=stamp.halfExtentsVoxels[2];
|
||||
const float round=std::min(0.35f,std::min(halfWidth,halfThickness)*0.15f);
|
||||
const float searchMargin=halfWidth+halfThickness+2.0f;
|
||||
const auto first=std::lower_bound(path.begin(),path.end(),x-searchMargin,
|
||||
[](const auto& point,float value){return point[0]<value;});
|
||||
const auto last=std::upper_bound(first,path.end(),x+searchMargin,
|
||||
[](float value,const auto& point){return value<point[0];});
|
||||
const std::size_t firstIndex=static_cast<std::size_t>(first-path.begin());
|
||||
const std::size_t lastIndex=static_cast<std::size_t>(last-path.begin());
|
||||
const std::size_t segmentBegin=std::min(path.size()-2,
|
||||
firstIndex==0?std::size_t{0}:firstIndex-1);
|
||||
const std::size_t segmentEnd=std::min(path.size()-2,lastIndex);
|
||||
std::size_t nearestSegment=path.size();
|
||||
float nearestProjection=0.0f;
|
||||
float nearestDistanceSquared=std::numeric_limits<float>::infinity();
|
||||
for(std::size_t segment=segmentBegin;segment<=segmentEnd;++segment)
|
||||
{
|
||||
const auto& a=path[segment];const auto& b=path[segment+1];
|
||||
const float dx=b[0]-a[0],dy=b[1]-a[1];
|
||||
const float horizontalSquared=dx*dx+dy*dy;
|
||||
if(!(horizontalSquared>1.0e-10f))continue;
|
||||
const float projection=std::clamp(((x-a[0])*dx+(y-a[1])*dy)
|
||||
/horizontalSquared,0.0f,1.0f);
|
||||
const float nearestX=a[0]+projection*dx;
|
||||
const float nearestY=a[1]+projection*dy;
|
||||
const float offsetX=x-nearestX,offsetY=y-nearestY;
|
||||
const float distanceSquared=offsetX*offsetX+offsetY*offsetY;
|
||||
if(distanceSquared<nearestDistanceSquared
|
||||
||(distanceSquared==nearestDistanceSquared&&segment<nearestSegment))
|
||||
{
|
||||
nearestSegment=segment;
|
||||
nearestProjection=projection;
|
||||
nearestDistanceSquared=distanceSquared;
|
||||
}
|
||||
}
|
||||
if(nearestSegment>=path.size()-1)return std::numeric_limits<float>::infinity();
|
||||
const auto evalCenterline=[&](float parameter,std::array<float,3>& center,
|
||||
std::array<float,3>& tangent)
|
||||
{
|
||||
const auto& p0=path[nearestSegment];
|
||||
const auto& p1=path[nearestSegment+1];
|
||||
const auto& before=path[nearestSegment==0?nearestSegment:nearestSegment-1];
|
||||
const auto& after=path[nearestSegment+2<path.size()
|
||||
?nearestSegment+2:nearestSegment+1];
|
||||
const float t2=parameter*parameter,t3=t2*parameter;
|
||||
const float h00=2.0f*t3-3.0f*t2+1.0f;
|
||||
const float h10=t3-2.0f*t2+parameter;
|
||||
const float h01=-2.0f*t3+3.0f*t2;
|
||||
const float h11=t3-t2;
|
||||
const float d00=6.0f*t2-6.0f*parameter;
|
||||
const float d10=3.0f*t2-4.0f*parameter+1.0f;
|
||||
const float d01=-6.0f*t2+6.0f*parameter;
|
||||
const float d11=3.0f*t2-2.0f*parameter;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const float m0=nearestSegment==0?p1[axis]-p0[axis]
|
||||
:0.5f*(p1[axis]-before[axis]);
|
||||
const float m1=nearestSegment+2>=path.size()?p1[axis]-p0[axis]
|
||||
:0.5f*(after[axis]-p0[axis]);
|
||||
center[axis]=h00*p0[axis]+h10*m0+h01*p1[axis]+h11*m1;
|
||||
tangent[axis]=d00*p0[axis]+d10*m0+d01*p1[axis]+d11*m1;
|
||||
}
|
||||
};
|
||||
std::array<float,3> center{},tangent{};
|
||||
for(int refinement=0;refinement<2;++refinement)
|
||||
{
|
||||
evalCenterline(nearestProjection,center,tangent);
|
||||
const float derivativeSquared=tangent[0]*tangent[0]+tangent[1]*tangent[1];
|
||||
if(!(derivativeSquared>1.0e-10f))return std::numeric_limits<float>::infinity();
|
||||
const float offsetX=center[0]-x,offsetY=center[1]-y;
|
||||
nearestProjection=std::clamp(nearestProjection
|
||||
-(offsetX*tangent[0]+offsetY*tangent[1])/derivativeSquared,0.0f,1.0f);
|
||||
}
|
||||
evalCenterline(nearestProjection,center,tangent);
|
||||
const float tangentLength=std::sqrt(tangent[0]*tangent[0]
|
||||
+tangent[1]*tangent[1]+tangent[2]*tangent[2]);
|
||||
if(!(tangentLength>1.0e-8f))return std::numeric_limits<float>::infinity();
|
||||
for(float& component:tangent)component/=tangentLength;
|
||||
const float horizontalLength=std::sqrt(tangent[0]*tangent[0]
|
||||
+tangent[1]*tangent[1]);
|
||||
if(!(horizontalLength>1.0e-8f))return std::numeric_limits<float>::infinity();
|
||||
const std::array<float,3> side={-tangent[1]/horizontalLength,
|
||||
tangent[0]/horizontalLength,0.0f};
|
||||
const std::array<float,3> up={-tangent[2]*side[1],tangent[2]*side[0],
|
||||
tangent[0]*side[1]-tangent[1]*side[0]};
|
||||
const std::array<float,3> relative={x-center[0],y-center[1],z-center[2]};
|
||||
const auto dot=[&](const std::array<float,3>& axis)
|
||||
{return relative[0]*axis[0]+relative[1]*axis[1]+relative[2]*axis[2];};
|
||||
const float lateral=dot(side),vertical=dot(up);
|
||||
if((nearestSegment==0&&nearestProjection<=0.0f)
|
||||
||(nearestSegment+2==path.size()&&nearestProjection>=1.0f))
|
||||
return StampRoundedBoxSdf(dot(tangent),lateral,vertical,
|
||||
{halfThickness+round,halfWidth,halfThickness},round);
|
||||
const float qSide=std::abs(lateral)-halfWidth+round;
|
||||
const float qUp=std::abs(vertical)-halfThickness+round;
|
||||
const float outside=std::sqrt(std::max(qSide,0.0f)*std::max(qSide,0.0f)
|
||||
+std::max(qUp,0.0f)*std::max(qUp,0.0f));
|
||||
return outside+std::min(std::max(qSide,qUp),0.0f)-round;
|
||||
}
|
||||
float StampSdf(const FStampRuntime& stamp,float x,float y,float z) const
|
||||
{
|
||||
float localX=x-stamp.positionVoxels[0];
|
||||
@@ -7161,6 +7290,11 @@ private:
|
||||
break;
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment:
|
||||
{
|
||||
if(!stamp.sweepPathVoxels.empty())
|
||||
{
|
||||
distance=StampContinuousSweptDeckSdf(stamp,x,y,z);
|
||||
break;
|
||||
}
|
||||
std::array<float,3> tangent{},side{},up{},delta{};
|
||||
float lengthSquared=0.0f,horizontalSquared=0.0f;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
@@ -7199,6 +7333,15 @@ private:
|
||||
|
||||
static double StampAbsMinimum(double minimum,double maximum)
|
||||
{return minimum<=0.0&&maximum>=0.0?0.0:std::min(std::abs(minimum),std::abs(maximum));}
|
||||
static bool UsesCoarsePillarProxy(const FStampRuntime& stamp,std::int32_t sampleStep)
|
||||
{
|
||||
if(!stamp.record||stamp.record->kind
|
||||
!=VoxelForgeCore::ResolvedSdfStampKindV1::fillPillar
|
||||
||sampleStep<=0||stamp.irregularityAmplitudeVoxels>0.0f)return false;
|
||||
return static_cast<float>(sampleStep)>std::min(stamp.halfExtentsVoxels[0],
|
||||
stamp.halfExtentsVoxels[1])
|
||||
||static_cast<float>(sampleStep)>stamp.halfExtentsVoxels[2];
|
||||
}
|
||||
static double StampRoundedBoxSdfDouble(const std::array<double,3>& absolute,
|
||||
const std::array<double,3>& halfSize,double rounding)
|
||||
{
|
||||
@@ -7300,6 +7443,7 @@ private:
|
||||
if(index>=StampRuntime.size())return;
|
||||
const auto& runtime=StampRuntime[index];const auto& record=*runtime.record;
|
||||
if(record.strateIndex!=bound->StrateIndex)return;
|
||||
if(UsesCoarsePillarProxy(runtime,CurrentStampSamplingStep()))return;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(worldMicrometres[axis]<record.boundsMinimumMicrometres[axis]
|
||||
||worldMicrometres[axis]>record.boundsMaximumMicrometres[axis])return;
|
||||
@@ -7317,6 +7461,154 @@ private:
|
||||
apply(static_cast<std::uint32_t>(index));
|
||||
return composed;
|
||||
}
|
||||
|
||||
public:
|
||||
std::uint32_t AppendCoarsePillarProxies(FVoxelMeshData& mesh,
|
||||
const FIntVector& tileOrigin,std::int32_t tileExtent,std::int32_t sampleStep) const
|
||||
{
|
||||
if(!Owner||Owner->CoreVoxelSizeMicrons==0||tileExtent<=0||sampleStep<=0
|
||||
||StampRuntime.empty())return 0;
|
||||
constexpr int32 sideCount=24;
|
||||
constexpr double TwoPi=6.2831853071795864769;
|
||||
const int32 originalVertexCount=mesh.Vertices.Num();
|
||||
const int32 originalSurfaceVertices=std::clamp(mesh.NumSurfaceVertices,0,
|
||||
originalVertexCount);
|
||||
const bool hasNormals=mesh.Normals.Num()==originalVertexCount;
|
||||
const bool hasUvs=mesh.UVs.Num()==originalVertexCount;
|
||||
const bool hasColors=mesh.Colors.Num()==originalVertexCount;
|
||||
const double voxelCentimetres=Owner->CoreVoxelSizeMicrons/10000.0;
|
||||
const std::int64_t tileMinimum[3]={tileOrigin.X,tileOrigin.Y,tileOrigin.Z};
|
||||
const std::int64_t tileMaximum[3]={static_cast<std::int64_t>(tileOrigin.X)+tileExtent,
|
||||
static_cast<std::int64_t>(tileOrigin.Y)+tileExtent,
|
||||
static_cast<std::int64_t>(tileOrigin.Z)+tileExtent};
|
||||
std::vector<int32> proxyIndices;
|
||||
std::uint32_t proxyCount=0;
|
||||
const auto addVertex=[&](const FVector& position,const FVector& normal,
|
||||
const FVector2f& uv)
|
||||
{
|
||||
const int32 index=mesh.Vertices.Add(position);
|
||||
if(hasNormals)mesh.Normals.Add(normal);
|
||||
if(hasUvs)mesh.UVs.Add(uv);
|
||||
if(hasColors)mesh.Colors.Add(FColor::White);
|
||||
return index;
|
||||
};
|
||||
for(const auto& stamp:StampRuntime)
|
||||
{
|
||||
if(!UsesCoarsePillarProxy(stamp,sampleStep))continue;
|
||||
const auto& center=stamp.positionVoxels;
|
||||
if(center[0]<tileMinimum[0]||center[0]>=tileMaximum[0]
|
||||
||center[1]<tileMinimum[1]||center[1]>=tileMaximum[1]
|
||||
||center[2]<tileMinimum[2]||center[2]>=tileMaximum[2])continue;
|
||||
const double radiusX=stamp.halfExtentsVoxels[0];
|
||||
const double radiusY=stamp.halfExtentsVoxels[1];
|
||||
const double bottomVoxel=center[2]-stamp.halfExtentsVoxels[2]-1.0;
|
||||
const double topVoxel=center[2]+stamp.halfExtentsVoxels[2];
|
||||
const auto point=[&](double angle,double z)
|
||||
{
|
||||
return FVector((center[0]+radiusX*std::cos(angle))*voxelCentimetres,
|
||||
(center[1]+radiusY*std::sin(angle))*voxelCentimetres,
|
||||
z*voxelCentimetres);
|
||||
};
|
||||
const int32 capTopCenter=addVertex(FVector(center[0]*voxelCentimetres,
|
||||
center[1]*voxelCentimetres,topVoxel*voxelCentimetres),
|
||||
FVector(0.0,0.0,1.0),FVector2f(0.5f,0.5f));
|
||||
const int32 capBottomCenter=addVertex(FVector(center[0]*voxelCentimetres,
|
||||
center[1]*voxelCentimetres,bottomVoxel*voxelCentimetres),
|
||||
FVector(0.0,0.0,-1.0),FVector2f(0.5f,0.5f));
|
||||
for(int32 sideIndex=0;sideIndex<sideCount;++sideIndex)
|
||||
{
|
||||
const double u0=static_cast<double>(sideIndex)/sideCount;
|
||||
const double u1=static_cast<double>(sideIndex+1)/sideCount;
|
||||
const double a0=TwoPi*u0,a1=TwoPi*u1;
|
||||
const FVector n0=FVector(std::cos(a0)/radiusX,
|
||||
std::sin(a0)/radiusY,0.0).GetSafeNormal();
|
||||
const FVector n1=FVector(std::cos(a1)/radiusX,
|
||||
std::sin(a1)/radiusY,0.0).GetSafeNormal();
|
||||
const int32 bottomA=addVertex(point(a0,bottomVoxel),n0,FVector2f(u0,0.0f));
|
||||
const int32 bottomB=addVertex(point(a1,bottomVoxel),n1,FVector2f(u1,0.0f));
|
||||
const int32 topB=addVertex(point(a1,topVoxel),n1,FVector2f(u1,1.0f));
|
||||
const int32 topA=addVertex(point(a0,topVoxel),n0,FVector2f(u0,1.0f));
|
||||
proxyIndices.insert(proxyIndices.end(),{bottomA,bottomB,topB,
|
||||
bottomA,topB,topA});
|
||||
|
||||
const int32 topAIndex=addVertex(point(a0,topVoxel),FVector(0.0,0.0,1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a0)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a0))));
|
||||
const int32 topBIndex=addVertex(point(a1,topVoxel),FVector(0.0,0.0,1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a1)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a1))));
|
||||
proxyIndices.insert(proxyIndices.end(),{capTopCenter,topAIndex,topBIndex});
|
||||
|
||||
const int32 bottomAIndex=addVertex(point(a0,bottomVoxel),FVector(0.0,0.0,-1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a0)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a0))));
|
||||
const int32 bottomBIndex=addVertex(point(a1,bottomVoxel),FVector(0.0,0.0,-1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a1)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a1))));
|
||||
proxyIndices.insert(proxyIndices.end(),
|
||||
{capBottomCenter,bottomBIndex,bottomAIndex});
|
||||
}
|
||||
++proxyCount;
|
||||
}
|
||||
if(proxyIndices.empty())return 0;
|
||||
const int32 proxyVertexCount=mesh.Vertices.Num()-originalVertexCount;
|
||||
const auto insertProxyVertices=[&](auto& values)
|
||||
{
|
||||
if(values.Num()!=originalVertexCount+proxyVertexCount)return;
|
||||
using FArray=std::remove_reference_t<decltype(values)>;
|
||||
FArray reordered;
|
||||
reordered.Reserve(values.Num());
|
||||
for(int32 index=0;index<originalSurfaceVertices;++index)
|
||||
reordered.Add(values[index]);
|
||||
for(int32 index=originalVertexCount;index<values.Num();++index)
|
||||
reordered.Add(values[index]);
|
||||
for(int32 index=originalSurfaceVertices;index<originalVertexCount;++index)
|
||||
reordered.Add(values[index]);
|
||||
values=MoveTemp(reordered);
|
||||
};
|
||||
insertProxyVertices(mesh.Vertices);
|
||||
if(hasNormals)insertProxyVertices(mesh.Normals);
|
||||
if(hasUvs)insertProxyVertices(mesh.UVs);
|
||||
if(hasColors)insertProxyVertices(mesh.Colors);
|
||||
|
||||
const int32 oldTriangleCount=mesh.Triangles.Num()/3;
|
||||
const int32 ceilingStart=std::max(0,mesh.Triangles.Num()-mesh.NumCeilingTriangles*3);
|
||||
const int32 groundTriangleCount=ceilingStart/3;
|
||||
const int32 proxyTriangleCount=static_cast<int32>(proxyIndices.size()/3);
|
||||
TArray<int32> mergedIndices;
|
||||
mergedIndices.Reserve(mesh.Triangles.Num()+static_cast<int32>(proxyIndices.size()));
|
||||
for(int32 index=0;index<ceilingStart;++index)
|
||||
{
|
||||
const int32 oldIndex=mesh.Triangles[index];
|
||||
mergedIndices.Add(oldIndex>=originalSurfaceVertices
|
||||
?oldIndex+proxyVertexCount:oldIndex);
|
||||
}
|
||||
for(const int32 index:proxyIndices)
|
||||
mergedIndices.Add(originalSurfaceVertices+index-originalVertexCount);
|
||||
for(int32 index=ceilingStart;index<mesh.Triangles.Num();++index)
|
||||
{
|
||||
const int32 oldIndex=mesh.Triangles[index];
|
||||
mergedIndices.Add(oldIndex>=originalSurfaceVertices
|
||||
?oldIndex+proxyVertexCount:oldIndex);
|
||||
}
|
||||
mesh.Triangles=MoveTemp(mergedIndices);
|
||||
|
||||
TArray<uint8> mergedMasks;
|
||||
mergedMasks.Reserve(oldTriangleCount+proxyTriangleCount);
|
||||
for(int32 triangle=0;triangle<groundTriangleCount;++triangle)
|
||||
mergedMasks.Add(mesh.TriangleSkirtFaceMasks.IsValidIndex(triangle)
|
||||
?mesh.TriangleSkirtFaceMasks[triangle]:0);
|
||||
for(int32 triangle=0;triangle<proxyTriangleCount;++triangle)mergedMasks.Add(0);
|
||||
if(groundTriangleCount<oldTriangleCount)
|
||||
for(int32 triangle=groundTriangleCount;triangle<oldTriangleCount;++triangle)
|
||||
mergedMasks.Add(mesh.TriangleSkirtFaceMasks.IsValidIndex(triangle)
|
||||
?mesh.TriangleSkirtFaceMasks[triangle]:0);
|
||||
mesh.TriangleSkirtFaceMasks=MoveTemp(mergedMasks);
|
||||
mesh.NumSurfaceVertices=originalSurfaceVertices+proxyVertexCount;
|
||||
return proxyCount;
|
||||
}
|
||||
|
||||
private:
|
||||
float ApplyResolvedCavernGraph(float density,float X,float Y,float Z,
|
||||
float* nativeFloorSupport=nullptr) const
|
||||
{
|
||||
|
||||
@@ -2889,12 +2889,16 @@ namespace VoxelForgeCore
|
||||
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
||||
const auto MarchingCubesStart=std::chrono::steady_clock::now();
|
||||
FVoxelUniformBlockProofAuditStats BlockProofAudit;
|
||||
FVoxelMeshData mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr,
|
||||
options.profileMesherStages?&McStageTimings:nullptr,
|
||||
options.auditUniformBlockProofs?&BlockProofAudit:nullptr,
|
||||
options.forceFullDensityCapture);
|
||||
FVoxelMeshData mesh;
|
||||
{
|
||||
FStandaloneShapeWorld::FScopedStampSamplingStep stampSamplingStep(MeshStep);
|
||||
mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr,
|
||||
options.profileMesherStages?&McStageTimings:nullptr,
|
||||
options.auditUniformBlockProofs?&BlockProofAudit:nullptr,
|
||||
options.forceFullDensityCapture);
|
||||
}
|
||||
passageFeatureScope.copySweptTubeRuntimeCounts();
|
||||
if(options.auditUniformBlockProofs)
|
||||
{
|
||||
@@ -3107,6 +3111,13 @@ namespace VoxelForgeCore
|
||||
AppendStratumBoundaryFloors(world,farShape,nativeKey,step,mesh);
|
||||
AppendStratumBoundaryCeilings(world,farShape,nativeKey,step,mesh);
|
||||
}
|
||||
const std::uint32_t coarsePillarProxyCount=tileShape.AppendCoarsePillarProxies(
|
||||
mesh,origin,extent,MeshStep);
|
||||
if(coarsePillarProxyCount>0)
|
||||
std::printf("[CoarsePillarProxy] key=(%d,%d,%d,lod%d) sampling_step_vox=%d "
|
||||
"pillars=%u mode=exact_capped_cylinder status=EMITTED\n",
|
||||
key.coordinate.x,key.coordinate.y,key.coordinate.z,key.lod,MeshStep,
|
||||
coarsePillarProxyCount);
|
||||
l77RefineStage.Restore();
|
||||
|
||||
out.densitySampleCount=static_cast<std::uint64_t>(std::max<int64>(0,sampleCount));
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the L143 liminal pictures and walk-audit controls."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l143"))
|
||||
|
||||
FLAT_FIELD = {
|
||||
"version": 1,
|
||||
"kind": "flatField",
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures")
|
||||
args = parser.parse_args()
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
base = json.loads(OWNER.read_text(encoding="utf-8"))
|
||||
|
||||
monolith = dict(base)
|
||||
monolith["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
monolith["motif_family_override"] = {
|
||||
"version": 1,
|
||||
"strate_index": 0,
|
||||
"placement_policy": "seed,strate,stable_id",
|
||||
"motifs": [{
|
||||
"stable_id": "liminal/oversized-familiar-monolith-v1",
|
||||
"kind": "cappedCylinder",
|
||||
"radius_m": 1.6,
|
||||
"height_m": 26.0,
|
||||
"route_side_offset_m": 42.0,
|
||||
"placement_radius_m": 0.0,
|
||||
"fillet_radius_m": 0.6,
|
||||
"blend_width_m": 1.2,
|
||||
"minimum_spacing_m": 0.0,
|
||||
"yaw_quarter_turns": 0,
|
||||
"attachment": "seated_floor",
|
||||
}],
|
||||
}
|
||||
|
||||
hills = dict(base)
|
||||
hills["surface_profile_override"] = {
|
||||
"version": 1,
|
||||
"kind": "periodicHills",
|
||||
"amplitude_x_m": 2.2,
|
||||
"amplitude_y_m": 2.2,
|
||||
"wavelength_x_m": 80.0,
|
||||
"wavelength_y_m": 80.0,
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
|
||||
road_override = {
|
||||
"stable_id": "liminal/road-clouds-v2",
|
||||
"strate_index": 0,
|
||||
"start_xy_m": [120.0, 340.0],
|
||||
"path_length_m": 360.0,
|
||||
"curve_control_offset_m": 84.0,
|
||||
"deck_height_above_field_m": 44.0,
|
||||
"rise_control_m": 22.0,
|
||||
"half_width_m": 4.0,
|
||||
"half_thickness_m": 1.0,
|
||||
"sample_spacing_m": 1.0,
|
||||
"support_spacing_m": 24.0,
|
||||
"support_radius_m": 0.55,
|
||||
}
|
||||
road = dict(base)
|
||||
road["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
road["liminal_road_override"] = dict(road_override)
|
||||
|
||||
short_control = dict(base)
|
||||
short_control["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
short_override = dict(road_override)
|
||||
short_override["path_length_m"] = 180.0
|
||||
short_control["liminal_road_override"] = short_override
|
||||
|
||||
pillars = dict(base)
|
||||
pillars["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
pillars["liminal_pillar_grid_override"] = {
|
||||
"stable_id": "liminal/horizon-pillar-grid-v1",
|
||||
"strate_index": 0,
|
||||
"center_xy_m": [700.0, 800.0],
|
||||
"rows": 15,
|
||||
"columns": 15,
|
||||
"spacing_m": 76.0,
|
||||
"radius_m": 5.5,
|
||||
"height_m": 40.0,
|
||||
}
|
||||
|
||||
fixtures = (
|
||||
("flatField-monolith", monolith),
|
||||
("periodicHills", hills),
|
||||
("road-in-clouds", road),
|
||||
("road-short-control", short_control),
|
||||
("pillar-grid", pillars),
|
||||
)
|
||||
for name, fixture in fixtures:
|
||||
path = args.out / f"liminal-{name}.json"
|
||||
path.write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L143Fixture] name={name} file={path} status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,463 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L143 liminal determinism checks, walk audits and multi-LOD captures."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l143"))
|
||||
CAPTURE_DEFAULT = Path(__file__).resolve().parent / "captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
Q_PER_METRE = 1024.0
|
||||
FOG_COLOR = "0.66,0.73,0.77"
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
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 run_step(name: str, command: list[object], log: Path, *, env: dict[str, str],
|
||||
stamped: bool = False, timeout_seconds: int = 1740,
|
||||
expected_exit: int = 0) -> str:
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
binary = Path(str(command[0]))
|
||||
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected_stamp:
|
||||
raise RuntimeError(f"{name} refused a stale executable: {binary}")
|
||||
started = time.monotonic()
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, f"timeout", f"{timeout_seconds}s",
|
||||
*map(str, command)], cwd=ROOT, env=env,
|
||||
stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=timeout_seconds + 5, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
print(f"[L143Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if stamped and (first_stamp(output) != expected_stamp or current_stamp() != expected_stamp):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != expected_exit:
|
||||
raise RuntimeError(f"{name} exit={result.returncode}, expected={expected_exit}; see {log}")
|
||||
return output
|
||||
|
||||
|
||||
def route_frame(manifest_path: Path) -> tuple[tuple[float, float, float],
|
||||
tuple[float, float, float]]:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
strata = [entry for entry in manifest["plan"]["strates"]
|
||||
if entry.get("route") and len(entry["route"]) >= 2]
|
||||
if not strata:
|
||||
raise RuntimeError(f"no planned SurfaceWorld route in {manifest_path}")
|
||||
route = strata[0]["route"]
|
||||
points = [tuple(float(value) / Q_PER_METRE for value in item["position_q"])
|
||||
for item in route]
|
||||
lengths = [math.dist(a, b) for a, b in zip(points, points[1:])]
|
||||
total = sum(lengths)
|
||||
if total <= 1.0e-6:
|
||||
raise RuntimeError(f"degenerate planned route in {manifest_path}")
|
||||
|
||||
def at(distance: float) -> tuple[float, float, float]:
|
||||
remaining = max(0.0, min(total, distance))
|
||||
for index, (a, b, length) in enumerate(zip(points, points[1:], lengths)):
|
||||
if remaining <= length or index == len(lengths) - 1:
|
||||
if length <= 1.0e-9:
|
||||
return b
|
||||
t = remaining / length
|
||||
return tuple(a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
center = at(total * 0.5)
|
||||
before, after = at(total * 0.5 - 1.0), at(total * 0.5 + 1.0)
|
||||
dx, dy = after[0] - before[0], after[1] - before[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= 1.0e-9:
|
||||
raise RuntimeError(f"degenerate route tangent in {manifest_path}")
|
||||
return center, (dx / length, dy / length, 0.0)
|
||||
|
||||
|
||||
def camera_angles(camera, target) -> tuple[float, float]:
|
||||
dx, dy, dz = (target[index] - camera[index] for index in range(3))
|
||||
return math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy)))
|
||||
|
||||
|
||||
def point_toward(camera, target) -> tuple[float, ...]:
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
def stamp(layout: dict, stable_id: str) -> dict:
|
||||
for record in layout.get("stamps", []):
|
||||
if record.get("stable_id") == stable_id:
|
||||
return record
|
||||
raise RuntimeError(f"missing resolved stamp {stable_id}")
|
||||
|
||||
|
||||
def make_views(name: str, manifest_path: Path, layout_path: Path):
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
center, tangent = route_frame(manifest_path)
|
||||
tx, ty, _ = tangent
|
||||
nx, ny = -ty, tx
|
||||
base_height = manifest["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE
|
||||
if name == "flatField-monolith":
|
||||
monolith = stamp(layout, "liminal/oversized-familiar-monolith-v1")
|
||||
sx, sy, _ = (value / 1.0e6 for value in monolith["position_um"])
|
||||
floor = monolith["floor_um"] / 1.0e6
|
||||
eye_camera = (sx - tx * 36.0, sy - ty * 36.0, floor + 1.6)
|
||||
eye_target = (sx, sy, floor + 10.5)
|
||||
wide_camera = (sx - tx * 42.0 - nx * 18.0,
|
||||
sy - ty * 42.0 - ny * 18.0, floor + 42.0)
|
||||
wide_target = (sx, sy, floor + 8.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 24),
|
||||
"height_center": floor,
|
||||
"height_range": 38.0,
|
||||
}
|
||||
if name == "periodicHills":
|
||||
# Route points are capsule centers: +0.72 m places the view at 1.60 m above ground.
|
||||
eye_camera = (center[0], center[1], center[2] + 0.72)
|
||||
eye_target = (center[0] - tx * 360.0, center[1] - ty * 360.0,
|
||||
center[2] + 0.72)
|
||||
wide_camera = (center[0] - tx * 36.0 - nx * 28.0,
|
||||
center[1] - ty * 36.0 - ny * 28.0, center[2] + 30.0)
|
||||
wide_target = (center[0] - tx * 310.0, center[1] - ty * 310.0,
|
||||
center[2] + 0.72)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 28),
|
||||
"height_center": base_height,
|
||||
"height_range": 6.0,
|
||||
}
|
||||
if name == "road-in-clouds":
|
||||
deck = stamp(layout,"liminal/road-clouds-v2/deck/000")
|
||||
path_um = deck.get("sweep_path_um", [])
|
||||
if len(path_um) < 2:
|
||||
raise RuntimeError("road layout has no resolved continuous swept path")
|
||||
points = [tuple(value / 1.0e6 for value in point) for point in path_um]
|
||||
half_thickness = deck["half_extents_um"][2] / 1.0e6
|
||||
top = lambda point: point[2] + half_thickness
|
||||
index = max(1, len(points) // 12)
|
||||
camera_dx = points[index + 1][0] - points[index - 1][0]
|
||||
camera_dy = points[index + 1][1] - points[index - 1][1]
|
||||
camera_tangent_length = math.hypot(camera_dx, camera_dy)
|
||||
camera_tx, camera_ty = (camera_dx / camera_tangent_length,
|
||||
camera_dy / camera_tangent_length)
|
||||
camera_nx, camera_ny = -camera_ty, camera_tx
|
||||
half_width = deck["half_extents_um"][1] / 1.0e6
|
||||
eye_target_index = min(len(points) - 1, index + 25)
|
||||
eye_camera = (points[index][0] + camera_nx * (half_width + 2.0),
|
||||
points[index][1] + camera_ny * (half_width + 2.0),
|
||||
top(points[index]) + 1.6)
|
||||
eye_target = (points[eye_target_index][0] + camera_nx * (half_width - 0.4),
|
||||
points[eye_target_index][1] + camera_ny * (half_width - 0.4),
|
||||
top(points[eye_target_index]) + 0.3)
|
||||
middle = len(points) // 2
|
||||
dx = points[middle + 1][0] - points[middle - 1][0]
|
||||
dy = points[middle + 1][1] - points[middle - 1][1]
|
||||
tangent_length = math.hypot(dx, dy)
|
||||
tx, ty = dx / tangent_length, dy / tangent_length
|
||||
nx, ny = -ty, tx
|
||||
wide_target_index = min(len(points) - 1, middle + 30)
|
||||
wide_camera = (points[middle][0] - tx * 35.0 + nx * 30.0,
|
||||
points[middle][1] - ty * 35.0 + ny * 30.0,
|
||||
top(points[middle]) + 25.0)
|
||||
wide_target = (points[wide_target_index][0], points[wide_target_index][1],
|
||||
top(points[wide_target_index]) - 5.0)
|
||||
cloud_camera = (points[middle][0] - tx * 18.0 + nx * 12.0,
|
||||
points[middle][1] - ty * 18.0 + ny * 12.0,
|
||||
base_height + 1.6)
|
||||
cloud_target = (points[middle][0] + tx * 8.0,
|
||||
points[middle][1] + ty * 8.0,
|
||||
points[middle][2] - half_thickness * 0.35)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 3),
|
||||
"wide": (wide_camera, wide_target, 3),
|
||||
"cloud-up": (cloud_camera, cloud_target, 3),
|
||||
"height_center": base_height + 23.0,
|
||||
"height_range": 60.0,
|
||||
"road_deck": top(points[middle]),
|
||||
"road_width": 2.0 * half_width,
|
||||
}
|
||||
if name == "pillar-grid":
|
||||
pillars = [row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id", "").startswith("liminal/horizon-pillar-grid-v1/pillar/")]
|
||||
if len(pillars) < 9:
|
||||
raise RuntimeError("pillar-grid layout has too few resolved supports")
|
||||
xs = [row["position_um"][0] / 1.0e6 for row in pillars]
|
||||
ys = [row["position_um"][1] / 1.0e6 for row in pillars]
|
||||
floor = min((row["position_um"][2] - row["half_extents_um"][2]) / 1.0e6
|
||||
for row in pillars)
|
||||
cx, cy = (min(xs) + max(xs)) * 0.5, (min(ys) + max(ys)) * 0.5
|
||||
eye_camera = (min(xs) - 78.0, cy, floor + 1.6)
|
||||
eye_target = (max(xs) + 90.0, cy, floor + 1.6)
|
||||
wide_camera = (cx - 280.0, cy - 420.0, floor + 230.0)
|
||||
wide_target = (cx, cy, floor + 6.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 3),
|
||||
"wide": (wide_camera, wide_target, 3),
|
||||
"height_center": floor + 12.0,
|
||||
"height_range": 42.0,
|
||||
}
|
||||
raise RuntimeError(f"no capture framing for fixture {name}")
|
||||
|
||||
|
||||
def headless_command(binary: Path, fixture: Path, camera, mesh: Path,
|
||||
manifest: Path, layout: Path, workers: int,
|
||||
clip_radius: int, *, hash_tiles: bool = False,
|
||||
audit_road: bool = False, max_level: int = 0,
|
||||
far_lods: bool = False,
|
||||
max_clip_override: int | None = None) -> list[object]:
|
||||
max_clip_level = (max_clip_override if max_clip_override is not None else
|
||||
8 if far_lods else max_level)
|
||||
command: list[object] = [binary, fixture, f"--workers={workers}",
|
||||
"--seed=0", "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={clip_radius}",
|
||||
f"--max-clip-level={max_clip_level}",
|
||||
f"--max-level={max_level}", "--render-distance-chunks=0",
|
||||
f"--camera={fmt(point_toward(camera, (camera[0], camera[1] + 1.0, camera[2])))}",
|
||||
f"--mesh-export={mesh}", f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
if hash_tiles:
|
||||
command.append("--stream-hash-tiles")
|
||||
if audit_road:
|
||||
command.append("--audit-liminal-road-walk")
|
||||
return command
|
||||
|
||||
|
||||
def determinism_digest(output: str, manifest_path: Path,
|
||||
layout_path: Path) -> tuple[str, str, str]:
|
||||
tile_hashes = sorted(line for line in output.splitlines()
|
||||
if line.startswith("[PipelineTileHash] "))
|
||||
if not manifest_path.is_file() or not layout_path.is_file() or not tile_hashes:
|
||||
raise RuntimeError("determinism run omitted manifest, layout or per-tile hashes")
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
manifest_digest = (manifest["content_hash"],manifest["recipe"]["content_hash"],
|
||||
manifest["plan"]["content_hash"])
|
||||
return ("|".join(manifest_digest),layout["stable_identity"],"\n".join(tile_hashes))
|
||||
|
||||
|
||||
def run_determinism(name: str, fixture: Path, view, *, binaries: dict[str, Path],
|
||||
out: Path, env: dict[str, str], max_level: int = 0,
|
||||
far_lods: bool = False, max_clip_override: int | None = None) -> None:
|
||||
camera, _, _ = view["eye"]
|
||||
clip_radius = 1
|
||||
max_clip = 0
|
||||
baseline = None
|
||||
cells = 0
|
||||
for compiler, workers in ((compiler, workers)
|
||||
for compiler in ("clang", "gcc")
|
||||
for workers in (1, 8, 12)):
|
||||
tag = f"{name}-{compiler}-w{workers}"
|
||||
run_dir = out / "determinism" / tag
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
mesh = run_dir / "feature.vfmesh"
|
||||
manifest = run_dir / "manifest.json"
|
||||
layout = run_dir / "layout.json"
|
||||
command = headless_command(binaries[compiler], fixture,camera,mesh,manifest,
|
||||
layout,workers,max_clip,hash_tiles=True,max_level=max_level,
|
||||
far_lods=far_lods,max_clip_override=max_clip_override)
|
||||
output = run_step(tag,command,out/"logs"/f"determinism-{tag}.log",
|
||||
env=env,stamped=True)
|
||||
digest = determinism_digest(output,manifest,layout)
|
||||
if baseline is None:
|
||||
baseline = digest
|
||||
elif digest != baseline:
|
||||
raise RuntimeError(f"{name} diverged at {compiler} W{workers}; see determinism logs")
|
||||
cells += 1
|
||||
print(f"[L143Determinism] fixture={name} compiler_worker_cells={cells} identical=yes status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def export_and_capture(name: str, fixture: Path, views: dict, *,
|
||||
headless: Path, renderer: Path, out: Path,
|
||||
artifact_dir: Path, env: dict[str, str]) -> None:
|
||||
view_names = ("eye", "wide", "cloud-up") if name == "road-in-clouds" else ("eye", "wide")
|
||||
for view_name in view_names:
|
||||
camera, target, clip_radius = views[view_name]
|
||||
clip_radius = min(clip_radius,3)
|
||||
stem = f"{name}-{view_name}"
|
||||
mesh = out / "capture-meshes" / f"{stem}.vfmesh"
|
||||
manifest = out / "capture-manifests" / f"{stem}.json"
|
||||
layout = out / "capture-layouts" / f"{stem}.json"
|
||||
png = out / "capture-png" / f"{stem}.png"
|
||||
image = artifact_dir / f"{stem}.jpg"
|
||||
for path in (mesh,manifest,layout,png,image):
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
command = headless_command(headless,fixture,camera,mesh,manifest,layout,12,
|
||||
clip_radius,max_level=-1,far_lods=True,
|
||||
max_clip_override=2 if name == "road-in-clouds" else None)
|
||||
output = run_step(f"capture-{stem}-export",command,
|
||||
out/"logs"/f"capture-{stem}-export.log",env=env,stamped=True)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{stem} produced an empty mesh")
|
||||
yaw, pitch = camera_angles(camera,target)
|
||||
height_center = views["height_center"]
|
||||
height_range = views["height_range"]
|
||||
if name == "road-in-clouds":
|
||||
fog_start,fog_end,fog_color=(24,260,"0.88,0.93,0.95")
|
||||
elif name == "pillar-grid":
|
||||
fog_start,fog_end,fog_color=220,1250,"0.82,0.88,0.91"
|
||||
elif name == "periodicHills":
|
||||
fog_start,fog_end,fog_color=210,1000,"0.78,0.84,0.88"
|
||||
else:
|
||||
fog_start,fog_end,fog_color=30,105,FOG_COLOR
|
||||
renderer_command = [renderer,mesh,png,"--size=1600x900","--fov-deg=72",
|
||||
f"--camera-m={fmt(camera)}",f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}","--tile-skirts=on","--shade=readable",
|
||||
"--headlamp=off","--height-tint=on",f"--height-center-m={height_center:.6f}",
|
||||
f"--height-range-m={height_range:.6f}","--light-direction=-0.42,-0.58,0.70",
|
||||
"--fog=on",f"--fog-start-m={fog_start}",f"--fog-end-m={fog_end}",
|
||||
f"--fog-color={fog_color}"]
|
||||
run_step(f"capture-{stem}-render",renderer_command,
|
||||
out/"logs"/f"capture-{stem}-render.log",env=env,stamped=True)
|
||||
run_step(f"capture-{stem}-jpeg",["ffmpeg","-nostdin","-hide_banner","-loglevel",
|
||||
"error","-y","-i",png,"-frames:v","1","-q:v","4",image],
|
||||
out/"logs"/f"capture-{stem}-jpeg.log",env=env)
|
||||
quality = run_step(f"capture-{stem}-quality",
|
||||
[sys.executable,ROOT/"Standalone/Tools/verify/capture_quality.py",image],
|
||||
out/"logs"/f"capture-{stem}-quality.log",env=env)
|
||||
if "[CaptureQuality]" not in quality or not image.is_file() \
|
||||
or image.stat().st_size > 300_000:
|
||||
raise RuntimeError(f"{stem} capture failed quality or size checks")
|
||||
eye_height = "1.600m" if view_name in ("eye", "cloud-up") else "wide"
|
||||
print(f"[L143Capture] fixture={name} view={view_name} eye_height={eye_height} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(target)}) bytes={image.stat().st_size} "
|
||||
f"sha256={sha256(image)} status=PASS",flush=True)
|
||||
|
||||
|
||||
def run_road_walk_audit(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-walk-audit"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-walk-audit",headless_command(headless,fixture,camera,
|
||||
run_dir/"audit.vfmesh",run_dir/"manifest.json",run_dir/"layout.json",12,1,
|
||||
audit_road=True),out/"logs"/"road-walk-audit.log",env=env,stamped=True)
|
||||
if not re.search(r"\[LiminalRoadWalkAudit\].*status=PASS",output):
|
||||
raise RuntimeError("raised road failed the Core capsule walk audit")
|
||||
|
||||
|
||||
def run_road_short_negative_control(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-short-negative-control"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-short-negative-control",
|
||||
headless_command(headless,fixture,camera,run_dir/"control.vfmesh",
|
||||
run_dir/"manifest.json",run_dir/"layout.json",12,1,audit_road=True),
|
||||
out/"logs"/"road-short-negative-control.log",env=env,stamped=True,
|
||||
expected_exit=14)
|
||||
match = re.search(r"\[LiminalRoadWalkAudit\].*path_length_m=([0-9.]+).*status=FAIL",output)
|
||||
if not match or float(match.group(1)) >= 300.0:
|
||||
raise RuntimeError("short-road negative control did not falsify the length gate")
|
||||
print(f"[L143RoadNegativeControl] path_length_m={match.group(1)} "
|
||||
"expected=FAIL status=PASS",flush=True)
|
||||
|
||||
|
||||
def retain_unchanged_captures(artifact_dir: Path) -> None:
|
||||
reference_dir = Path(__file__).resolve().parents[1] / "Linux142" / "captures"
|
||||
for name in ("flatField-monolith", "periodicHills"):
|
||||
for view in ("eye", "wide"):
|
||||
filename = f"{name}-{view}.jpg"
|
||||
source, destination = reference_dir / filename, artifact_dir / filename
|
||||
if not source.is_file():
|
||||
raise RuntimeError(f"missing L142 reference capture {source}")
|
||||
shutil.copy2(source,destination)
|
||||
if sha256(source) != sha256(destination):
|
||||
raise RuntimeError(f"unchanged reference capture copy differs: {filename}")
|
||||
print(f"[L143Capture] fixture={name} view={view} retained_from=Linux142 "
|
||||
f"sha256={sha256(destination)} status=PASS",flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT)
|
||||
parser.add_argument("--artifact-dir", type=Path, default=CAPTURE_DEFAULT)
|
||||
parser.add_argument("--headless-clang", type=Path, required=True)
|
||||
parser.add_argument("--headless-gcc", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
out = args.out.resolve()
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
fixture_dir = out / "fixtures"
|
||||
for directory in (out,fixture_dir,artifact_dir):
|
||||
directory.mkdir(parents=True,exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(out)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT/"Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(out/"xdg-cache")
|
||||
run_step("build-fixtures",[sys.executable,
|
||||
ROOT/"Standalone/Research/WorldAssemblerM3/Linux143/BuildL143Fixtures.py",
|
||||
f"--out={fixture_dir}"],out/"logs"/"build-fixtures.log",env=env)
|
||||
|
||||
binaries = {"clang":args.headless_clang.resolve(),"gcc":args.headless_gcc.resolve()}
|
||||
fixtures = {
|
||||
"flatField-monolith":fixture_dir/"liminal-flatField-monolith.json",
|
||||
"periodicHills":fixture_dir/"liminal-periodicHills.json",
|
||||
"road-in-clouds":fixture_dir/"liminal-road-in-clouds.json",
|
||||
"pillar-grid":fixture_dir/"liminal-pillar-grid.json",
|
||||
}
|
||||
probed = {}
|
||||
for name,fixture in fixtures.items():
|
||||
probe_dir=out/"plan-probes"/name
|
||||
probe_dir.mkdir(parents=True,exist_ok=True)
|
||||
camera=(0.0,0.0,-500.0)
|
||||
output=run_step(f"plan-probe-{name}",headless_command(binaries["clang"],fixture,
|
||||
camera,probe_dir/"probe.vfmesh",probe_dir/"manifest.json",
|
||||
probe_dir/"layout.json",12,1),out/"logs"/f"plan-probe-{name}.log",
|
||||
env=env,stamped=True)
|
||||
if "[WorldAssemblerFullManifest]" not in output:
|
||||
raise RuntimeError(f"{name} probe omitted its full plan manifest")
|
||||
probed[name]=make_views(name,probe_dir/"manifest.json",probe_dir/"layout.json")
|
||||
|
||||
for name,fixture in fixtures.items():
|
||||
run_determinism(name,fixture,probed[name],binaries=binaries,out=out,env=env)
|
||||
run_determinism("pillar-grid-far-lod",fixtures["pillar-grid"],probed["pillar-grid"],
|
||||
binaries=binaries,out=out,env=env,max_level=-1,far_lods=True,max_clip_override=8)
|
||||
run_road_walk_audit(fixtures["road-in-clouds"],probed["road-in-clouds"],
|
||||
headless=binaries["clang"],out=out,env=env)
|
||||
run_road_short_negative_control(fixture_dir/"liminal-road-short-control.json",
|
||||
probed["road-in-clouds"],headless=binaries["clang"],out=out,env=env)
|
||||
retain_unchanged_captures(artifact_dir)
|
||||
for name in ("road-in-clouds", "pillar-grid"):
|
||||
fixture=fixtures[name]
|
||||
export_and_capture(name,fixture,probed[name],headless=binaries["clang"],
|
||||
renderer=args.renderer.resolve(),out=out,artifact_dir=artifact_dir,env=env)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L143Capture] status=FAIL error={error}",file=sys.stderr,flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,64 @@
|
||||
# L143 — liminal polish
|
||||
|
||||
## Captures
|
||||
|
||||
### Road
|
||||
|
||||
Eye-height:
|
||||
|
||||

|
||||
|
||||
Wide:
|
||||
|
||||

|
||||
|
||||
Looking up from the cloud floor:
|
||||
|
||||

|
||||
|
||||
### Pillar grid
|
||||
|
||||
Eye-height:
|
||||
|
||||

|
||||
|
||||
Wide:
|
||||
|
||||

|
||||
|
||||
### Kept from L142
|
||||
|
||||
These four captures are byte-identical to L142.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Result
|
||||
|
||||
The road is one continuous swept deck, and the distant pillar silhouettes remain complete and consistent. The monolith and periodic hills were retained unchanged.
|
||||
|
||||
## Changes and causes
|
||||
|
||||
- The road edge broke where independently capped deck segments overlapped. The road now resolves to one swept-deck stamp with a shared cubic-Hermite centerline and continuous cross-section; the centerline is sampled at 1 m spacing (361 points). The 8 m deck is 376.829 m long and retains 17 support posts. The sweep path is serialized in stamp-vocabulary V5.
|
||||
- Pillars disappeared or narrowed when the coarse marching-cubes lattice skipped their cross-section. The pillar stamp's interval proof is unsupported, so it conservatively kept tiles but did not protect the mesh from undersampling. At LOD4 the sample spacing is 32 voxels (8 m), already greater than the 5.5 m pillar radius; spacing rises to 64 and 128 voxels at LOD5 and LOD6. The shared meshing path now emits deterministic 24-sided capped-cylinder proxies for regular `fillPillar` stamps once the sample step exceeds their radius or half-height. The same rule applies outside the liminal fixture. The radial approximation error for this fixture is about 4.7 cm, below one 25 cm voxel.
|
||||
|
||||
## Measurements
|
||||
|
||||
- Road walk audit covered the full 376.829 m route at 1,081 surface stations, with 68,103 capsule samples and 7,096 support samples: 0 blocked, 0 unsupported, 0 grade failures, and 0 surface misses. Measured maximum grade was 8.512° against a 15° limit; deck width was 8 m against a 3 m minimum. `road-short-negative-control` failed the walk audit as expected at 210.351 m.
|
||||
- Determinism covered five fixtures—monolith, periodic hills, road, pillar grid, and far-LOD pillar grid—across Clang/GCC and W1/W8/W12 (six compiler/worker cells per fixture). All five reported identical outputs.
|
||||
- Clang and GCC builds passed on source stamp `1baa3a711f7f96df2e70fc93579f41ad0b0211eb42fcb51dabf522e33d888877`. The capture runner and capture-quality checks passed for all requested views.
|
||||
- Fast tier passed in 292.3 s. Its pinned L130-v227 reach-unit owner hashes match exactly: 12/12 rows for each owner fixture, with W1 and W12 identical. Historical L46/L53 hash comparisons remain diagnostic-only and are not the current owner-hash gate.
|
||||
|
||||
## Checks and limits
|
||||
|
||||
- Passed: road audit and short-road negative control; five-fixture Clang/GCC determinism; Clang and GCC builds; capture quality; fast tier; `git diff --check`.
|
||||
- Not run: Unreal build (no Unreal `Source/` files changed) and L80 performance timing (this round made no performance claim).
|
||||
- The coarse proxy is limited to regular `fillPillar` stamps; irregular pillars and other stamp kinds continue through their existing SDF path. The proxy was exercised at LOD4–6 in the pillar-grid captures.
|
||||
- Unreal `Source/` files changed: none. Changed standalone source files: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/src/ShapeWorldInternal.h`, `Standalone/Core/src/VoxelForgeCore.cpp`, `Standalone/Source/StreamBenchmarkHeadless.cpp`, `Standalone/Source/StreamBenchmarkPipeline.h`, and `Standalone/Source/WorldAssemblyPrototype.h`.
|
||||
|
||||
Detailed logs are in `Saved/Linux/l143/`; the capture and audit transcript is `Saved/Linux/l143/logs/capture-runner.log`, the walk audit is `Saved/Linux/l143/logs/road-walk-audit.log`, and the fast-tier summary is `Saved/Linux/l143/fast-summary.json`.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -1935,15 +1935,24 @@ int main(int argc,char** argv)
|
||||
std::sort(deck.begin(),deck.end(),[](const auto* a,const auto* b)
|
||||
{return a->stableId<b->stableId;});
|
||||
double pathLength=0.0,maxGradeDegrees=0.0;
|
||||
for(std::size_t i=0;i<deck.size();++i)
|
||||
std::vector<std::array<std::int64_t,3>> centerline;
|
||||
if(deck.size()==1&&!deck.front()->sweepPathMicrometres.empty())
|
||||
centerline=deck.front()->sweepPathMicrometres;
|
||||
else
|
||||
{
|
||||
const auto* segment=deck[i];
|
||||
const double dx=(segment->sweepEndpointsMicrometres[1][0]
|
||||
-segment->sweepEndpointsMicrometres[0][0])/1.0e6;
|
||||
const double dy=(segment->sweepEndpointsMicrometres[1][1]
|
||||
-segment->sweepEndpointsMicrometres[0][1])/1.0e6;
|
||||
const double dz=std::abs(segment->sweepEndpointsMicrometres[1][2]
|
||||
-segment->sweepEndpointsMicrometres[0][2])/1.0e6;
|
||||
for(std::size_t i=0;i<deck.size();++i)
|
||||
{
|
||||
const auto* segment=deck[i];
|
||||
if(i==0)centerline.push_back(segment->sweepEndpointsMicrometres[0]);
|
||||
centerline.push_back(segment->sweepEndpointsMicrometres[1]);
|
||||
}
|
||||
}
|
||||
for(std::size_t i=1;i<centerline.size();++i)
|
||||
{
|
||||
const auto& a=centerline[i-1];const auto& b=centerline[i];
|
||||
const double dx=(b[0]-a[0])/1.0e6;
|
||||
const double dy=(b[1]-a[1])/1.0e6;
|
||||
const double dz=std::abs(b[2]-a[2])/1.0e6;
|
||||
const double horizontal=std::hypot(dx,dy);
|
||||
pathLength+=horizontal;
|
||||
maxGradeDegrees=std::max(maxGradeDegrees,
|
||||
@@ -1952,14 +1961,16 @@ int main(int argc,char** argv)
|
||||
const double width=deck.empty()?0.0:
|
||||
2.0*deck.front()->halfExtentsMicrometres[1]/1.0e6;
|
||||
std::printf("[LiminalRoadResolved] vocabulary=%u id=liminal/road-clouds-v2 "
|
||||
"strate=%d shape=swept_deck_segments deck_segments=%zu supports=%zu "
|
||||
"strate=%d shape=continuous_swept_deck deck_stamps=%zu centerline_points=%zu supports=%zu "
|
||||
"path_length_m=%.3f width_m=%.3f grade_max_deg=%.3f "
|
||||
"placement=free route_keepout=PASS band_clearance=PASS status=%s\n",
|
||||
resolvedLayout.stampVocabularyVersion,
|
||||
deck.empty()?-1:deck.front()->strateIndex,deck.size(),supportCount,pathLength,
|
||||
width,maxGradeDegrees,deck.size()>1&&pathLength>=300.0&&width>=3.0
|
||||
deck.empty()?-1:deck.front()->strateIndex,deck.size(),centerline.size(),
|
||||
supportCount,pathLength,width,maxGradeDegrees,
|
||||
deck.size()==1&¢erline.size()>=2&&pathLength>=300.0&&width>=3.0
|
||||
&&maxGradeDegrees<=15.0?"PASS":"FAIL");
|
||||
if(deck.size()<2||pathLength<80.0||width<3.0||maxGradeDegrees>15.0)
|
||||
if(deck.size()!=1||centerline.size()<2||pathLength<80.0
|
||||
||width<3.0||maxGradeDegrees>15.0)
|
||||
{std::fprintf(stderr,"Liminal road resolved outside the walkable road bounds\n");return 2;}
|
||||
}
|
||||
if(hasLiminalPillarGrid)
|
||||
|
||||
@@ -2981,28 +2981,48 @@ namespace StandaloneStreamBenchmark
|
||||
constexpr double HalfCapsuleHeightMetres=CapsuleHeightMetres*0.5;
|
||||
constexpr double SlopeCapsuleClearanceMetres=0.12;
|
||||
std::vector<std::array<double,3>> roadPath;
|
||||
roadPath.reserve(deck.size()*4);
|
||||
std::vector<std::array<double,3>> centerline;
|
||||
centerline.reserve(deck.size()+1);
|
||||
bool surfacePathValid=true;
|
||||
for(std::size_t i=0;i<deck.size();++i)
|
||||
const bool continuousDeck=deck.size()==1
|
||||
&&deck.front()->kind
|
||||
==VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&deck.front()->sweepPathMicrometres.size()>=2;
|
||||
bool surfacePathValid=continuousDeck;
|
||||
if(continuousDeck)
|
||||
{
|
||||
const auto* segment=deck[i];
|
||||
if(segment->kind!=VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
surfacePathValid=false;
|
||||
const auto point=[&](std::size_t endpoint)
|
||||
{return std::array<double,3>{
|
||||
segment->sweepEndpointsMicrometres[endpoint][0]/1.0e6,
|
||||
segment->sweepEndpointsMicrometres[endpoint][1]/1.0e6,
|
||||
segment->sweepEndpointsMicrometres[endpoint][2]/1.0e6};};
|
||||
const auto a=point(0),b=point(1);
|
||||
if(i==0)centerline.push_back(a);
|
||||
else if(std::abs(centerline.back()[0]-a[0])>1.0e-6
|
||||
||std::abs(centerline.back()[1]-a[1])>1.0e-6
|
||||
||std::abs(centerline.back()[2]-a[2])>1.0e-6)
|
||||
surfacePathValid=false;
|
||||
centerline.push_back(b);
|
||||
centerline.reserve(deck.front()->sweepPathMicrometres.size());
|
||||
for(const auto& point:deck.front()->sweepPathMicrometres)
|
||||
centerline.push_back({point[0]/1.0e6,point[1]/1.0e6,point[2]/1.0e6});
|
||||
surfacePathValid=surfacePathValid
|
||||
&¢erline.front()[0]
|
||||
==deck.front()->sweepEndpointsMicrometres[0][0]/1.0e6
|
||||
&¢erline.back()[0]
|
||||
==deck.front()->sweepEndpointsMicrometres[1][0]/1.0e6;
|
||||
}
|
||||
else
|
||||
{
|
||||
centerline.reserve(deck.size()+1);
|
||||
surfacePathValid=true;
|
||||
for(std::size_t i=0;i<deck.size();++i)
|
||||
{
|
||||
const auto* segment=deck[i];
|
||||
if(segment->kind
|
||||
!=VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
surfacePathValid=false;
|
||||
const auto point=[&](std::size_t endpoint)
|
||||
{return std::array<double,3>{
|
||||
segment->sweepEndpointsMicrometres[endpoint][0]/1.0e6,
|
||||
segment->sweepEndpointsMicrometres[endpoint][1]/1.0e6,
|
||||
segment->sweepEndpointsMicrometres[endpoint][2]/1.0e6};};
|
||||
const auto a=point(0),b=point(1);
|
||||
if(i==0)centerline.push_back(a);
|
||||
else if(std::abs(centerline.back()[0]-a[0])>1.0e-6
|
||||
||std::abs(centerline.back()[1]-a[1])>1.0e-6
|
||||
||std::abs(centerline.back()[2]-a[2])>1.0e-6)
|
||||
surfacePathValid=false;
|
||||
centerline.push_back(b);
|
||||
}
|
||||
}
|
||||
roadPath.reserve(centerline.size()*4);
|
||||
double pathLengthMetres=0.0,maxGradeDegrees=0.0;
|
||||
std::uint64_t surfaceProbes=0,surfaceMisses=0;
|
||||
const auto surfaceTopAt=[&](double xMetres,double yMetres,
|
||||
@@ -3010,11 +3030,18 @@ namespace StandaloneStreamBenchmark
|
||||
{
|
||||
++surfaceProbes;
|
||||
double low=nominalTop-0.25,high=nominalTop+1.0;
|
||||
if(Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres,
|
||||
low/coreMetres})>=0.0f
|
||||
||Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres,
|
||||
high/coreMetres})<0.0f)
|
||||
{++surfaceMisses;return false;}
|
||||
const float lowDensity=Shape.densityAt({xMetres/coreMetres,
|
||||
yMetres/coreMetres,low/coreMetres});
|
||||
const float highDensity=Shape.densityAt({xMetres/coreMetres,
|
||||
yMetres/coreMetres,high/coreMetres});
|
||||
if(lowDensity>=0.0f||highDensity<0.0f)
|
||||
{
|
||||
++surfaceMisses;
|
||||
std::printf("[LiminalRoadSurfaceProbeMiss] x_m=%.6f y_m=%.6f "
|
||||
"nominal_top_m=%.6f low_density=%.6f high_density=%.6f\n",
|
||||
xMetres,yMetres,nominalTop,lowDensity,highDensity);
|
||||
return false;
|
||||
}
|
||||
for(int iteration=0;iteration<20;++iteration)
|
||||
{
|
||||
const double middle=(low+high)*0.5;
|
||||
@@ -3059,8 +3086,7 @@ namespace StandaloneStreamBenchmark
|
||||
const FWalkAudit roadAudit=auditPlayerPath(Shape,roadPath,
|
||||
0.125,0.125,1.125,3,0.45);
|
||||
const double widthMetres=2.0*deck.front()->halfExtentsMicrometres[1]/1.0e6;
|
||||
passed=std::all_of(deck.begin(),deck.end(),[](const auto* road)
|
||||
{return road->kind==VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment;})
|
||||
passed=continuousDeck
|
||||
&&pathLengthMetres>=300.0&&widthMetres>=3.0
|
||||
&&maxGradeDegrees<=15.0&&surfacePathValid&&roadAudit.passed;
|
||||
std::printf("[LiminalRoadWalkAudit] id=liminal/road-clouds-v2 "
|
||||
|
||||
@@ -707,26 +707,16 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
requests.push_back(std::move(request));
|
||||
};
|
||||
const std::int64_t deckSmoothUm=250000;
|
||||
const std::int64_t segmentOverlapUm=350000;
|
||||
for(std::uint32_t i=1;i<path.size();++i)
|
||||
{
|
||||
const auto& a=path[i-1];const auto& b=path[i];
|
||||
const double dx=static_cast<double>(b[0]-a[0]);
|
||||
const double dy=static_cast<double>(b[1]-a[1]);
|
||||
const double dz=static_cast<double>(b[2]-a[2]);
|
||||
const std::int64_t halfLengthUm=static_cast<std::int64_t>(std::ceil(
|
||||
std::sqrt(dx*dx+dy*dy+dz*dz)*0.5))+segmentOverlapUm;
|
||||
ResolvedLayoutStampRequestV1 request;
|
||||
request.stableId=*id+"/deck/"+std::to_string(10000+i-1).substr(1);
|
||||
request.strateIndex=strateIndex;
|
||||
request.kind=ResolvedSdfStampKindV1::fillSweptDeckSegment;
|
||||
request.anchorPositionMicrometres={
|
||||
(a[0]+b[0])/2,(a[1]+b[1])/2,(a[2]+b[2])/2};
|
||||
request.sweepEndpointsMicrometres={a,b};
|
||||
request.halfExtentsMicrometres={halfLengthUm,halfWidthUm,halfThicknessUm};
|
||||
request.smoothWidthMicrometres=deckSmoothUm;
|
||||
requests.push_back(std::move(request));
|
||||
}
|
||||
ResolvedLayoutStampRequestV1 deck;
|
||||
deck.stableId=*id+"/deck/000";
|
||||
deck.strateIndex=strateIndex;
|
||||
deck.kind=ResolvedSdfStampKindV1::fillSweptDeckSegment;
|
||||
deck.anchorPositionMicrometres=path[path.size()/2];
|
||||
deck.sweepEndpointsMicrometres={path.front(),path.back()};
|
||||
deck.sweepPathMicrometres=path;
|
||||
deck.halfExtentsMicrometres={500000,halfWidthUm,halfThicknessUm};
|
||||
deck.smoothWidthMicrometres=deckSmoothUm;
|
||||
requests.push_back(std::move(deck));
|
||||
const std::int64_t supportBaseUm=fieldFloorMicrometres-2000000;
|
||||
const std::int64_t supportRadiusUm=metresToMicrometres(supportRadius);
|
||||
double distanceSinceSupport=supportSpacing;
|
||||
|
||||