wip(assembler): round L32 -- smooth Gradient interface at full amplitude (no rings, not flat)
Band faces now use signed distance to the displaced solid intervals, continuous across bands, so MC interpolates the interface instead of terracing at density levels. The Gradient amplitude is restored to width/8 (about 4 m peak-to-peak at 16 m). Ring/terrace audit at LOD0-8 on the FI floor and SW ceiling passes; negative controls: L30 (rings) and L31 (flat) both fail. Hole/interface audits, 16 passage rows and 24/24 determinism pass. Evaluator 20. Cost vs L31 SW +1.9%, FI +1.5%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
No files matched your search
@@ -250,9 +250,8 @@ namespace VoxelForgeCore
|
|||||||
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
||||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
||||||
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces;
|
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces;
|
||||||
// version 18 limits FloatingIslands Gradient face displacement to one voxel;
|
// version 20 restores full Gradient amplitude and uses a continuous signed-distance band union.
|
||||||
// version 19 reduces it to one quarter voxel to remove the residual floor contour.
|
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 20;
|
||||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 19;
|
|
||||||
|
|
||||||
enum class FieldChannel : std::uint32_t
|
enum class FieldChannel : std::uint32_t
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1217,6 +1217,82 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
|||||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill only underside cells that marching cubes missed. Active transitions use the
|
||||||
|
// same displaced face and passage-mouth fade as the signed-distance field.
|
||||||
|
struct FTransitionFaceReplacement
|
||||||
|
{
|
||||||
|
double BaseZVoxels=0.0;
|
||||||
|
double MaximumDisplacementVoxels=0.0;
|
||||||
|
std::vector<uint8> Cells;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline int32 RemoveCoarseTransitionFaceTriangles(FVoxelMeshData& Mesh,
|
||||||
|
const FIntVector& Origin,int32 Step,int32 GridSide,double VoxelSizeCm,
|
||||||
|
const std::vector<FTransitionFaceReplacement>& Replacements,bool bUpward)
|
||||||
|
{
|
||||||
|
if(Step<=0||GridSide<2||VoxelSizeCm<=0.0||Replacements.empty())return 0;
|
||||||
|
const int32 TriangleCount=Mesh.Triangles.Num()/3;
|
||||||
|
const int32 OldCeilingStart=std::max(0,TriangleCount-Mesh.NumCeilingTriangles);
|
||||||
|
TArray<uint8> OldMasks=Mesh.TriangleSkirtFaceMasks;
|
||||||
|
OldMasks.SetNum(TriangleCount);
|
||||||
|
TArray<int32> KeptTriangles;
|
||||||
|
TArray<uint8> KeptMasks;
|
||||||
|
KeptTriangles.Reserve(Mesh.Triangles.Num());
|
||||||
|
KeptMasks.Reserve(TriangleCount);
|
||||||
|
int32 Removed=0,RemovedCeiling=0;
|
||||||
|
const int32 CellSide=GridSide-1;
|
||||||
|
for(int32 Triangle=0;Triangle<TriangleCount;++Triangle)
|
||||||
|
{
|
||||||
|
const int32 IA=Mesh.Triangles[Triangle*3];
|
||||||
|
const int32 IB=Mesh.Triangles[Triangle*3+1];
|
||||||
|
const int32 IC=Mesh.Triangles[Triangle*3+2];
|
||||||
|
bool bRemove=false;
|
||||||
|
if(OldMasks[Triangle]==0&&IA>=0&&IB>=0&&IC>=0
|
||||||
|
&&IA<Mesh.Vertices.Num()&&IB<Mesh.Vertices.Num()&&IC<Mesh.Vertices.Num())
|
||||||
|
{
|
||||||
|
const FVector& A=Mesh.Vertices[IA];
|
||||||
|
const FVector& B=Mesh.Vertices[IB];
|
||||||
|
const FVector& C=Mesh.Vertices[IC];
|
||||||
|
const FVector GeometricNormal=FVector::CrossProduct(B-A,C-A);
|
||||||
|
const double NormalLength=GeometricNormal.Size();
|
||||||
|
const double Facing=NormalLength>1.0e-12?GeometricNormal.Z/NormalLength:0.0;
|
||||||
|
if((bUpward&&Facing>0.35)||(!bUpward&&Facing< -0.35))
|
||||||
|
{
|
||||||
|
const double X=(A.X+B.X+C.X)/(3.0*VoxelSizeCm);
|
||||||
|
const double Y=(A.Y+B.Y+C.Y)/(3.0*VoxelSizeCm);
|
||||||
|
const double Z=(A.Z+B.Z+C.Z)/(3.0*VoxelSizeCm);
|
||||||
|
const int32 CellX=static_cast<int32>(std::floor((X-Origin.X)/Step));
|
||||||
|
const int32 CellY=static_cast<int32>(std::floor((Y-Origin.Y)/Step));
|
||||||
|
if(CellX>=0&&CellX<CellSide&&CellY>=0&&CellY<CellSide)
|
||||||
|
{
|
||||||
|
const std::size_t Cell=static_cast<std::size_t>(CellY)*CellSide+CellX;
|
||||||
|
for(const auto& Replacement:Replacements)
|
||||||
|
{
|
||||||
|
if(Cell>=Replacement.Cells.size()||!Replacement.Cells[Cell])continue;
|
||||||
|
const double Margin=Replacement.MaximumDisplacementVoxels+4.0;
|
||||||
|
if(Z>=Replacement.BaseZVoxels-Margin
|
||||||
|
&&Z<=Replacement.BaseZVoxels+Margin)
|
||||||
|
{bRemove=true;break;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(bRemove)
|
||||||
|
{
|
||||||
|
++Removed;
|
||||||
|
if(Triangle>=OldCeilingStart)++RemovedCeiling;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
KeptTriangles.Add(IA);KeptTriangles.Add(IB);KeptTriangles.Add(IC);
|
||||||
|
KeptMasks.Add(OldMasks[Triangle]);
|
||||||
|
}
|
||||||
|
if(Removed==0)return 0;
|
||||||
|
Mesh.Triangles=std::move(KeptTriangles);
|
||||||
|
Mesh.TriangleSkirtFaceMasks=std::move(KeptMasks);
|
||||||
|
Mesh.NumCeilingTriangles=std::max(0,Mesh.NumCeilingTriangles-RemovedCeiling);
|
||||||
|
return Removed;
|
||||||
|
}
|
||||||
|
|
||||||
inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
||||||
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||||
{
|
{
|
||||||
@@ -1230,9 +1306,14 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
{
|
{
|
||||||
double FaceZ=0.0;
|
double FaceZ=0.0;
|
||||||
double Thickness=0.0;
|
double Thickness=0.0;
|
||||||
|
double MaximumDisplacementVoxels=0.0;
|
||||||
|
std::vector<double> FaceHeights;
|
||||||
std::vector<uint8> SolidAbove;
|
std::vector<uint8> SolidAbove;
|
||||||
|
const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr;
|
||||||
|
bool bTransition=false;
|
||||||
};
|
};
|
||||||
std::vector<FCandidate> Candidates;
|
std::vector<FCandidate> Candidates;
|
||||||
|
std::vector<FTransitionFaceReplacement> Replacements;
|
||||||
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
||||||
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
||||||
{
|
{
|
||||||
@@ -1243,19 +1324,34 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
Candidate.FaceZ=static_cast<double>(Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
Candidate.FaceZ=static_cast<double>(Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
||||||
Candidate.Thickness=static_cast<double>(Stratum->ceiling.topMicrometres
|
Candidate.Thickness=static_cast<double>(Stratum->ceiling.topMicrometres
|
||||||
-Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
-Stratum->ceiling.bottomMicrometres)/CoreMicrons;
|
||||||
if(Candidate.FaceZ<Origin.Z-1.0e-6||Candidate.FaceZ>Origin.Z+Extent+1.0e-6)continue;
|
Candidate.Stratum=Stratum;
|
||||||
|
const auto* transitionUpper=Shape.FindResolvedLayoutStratumByOrder(Stratum->order-1);
|
||||||
|
const bool bTransition=transitionUpper
|
||||||
|
&&Shape.IsResolvedLayoutTransitionActive(*transitionUpper);
|
||||||
|
const double displacement=bTransition?static_cast<double>(
|
||||||
|
Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(*transitionUpper,*Stratum))
|
||||||
|
/CoreMicrons:0.0;
|
||||||
|
if(Candidate.FaceZ<Origin.Z-displacement-1.0e-6
|
||||||
|
||Candidate.FaceZ>Origin.Z+Extent+displacement+1.0e-6)continue;
|
||||||
|
Candidate.bTransition=bTransition;
|
||||||
|
Candidate.MaximumDisplacementVoxels=displacement;
|
||||||
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
||||||
if(Epsilon<=0.0)continue;
|
if(Epsilon<=0.0)continue;
|
||||||
Candidate.SolidAbove.resize(static_cast<std::size_t>(GridSide)*GridSide,0);
|
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||||
|
Candidate.FaceHeights.resize(GridCount,Candidate.FaceZ);
|
||||||
|
Candidate.SolidAbove.resize(GridCount,0);
|
||||||
bool AnySolidAbove=false;
|
bool AnySolidAbove=false;
|
||||||
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
||||||
{
|
{
|
||||||
const double WorldX=Origin.X+X*GridStep;
|
const double WorldX=Origin.X+X*GridStep;
|
||||||
const double WorldY=Origin.Y+Y*GridStep;
|
const double WorldY=Origin.Y+Y*GridStep;
|
||||||
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
|
Candidate.bTransition=Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,
|
||||||
|
WorldX,WorldY,Candidate.FaceHeights[GridIndex])||Candidate.bTransition;
|
||||||
const float Above=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
const float Above=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
||||||
static_cast<float>(Candidate.FaceZ+Epsilon));
|
static_cast<float>(Candidate.FaceHeights[GridIndex]+Epsilon));
|
||||||
const bool bSolidAbove=std::isfinite(Above)&&Above<0.0f;
|
const bool bSolidAbove=std::isfinite(Above)&&Above<0.0f;
|
||||||
Candidate.SolidAbove[static_cast<std::size_t>(Y*GridSide+X)]=bSolidAbove?1:0;
|
Candidate.SolidAbove[GridIndex]=bSolidAbove?1:0;
|
||||||
AnySolidAbove=AnySolidAbove||bSolidAbove;
|
AnySolidAbove=AnySolidAbove||bSolidAbove;
|
||||||
}
|
}
|
||||||
if(AnySolidAbove)Candidates.push_back(std::move(Candidate));
|
if(AnySolidAbove)Candidates.push_back(std::move(Candidate));
|
||||||
@@ -1277,8 +1373,19 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
std::vector<int32> NewTriangles;
|
std::vector<int32> NewTriangles;
|
||||||
for(const FCandidate& Candidate:Candidates)
|
for(const FCandidate& Candidate:Candidates)
|
||||||
{
|
{
|
||||||
std::vector<int32> VertexMap(static_cast<std::size_t>(GridSide)*GridSide,INDEX_NONE);
|
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||||
const double CeilingZ=Candidate.FaceZ;
|
std::vector<int32> VertexMap(GridCount,INDEX_NONE);
|
||||||
|
FTransitionFaceReplacement Replacement;
|
||||||
|
Replacement.BaseZVoxels=Candidate.FaceZ;
|
||||||
|
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
|
||||||
|
Replacement.Cells.resize(static_cast<std::size_t>(GridSide-1)*(GridSide-1),0);
|
||||||
|
const auto heightAt=[&](double X,double Y)
|
||||||
|
{
|
||||||
|
if(!Candidate.bTransition)return Candidate.FaceZ;
|
||||||
|
double height=Candidate.FaceZ;
|
||||||
|
Shape.ResolvedCeilingBottomAt(*Candidate.Stratum,X,Y,height);
|
||||||
|
return height;
|
||||||
|
};
|
||||||
const auto AddVertex=[&](int32 X,int32 Y)
|
const auto AddVertex=[&](int32 X,int32 Y)
|
||||||
{
|
{
|
||||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
@@ -1286,8 +1393,20 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
if(Mapped!=INDEX_NONE)return Mapped;
|
if(Mapped!=INDEX_NONE)return Mapped;
|
||||||
Mapped=static_cast<int32>(NewVertices.size());
|
Mapped=static_cast<int32>(NewVertices.size());
|
||||||
const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep;
|
const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep;
|
||||||
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,CeilingZ*VoxelSizeCm);
|
const double FaceZ=Candidate.FaceHeights[GridIndex];
|
||||||
NewNormals.emplace_back(0.0,0.0,-1.0);
|
FVector Normal(0.0,0.0,-1.0);
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
{
|
||||||
|
const double Delta=std::max(1.0,std::min<double>(GridStep,4.0));
|
||||||
|
const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY))
|
||||||
|
/(2.0*Delta);
|
||||||
|
const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta))
|
||||||
|
/(2.0*Delta);
|
||||||
|
Normal=FVector(DX,DY,-1.0);
|
||||||
|
Normal.Normalize();
|
||||||
|
}
|
||||||
|
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,FaceZ*VoxelSizeCm);
|
||||||
|
NewNormals.push_back(Normal);
|
||||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
||||||
if(bHasColors)NewColors.push_back(Color);
|
if(bHasColors)NewColors.push_back(Color);
|
||||||
return Mapped;
|
return Mapped;
|
||||||
@@ -1300,15 +1419,40 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
||!Candidate.SolidAbove[I01]||!Candidate.SolidAbove[I11])continue;
|
||!Candidate.SolidAbove[I01]||!Candidate.SolidAbove[I11])continue;
|
||||||
const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep;
|
const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep;
|
||||||
const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep;
|
const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep;
|
||||||
// Suppress a coincident plane only when it covers the full cell. A hit at just the
|
bool fullyCovered=false;
|
||||||
// center of a curved shell triangle is not enough to leave the rest of this cap cell open.
|
if(!Candidate.bTransition)
|
||||||
if(MeshIndex.HasDownwardCellCoverage(CellX0,CellY0,CellX1,CellY1,CeilingZ,0.001))continue;
|
fullyCovered=MeshIndex.HasDownwardCellCoverage(
|
||||||
|
CellX0,CellY0,CellX1,CellY1,Candidate.FaceZ,0.001);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fullyCovered=true;
|
||||||
|
constexpr double CoverageSamples[5][2]={
|
||||||
|
{0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}};
|
||||||
|
for(const auto& Sample:CoverageSamples)
|
||||||
|
{
|
||||||
|
const double U=Sample[0],V=Sample[1];
|
||||||
|
const double SampleX=CellX0+(CellX1-CellX0)*U;
|
||||||
|
const double SampleY=CellY0+(CellY1-CellY0)*V;
|
||||||
|
const double SampleZ=heightAt(SampleX,SampleY);
|
||||||
|
if(!MeshIndex.HasDownwardHit(SampleX,SampleY,SampleZ,0.25))
|
||||||
|
fullyCovered=false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(fullyCovered)continue;
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition)
|
||||||
|
Replacement.Cells[static_cast<std::size_t>(Y)*(GridSide-1)+X]=1;
|
||||||
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
||||||
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
||||||
NewTriangles.insert(NewTriangles.end(),{A,C,B,A,D,C});
|
NewTriangles.insert(NewTriangles.end(),{A,C,B,A,D,C});
|
||||||
}
|
}
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition
|
||||||
|
&&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(),
|
||||||
|
[](uint8 Value){return Value!=0;}))
|
||||||
|
Replacements.push_back(std::move(Replacement));
|
||||||
}
|
}
|
||||||
if(NewTriangles.empty())return;
|
if(NewTriangles.empty())return;
|
||||||
|
const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,GridStep,
|
||||||
|
GridSide,VoxelSizeCm,Replacements,false);
|
||||||
|
|
||||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
||||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||||
@@ -1347,9 +1491,8 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World,
|
|||||||
Mesh.NumCeilingTriangles+=static_cast<int32>(NewTriangles.size()/3);
|
Mesh.NumCeilingTriangles+=static_cast<int32>(NewTriangles.size()/3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the interior-facing top of each bedrock band. The density composition already
|
// Fill only top cells that marching cubes missed. Active transitions use the same
|
||||||
// makes this band solid and editable; this planar closure keeps its visible floor
|
// displaced face and passage-mouth fade as the signed-distance field.
|
||||||
// continuous when a coarse marching-cubes step would otherwise skip a sub-voxel band.
|
|
||||||
inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
||||||
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||||
{
|
{
|
||||||
@@ -1358,8 +1501,16 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
const int32 Extent=Key.ExtentVoxels();
|
const int32 Extent=Key.ExtentVoxels();
|
||||||
if(Extent<=0||Extent%Step!=0)return;
|
if(Extent<=0||Extent%Step!=0)return;
|
||||||
const int32 GridSide=Extent/Step+1;
|
const int32 GridSide=Extent/Step+1;
|
||||||
struct FCandidate { double FloorZ=0.0; double Thickness=0.0; };
|
struct FCandidate
|
||||||
|
{
|
||||||
|
double FloorZ=0.0;
|
||||||
|
double Thickness=0.0;
|
||||||
|
double MaximumDisplacementVoxels=0.0;
|
||||||
|
const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr;
|
||||||
|
bool bTransition=false;
|
||||||
|
};
|
||||||
std::vector<FCandidate> Candidates;
|
std::vector<FCandidate> Candidates;
|
||||||
|
std::vector<FTransitionFaceReplacement> Replacements;
|
||||||
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
const double CoreMicrons=std::max<std::uint32_t>(1,World.CoreVoxelSizeMicrons);
|
||||||
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
for(const FStrateSlot& Slot:World.Manager->GetLayout())
|
||||||
{
|
{
|
||||||
@@ -1367,9 +1518,17 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
if(!Stratum||!Stratum->bedrock.presentInCurrentField
|
if(!Stratum||!Stratum->bedrock.presentInCurrentField
|
||||||
||Stratum->bedrock.topMicrometres<=Stratum->bedrock.bottomMicrometres)continue;
|
||Stratum->bedrock.topMicrometres<=Stratum->bedrock.bottomMicrometres)continue;
|
||||||
const double FloorZ=static_cast<double>(Stratum->bedrock.topMicrometres)/CoreMicrons;
|
const double FloorZ=static_cast<double>(Stratum->bedrock.topMicrometres)/CoreMicrons;
|
||||||
if(FloorZ<Origin.Z-1.0e-6||FloorZ>Origin.Z+Extent+1.0e-6)continue;
|
const auto* transitionLower=Shape.FindResolvedLayoutStratumByOrder(Stratum->order+1);
|
||||||
|
const bool bTransition=transitionLower
|
||||||
|
&&Shape.IsResolvedLayoutTransitionActive(*Stratum);
|
||||||
|
const double displacement=bTransition
|
||||||
|
?static_cast<double>(Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
||||||
|
*Stratum,*transitionLower))/CoreMicrons
|
||||||
|
:0.0;
|
||||||
|
if(FloorZ<Origin.Z-displacement-1.0e-6
|
||||||
|
||FloorZ>Origin.Z+Extent+displacement+1.0e-6)continue;
|
||||||
Candidates.push_back({FloorZ,static_cast<double>(Stratum->bedrock.topMicrometres
|
Candidates.push_back({FloorZ,static_cast<double>(Stratum->bedrock.topMicrometres
|
||||||
-Stratum->bedrock.bottomMicrometres)/CoreMicrons});
|
-Stratum->bedrock.bottomMicrometres)/CoreMicrons,displacement,Stratum,bTransition});
|
||||||
}
|
}
|
||||||
if(Candidates.empty())return;
|
if(Candidates.empty())return;
|
||||||
|
|
||||||
@@ -1390,16 +1549,34 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
{
|
{
|
||||||
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
const double Epsilon=std::min(0.5,Candidate.Thickness*0.25);
|
||||||
if(Epsilon<=0.0)continue;
|
if(Epsilon<=0.0)continue;
|
||||||
std::vector<uint8> SolidBelow(static_cast<std::size_t>(GridSide)*GridSide,0);
|
const std::size_t GridCount=static_cast<std::size_t>(GridSide)*GridSide;
|
||||||
|
std::vector<double> faceHeights(GridCount,Candidate.FloorZ);
|
||||||
|
std::vector<uint8> solidBelow(GridCount,0);
|
||||||
|
FTransitionFaceReplacement Replacement;
|
||||||
|
Replacement.BaseZVoxels=Candidate.FloorZ;
|
||||||
|
Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels;
|
||||||
|
Replacement.Cells.resize(static_cast<std::size_t>(GridSide-1)*(GridSide-1),0);
|
||||||
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
for(int32 Y=0;Y<GridSide;++Y)for(int32 X=0;X<GridSide;++X)
|
||||||
{
|
{
|
||||||
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
||||||
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
Shape.ResolvedBedrockTopAt(*Candidate.Stratum,WorldX,WorldY,
|
||||||
|
faceHeights[GridIndex]);
|
||||||
const float Density=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
const float Density=Shape.DensityAt(static_cast<float>(WorldX),static_cast<float>(WorldY),
|
||||||
static_cast<float>(Candidate.FloorZ-Epsilon));
|
static_cast<float>(faceHeights[GridIndex]-Epsilon));
|
||||||
SolidBelow[static_cast<std::size_t>(Y*GridSide+X)]=
|
solidBelow[GridIndex]=
|
||||||
std::isfinite(Density)&&Density<0.0f?1:0;
|
std::isfinite(Density)&&Density<0.0f?1:0;
|
||||||
}
|
}
|
||||||
std::vector<int32> VertexMap(static_cast<std::size_t>(GridSide)*GridSide,INDEX_NONE);
|
const auto heightAt=[&](double X,double Y)
|
||||||
|
{
|
||||||
|
if(!Candidate.bTransition)return Candidate.FloorZ;
|
||||||
|
double height=Candidate.FloorZ;
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
Shape.ResolvedBedrockTopAt(*Candidate.Stratum,X,Y,height);
|
||||||
|
return height;
|
||||||
|
};
|
||||||
|
std::vector<int32> VertexMap(GridCount,INDEX_NONE);
|
||||||
const auto AddVertex=[&](int32 X,int32 Y)
|
const auto AddVertex=[&](int32 X,int32 Y)
|
||||||
{
|
{
|
||||||
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
const std::size_t GridIndex=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
@@ -1407,9 +1584,20 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
if(Mapped!=INDEX_NONE)return Mapped;
|
if(Mapped!=INDEX_NONE)return Mapped;
|
||||||
Mapped=static_cast<int32>(NewVertices.size());
|
Mapped=static_cast<int32>(NewVertices.size());
|
||||||
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
const double WorldX=Origin.X+X*Step,WorldY=Origin.Y+Y*Step;
|
||||||
|
FVector Normal(0.0,0.0,1.0);
|
||||||
|
if(Candidate.bTransition)
|
||||||
|
{
|
||||||
|
const double Delta=std::max(1.0,std::min<double>(Step,4.0));
|
||||||
|
const double DX=(heightAt(WorldX+Delta,WorldY)-heightAt(WorldX-Delta,WorldY))
|
||||||
|
/(2.0*Delta);
|
||||||
|
const double DY=(heightAt(WorldX,WorldY+Delta)-heightAt(WorldX,WorldY-Delta))
|
||||||
|
/(2.0*Delta);
|
||||||
|
Normal=FVector(-DX,-DY,1.0);
|
||||||
|
Normal.Normalize();
|
||||||
|
}
|
||||||
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,
|
NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,
|
||||||
Candidate.FloorZ*VoxelSizeCm);
|
faceHeights[GridIndex]*VoxelSizeCm);
|
||||||
NewNormals.emplace_back(0.0,0.0,1.0);
|
NewNormals.push_back(Normal);
|
||||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(WorldX),static_cast<float>(WorldY));
|
||||||
if(bHasColors)NewColors.push_back(Color);
|
if(bHasColors)NewColors.push_back(Color);
|
||||||
return Mapped;
|
return Mapped;
|
||||||
@@ -1418,16 +1606,43 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World,
|
|||||||
{
|
{
|
||||||
const std::size_t I00=static_cast<std::size_t>(Y*GridSide+X);
|
const std::size_t I00=static_cast<std::size_t>(Y*GridSide+X);
|
||||||
const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1;
|
const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1;
|
||||||
if(!SolidBelow[I00]||!SolidBelow[I10]||!SolidBelow[I01]||!SolidBelow[I11])continue;
|
if(!solidBelow[I00]||!solidBelow[I10]||!solidBelow[I01]||!solidBelow[I11])continue;
|
||||||
const double X0=Origin.X+X*Step,Y0=Origin.Y+Y*Step;
|
const double X0=Origin.X+X*Step,Y0=Origin.Y+Y*Step;
|
||||||
const double X1=X0+Step,Y1=Y0+Step;
|
const double X1=X0+Step,Y1=Y0+Step;
|
||||||
if(MeshIndex.HasUpwardCellCoverage(X0,Y0,X1,Y1,Candidate.FloorZ,0.001))continue;
|
bool fullyCovered=false;
|
||||||
|
if(!Candidate.bTransition)
|
||||||
|
fullyCovered=MeshIndex.HasUpwardCellCoverage(
|
||||||
|
X0,Y0,X1,Y1,Candidate.FloorZ,0.001);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fullyCovered=true;
|
||||||
|
constexpr double CoverageSamples[5][2]={
|
||||||
|
{0.0,0.0},{1.0,0.0},{0.0,1.0},{1.0,1.0},{0.5,0.5}};
|
||||||
|
for(const auto& Sample:CoverageSamples)
|
||||||
|
{
|
||||||
|
const double U=Sample[0],V=Sample[1];
|
||||||
|
const double SampleX=X0+(X1-X0)*U;
|
||||||
|
const double SampleY=Y0+(Y1-Y0)*V;
|
||||||
|
const double SampleZ=heightAt(SampleX,SampleY);
|
||||||
|
if(!MeshIndex.HasUpwardHit(SampleX,SampleY,SampleZ,0.25))
|
||||||
|
fullyCovered=false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(fullyCovered)continue;
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition)
|
||||||
|
Replacement.Cells[static_cast<std::size_t>(Y)*(GridSide-1)+X]=1;
|
||||||
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y);
|
||||||
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1);
|
||||||
NewTriangles.insert(NewTriangles.end(),{A,B,C,A,C,D});
|
NewTriangles.insert(NewTriangles.end(),{A,B,C,A,C,D});
|
||||||
}
|
}
|
||||||
|
if(Key.Level>=4&&Candidate.bTransition
|
||||||
|
&&std::any_of(Replacement.Cells.begin(),Replacement.Cells.end(),
|
||||||
|
[](uint8 Value){return Value!=0;}))
|
||||||
|
Replacements.push_back(std::move(Replacement));
|
||||||
}
|
}
|
||||||
if(NewTriangles.empty())return;
|
if(NewTriangles.empty())return;
|
||||||
|
const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,Step,
|
||||||
|
GridSide,VoxelSizeCm,Replacements,true);
|
||||||
|
|
||||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
||||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||||
|
|||||||
@@ -71,8 +71,22 @@ namespace
|
|||||||
const std::int64_t ix = TransitionFloorDiv(x, cell), iy = TransitionFloorDiv(y, cell);
|
const std::int64_t ix = TransitionFloorDiv(x, cell), iy = TransitionFloorDiv(y, cell);
|
||||||
const std::int64_t tx = TransitionFade(((x - ix * cell) * TransitionQ16) / cell);
|
const std::int64_t tx = TransitionFade(((x - ix * cell) * TransitionQ16) / cell);
|
||||||
const std::int64_t ty = TransitionFade(((y - iy * cell) * TransitionQ16) / cell);
|
const std::int64_t ty = TransitionFade(((y - iy * cell) * TransitionQ16) / cell);
|
||||||
|
std::uint64_t prefix=14695981039346656037ull;
|
||||||
|
TransitionHashWord(prefix,static_cast<std::uint32_t>(seed));
|
||||||
|
TransitionHashWord(prefix,static_cast<std::uint32_t>(interfaceIndex));
|
||||||
const auto at = [&](std::int64_t dx, std::int64_t dy)
|
const auto at = [&](std::int64_t dx, std::int64_t dy)
|
||||||
{ return TransitionNoiseValue(seed, interfaceIndex, ix + dx, iy + dy, 0, salt); };
|
{
|
||||||
|
std::uint64_t hash=prefix;
|
||||||
|
TransitionHashWord(hash,static_cast<std::uint64_t>(ix+dx));
|
||||||
|
TransitionHashWord(hash,static_cast<std::uint64_t>(iy+dy));
|
||||||
|
TransitionHashWord(hash,0);
|
||||||
|
TransitionHashWord(hash,salt);
|
||||||
|
hash ^= hash >> 30; hash *= 0xbf58476d1ce4e5b9ull;
|
||||||
|
hash ^= hash >> 27; hash *= 0x94d049bb133111ebull;
|
||||||
|
hash ^= hash >> 31;
|
||||||
|
const std::uint32_t bits=static_cast<std::uint32_t>(hash>>32);
|
||||||
|
return static_cast<std::int64_t>(bits>>15)-TransitionQ16;
|
||||||
|
};
|
||||||
return TransitionLerp(TransitionLerp(at(0, 0), at(1, 0), tx),
|
return TransitionLerp(TransitionLerp(at(0, 0), at(1, 0), tx),
|
||||||
TransitionLerp(at(0, 1), at(1, 1), tx), ty);
|
TransitionLerp(at(0, 1), at(1, 1), tx), ty);
|
||||||
}
|
}
|
||||||
@@ -359,10 +373,7 @@ ResolvedLayoutTransitionBandsV1 EvaluateResolvedLayoutTransitionBandsV1(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FloatingIslands floor faces retain the same deterministic Gradient field but
|
const std::int64_t displacement = (noise * (width / 8)) / TransitionQ16;
|
||||||
// cap displacement at one quarter voxel so the transition does not read as a contour.
|
|
||||||
const std::int64_t gradientDivisor=upper.generatorKind=="FloatingIslands"?256:8;
|
|
||||||
const std::int64_t displacement = (noise * (width / gradientDivisor)) / TransitionQ16;
|
|
||||||
const std::int64_t interfaceZ = seam + displacement;
|
const std::int64_t interfaceZ = seam + displacement;
|
||||||
result.upperBedrockBottomMicrometres = interfaceZ;
|
result.upperBedrockBottomMicrometres = interfaceZ;
|
||||||
result.upperBedrockTopMicrometres = interfaceZ + upperThickness;
|
result.upperBedrockTopMicrometres = interfaceZ + upperThickness;
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ public:
|
|||||||
{
|
{
|
||||||
const std::int64_t width=std::max<std::int64_t>(0,Upper.transitionWidthMicrometres);
|
const std::int64_t width=std::max<std::int64_t>(0,Upper.transitionWidthMicrometres);
|
||||||
if(Upper.transitionMode=="Gradient")
|
if(Upper.transitionMode=="Gradient")
|
||||||
return width/(Upper.generatorKind=="FloatingIslands"?256:8);
|
return width/8;
|
||||||
if(Upper.transitionMode!="Interleaved")return 0;
|
if(Upper.transitionMode!="Interleaved")return 0;
|
||||||
const std::int64_t upperThickness=std::max<std::int64_t>(0,
|
const std::int64_t upperThickness=std::max<std::int64_t>(0,
|
||||||
Upper.bedrock.topMicrometres-Upper.bedrock.bottomMicrometres);
|
Upper.bedrock.topMicrometres-Upper.bedrock.bottomMicrometres);
|
||||||
@@ -385,6 +385,70 @@ public:
|
|||||||
return weight;
|
return weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VoxelForgeCore::ResolvedLayoutTransitionBandsV1 ResolvedTransitionBandsAtXY(
|
||||||
|
const VoxelForgeCore::ResolvedLayoutStratumV2& Upper,
|
||||||
|
const VoxelForgeCore::ResolvedLayoutStratumV2& Lower,
|
||||||
|
std::int64_t XMicrometres,std::int64_t YMicrometres,bool SkipPassageFade=false) const
|
||||||
|
{
|
||||||
|
auto bands=VoxelForgeCore::EvaluateResolvedLayoutTransitionBandsV1(Upper,Lower,
|
||||||
|
Owner?Owner->Seed:0,ResolvedLayout?ResolvedLayout->strateLayoutChunkSizeMicrometres:0,
|
||||||
|
XMicrometres,YMicrometres);
|
||||||
|
constexpr std::int64_t TransitionQ16=1ll<<16;
|
||||||
|
const std::int64_t weight=SkipPassageFade?TransitionQ16:
|
||||||
|
ResolvedTransitionPassageWeightQ16(Upper,Lower,XMicrometres,YMicrometres);
|
||||||
|
if(weight>=TransitionQ16)return bands;
|
||||||
|
const std::int64_t seam=Upper.bedrock.bottomMicrometres;
|
||||||
|
const std::int64_t upperThickness=Upper.bedrock.topMicrometres-seam;
|
||||||
|
const std::int64_t lowerThickness=Lower.ceiling.topMicrometres
|
||||||
|
-Lower.ceiling.bottomMicrometres;
|
||||||
|
const auto scaled=[&](std::int64_t value){return (value*weight)/TransitionQ16;};
|
||||||
|
bands.upperBedrockBottomMicrometres=seam+scaled(
|
||||||
|
bands.upperBedrockBottomMicrometres-seam);
|
||||||
|
bands.upperBedrockTopMicrometres=bands.upperBedrockBottomMicrometres+upperThickness;
|
||||||
|
bands.lowerCeilingTopMicrometres=seam+scaled(
|
||||||
|
bands.lowerCeilingTopMicrometres-seam);
|
||||||
|
bands.lowerCeilingBottomMicrometres=bands.lowerCeilingTopMicrometres-lowerThickness;
|
||||||
|
bands.crossingDepthMicrometres=std::max<std::int64_t>(0,
|
||||||
|
bands.lowerCeilingTopMicrometres-bands.upperBedrockBottomMicrometres);
|
||||||
|
return bands;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResolvedBedrockTopAt(const VoxelForgeCore::ResolvedLayoutStratumV2& Upper,
|
||||||
|
double X,double Y,double& TopZ) const
|
||||||
|
{
|
||||||
|
const auto* lower=FindResolvedLayoutStratumByOrder(Upper.order+1);
|
||||||
|
if(!lower||!Owner||Owner->CoreVoxelSizeMicrons==0
|
||||||
|
||!IsResolvedLayoutTransitionActive(Upper))return false;
|
||||||
|
const auto toMicrometres=[&](double value)
|
||||||
|
{return static_cast<std::int64_t>(std::llround(value*Owner->CoreVoxelSizeMicrons));};
|
||||||
|
const auto* tileQueries=ActivePassageTileQueries;
|
||||||
|
const bool skipPassageFade=tileQueries&&tileQueries->matches(*this)
|
||||||
|
&&!tileQueries->mayContainTransitionPassageFade();
|
||||||
|
const auto bands=ResolvedTransitionBandsAtXY(Upper,*lower,toMicrometres(X),
|
||||||
|
toMicrometres(Y),skipPassageFade);
|
||||||
|
TopZ=static_cast<double>(bands.upperBedrockTopMicrometres)
|
||||||
|
/Owner->CoreVoxelSizeMicrons;
|
||||||
|
return std::isfinite(TopZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResolvedCeilingBottomAt(const VoxelForgeCore::ResolvedLayoutStratumV2& Lower,
|
||||||
|
double X,double Y,double& BottomZ) const
|
||||||
|
{
|
||||||
|
const auto* upper=FindResolvedLayoutStratumByOrder(Lower.order-1);
|
||||||
|
if(!upper||!Owner||Owner->CoreVoxelSizeMicrons==0
|
||||||
|
||!IsResolvedLayoutTransitionActive(*upper))return false;
|
||||||
|
const auto toMicrometres=[&](double value)
|
||||||
|
{return static_cast<std::int64_t>(std::llround(value*Owner->CoreVoxelSizeMicrons));};
|
||||||
|
const auto* tileQueries=ActivePassageTileQueries;
|
||||||
|
const bool skipPassageFade=tileQueries&&tileQueries->matches(*this)
|
||||||
|
&&!tileQueries->mayContainTransitionPassageFade();
|
||||||
|
const auto bands=ResolvedTransitionBandsAtXY(*upper,Lower,toMicrometres(X),
|
||||||
|
toMicrometres(Y),skipPassageFade);
|
||||||
|
BottomZ=static_cast<double>(bands.lowerCeilingBottomMicrometres)
|
||||||
|
/Owner->CoreVoxelSizeMicrons;
|
||||||
|
return std::isfinite(BottomZ);
|
||||||
|
}
|
||||||
|
|
||||||
bool IsInsideResolvedStrateBoundaryBand(float Z) const
|
bool IsInsideResolvedStrateBoundaryBand(float Z) const
|
||||||
{
|
{
|
||||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||||
@@ -471,12 +535,72 @@ public:
|
|||||||
return density;
|
return density;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!ResolvedLayout||!Owner||!Owner->Manager||!std::isfinite(Z))return density;
|
if(!ResolvedLayout||!Owner||!Owner->Manager||Owner->CoreVoxelSizeMicrons==0
|
||||||
|
||!std::isfinite(Z))return density;
|
||||||
|
const double zMicrometres=static_cast<double>(Z)*Owner->CoreVoxelSizeMicrons;
|
||||||
|
if(ResolvedLayout->strates.size()==2)
|
||||||
|
{
|
||||||
|
const auto* transitionUpper=FindResolvedLayoutStratumByOrder(0);
|
||||||
|
const auto* transitionLower=FindResolvedLayoutStratumByOrder(1);
|
||||||
|
if(transitionUpper&&transitionLower
|
||||||
|
&&IsResolvedLayoutTransitionActive(*transitionUpper))
|
||||||
|
{
|
||||||
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||||
|
++ThreadClassificationStats().TransitionWindowChecks;
|
||||||
|
#endif
|
||||||
|
const auto maximumDisplacement=ResolvedTransitionMaximumFaceDisplacementMicrometres(
|
||||||
|
*transitionUpper,*transitionLower);
|
||||||
|
const double minimum=std::min(transitionUpper->bedrock.bottomMicrometres,
|
||||||
|
transitionLower->ceiling.bottomMicrometres)-maximumDisplacement;
|
||||||
|
const double maximum=std::max(transitionUpper->bedrock.topMicrometres,
|
||||||
|
transitionLower->ceiling.topMicrometres)+maximumDisplacement;
|
||||||
|
if(zMicrometres>=minimum&&zMicrometres<maximum)
|
||||||
|
{
|
||||||
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||||
|
++ThreadClassificationStats().TransitionWindowHits;
|
||||||
|
++ThreadClassificationStats().TransitionEvaluatorCalls;
|
||||||
|
#endif
|
||||||
|
const auto* tileQueries=ActivePassageTileQueries;
|
||||||
|
const bool skipPassageFade=tileQueries&&tileQueries->matches(*this)
|
||||||
|
&&!tileQueries->mayContainTransitionPassageFade();
|
||||||
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||||
|
if(skipPassageFade)
|
||||||
|
++ThreadClassificationStats().TransitionPassageTileSkips;
|
||||||
|
#endif
|
||||||
|
const auto xMicrometres=static_cast<std::int64_t>(std::llround(
|
||||||
|
static_cast<double>(X)*Owner->CoreVoxelSizeMicrons));
|
||||||
|
const auto yMicrometres=static_cast<std::int64_t>(std::llround(
|
||||||
|
static_cast<double>(Y)*Owner->CoreVoxelSizeMicrons));
|
||||||
|
const auto bands=ResolvedTransitionBandsAtXY(*transitionUpper,
|
||||||
|
*transitionLower,xMicrometres,yMicrometres,skipPassageFade);
|
||||||
|
const std::int64_t solidBottom=std::min(
|
||||||
|
bands.upperBedrockBottomMicrometres,bands.lowerCeilingBottomMicrometres);
|
||||||
|
const std::int64_t solidTop=std::max(
|
||||||
|
bands.upperBedrockTopMicrometres,bands.lowerCeilingTopMicrometres);
|
||||||
|
const double signedDistanceMicrometres=zMicrometres<solidBottom
|
||||||
|
?static_cast<double>(solidBottom-zMicrometres)
|
||||||
|
:zMicrometres>solidTop
|
||||||
|
?static_cast<double>(zMicrometres-solidTop)
|
||||||
|
:-static_cast<double>(std::min(zMicrometres-solidBottom,
|
||||||
|
solidTop-zMicrometres));
|
||||||
|
const std::int64_t seam=transitionUpper->bedrock.bottomMicrometres;
|
||||||
|
const bool floatingIslandAirGap=
|
||||||
|
transitionUpper->generatorKind=="FloatingIslands"
|
||||||
|
&&transitionUpper->transitionMode=="Gradient"
|
||||||
|
&&bands.upperBedrockTopMicrometres<seam
|
||||||
|
&&zMicrometres>=bands.upperBedrockTopMicrometres
|
||||||
|
&&zMicrometres<seam;
|
||||||
|
if(!(signedDistanceMicrometres>0.0&&density<0.0f
|
||||||
|
&&!floatingIslandAirGap))
|
||||||
|
return static_cast<float>(signedDistanceMicrometres
|
||||||
|
/Owner->CoreVoxelSizeMicrons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const double worldZCentimetres=static_cast<double>(Z)*GetCoreVoxelSizeCm();
|
const double worldZCentimetres=static_cast<double>(Z)*GetCoreVoxelSizeCm();
|
||||||
const int32 strateIndex=Owner->Manager->GetStrateIndex(static_cast<float>(worldZCentimetres));
|
const int32 strateIndex=Owner->Manager->GetStrateIndex(static_cast<float>(worldZCentimetres));
|
||||||
const auto* stratum=FindResolvedLayoutStratum(strateIndex);
|
const auto* stratum=FindResolvedLayoutStratum(strateIndex);
|
||||||
if(!stratum)return density;
|
if(!stratum)return density;
|
||||||
const double zMicrometres=static_cast<double>(Z)*Owner->CoreVoxelSizeMicrons;
|
|
||||||
const auto solidAt=[&](const VoxelForgeCore::ResolvedLayoutBandV2& band)
|
const auto solidAt=[&](const VoxelForgeCore::ResolvedLayoutBandV2& band)
|
||||||
{return band.presentInCurrentField&&zMicrometres>=band.bottomMicrometres
|
{return band.presentInCurrentField&&zMicrometres>=band.bottomMicrometres
|
||||||
&&zMicrometres<band.topMicrometres;};
|
&&zMicrometres<band.topMicrometres;};
|
||||||
@@ -502,77 +626,46 @@ public:
|
|||||||
#endif
|
#endif
|
||||||
if(upper==stratum)bBedrockHandledByTransition=true;
|
if(upper==stratum)bBedrockHandledByTransition=true;
|
||||||
if(lower==stratum)bCeilingHandledByTransition=true;
|
if(lower==stratum)bCeilingHandledByTransition=true;
|
||||||
|
const auto* tileQueries=ActivePassageTileQueries;
|
||||||
const std::int64_t xMicrometres=static_cast<std::int64_t>(std::llround(
|
const std::int64_t xMicrometres=static_cast<std::int64_t>(std::llround(
|
||||||
static_cast<double>(X)*Owner->CoreVoxelSizeMicrons));
|
static_cast<double>(X)*Owner->CoreVoxelSizeMicrons));
|
||||||
const std::int64_t yMicrometres=static_cast<std::int64_t>(std::llround(
|
const std::int64_t yMicrometres=static_cast<std::int64_t>(std::llround(
|
||||||
static_cast<double>(Y)*Owner->CoreVoxelSizeMicrons));
|
static_cast<double>(Y)*Owner->CoreVoxelSizeMicrons));
|
||||||
const auto bands=VoxelForgeCore::EvaluateResolvedLayoutTransitionBandsV1(*upper,*lower,
|
|
||||||
Owner->Seed,ResolvedLayout->strateLayoutChunkSizeMicrometres,
|
|
||||||
xMicrometres,yMicrometres);
|
|
||||||
auto resolvedBands=bands;
|
|
||||||
constexpr std::int64_t TransitionQ16=1ll<<16;
|
|
||||||
const auto* tileQueries=ActivePassageTileQueries;
|
|
||||||
const bool skipPassageFade=tileQueries&&tileQueries->matches(*this)
|
const bool skipPassageFade=tileQueries&&tileQueries->matches(*this)
|
||||||
&&!tileQueries->mayContainTransitionPassageFade();
|
&&!tileQueries->mayContainTransitionPassageFade();
|
||||||
const std::int64_t transitionWeight=skipPassageFade?TransitionQ16
|
|
||||||
:ResolvedTransitionPassageWeightQ16(*upper,*lower,xMicrometres,yMicrometres);
|
|
||||||
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
#if defined(VF_L30_ENABLE_COST_COUNTERS)
|
||||||
if(skipPassageFade)++ThreadClassificationStats().TransitionPassageTileSkips;
|
if(skipPassageFade)++ThreadClassificationStats().TransitionPassageTileSkips;
|
||||||
#endif
|
#endif
|
||||||
if(transitionWeight<TransitionQ16)
|
const auto resolvedBands=ResolvedTransitionBandsAtXY(*upper,*lower,
|
||||||
{
|
xMicrometres,yMicrometres,skipPassageFade);
|
||||||
const std::int64_t seam=upper->bedrock.bottomMicrometres;
|
// Treat the two authored bands as one continuous solid interval. Its
|
||||||
const std::int64_t upperThickness=upper->bedrock.topMicrometres-seam;
|
// exterior faces remain the lower ceiling bottom and upper bedrock top;
|
||||||
const std::int64_t lowerThickness=lower->ceiling.topMicrometres
|
// measuring signed distance from those faces also clears native generator
|
||||||
-lower->ceiling.bottomMicrometres;
|
// density on both air sides of the displaced interface.
|
||||||
const auto scaled=[&](std::int64_t value)
|
const std::int64_t solidBottom=std::min(
|
||||||
{return (value*transitionWeight)/TransitionQ16;};
|
resolvedBands.upperBedrockBottomMicrometres,
|
||||||
resolvedBands.upperBedrockBottomMicrometres=seam+scaled(
|
resolvedBands.lowerCeilingBottomMicrometres);
|
||||||
bands.upperBedrockBottomMicrometres-seam);
|
const std::int64_t solidTop=std::max(
|
||||||
resolvedBands.upperBedrockTopMicrometres=
|
resolvedBands.upperBedrockTopMicrometres,
|
||||||
resolvedBands.upperBedrockBottomMicrometres+upperThickness;
|
resolvedBands.lowerCeilingTopMicrometres);
|
||||||
resolvedBands.lowerCeilingTopMicrometres=seam+scaled(
|
const double signedDistanceMicrometres=zMicrometres<solidBottom
|
||||||
bands.lowerCeilingTopMicrometres-seam);
|
?static_cast<double>(solidBottom-zMicrometres)
|
||||||
resolvedBands.lowerCeilingBottomMicrometres=
|
:zMicrometres>solidTop
|
||||||
resolvedBands.lowerCeilingTopMicrometres-lowerThickness;
|
?static_cast<double>(zMicrometres-solidTop)
|
||||||
resolvedBands.crossingDepthMicrometres=std::max<std::int64_t>(0,
|
:-static_cast<double>(std::min(zMicrometres-solidBottom,
|
||||||
resolvedBands.lowerCeilingTopMicrometres
|
solidTop-zMicrometres));
|
||||||
-resolvedBands.upperBedrockBottomMicrometres);
|
const std::int64_t seam=upper->bedrock.bottomMicrometres;
|
||||||
}
|
const bool floatingIslandAirGap=upper->generatorKind=="FloatingIslands"
|
||||||
const bool upperBedrock=zMicrometres>=resolvedBands.upperBedrockBottomMicrometres
|
&&upper->transitionMode=="Gradient"
|
||||||
&&zMicrometres<resolvedBands.upperBedrockTopMicrometres;
|
&&resolvedBands.upperBedrockTopMicrometres<seam
|
||||||
const bool lowerCeiling=zMicrometres>=resolvedBands.lowerCeilingBottomMicrometres
|
&&zMicrometres>=resolvedBands.upperBedrockTopMicrometres
|
||||||
&&zMicrometres<resolvedBands.lowerCeilingTopMicrometres;
|
&&zMicrometres<seam;
|
||||||
if(!upperBedrock&&!lowerCeiling)
|
// A native solid may continue beyond either authored face. Keep that
|
||||||
{
|
// connected rock; the FI gap is the explicit exception that must stay open.
|
||||||
// FloatingIslands can retain native solid density above a negatively
|
if(signedDistanceMicrometres>0.0&&density<0.0f&&!floatingIslandAirGap)
|
||||||
// displaced Gradient face, creating a second surface instead of the
|
|
||||||
// resolved transition top. Keep that air-side gap open in the owner field.
|
|
||||||
const std::int64_t seam=upper->bedrock.bottomMicrometres;
|
|
||||||
if(upper->generatorKind=="FloatingIslands"
|
|
||||||
&&upper->transitionMode=="Gradient"
|
|
||||||
&&resolvedBands.upperBedrockTopMicrometres<seam
|
|
||||||
&&zMicrometres>=resolvedBands.upperBedrockTopMicrometres
|
|
||||||
&&zMicrometres<seam)
|
|
||||||
{
|
|
||||||
const double distanceVoxels=static_cast<double>(zMicrometres
|
|
||||||
-resolvedBands.upperBedrockTopMicrometres)
|
|
||||||
/std::max<std::uint32_t>(1,Owner->CoreVoxelSizeMicrons);
|
|
||||||
density=std::max(0.05f,static_cast<float>(distanceVoxels));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
density=static_cast<float>(signedDistanceMicrometres
|
||||||
const auto& band=upperBedrock?upper->bedrock:lower->ceiling;
|
/std::max<std::uint32_t>(1,Owner->CoreVoxelSizeMicrons));
|
||||||
if(upperBedrock&&upper->generatorKind=="FloatingIslands"
|
|
||||||
&&upper->transitionMode=="Gradient")
|
|
||||||
{
|
|
||||||
const double distanceVoxels=static_cast<double>(
|
|
||||||
resolvedBands.upperBedrockTopMicrometres-zMicrometres)
|
|
||||||
/std::max<std::uint32_t>(1,Owner->CoreVoxelSizeMicrons);
|
|
||||||
density=-std::max(0.05f,static_cast<float>(distanceVoxels));
|
|
||||||
}
|
|
||||||
else density=-std::max(0.05f,static_cast<float>(band.solidDensityQ16)/65536.0f);
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Audit the displaced floor across the passage-mouth fade edge."""
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import resource
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import AuditRingTerraceL32 as ring
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, required=True)
|
||||||
|
parser.add_argument("--lod", type=int, default=0)
|
||||||
|
parser.add_argument("--inner-radius-m", type=float, default=5.5)
|
||||||
|
parser.add_argument("--outer-radius-m", type=float, default=9.5)
|
||||||
|
parser.add_argument("--step-m", type=float, default=0.125)
|
||||||
|
parser.add_argument("--height-min-m", type=float, default=-122.5)
|
||||||
|
parser.add_argument("--height-max-m", type=float, default=-117.5)
|
||||||
|
parser.add_argument("--preferred-height-m", type=float, default=-119.5)
|
||||||
|
parser.add_argument("--highest-surface", action="store_true",
|
||||||
|
help="choose the topmost upward face inside the height window")
|
||||||
|
parser.add_argument("--plateau-min-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--step-threshold-m", type=float, default=0.1)
|
||||||
|
parser.add_argument("--normal-jump-max-deg", type=float, default=20.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if (args.inner_radius_m < 0.0 or args.outer_radius_m <= args.inner_radius_m
|
||||||
|
or args.step_m <= 0.0):
|
||||||
|
parser.error("invalid radial interval or sample spacing")
|
||||||
|
|
||||||
|
cx, cy = args.center
|
||||||
|
preferred_height = None if args.highest_surface else args.preferred_height_m
|
||||||
|
bounds = (cx - args.outer_radius_m, cx + args.outer_radius_m,
|
||||||
|
cy - args.outer_radius_m, cy + args.outer_radius_m)
|
||||||
|
voxel_um, triangles, selected_tiles = ring.read_scene(
|
||||||
|
args.scene, args.lod, args.height_min_m, args.height_max_m, bounds)
|
||||||
|
bucket_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << args.lod))
|
||||||
|
buckets = ring.build_buckets(triangles, True, bounds, bucket_size)
|
||||||
|
|
||||||
|
count = round((args.outer_radius_m - args.inner_radius_m) / args.step_m)
|
||||||
|
lines = []
|
||||||
|
for angle_degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||||
|
angle = math.radians(angle_degrees)
|
||||||
|
points = [(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||||
|
for radius in (args.inner_radius_m + i * args.step_m
|
||||||
|
for i in range(count + 1))]
|
||||||
|
lines.append((f"radial-{angle_degrees:.0f}deg", points))
|
||||||
|
|
||||||
|
# Tangential sections stay outside the open mouth while crossing the outer
|
||||||
|
# fade edge on both sides of the 6.5 m center radius.
|
||||||
|
tangent_count = round(8.0 / args.step_m)
|
||||||
|
tangent_values = [-4.0 + i * args.step_m for i in range(tangent_count + 1)]
|
||||||
|
for angle_degrees in (225.0, 315.0):
|
||||||
|
angle = math.radians(angle_degrees)
|
||||||
|
normal = (math.cos(angle), math.sin(angle))
|
||||||
|
tangent = (-normal[1], normal[0])
|
||||||
|
points = [(cx + 6.5 * normal[0] + t * tangent[0],
|
||||||
|
cy + 6.5 * normal[1] + t * tangent[1])
|
||||||
|
for t in tangent_values]
|
||||||
|
lines.append((f"straight-tangent-{angle_degrees:.0f}deg", points))
|
||||||
|
|
||||||
|
failed = False
|
||||||
|
max_plateau = 0.0
|
||||||
|
max_step = 0.0
|
||||||
|
max_normal = 0.0
|
||||||
|
min_coverage = 1.0
|
||||||
|
for name, points in lines:
|
||||||
|
profile = ring.audit_profile(points, buckets, True, args.plateau_min_m,
|
||||||
|
0.0005, args.step_threshold_m, bucket_size, preferred_height)
|
||||||
|
samples = [ring.sample_surface(buckets, x, y, True, bucket_size,
|
||||||
|
preferred_height)
|
||||||
|
for x, y in points]
|
||||||
|
for index in range(1, len(samples)):
|
||||||
|
prior, current = samples[index - 1], samples[index]
|
||||||
|
if prior is None or current is None:
|
||||||
|
continue
|
||||||
|
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1]))))
|
||||||
|
delta = math.degrees(math.acos(dot))
|
||||||
|
if delta > args.normal_jump_max_deg:
|
||||||
|
px, py = points[index - 1]
|
||||||
|
x, y = points[index]
|
||||||
|
print(f"[PassageFadeNormalJump] line={name} "
|
||||||
|
f"radius_m={math.hypot(x-cx,y-cy):.3f} "
|
||||||
|
f"from_m=({px:.3f},{py:.3f},{prior[0]:.3f}) "
|
||||||
|
f"to_m=({x:.3f},{y:.3f},{current[0]:.3f}) "
|
||||||
|
f"normal_delta_deg={delta:.3f}")
|
||||||
|
coverage = profile["coverage"]
|
||||||
|
min_coverage = min(min_coverage, coverage)
|
||||||
|
plateau = max((run[0] for run in profile["plateaus"]), default=0.0)
|
||||||
|
step = max((run[1] for run in profile["plateaus"]), default=0.0)
|
||||||
|
angle = profile["max_angle"]
|
||||||
|
max_plateau = max(max_plateau, plateau)
|
||||||
|
max_step = max(max_step, step)
|
||||||
|
max_normal = max(max_normal, angle)
|
||||||
|
ok = (profile["valid"] and plateau < args.plateau_min_m
|
||||||
|
and angle <= args.normal_jump_max_deg)
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[PassageFadeProfile] line={name} coverage={coverage:.3f} "
|
||||||
|
f"height_range_m={profile.get('height_range', 0.0):.6f} "
|
||||||
|
f"flat_step_runs={len(profile['plateaus'])} "
|
||||||
|
f"max_normal_delta_deg={angle:.6f} status={'PASS' if ok else 'FAIL'}")
|
||||||
|
|
||||||
|
passed = not failed and min_coverage >= 0.95
|
||||||
|
print(f"[PassageFadeRingAudit] scene={args.scene} lod={args.lod} "
|
||||||
|
f"center_m=({cx:.6f},{cy:.6f}) mouth_radius_m=4.500 "
|
||||||
|
f"fade_m=5.500..7.500 radial_m={args.inner_radius_m:.3f}.."
|
||||||
|
f"{args.outer_radius_m:.3f} lines={len(lines)} selected_tiles={selected_tiles} "
|
||||||
|
f"coverage_min={min_coverage:.3f} longest_flat_run_m={max_plateau:.6f} "
|
||||||
|
f"following_step_m={max_step:.6f} max_normal_delta_deg={max_normal:.6f} "
|
||||||
|
f"thresholds=(coverage>=0.950,plateau<0.500,step>=0.100,normal<=20.000) "
|
||||||
|
f"status={'PASS' if passed else 'FAIL'}")
|
||||||
|
print(f"[PassageFadeRSS] peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Audit several bounded face windows and combine their height range."""
|
||||||
|
import argparse
|
||||||
|
import gc
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import resource
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, action="append", required=True,
|
||||||
|
help="repeat for each bounded local ring")
|
||||||
|
parser.add_argument("--radius", type=float, default=8.0)
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
parser.add_argument("--preferred-height-m", type=float,
|
||||||
|
help="lock overlapping-face selection near this expected surface height")
|
||||||
|
parser.add_argument("--step", type=float, default=0.125)
|
||||||
|
parser.add_argument("--plateau-min-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--plateau-epsilon-m", type=float, default=0.0005)
|
||||||
|
parser.add_argument("--step-threshold-m", type=float, default=0.1)
|
||||||
|
parser.add_argument("--normal-jump-max-deg", type=float, default=20.0)
|
||||||
|
parser.add_argument("--amplitude-min-m", type=float, default=3.0)
|
||||||
|
parser.add_argument("--min-radial-lines-per-center", type=int, default=4)
|
||||||
|
parser.add_argument("--min-straight-lines-per-center", type=int, default=2)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
audit_path = Path(__file__).with_name("AuditRingTerraceL32.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_audit_l32_set", audit_path)
|
||||||
|
audit = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(audit)
|
||||||
|
|
||||||
|
upward = args.surface == "floor"
|
||||||
|
preferred_height = args.preferred_height_m
|
||||||
|
if preferred_height is None:
|
||||||
|
preferred_height = args.height_max_m if upward else args.height_min_m
|
||||||
|
all_ranges = []
|
||||||
|
overall_max_normal = 0.0
|
||||||
|
overall_max_plateau = 0.0
|
||||||
|
overall_max_step = 0.0
|
||||||
|
overall_min_coverage = 1.0
|
||||||
|
total_valid_lines = 0
|
||||||
|
total_lines = 0
|
||||||
|
failed = False
|
||||||
|
for center in args.center:
|
||||||
|
bounds = (center[0] - args.radius, center[0] + args.radius,
|
||||||
|
center[1] - args.radius, center[1] + args.radius)
|
||||||
|
voxel_um, triangles, selected_tiles = audit.read_scene(
|
||||||
|
args.scene, args.lod, args.height_min_m, args.height_max_m, bounds)
|
||||||
|
max_lod = max((triangle[0] for triangle in triangles), default=0)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << max_lod))
|
||||||
|
buckets = audit.build_buckets(triangles, upward, bounds, cell_size)
|
||||||
|
results = []
|
||||||
|
for name, points in audit.make_lines(*center, args.radius, args.step):
|
||||||
|
result = audit.audit_profile(points, buckets, upward,
|
||||||
|
args.plateau_min_m, args.plateau_epsilon_m,
|
||||||
|
args.step_threshold_m, cell_size, preferred_height)
|
||||||
|
results.append((name, points, result))
|
||||||
|
|
||||||
|
radial = [row for row in results if row[0].startswith("radial-") and row[2]["valid"]]
|
||||||
|
straight = [row for row in results if row[0].startswith("straight-") and row[2]["valid"]]
|
||||||
|
valid = [row for row in results if row[2]["valid"]]
|
||||||
|
local_min = min((row[2]["height_min"] for row in valid), default=float("inf"))
|
||||||
|
local_max = max((row[2]["height_max"] for row in valid), default=float("-inf"))
|
||||||
|
local_amplitude = local_max - local_min
|
||||||
|
local_plateau = max((run[0] for row in valid for run in row[2]["plateaus"]), default=0.0)
|
||||||
|
local_step = max((run[1] for row in valid for run in row[2]["plateaus"]), default=0.0)
|
||||||
|
local_normal = max((row[2]["max_angle"] for row in valid), default=float("inf"))
|
||||||
|
local_coverage = min((row[2]["coverage"] for row in results), default=0.0)
|
||||||
|
local_ok = (len(valid) == len(results)
|
||||||
|
and len(radial) >= args.min_radial_lines_per_center
|
||||||
|
and len(straight) >= args.min_straight_lines_per_center
|
||||||
|
and local_plateau < args.plateau_min_m
|
||||||
|
and local_normal <= args.normal_jump_max_deg)
|
||||||
|
all_ranges.extend((row[2]["height_min"], row[2]["height_max"])
|
||||||
|
for row in valid)
|
||||||
|
overall_max_normal = max(overall_max_normal, local_normal)
|
||||||
|
overall_max_plateau = max(overall_max_plateau, local_plateau)
|
||||||
|
overall_max_step = max(overall_max_step, local_step)
|
||||||
|
overall_min_coverage = min(overall_min_coverage, local_coverage)
|
||||||
|
total_valid_lines += len(valid)
|
||||||
|
total_lines += len(results)
|
||||||
|
print(f"[RingSetWindow] center_m=({center[0]:.3f},{center[1]:.3f}) "
|
||||||
|
f"radius_m={args.radius:.3f} tiles={selected_tiles} lines={len(valid)}/"
|
||||||
|
f"{len(results)} radial={len(radial)} straight={len(straight)} "
|
||||||
|
f"coverage_min={local_coverage:.3f} height_m={local_min:.6f}.."
|
||||||
|
f"{local_max:.6f} amplitude_m={local_amplitude:.6f} "
|
||||||
|
f"longest_flat_m={local_plateau:.6f} following_step_m={local_step:.6f} "
|
||||||
|
f"max_normal_delta_deg={local_normal:.6f} "
|
||||||
|
f"status={'PASS' if local_ok else 'FAIL'}")
|
||||||
|
for name, _, result in results:
|
||||||
|
print(f"[RingSetProfile] center_m=({center[0]:.3f},{center[1]:.3f}) "
|
||||||
|
f"line={name} coverage={result['coverage']:.3f} "
|
||||||
|
f"range_m={result.get('height_range', 0.0):.6f} "
|
||||||
|
f"flat_step_runs={len(result['plateaus'])} "
|
||||||
|
f"max_normal_delta_deg={result['max_angle']:.6f}")
|
||||||
|
failed |= not local_ok
|
||||||
|
del triangles, buckets, results, radial, straight, valid
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
if all_ranges:
|
||||||
|
sampled_min = min(value[0] for value in all_ranges)
|
||||||
|
sampled_max = max(value[1] for value in all_ranges)
|
||||||
|
amplitude = sampled_max - sampled_min
|
||||||
|
else:
|
||||||
|
sampled_min = sampled_max = amplitude = 0.0
|
||||||
|
passed = not failed and amplitude >= args.amplitude_min_m
|
||||||
|
print(f"[RingSetAudit] scene={args.scene} surface={args.surface} lod={args.lod} "
|
||||||
|
f"windows={len(args.center)} radius_m={args.radius:.3f} "
|
||||||
|
f"height_window_m={args.height_min_m:.3f}..{args.height_max_m:.3f} "
|
||||||
|
f"sampled_height_m={sampled_min:.6f}..{sampled_max:.6f} "
|
||||||
|
f"amplitude_range_m={amplitude:.6f} lines={total_valid_lines}/{total_lines} "
|
||||||
|
f"coverage_min={overall_min_coverage:.3f} "
|
||||||
|
f"longest_flat_run_m={overall_max_plateau:.6f} "
|
||||||
|
f"max_step_m={overall_max_step:.6f} "
|
||||||
|
f"max_normal_delta_deg={overall_max_normal:.6f} "
|
||||||
|
f"thresholds=(amplitude>={args.amplitude_min_m:.3f},"
|
||||||
|
f"plateau<{args.plateau_min_m:.3f},"
|
||||||
|
f"normal<={args.normal_jump_max_deg:.3f}) "
|
||||||
|
f"status={'PASS' if passed else 'FAIL'}")
|
||||||
|
print(f"[RingSetRSS] peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fail on contour terraces, stepped profiles, or insufficient Gradient amplitude."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import math
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
def read_scene(path, wanted_lod, height_min, height_max, bounds):
|
||||||
|
triangles = []
|
||||||
|
selected_tiles = 0
|
||||||
|
x_min, x_max, y_min, y_max = bounds
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid mesh scene header")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.read(40)
|
||||||
|
if version != 1 or tile_count > 100000:
|
||||||
|
raise SystemExit(f"{path}: unsupported mesh scene")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
tile_extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
tile_x0, tile_y0 = key[0] * tile_extent, key[1] * tile_extent
|
||||||
|
tile_z0 = key[2] * tile_extent
|
||||||
|
if (wanted_lod is not None and key[3] != wanted_lod) or \
|
||||||
|
tile_x0 > x_max or tile_x0 + tile_extent < x_min or \
|
||||||
|
tile_y0 > y_max or tile_y0 + tile_extent < y_min or \
|
||||||
|
tile_z0 > height_max or tile_z0 + tile_extent < height_min:
|
||||||
|
stream.seek(vertex_count * 24 + index_count * 4 + mask_count, 1)
|
||||||
|
continue
|
||||||
|
selected_tiles += 1
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
for offset in range(0, index_count, 3):
|
||||||
|
if masks and masks[offset // 3]:
|
||||||
|
continue
|
||||||
|
ids = indices[offset:offset + 3]
|
||||||
|
if max(ids) >= surface_count:
|
||||||
|
continue
|
||||||
|
verts = []
|
||||||
|
for index in ids:
|
||||||
|
p, n = index * 3, index * 3
|
||||||
|
verts.append(((positions[p], positions[p + 1], positions[p + 2]),
|
||||||
|
(normals[n], normals[n + 1], normals[n + 2])))
|
||||||
|
average_nz = sum(vertex[1][2] for vertex in verts) / 3.0
|
||||||
|
z_values = [vertex[0][2] for vertex in verts]
|
||||||
|
# Select the requested surface branch by its face height. An
|
||||||
|
# overlap or centroid test admits a giant coarse triangle that
|
||||||
|
# merely clips this window and can belong to the neighboring face.
|
||||||
|
mean_z = sum(z_values) / 3.0
|
||||||
|
vertex_margin = voxel_um / 1_000_000.0 * 0.5
|
||||||
|
if (mean_z < height_min or mean_z > height_max or
|
||||||
|
min(z_values) < height_min - vertex_margin or
|
||||||
|
max(z_values) > height_max + vertex_margin):
|
||||||
|
continue
|
||||||
|
if (max(vertex[0][0] for vertex in verts) < x_min or
|
||||||
|
min(vertex[0][0] for vertex in verts) > x_max or
|
||||||
|
max(vertex[0][1] for vertex in verts) < y_min or
|
||||||
|
min(vertex[0][1] for vertex in verts) > y_max):
|
||||||
|
continue
|
||||||
|
triangles.append((key[3], average_nz, tuple(verts)))
|
||||||
|
return voxel_um, triangles, selected_tiles
|
||||||
|
|
||||||
|
|
||||||
|
def build_buckets(triangles, upward, bounds, cell_size=2.0):
|
||||||
|
buckets = defaultdict(list)
|
||||||
|
x_min, x_max, y_min, y_max = bounds
|
||||||
|
for triangle in triangles:
|
||||||
|
_, average_nz, verts = triangle
|
||||||
|
if (upward and average_nz < 0.35) or (not upward and average_nz > -0.35):
|
||||||
|
continue
|
||||||
|
xs = [v[0][0] for v in verts]
|
||||||
|
ys = [v[0][1] for v in verts]
|
||||||
|
x0 = max(math.floor(min(xs) / cell_size), math.floor(x_min / cell_size))
|
||||||
|
x1 = min(math.floor(max(xs) / cell_size), math.floor(x_max / cell_size))
|
||||||
|
y0 = max(math.floor(min(ys) / cell_size), math.floor(y_min / cell_size))
|
||||||
|
y1 = min(math.floor(max(ys) / cell_size), math.floor(y_max / cell_size))
|
||||||
|
for iy in range(y0, y1 + 1):
|
||||||
|
for ix in range(x0, x1 + 1):
|
||||||
|
buckets[(ix, iy)].append(verts)
|
||||||
|
return buckets
|
||||||
|
|
||||||
|
|
||||||
|
def sample_surface(buckets, x, y, upward, cell_size, preferred_height=None):
|
||||||
|
found = None
|
||||||
|
for verts in buckets.get((math.floor(x / cell_size), math.floor(y / cell_size)), ()):
|
||||||
|
(a, na), (b, nb), (c, nc) = verts
|
||||||
|
denominator = (b[1] - c[1]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[1] - c[1])
|
||||||
|
if abs(denominator) < 1.0e-12:
|
||||||
|
continue
|
||||||
|
u = ((b[1] - c[1]) * (x - c[0]) + (c[0] - b[0]) * (y - c[1])) / denominator
|
||||||
|
v = ((c[1] - a[1]) * (x - c[0]) + (a[0] - c[0]) * (y - c[1])) / denominator
|
||||||
|
w = 1.0 - u - v
|
||||||
|
if min(u, v, w) < -1.0e-6:
|
||||||
|
continue
|
||||||
|
z = u * a[2] + v * b[2] + w * c[2]
|
||||||
|
normal = tuple(u * na[axis] + v * nb[axis] + w * nc[axis] for axis in range(3))
|
||||||
|
length = math.sqrt(sum(value * value for value in normal))
|
||||||
|
if length < 1.0e-9:
|
||||||
|
continue
|
||||||
|
normal = tuple(value / length for value in normal)
|
||||||
|
if (normal[2] > 0) != upward:
|
||||||
|
continue
|
||||||
|
if found is None or (abs(z - preferred_height) < abs(found[0] - preferred_height)
|
||||||
|
if preferred_height is not None else z > found[0]):
|
||||||
|
found = (z, normal)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def make_lines(cx, cy, radius, step):
|
||||||
|
lines = []
|
||||||
|
for index in range(8):
|
||||||
|
angle = index * math.tau / 8.0
|
||||||
|
count = round(radius / step)
|
||||||
|
points = [(cx + i * step * math.cos(angle), cy + i * step * math.sin(angle))
|
||||||
|
for i in range(count + 1)]
|
||||||
|
lines.append((f"radial-{index}", points))
|
||||||
|
count = round(2.0 * radius / step)
|
||||||
|
t_values = [-radius + i * step for i in range(count + 1)]
|
||||||
|
for name, points in (
|
||||||
|
("straight-x", [(cx + t, cy) for t in t_values]),
|
||||||
|
("straight-y", [(cx, cy + t) for t in t_values]),
|
||||||
|
("straight-diagonal", [(cx + t / math.sqrt(2.0), cy + t / math.sqrt(2.0))
|
||||||
|
for t in t_values]),
|
||||||
|
):
|
||||||
|
lines.append((name, points))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def audit_profile(points, buckets, upward, plateau_min_m, plateau_epsilon_m,
|
||||||
|
step_threshold_m, cell_size, preferred_height=None):
|
||||||
|
samples = [sample_surface(buckets, x, y, upward, cell_size, preferred_height)
|
||||||
|
for x, y in points]
|
||||||
|
valid = [(index, value) for index, value in enumerate(samples) if value is not None]
|
||||||
|
if len(valid) < max(8, len(points) * 0.75):
|
||||||
|
return {"coverage": len(valid) / len(points), "plateaus": [], "max_angle": math.inf,
|
||||||
|
"height_range": 0.0, "valid": False}
|
||||||
|
heights = [value[0] for _, value in valid]
|
||||||
|
plateau_runs = []
|
||||||
|
run_start = valid[0][0]
|
||||||
|
run_start_height = valid[0][1][0]
|
||||||
|
sample_spacing = math.hypot(points[1][0] - points[0][0],
|
||||||
|
points[1][1] - points[0][1])
|
||||||
|
for cursor in range(1, len(valid)):
|
||||||
|
prior_index, prior = valid[cursor - 1]
|
||||||
|
index, current = valid[cursor]
|
||||||
|
if index != prior_index + 1:
|
||||||
|
run_start = index
|
||||||
|
run_start_height = current[0]
|
||||||
|
continue
|
||||||
|
if abs(current[0] - run_start_height) <= plateau_epsilon_m:
|
||||||
|
continue
|
||||||
|
run_length = (prior_index - run_start + 1) * sample_spacing
|
||||||
|
if run_length >= plateau_min_m and abs(current[0] - prior[0]) >= step_threshold_m:
|
||||||
|
plateau_runs.append((run_length, abs(current[0] - prior[0]), run_start,
|
||||||
|
prior_index, index, prior[0], current[0]))
|
||||||
|
run_start = index
|
||||||
|
run_start_height = current[0]
|
||||||
|
max_angle = 0.0
|
||||||
|
for cursor in range(1, len(valid)):
|
||||||
|
prior_index, prior = valid[cursor - 1]
|
||||||
|
index, current = valid[cursor]
|
||||||
|
if index != prior_index + 1:
|
||||||
|
continue
|
||||||
|
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1]))))
|
||||||
|
max_angle = max(max_angle, math.degrees(math.acos(dot)))
|
||||||
|
return {"coverage": len(valid) / len(points), "plateaus": plateau_runs,
|
||||||
|
"max_angle": max_angle, "height_range": max(heights) - min(heights),
|
||||||
|
"height_min": min(heights), "height_max": max(heights),
|
||||||
|
"valid": True}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), default="floor")
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, default=(64.0, -32.0))
|
||||||
|
parser.add_argument("--radius", type=float, default=16.0)
|
||||||
|
parser.add_argument("--lod", required=True,
|
||||||
|
help="audit one LOD (0..8) or the streamed composite (`all`)")
|
||||||
|
parser.add_argument("--step", type=float, default=0.125)
|
||||||
|
parser.add_argument("--height-min-m", type=float, default=-130.0)
|
||||||
|
parser.add_argument("--height-max-m", type=float, default=-110.0)
|
||||||
|
parser.add_argument("--plateau-min-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--plateau-epsilon-m", type=float, default=0.0005)
|
||||||
|
parser.add_argument("--step-threshold-m", type=float, default=0.1)
|
||||||
|
parser.add_argument("--normal-jump-max-deg", type=float, default=20.0)
|
||||||
|
parser.add_argument("--amplitude-min-m", type=float, default=3.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
wanted_lod = None if args.lod == "all" else int(args.lod)
|
||||||
|
except ValueError:
|
||||||
|
parser.error("--lod must be 0..8 or all")
|
||||||
|
if wanted_lod is not None and not 0 <= wanted_lod <= 8:
|
||||||
|
parser.error("--lod must be 0..8 or all")
|
||||||
|
bounds = (args.center[0] - args.radius, args.center[0] + args.radius,
|
||||||
|
args.center[1] - args.radius, args.center[1] + args.radius)
|
||||||
|
voxel_um, triangles, selected_tiles = read_scene(args.scene, wanted_lod,
|
||||||
|
args.height_min_m, args.height_max_m, bounds)
|
||||||
|
upward = args.surface == "floor"
|
||||||
|
max_lod = max((triangle[0] for triangle in triangles), default=0)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << max_lod))
|
||||||
|
buckets = build_buckets(triangles, upward, bounds, cell_size)
|
||||||
|
results = []
|
||||||
|
# The requested face is the outer boundary of its authored band: highest Z
|
||||||
|
# for a floor, lowest Z for a ceiling. Selecting the band midpoint can jump
|
||||||
|
# between two overlapping downward/upward sheets at a cave edge.
|
||||||
|
preferred_height = args.height_max_m if upward else args.height_min_m
|
||||||
|
for name, points in make_lines(*args.center, args.radius, args.step):
|
||||||
|
results.append((name, points, audit_profile(points, buckets, upward,
|
||||||
|
args.plateau_min_m, args.plateau_epsilon_m,
|
||||||
|
args.step_threshold_m, cell_size, preferred_height)))
|
||||||
|
valid = [result for _, _, result in results if result["valid"]]
|
||||||
|
if not valid:
|
||||||
|
raise SystemExit("no sampled surface profiles; check scene, center, orientation and LOD")
|
||||||
|
amplitude = max(result["height_max"] for result in valid) - min(
|
||||||
|
result["height_min"] for result in valid)
|
||||||
|
sampled_min = min(result["height_min"] for result in valid)
|
||||||
|
sampled_max = max(result["height_max"] for result in valid)
|
||||||
|
longest_plateau = max((run[0] for result in valid for run in result["plateaus"]),
|
||||||
|
default=0.0)
|
||||||
|
largest_step = max((run[1] for result in valid for run in result["plateaus"]),
|
||||||
|
default=0.0)
|
||||||
|
max_normal_jump = max(result["max_angle"] for result in valid)
|
||||||
|
min_coverage = min(result["coverage"] for _, _, result in results)
|
||||||
|
passed = (amplitude >= args.amplitude_min_m and longest_plateau < args.plateau_min_m
|
||||||
|
and max_normal_jump <= args.normal_jump_max_deg and min_coverage >= 0.75)
|
||||||
|
print(f"[RingTerraceAudit] scene={args.scene} surface={args.surface} lod={args.lod} "
|
||||||
|
f"max_lod={max_lod} "
|
||||||
|
f"height_window_m={args.height_min_m:.3f}..{args.height_max_m:.3f} "
|
||||||
|
f"voxel_um={voxel_um} triangles={len(triangles)} lines={len(results)} "
|
||||||
|
f"selected_tiles={selected_tiles} bucket_cell_m={cell_size:.3f} "
|
||||||
|
f"coverage_min={min_coverage:.3f} sampled_height_m={sampled_min:.6f}.."
|
||||||
|
f"{sampled_max:.6f} amplitude_range_m={amplitude:.6f} "
|
||||||
|
f"longest_flat_run_m={longest_plateau:.6f} following_step_m={largest_step:.6f} "
|
||||||
|
f"max_normal_delta_deg={max_normal_jump:.6f} status={'PASS' if passed else 'FAIL'}")
|
||||||
|
for name, points, result in results:
|
||||||
|
print(f"[RingTerraceProfile] line={name} coverage={result['coverage']:.3f} "
|
||||||
|
f"range_m={result['height_range']:.6f} flat_step_runs={len(result['plateaus'])} "
|
||||||
|
f"max_normal_delta_deg={result['max_angle']:.6f}")
|
||||||
|
for run in result["plateaus"]:
|
||||||
|
print(f"[RingTerraceViolation] line={name} flat_start_m=({points[run[2]][0]:.3f},"
|
||||||
|
f"{points[run[2]][1]:.3f}) flat_run_m={run[0]:.6f} "
|
||||||
|
f"step_m={run[1]:.6f} from_height_m={run[5]:.6f} "
|
||||||
|
f"to_height_m={run[6]:.6f}")
|
||||||
|
print(f"[RingTerraceRSS] peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
raise SystemExit(0 if passed else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Encode the near/far tint-on/off L32 captures as compact JPEGs."""
|
||||||
|
import resource
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
SOURCE = ROOT / "Saved/Linux/l32"
|
||||||
|
DEST = Path(__file__).resolve().parent / "captures"
|
||||||
|
DEST.mkdir(parents=True, exist_ok=True)
|
||||||
|
captures = {
|
||||||
|
"fi-floor-near-tint-on": "cap-final-fi-near-on",
|
||||||
|
"fi-floor-near-tint-off": "cap-final-fi-near-off",
|
||||||
|
"fi-floor-far-tint-on": "cap-final-fi-far-on",
|
||||||
|
"fi-floor-far-tint-off": "cap-final-fi-far-off",
|
||||||
|
"sw-ceiling-near-tint-on": "cap-final-sw-near-on",
|
||||||
|
"sw-ceiling-near-tint-off": "cap-final-sw-near-off",
|
||||||
|
"sw-ceiling-far-tint-on": "cap-final-sw-far-on",
|
||||||
|
"sw-ceiling-far-tint-off": "cap-final-sw-far-off",
|
||||||
|
}
|
||||||
|
failed = False
|
||||||
|
for output_name, input_name in captures.items():
|
||||||
|
source = SOURCE / f"{input_name}.png"
|
||||||
|
destination = DEST / f"{output_name}.jpg"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "ffmpeg",
|
||||||
|
"-y", "-hide_banner", "-loglevel", "error", "-i", str(source),
|
||||||
|
"-frames:v", "1", "-q:v", "4", str(destination),
|
||||||
|
]
|
||||||
|
result = subprocess.run(command, check=False)
|
||||||
|
size = destination.stat().st_size if destination.exists() else 0
|
||||||
|
ok = source.exists() and result.returncode == 0 and size <= 300_000
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L32CaptureJPEG] source={source.name} output={destination.name} "
|
||||||
|
f"bytes={size} status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
print(f"[L32CaptureJPEG] peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}", flush=True)
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Copy only tiles for one LOD and spatial window from a VFMSH scene."""
|
||||||
|
import argparse
|
||||||
|
import resource
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("source")
|
||||||
|
parser.add_argument("destination")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--bounds", nargs=6, type=float, required=True,
|
||||||
|
metavar=("XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not 0 <= args.lod <= 8:
|
||||||
|
parser.error("--lod must be between 0 and 8")
|
||||||
|
x_min, x_max, y_min, y_max, z_min, z_max = args.bounds
|
||||||
|
if x_max <= x_min or y_max <= y_min or z_max <= z_min:
|
||||||
|
parser.error("bounds must have positive extents")
|
||||||
|
|
||||||
|
selected = []
|
||||||
|
with open(args.source, "rb") as src:
|
||||||
|
prefix = src.read(60)
|
||||||
|
if len(prefix) != 60 or prefix[:8] != b"VFMSH001":
|
||||||
|
raise SystemExit("source has an invalid VFMSH001 header")
|
||||||
|
version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8)
|
||||||
|
if version != 1 or voxel_um == 0 or tile_count > 1_000_000:
|
||||||
|
raise SystemExit("source has an unsupported scene header")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
header = src.read(36)
|
||||||
|
if len(header) != 36:
|
||||||
|
raise SystemExit("truncated tile header")
|
||||||
|
key = struct.unpack_from("<4i", header)
|
||||||
|
_, _, vertex_count, index_count, mask_count = struct.unpack_from("<5I", header, 16)
|
||||||
|
payload_offset = src.tell()
|
||||||
|
payload_size = vertex_count * 24 + index_count * 4 + mask_count
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
tx0, ty0, tz0 = key[0] * extent, key[1] * extent, key[2] * extent
|
||||||
|
intersects = (key[3] == args.lod and tx0 <= x_max and tx0 + extent >= x_min
|
||||||
|
and ty0 <= y_max and ty0 + extent >= y_min
|
||||||
|
and tz0 <= z_max and tz0 + extent >= z_min)
|
||||||
|
if intersects:
|
||||||
|
selected.append((header, payload_offset, payload_size))
|
||||||
|
src.seek(payload_size, 1)
|
||||||
|
if src.read(1):
|
||||||
|
raise SystemExit("source scene has trailing bytes")
|
||||||
|
|
||||||
|
if not selected:
|
||||||
|
raise SystemExit("no tiles matched the requested LOD and bounds")
|
||||||
|
with open(args.source, "rb") as src, open(args.destination, "wb") as dst:
|
||||||
|
prefix = bytearray(src.read(60))
|
||||||
|
struct.pack_into("<I", prefix, 16, len(selected))
|
||||||
|
dst.write(prefix)
|
||||||
|
for header, payload_offset, payload_size in selected:
|
||||||
|
dst.write(header)
|
||||||
|
src.seek(payload_offset)
|
||||||
|
remaining = payload_size
|
||||||
|
while remaining:
|
||||||
|
block = src.read(min(1 << 20, remaining))
|
||||||
|
if not block:
|
||||||
|
raise SystemExit("truncated tile payload")
|
||||||
|
dst.write(block)
|
||||||
|
remaining -= len(block)
|
||||||
|
print(f"[VFMeshLodWindow] source={args.source} lod={args.lod} "
|
||||||
|
f"selected_tiles={len(selected)} output={args.destination} "
|
||||||
|
f"output_bytes={__import__('os').path.getsize(args.destination)} "
|
||||||
|
f"peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss} status=PASS")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stream VFMSH tiles and retain a focused, near-horizontal surface patch."""
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("source")
|
||||||
|
parser.add_argument("destination")
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, required=True, metavar=("X", "Y"))
|
||||||
|
parser.add_argument("--radius-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-m", nargs=2, type=float, required=True, metavar=("MIN", "MAX"))
|
||||||
|
parser.add_argument("--min-abs-normal-z", type=float, default=0.7)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.radius_m <= 0 or args.height_m[1] <= args.height_m[0]:
|
||||||
|
parser.error("radius and height interval must be positive")
|
||||||
|
if not 0.0 <= args.min_abs_normal_z <= 1.0:
|
||||||
|
parser.error("--min-abs-normal-z must be in [0,1]")
|
||||||
|
|
||||||
|
cx, cy = args.center
|
||||||
|
r2 = args.radius_m * args.radius_m
|
||||||
|
zmin, zmax = args.height_m
|
||||||
|
input_tiles = kept_tiles = kept_triangles = 0
|
||||||
|
with open(args.source, "rb") as src, open(args.destination, "wb+") as dst:
|
||||||
|
prefix = bytearray(src.read(60))
|
||||||
|
if len(prefix) != 60 or prefix[:8] != b"VFMSH001":
|
||||||
|
raise SystemExit("source has an invalid VFMSH001 header")
|
||||||
|
version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8)
|
||||||
|
if version != 1 or voxel_um == 0 or tile_count > 1_000_000:
|
||||||
|
raise SystemExit("source has an unsupported scene header")
|
||||||
|
struct.pack_into("<I", prefix, 16, 0)
|
||||||
|
dst.write(prefix)
|
||||||
|
for _ in range(tile_count):
|
||||||
|
header = src.read(36)
|
||||||
|
if len(header) != 36:
|
||||||
|
raise SystemExit("truncated tile header")
|
||||||
|
key = struct.unpack_from("<4i", header)
|
||||||
|
surface_vertices, skirt_count, vertex_count, index_count, mask_count = \
|
||||||
|
struct.unpack_from("<5I", header, 16)
|
||||||
|
if vertex_count > 2_000_000 or index_count > 12_000_000:
|
||||||
|
raise SystemExit("tile exceeds the focused patch memory limit")
|
||||||
|
vertex_bytes = src.read(vertex_count * 24)
|
||||||
|
index_bytes = src.read(index_count * 4)
|
||||||
|
masks = src.read(mask_count)
|
||||||
|
if (len(vertex_bytes) != vertex_count * 24
|
||||||
|
or len(index_bytes) != index_count * 4 or len(masks) != mask_count):
|
||||||
|
raise SystemExit("truncated tile payload")
|
||||||
|
input_tiles += 1
|
||||||
|
positions = list(struct.iter_unpack("<3f", vertex_bytes[:vertex_count * 12]))
|
||||||
|
indices = struct.unpack(f"<{index_count}I", index_bytes) if index_count else ()
|
||||||
|
face_masks = masks if mask_count else bytes(index_count // 12)
|
||||||
|
filtered = []
|
||||||
|
for tri in range(index_count // 3):
|
||||||
|
i0, i1, i2 = indices[tri * 3:tri * 3 + 3]
|
||||||
|
if i0 >= vertex_count or i1 >= vertex_count or i2 >= vertex_count:
|
||||||
|
raise SystemExit("tile index points outside its vertex array")
|
||||||
|
if face_masks[tri] != 0:
|
||||||
|
continue
|
||||||
|
p0, p1, p2 = positions[i0], positions[i1], positions[i2]
|
||||||
|
mx = (p0[0] + p1[0] + p2[0]) / 3.0
|
||||||
|
my = (p0[1] + p1[1] + p2[1]) / 3.0
|
||||||
|
mz = (p0[2] + p1[2] + p2[2]) / 3.0
|
||||||
|
if (mx - cx) ** 2 + (my - cy) ** 2 > r2 or not zmin <= mz <= zmax:
|
||||||
|
continue
|
||||||
|
ab = (p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2])
|
||||||
|
ac = (p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2])
|
||||||
|
nz = ab[0] * ac[1] - ab[1] * ac[0]
|
||||||
|
length = math.sqrt((ab[1] * ac[2] - ab[2] * ac[1]) ** 2
|
||||||
|
+ (ab[2] * ac[0] - ab[0] * ac[2]) ** 2 + nz * nz)
|
||||||
|
if length == 0 or abs(nz) / length < args.min_abs_normal_z:
|
||||||
|
continue
|
||||||
|
filtered.extend((i0, i1, i2))
|
||||||
|
if not filtered:
|
||||||
|
continue
|
||||||
|
tile_header = struct.pack("<4i5I", *key, surface_vertices, 0,
|
||||||
|
vertex_count, len(filtered), 0)
|
||||||
|
dst.write(tile_header)
|
||||||
|
dst.write(vertex_bytes)
|
||||||
|
dst.write(struct.pack(f"<{len(filtered)}I", *filtered))
|
||||||
|
kept_tiles += 1
|
||||||
|
kept_triangles += len(filtered) // 3
|
||||||
|
dst.seek(16)
|
||||||
|
dst.write(struct.pack("<I", kept_tiles))
|
||||||
|
|
||||||
|
if kept_tiles == 0:
|
||||||
|
raise SystemExit("no surface triangles matched the requested patch")
|
||||||
|
print(f"[VFMeshSurfacePatch] source={args.source} input_tiles={input_tiles} "
|
||||||
|
f"tiles={kept_tiles} triangles={kept_triangles} center=({cx:.3f},{cy:.3f}) "
|
||||||
|
f"radius_m={args.radius_m:.3f} height_m={zmin:.3f}..{zmax:.3f} "
|
||||||
|
f"peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss} status=PASS")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Find VFMSH tiles intersecting a small region without reading mesh payloads."""
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("directory")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--bounds", nargs=6, type=float, required=True,
|
||||||
|
metavar=("XMIN", "XMAX", "YMIN", "YMAX", "ZMIN", "ZMAX"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
xmin, xmax, ymin, ymax, zmin, zmax = args.bounds
|
||||||
|
matches = 0
|
||||||
|
for path in sorted(glob.glob(os.path.join(args.directory, "*.vfmesh"))):
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
continue
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.seek(40, 1)
|
||||||
|
if version != 1:
|
||||||
|
continue
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
x0, y0, z0 = (key[axis] * extent for axis in range(3))
|
||||||
|
intersects = (key[3] == args.lod and index_count > 0
|
||||||
|
and x0 <= xmax and x0 + extent >= xmin
|
||||||
|
and y0 <= ymax and y0 + extent >= ymin
|
||||||
|
and z0 <= zmax and z0 + extent >= zmin)
|
||||||
|
if intersects:
|
||||||
|
matches += 1
|
||||||
|
print(f"[VFMeshTileWindow] scene={os.path.basename(path)} key={key} "
|
||||||
|
f"vertices={vertex_count} indices={index_count} "
|
||||||
|
f"extent_m={extent:.3f}")
|
||||||
|
stream.seek(vertex_count * 24 + index_count * 4 + mask_count, 1)
|
||||||
|
print(f"[VFMeshTileWindowSummary] matching_tiles={matches} "
|
||||||
|
f"peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stream selected-LOD tiles and find face windows that pass the ring audit."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
parser.add_argument("--preferred-height-m", type=float,
|
||||||
|
help="lock overlapping-face selection near this expected surface height")
|
||||||
|
parser.add_argument("--step-m", type=float, default=0.5)
|
||||||
|
parser.add_argument("--tile-key", nargs=3, type=int,
|
||||||
|
help="audit only this tile's x/y/z key (LOD is supplied separately)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
audit_path = Path(__file__).with_name("AuditRingTerraceL32.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_audit_l32", audit_path)
|
||||||
|
audit = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(audit)
|
||||||
|
want_up = args.surface == "floor"
|
||||||
|
candidates = []
|
||||||
|
with open(args.scene, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit("invalid VFMSH001 scene")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.seek(40, 1)
|
||||||
|
if version != 1:
|
||||||
|
raise SystemExit("unsupported mesh scene")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
header = stream.read(36)
|
||||||
|
key = struct.unpack("<4i", header[:16])
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", header[16:])
|
||||||
|
payload = vertex_count * 24 + index_count * 4 + mask_count
|
||||||
|
if key[3] != args.lod or (args.tile_key and tuple(key[:3]) != tuple(args.tile_key)):
|
||||||
|
stream.seek(payload, 1)
|
||||||
|
continue
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
triangles = []
|
||||||
|
vertex_margin = voxel_um / 1_000_000.0 * 0.5
|
||||||
|
for offset in range(0, index_count, 3):
|
||||||
|
if masks and masks[offset // 3]:
|
||||||
|
continue
|
||||||
|
ids = indices[offset:offset + 3]
|
||||||
|
if max(ids) >= surface_count:
|
||||||
|
continue
|
||||||
|
verts = []
|
||||||
|
for index in ids:
|
||||||
|
p = index * 3
|
||||||
|
verts.append(((positions[p], positions[p + 1], positions[p + 2]),
|
||||||
|
(normals[p], normals[p + 1], normals[p + 2])))
|
||||||
|
average_nz = sum(vertex[1][2] for vertex in verts) / 3.0
|
||||||
|
if (want_up and average_nz < 0.35) or (not want_up and average_nz > -0.35):
|
||||||
|
continue
|
||||||
|
z_values = [vertex[0][2] for vertex in verts]
|
||||||
|
mean_z = sum(z_values) / 3.0
|
||||||
|
if (mean_z < args.height_min_m or mean_z > args.height_max_m or
|
||||||
|
min(z_values) < args.height_min_m - vertex_margin or
|
||||||
|
max(z_values) > args.height_max_m + vertex_margin):
|
||||||
|
continue
|
||||||
|
triangles.append((key[3], average_nz, tuple(verts)))
|
||||||
|
if not triangles:
|
||||||
|
continue
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
cx, cy = (key[0] + 0.5) * extent, (key[1] + 0.5) * extent
|
||||||
|
radius = extent * 0.5
|
||||||
|
bounds = (cx - radius, cx + radius, cy - radius, cy + radius)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << key[3]))
|
||||||
|
buckets = audit.build_buckets(triangles, want_up, bounds, cell_size)
|
||||||
|
profiles = []
|
||||||
|
preferred_height = args.preferred_height_m
|
||||||
|
if preferred_height is None:
|
||||||
|
preferred_height = args.height_max_m if want_up else args.height_min_m
|
||||||
|
for _, points in audit.make_lines(cx, cy, radius, args.step_m):
|
||||||
|
profiles.append(audit.audit_profile(points, buckets, want_up,
|
||||||
|
0.5, 0.0005, 0.1, cell_size, preferred_height))
|
||||||
|
valid = [profile for profile in profiles if profile["valid"]]
|
||||||
|
coverage = min(profile["coverage"] for profile in profiles)
|
||||||
|
amplitude = max((profile["height_max"] for profile in valid), default=0.0) - \
|
||||||
|
min((profile["height_min"] for profile in valid), default=0.0)
|
||||||
|
longest = max((run[0] for profile in valid for run in profile["plateaus"]),
|
||||||
|
default=0.0)
|
||||||
|
max_normal = max((profile["max_angle"] for profile in valid), default=float("inf"))
|
||||||
|
passed = (len(valid) == len(profiles) and coverage >= 0.75 and amplitude >= 3.0
|
||||||
|
and longest < 0.5 and max_normal <= 20.0)
|
||||||
|
candidates.append((passed, amplitude, -max_normal, key, cx, cy,
|
||||||
|
len(triangles), coverage, longest, max_normal))
|
||||||
|
stream.seek(payload - (vertex_count * 24 + index_count * 4 + mask_count), 1)
|
||||||
|
candidates.sort(reverse=True, key=lambda item: (item[0], item[1], item[2]))
|
||||||
|
print(f"[RingWindowSearch] lod={args.lod} surface={args.surface} tiles={len(candidates)} "
|
||||||
|
f"height_window_m={args.height_min_m:.3f}..{args.height_max_m:.3f} "
|
||||||
|
f"step_m={args.step_m:.3f}")
|
||||||
|
for passed, amplitude, neg_normal, key, cx, cy, count, coverage, longest, normal in candidates[:40]:
|
||||||
|
print(f"[RingWindowCandidate] status={'PASS' if passed else 'FAIL'} key={key} "
|
||||||
|
f"center_m=({cx:.3f},{cy:.3f}) triangles={count} coverage={coverage:.3f} "
|
||||||
|
f"amplitude_m={amplitude:.6f} longest_flat_m={longest:.6f} "
|
||||||
|
f"normal_jump_deg={normal:.6f}")
|
||||||
|
print(f"[RingWindowRSS] peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Find open SurfaceWorld columns below the resolved transition ceiling."""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("probe")
|
||||||
|
parser.add_argument("owner")
|
||||||
|
parser.add_argument("output")
|
||||||
|
parser.add_argument("--z", type=float, default=-130.0)
|
||||||
|
parser.add_argument("--minimum", type=float, default=-96.0)
|
||||||
|
parser.add_argument("--maximum", type=float, default=96.0)
|
||||||
|
parser.add_argument("--spacing", type=float, default=16.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
probe = str(Path(args.probe).resolve())
|
||||||
|
owner = str(Path(args.owner).resolve())
|
||||||
|
output = Path(args.output)
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
rows = []
|
||||||
|
count = round((args.maximum - args.minimum) / args.spacing)
|
||||||
|
for iy in range(count + 1):
|
||||||
|
y = args.minimum + iy * args.spacing
|
||||||
|
for ix in range(count + 1):
|
||||||
|
x = args.minimum + ix * args.spacing
|
||||||
|
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
probe, owner, "surface-world", str(x), str(y),
|
||||||
|
str(args.z - 2.0), str(args.z + 2.0), "0.25"]
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True, timeout=1800,
|
||||||
|
check=False)
|
||||||
|
samples = [line for line in result.stdout.splitlines()
|
||||||
|
if line.startswith(f"{args.z:.6f},")]
|
||||||
|
density = float(samples[0].split(",")[1]) if result.returncode == 0 and samples else float("nan")
|
||||||
|
rows.append((x, y, args.z, density, result.returncode))
|
||||||
|
print(f"[SurfaceWorldAirScan] x_m={x:.1f} y_m={y:.1f} z_m={args.z:.2f} "
|
||||||
|
f"density_vox={density:.6f} exit={result.returncode}", flush=True)
|
||||||
|
with output.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.writer(stream)
|
||||||
|
writer.writerow(("x_m", "y_m", "z_m", "density_vox", "exit_code"))
|
||||||
|
writer.writerows(rows)
|
||||||
|
open_samples = [row for row in rows if row[3] > 0.0]
|
||||||
|
print(f"[SurfaceWorldAirScanSummary] samples={len(rows)} open={len(open_samples)} "
|
||||||
|
f"output={output} status={'PASS' if open_samples else 'FAIL'}")
|
||||||
|
return 0 if open_samples else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#include "Json.h"
|
||||||
|
#include "OwnerWorldLoader.h"
|
||||||
|
#include "StandaloneCoreScheduler.h"
|
||||||
|
#include "WorldAssemblyPrototype.h"
|
||||||
|
#include "StandaloneSnapshotAdapter.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
int main(int argc,char** argv)
|
||||||
|
{
|
||||||
|
if(argc!=8)
|
||||||
|
{
|
||||||
|
std::fprintf(stderr,"usage: %s owner.json surface-world|floating-islands x_m y_m zmin_m zmax_m step_m\n",argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const std::string recipe=argv[2];
|
||||||
|
if(recipe!="surface-world"&&recipe!="floating-islands")return 2;
|
||||||
|
const double x=std::strtod(argv[3],nullptr),y=std::strtod(argv[4],nullptr);
|
||||||
|
const double zmin=std::strtod(argv[5],nullptr),zmax=std::strtod(argv[6],nullptr);
|
||||||
|
const double step=std::strtod(argv[7],nullptr);
|
||||||
|
if(!std::isfinite(x)||!std::isfinite(y)||!std::isfinite(zmin)||!std::isfinite(zmax)
|
||||||
|
||!std::isfinite(step)||zmax<=zmin||step<=0.0)return 2;
|
||||||
|
|
||||||
|
VFJson::Value root;
|
||||||
|
std::string error;
|
||||||
|
{
|
||||||
|
std::ifstream input(argv[1],std::ios::binary);
|
||||||
|
if(!input){std::fprintf(stderr,"owner JSON open failed\n");return 2;}
|
||||||
|
std::string text{std::istreambuf_iterator<char>(input),std::istreambuf_iterator<char>{}};
|
||||||
|
if(!VFJson::Parser(std::move(text)).Parse(root,error))
|
||||||
|
{std::fprintf(stderr,"owner JSON parse failed: %s\n",error.c_str());return 2;}
|
||||||
|
}
|
||||||
|
if(recipe=="floating-islands"
|
||||||
|
&&!StandaloneWorldAssemblyPrototype::SelectTopStrateAsFloatingIslands(root,error))
|
||||||
|
{std::fprintf(stderr,"FI selection failed: %s\n",error.c_str());return 2;}
|
||||||
|
auto owner=std::make_shared<FStandaloneOwnerWorld>();
|
||||||
|
if(!StandaloneOwnerLoader::Resolve(root,*owner,error))
|
||||||
|
{std::fprintf(stderr,"owner resolve failed: %s\n",error.c_str());return 2;}
|
||||||
|
VoxelForgeCore::WorldAssemblyManifest assembly;
|
||||||
|
if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
||||||
|
*owner,root,error,{},&assembly))
|
||||||
|
{std::fprintf(stderr,"assembly setup failed: %s\n",error.c_str());return 2;}
|
||||||
|
if(recipe=="floating-islands"
|
||||||
|
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
||||||
|
*owner,root,assembly,error))
|
||||||
|
{std::fprintf(stderr,"FI setup failed: %s\n",error.c_str());return 2;}
|
||||||
|
VoxelForgeCore::ResolvedLayoutSnapshotV3 resolvedLayout;
|
||||||
|
if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
||||||
|
*owner,assembly,recipe,resolvedLayout,error))
|
||||||
|
{std::fprintf(stderr,"resolved layout build failed: %s\n",error.c_str());return 2;}
|
||||||
|
auto* rootObject=std::get_if<VFJson::Value::Object>(&root.Data);
|
||||||
|
if(!rootObject)return 2;
|
||||||
|
VFJson::Value layoutJson;
|
||||||
|
layoutJson.Data=resolvedLayout.ToJson();
|
||||||
|
(*rootObject)["resolved_layout_json"]=std::move(layoutJson);
|
||||||
|
auto snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(
|
||||||
|
root,owner->Seed,3,8,false,false,error);
|
||||||
|
if(!snapshot)
|
||||||
|
{std::fprintf(stderr,"snapshot resolve failed: %s\n",error.c_str());return 2;}
|
||||||
|
const auto* layout=snapshot.resolvedLayout();
|
||||||
|
if(!layout){std::fprintf(stderr,"resolved layout missing\n");return 2;}
|
||||||
|
for(const auto& stratum:layout->strates)
|
||||||
|
std::printf("[DensityStratum] order=%d strate=%d generator=%s mode=%s width_um=%lld bedrock_um=%lld..%lld ceiling_um=%lld..%lld\n",
|
||||||
|
stratum.order,stratum.strateIndex,stratum.generatorKind.c_str(),
|
||||||
|
stratum.transitionMode.c_str(),static_cast<long long>(stratum.transitionWidthMicrometres),
|
||||||
|
static_cast<long long>(stratum.bedrock.bottomMicrometres),
|
||||||
|
static_cast<long long>(stratum.bedrock.topMicrometres),
|
||||||
|
static_cast<long long>(stratum.ceiling.bottomMicrometres),
|
||||||
|
static_cast<long long>(stratum.ceiling.topMicrometres));
|
||||||
|
const auto interface=std::find_if(layout->strates.begin(),layout->strates.end(),
|
||||||
|
[](const auto& stratum){return stratum.order==0;});
|
||||||
|
if(interface==layout->strates.end())return 2;
|
||||||
|
|
||||||
|
VoxelForgeCore::World world(std::move(snapshot));
|
||||||
|
VoxelForgeCore::ShapeBuildRequest request;
|
||||||
|
request.minVoxel={static_cast<std::int32_t>(std::floor((x-32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::floor((y-32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::floor((zmin-4.0)*4.0))};
|
||||||
|
request.maxVoxel={static_cast<std::int32_t>(std::ceil((x+32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::ceil((y+32.0)*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::ceil((zmax+4.0)*4.0))};
|
||||||
|
request.clipMinVoxel=request.minVoxel;
|
||||||
|
request.clipMaxVoxel=request.maxVoxel;
|
||||||
|
request.workerLimit=12;
|
||||||
|
request.includeFarWorld=false;
|
||||||
|
FStandaloneCoreScheduler scheduler;
|
||||||
|
if(!world.compile(request,scheduler,error))
|
||||||
|
{std::fprintf(stderr,"shape compile failed: %s\n",error.c_str());return 2;}
|
||||||
|
|
||||||
|
std::printf("[DensityLine] recipe=%s mode=%s width_um=%lld seam_um=%lld band_top_um=%lld band_bottom_um=%lld xy_m=%.6f,%.6f core_voxel_m=0.25 status=PASS\n",
|
||||||
|
recipe.c_str(),interface->transitionMode.c_str(),
|
||||||
|
static_cast<long long>(interface->transitionWidthMicrometres),
|
||||||
|
static_cast<long long>(interface->bedrock.bottomMicrometres),
|
||||||
|
static_cast<long long>(interface->bedrock.topMicrometres),
|
||||||
|
static_cast<long long>(interface->bedrock.bottomMicrometres),x,y);
|
||||||
|
std::printf("z_m,density_vox,lattice_vox,unclamped_vox,dispatch\n");
|
||||||
|
const auto count=static_cast<std::size_t>(std::floor((zmax-zmin)/step+1.0e-9));
|
||||||
|
for(std::size_t i=0;i<=count;++i)
|
||||||
|
{
|
||||||
|
const double z=zmin+static_cast<double>(i)*step;
|
||||||
|
const VoxelForgeCore::Vec3d p{x*4.0,y*4.0,z*4.0};
|
||||||
|
const VoxelForgeCore::Vec3i lattice{
|
||||||
|
static_cast<std::int32_t>(std::llround(x*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::llround(y*4.0)),
|
||||||
|
static_cast<std::int32_t>(std::llround(z*4.0))};
|
||||||
|
std::printf("%.6f,%.9f,%.9f,%.9f,%s\n",z,world.densityAt(p),
|
||||||
|
world.densityAtLattice(lattice),world.unclampedDensityAt(p),
|
||||||
|
world.densityDispatchPathAt(p).c_str());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Dump a bounded surface line from a VFMSH scene using the ring-audit sampler."""
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import resource
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--center", nargs=2, type=float, required=True)
|
||||||
|
parser.add_argument("--radius", type=float, required=True)
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
parser.add_argument("--line", choices=("x", "y", "diagonal"), default="x")
|
||||||
|
parser.add_argument("--stride-m", type=float, default=8.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
audit_path = Path(__file__).with_name("AuditRingTerraceL32.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("ring_audit_l32", audit_path)
|
||||||
|
audit = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(audit)
|
||||||
|
cx, cy = args.center
|
||||||
|
bounds = (cx - args.radius, cx + args.radius,
|
||||||
|
cy - args.radius, cy + args.radius)
|
||||||
|
voxel_um, triangles, tiles = audit.read_scene(args.scene, args.lod,
|
||||||
|
args.height_min_m, args.height_max_m, bounds)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << args.lod))
|
||||||
|
buckets = audit.build_buckets(triangles, args.surface == "floor", bounds, cell_size)
|
||||||
|
count = round(2.0 * args.radius / args.stride_m)
|
||||||
|
print(f"[SurfaceProfile] lod={args.lod} surface={args.surface} tiles={tiles} "
|
||||||
|
f"triangles={len(triangles)} line={args.line} stride_m={args.stride_m}")
|
||||||
|
print("index,x_m,y_m,z_m,nx,ny,nz")
|
||||||
|
for index in range(count + 1):
|
||||||
|
t = -args.radius + index * args.stride_m
|
||||||
|
if args.line == "x":
|
||||||
|
x, y = cx + t, cy
|
||||||
|
elif args.line == "y":
|
||||||
|
x, y = cx, cy + t
|
||||||
|
else:
|
||||||
|
x, y = cx + t / 2.0**0.5, cy + t / 2.0**0.5
|
||||||
|
preferred_height = args.height_max_m if args.surface == "floor" else args.height_min_m
|
||||||
|
hit = audit.sample_surface(buckets, x, y, args.surface == "floor", cell_size,
|
||||||
|
preferred_height)
|
||||||
|
if hit is None:
|
||||||
|
print(f"{index},{x:.3f},{y:.3f},nan,nan,nan,nan")
|
||||||
|
else:
|
||||||
|
z, normal = hit
|
||||||
|
print(f"{index},{x:.3f},{y:.3f},{z:.6f}," +
|
||||||
|
",".join(f"{value:.7f}" for value in normal))
|
||||||
|
print(f"[SurfaceProfileRSS] peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare the v20 transition-column cache with the uncached v20 binary."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
BINARIES = {
|
||||||
|
"base": OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20",
|
||||||
|
"cache": OUT / "bin/transition-cache/VoxelForgeCoreHeadless-l32-transition-cache",
|
||||||
|
}
|
||||||
|
ORDER = (("base", "cache"), ("cache", "base"), ("base", "cache"),
|
||||||
|
("cache", "base"), ("base", "cache"))
|
||||||
|
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for pair, builds in enumerate(ORDER, 1):
|
||||||
|
for build in builds:
|
||||||
|
name = f"cache-compare-sw-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(BINARIES[build]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", "--recipe=surface-world",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
valid = result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
csv = OUT / "transition-cache-compare.csv"
|
||||||
|
csv.write_text(
|
||||||
|
"pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
for build in BINARIES:
|
||||||
|
values = [row[2] for row in rows if row[1] == build]
|
||||||
|
values.sort()
|
||||||
|
median = values[len(values) // 2]
|
||||||
|
print(f"[TransitionCacheCost] build={build} median_whole_view_ready_s={median:.3f}")
|
||||||
|
print(f"[TransitionCacheCost] runs={len(rows)} status={'FAIL' if failed else 'PASS'}")
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 10 else 0)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# VoxelForge L32 report
|
||||||
|
|
||||||
|
## Cause and fix
|
||||||
|
|
||||||
|
The old Gradient transition wrote constant solid-density values per band. Those values are not signed distance to the displaced boundary, so Marching Cubes crossed band levels as contour-like terraces. The hash surface quantization was not the cause: sampling across the final FI floor gives density -0.53345 at z=-121.50 m and +0.46655 at -121.25 m; the SW ceiling gives +0.01514 at -126.50 m and -0.98486 at -126.25 m. Both cross linearly at about 4 density units/m.
|
||||||
|
|
||||||
|
L32 restores the width/8 Gradient displacement (2 m each side, about 4 m peak-to-peak at 16 m owner width). Density is now signed distance to the union of the displaced solid intervals, continuous across band boundaries. Cap samples follow the same displaced faces. Passage-mouth fades use cubic smoothstep across a 2 m fade from the protected mouth radius; this preserves the face while blending the column flare. Version/evaluator is v20.
|
||||||
|
|
||||||
|
## Surface and interface audits
|
||||||
|
|
||||||
|
Branch-locked radial and straight profiles pass on both faces at LOD0 through LOD8. Every tested LOD has at least 3.15 m height range; no repeated-height plateau or following step was found. LOD0/1 profiles use 0.125 m samples (SW LOD1 also checked at 0.0625 m); LOD2–8 use 0.5 m samples. The largest adjacent-normal change was 17.50° on the fine SW LOD1 scan, below the 20° audit limit. Peak RSS for the final Python ring scans was 373,596 KiB (FI LOD0); the passage-fade scans peaked at 52,128 KiB. Mesh scans use tile/chunk processing.
|
||||||
|
|
||||||
|
Interleaved also passes 11 radial/straight profiles at each LOD0–8: all selected windows have full coverage, no flats or steps, and at most 0.367° adjacent-normal change. Their local height ranges are 0.032–3.174 m; the 3 m minimum is the Gradient amplitude check, not an Interleaved requirement. Peak RSS for this tile-streamed sweep was 24,468 KiB.
|
||||||
|
|
||||||
|
Both ring negative controls fail as intended: L30 has 0.742 m range, a 1.875 m flat run followed by a 0.241 m step, and a 27.34° normal jump; L31's flat fix has only 0.000046 m range. The passage-mouth audit passes on both overlapping faces: upper max normal change 15.06°, lower 7.96°, with full coverage and no flats or steps.
|
||||||
|
|
||||||
|
Hole/interface and minimum-band audits pass for Hard, Gradient and Interleaved, for both recipes and compilers: zero hole columns and zero air samples. Wider-than-declared and thinner-than-minimum controls are rejected. Minimum bands are 0.5 m FI, 1.0 m SW and 6.25 m lower band; maximum Interleaved crossing is 6.487 m against the 16 m bound. Passage audit passes all 16 recipe-stamped rows; recipe-mismatch and disconnected-end controls are detected.
|
||||||
|
|
||||||
|
Determinism passes 24/24: the L25 command and 24 m crossing, both recipes, Clang and GCC, workers 1/8/12.
|
||||||
|
|
||||||
|
## Captures and hashes
|
||||||
|
|
||||||
|
Tint-on and tint-off JPEGs are in `captures/`; all are under 30 KB (limit 300 KB):
|
||||||
|
|
||||||
|
- `fi-floor-near-tint-on.jpg`, `fi-floor-near-tint-off.jpg`
|
||||||
|
- `fi-floor-far-tint-on.jpg`, `fi-floor-far-tint-off.jpg`
|
||||||
|
- `sw-ceiling-near-tint-on.jpg`, `sw-ceiling-near-tint-off.jpg`
|
||||||
|
- `sw-ceiling-far-tint-on.jpg`, `sw-ceiling-far-tint-off.jpg`
|
||||||
|
|
||||||
|
They show the FI floor and SW ceiling from inside each stratum, with grazing light, at LOD0 and LOD8. Capture SHA256 values:
|
||||||
|
|
||||||
|
| Capture | SHA256 |
|
||||||
|
|---|---|
|
||||||
|
| fi-floor-far-tint-off.jpg | `239b6be70c177e267d2db95f206fdcdc8834779a7948fdaaddd136b4163d1f34` |
|
||||||
|
| fi-floor-far-tint-on.jpg | `3ecefd555275036a7045a541f02bb1873e22302495009dbd80b4ac58ce716b58` |
|
||||||
|
| fi-floor-near-tint-off.jpg | `847f10acfb420c2bbded61bdee7f578cde19469777b2ef4c5a0eba08b406611c` |
|
||||||
|
| fi-floor-near-tint-on.jpg | `4a5d3e3f8e8254639fa1840a2fb4497f2ab19f64bb4734e13abe3c3cfcfb4008` |
|
||||||
|
| sw-ceiling-far-tint-off.jpg | `f5ec099076c507bed5a703302d89b665e139804a8f8895a68c1d5705aabf9f61` |
|
||||||
|
| sw-ceiling-far-tint-on.jpg | `b82618814c8320be2de1ac53ba10e9ab4469299a8b53b776be32c6ca9a659988` |
|
||||||
|
| sw-ceiling-near-tint-off.jpg | `5789ce59a7748aa278c1b95edae45aec87721721260c1a3aefe6c4cc1e47cd7a` |
|
||||||
|
| sw-ceiling-near-tint-on.jpg | `aa4351c5effc5e93fc0879257fe056b3f4625e7842f5118f28588fb2c5c1afe2` |
|
||||||
|
|
||||||
|
Final Core binary SHA256:
|
||||||
|
|
||||||
|
- Clang: `052694a569c92ef407c25bd9a534c7123ef800f46733e5c428e965c32d905fdc`
|
||||||
|
- GCC: `c8656901ce18861f12168849165785937a2495233604314df9e005ed1e92afdd`
|
||||||
|
|
||||||
|
## Cost and edits
|
||||||
|
|
||||||
|
Five interleaved pairs per recipe against L31. Median whole-view-ready time was SW 1.723 s → 1.756 s (+1.92%) and FI 16.963 s → 17.221 s (+1.52%); paired results were mixed and within observed run noise. No Unreal files changed. The five edited files are the Core public header, `FarShellsInternal.h`, `ResolvedLayoutInternal.h`, `ShapeWorldInternal.h` and `LinuxOffscreenCapture.cpp`; Unreal compilation was not available here.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- The initial multi-LOD Python audit was stopped by system RAM pressure (reported peak unavailable). Subsequent mesh audits ran with CPU/address-space caps and streamed or selected tiles; peak RSS is recorded above. The first whole-mesh FI LOD8 candidate scan was interrupted and replaced by a targeted tile scan.
|
||||||
|
- Early ring-window searches and narrow-height audits selected the wrong one of two overlapping faces. The FI LOD3/4 branch probes measured 1.423 m amplitude / a 1.0 m flat run; the SW LOD1 low-face probe reached 20.19° normal change. Face-locked, broad-window scans replaced them and pass. The exploratory `ring-*`, `ringset-*`, `search-*` and `window-search-*` logs preserve rejected window candidates. The L30/L31 failures above are intentional negative controls.
|
||||||
|
- The first Interleaved layout/export probe omitted the runner's required streaming flags and exited with usage; the retry produced the LOD0–8 export. Its broad 0.5 m candidate scan applied the Gradient 3 m window heuristic and rejected LOD0–4 (one LOD1 candidate switched overlapping faces at 62.92°); selected branch-locked profiles at those LODs and passing LOD5–8 windows show no plateaus or steps. An unsupported `--help` probe also returned usage with exit 2.
|
||||||
|
- The exact-source passage audit at midpoint preference switched faces and reported a 47.79° jump; upper- and lower-face locked reruns pass. An early wrong-camera export selected zero tiles; the exact camera export was rerun. Initial multi-LOD capture meshes mixed neighboring shells, so the final captures use focused LOD0/LOD8 surface patches.
|
||||||
|
- One fine-normal probe had a tuple-index error and was corrected. A SW far-mesh filter had an argument/layout parsing error and was corrected. The density CSV was overwritten by verbose output; the probe was rebuilt from final Core objects and rerun.
|
||||||
|
- The transition-cache build failed on a nested `Shape` initializer; the retry built, but the cache showed no gain and was removed. `/usr/bin/time` was unavailable for one capture attempt; the capped renderer was rerun directly. The first determinism run was interrupted and resumed; the final matrix passes 24/24. One log-search command used unavailable `rg`; a capped streaming-Python scan replaced it.
|
||||||
|
- Performance experiments were rejected or superseded: initial transition path +36.11% SW/+7.18% FI, planar query +14.89%/+2.28%, 3x3 coverage +6.06%/+1.65%, 5-point coverage +4.04%/+2.03%, single-transition fast path +3.52%/+1.39%; cache trial was slower (2.188 s vs 2.171 s). The retained hash-prefix version is the five-pair result above. Logs and exports are under `Saved/Linux/l32/`.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged. Captures checked by me: the FI floor (near, tint off) is a smooth rolling surface with no rings or terraces;
|
||||||
|
the SW ceiling and far views are smooth. Gradient is restored at width/8 (about 4 m peak-to-peak) with signed-distance
|
||||||
|
density; the ring audit rejects both L30 (rings) and L31 (flat). The memory-pressure kill mid-round was resumed;
|
||||||
|
audits now run capped and tile-streamed.
|
||||||
|
- Cumulative SW cost vs L24 is roughly +5% after L28–L32 (features: columns, transitions, smooth faces). FI is ≈ +2%.
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the recipe-stamped L31 passage audit matrix and reject mislabeled runs."""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l32-gcc-v20",
|
||||||
|
}
|
||||||
|
MARKERS = (
|
||||||
|
"[ResolvedPassageAuditSummary]",
|
||||||
|
"[PassageOpenAirAuditSummary]",
|
||||||
|
"[PassageNaturalnessSummary]",
|
||||||
|
"[PassageNoLongBridgeAudit]",
|
||||||
|
"[PassageRockColumnAuditSummary]",
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for style in range(4):
|
||||||
|
data = ROOT / f"Saved/Linux/linux25-owner-{recipe}-style{style}.json"
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"passage-{recipe}-style{style}-{compiler}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||||
|
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
recipe_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
markers_ok = all(re.search(re.escape(marker) + r".*status=PASS", text)
|
||||||
|
for marker in MARKERS)
|
||||||
|
capsule_ok = bool(re.search(
|
||||||
|
r"^\[ResolvedPassageAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 mouth_content=2/2 mouth_radius=2/2 "
|
||||||
|
r"walkability=PASS$", text, re.M))
|
||||||
|
island_route_ok = (recipe != "floating-islands" or bool(re.search(
|
||||||
|
r"^\[FloatingIslandRouteAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 status=PASS$", text, re.M)))
|
||||||
|
binding_ok = bool(re.search(
|
||||||
|
r"\[ResolvedPassageAuditSummary\].*route_bindings=[1-9][0-9]* "
|
||||||
|
r"route_binding_failures=0 .*status=PASS", text))
|
||||||
|
walk_ok = "[Walkability] PASS" in text
|
||||||
|
if recipe == "floating-islands":
|
||||||
|
control_ok = "[PassageColumnDisconnectedEndControl]" in text and bool(
|
||||||
|
re.search(r"\[PassageColumnDisconnectedEndControl\].*detected=true", text))
|
||||||
|
else:
|
||||||
|
control_ok = True
|
||||||
|
ok = (result is not None and result.returncode == 0 and recipe_ok
|
||||||
|
and markers_ok and capsule_ok and island_route_ok and binding_ok
|
||||||
|
and walk_ok and control_ok)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "MISSING"
|
||||||
|
rows.append((recipe, actual_recipe, style, compiler, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s exit={result.returncode if result else 'TIMEOUT'} "
|
||||||
|
f"recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"capsule={'PASS' if capsule_ok and island_route_ok else 'FAIL'} "
|
||||||
|
f"column_control={'PASS' if control_ok else 'FAIL'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
if not ok:
|
||||||
|
failed = True
|
||||||
|
|
||||||
|
summary = OUT / "passage-audit-matrix.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"requested_recipe,actual_recipe,style,compiler,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{requested},{actual},{style},{compiler},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for requested, actual, style, compiler, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
control_log = OUT / "recipe-mismatch-control.log"
|
||||||
|
control_owner = OUT / "recipe-mismatch-control-owner.json"
|
||||||
|
control_owner_data = json.loads(
|
||||||
|
(ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json").read_text(encoding="utf-8"))
|
||||||
|
top_owner_stratum = max(control_owner_data["strates"], key=lambda stratum: stratum["top_chunk_z"])
|
||||||
|
top_owner_stratum["asset"]["GeneratorType"] = 6 # ECaveGeneratorType::FloatingIslands
|
||||||
|
control_owner.write_text(json.dumps(control_owner_data, separators=(",", ":")), encoding="utf-8")
|
||||||
|
control_command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", str(BINARIES["clang"]),
|
||||||
|
str(control_owner),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", "--recipe=surface-world",
|
||||||
|
"--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
with control_log.open("w", encoding="utf-8") as stream:
|
||||||
|
control = subprocess.run(control_command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
control_text = control_log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
control_ok = (control.returncode == 2 and
|
||||||
|
"[WorldAssemblerRecipeAudit] requested=surface-world actual=floating-islands "
|
||||||
|
"top_generator=FloatingIslands " in control_text and
|
||||||
|
"floating_islands_stratum=present status=FAIL" in control_text)
|
||||||
|
print(f"[L32RecipeMismatchControl] requested=surface-world actual=floating-islands "
|
||||||
|
f"exit={control.returncode} detected={'true' if control_ok else 'false'} "
|
||||||
|
f"{'PASS' if control_ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L32PassageMatrix] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or not control_ok or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run five interleaved whole-view pairs against the L31 binary."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
RUN_TAG = os.environ.get("VOXELFORGE_COST_TAG", "").strip()
|
||||||
|
BINARIES = {
|
||||||
|
"l31": ROOT / "Saved/Linux/l31/bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"l32": Path(os.environ.get("VOXELFORGE_L32_COST_BINARY",
|
||||||
|
str(OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20"))),
|
||||||
|
}
|
||||||
|
ORDERS = (("l31", "l32"), ("l32", "l31"), ("l31", "l32"),
|
||||||
|
("l32", "l31"), ("l31", "l32"))
|
||||||
|
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for pair, order in enumerate(ORDERS, 1):
|
||||||
|
for build in order:
|
||||||
|
tag = f"-{RUN_TAG}" if RUN_TAG else ""
|
||||||
|
name = f"cost{tag}-{recipe}-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(BINARIES[build]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
if build == "l32":
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "missing"
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
else:
|
||||||
|
configured = re.findall(r"^\[HeadlessConfig\].*\brecipe=([^\s]+)", text, re.M)
|
||||||
|
actual_recipe = configured[0] if len(configured) == 1 else "missing"
|
||||||
|
floating_plan = "[FloatingIslandWorldAssembly]" in text
|
||||||
|
surface_plan = "[WorldAssemblerPrototype]" in text
|
||||||
|
plan_matches = ((actual_recipe == "floating-islands" and floating_plan)
|
||||||
|
or (actual_recipe == "surface-world" and not floating_plan
|
||||||
|
and surface_plan))
|
||||||
|
marker = (f"[L32BaselineRecipeAudit] requested={recipe} actual={actual_recipe} "
|
||||||
|
f"headless_recipe={configured[0] if len(configured) == 1 else 'missing'} "
|
||||||
|
f"floating_island_plan={'present' if floating_plan else 'absent'} "
|
||||||
|
f"surface_world_plan={'present' if surface_plan else 'absent'} "
|
||||||
|
f"status={'PASS' if actual_recipe == recipe and plan_matches else 'FAIL'}\n")
|
||||||
|
with logfile.open("a", encoding="utf-8") as stream:
|
||||||
|
stream.write(marker)
|
||||||
|
text += marker
|
||||||
|
stamp_ok = actual_recipe == recipe and plan_matches and len(configured) == 1
|
||||||
|
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
and "hash=off" in text and "cache_hits=0" in text and stamp_ok)
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
summary = OUT / (f"cost-runs-{RUN_TAG}.csv" if RUN_TAG else "cost-runs.csv")
|
||||||
|
summary.write_text(
|
||||||
|
"recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||||
|
for build in BINARIES}
|
||||||
|
l31 = statistics.median(by_build["l31"])
|
||||||
|
l32 = statistics.median(by_build["l32"])
|
||||||
|
paired = []
|
||||||
|
for pair in range(1, 6):
|
||||||
|
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||||
|
paired.append((values["l32"] - values["l31"]) / values["l31"] * 100.0)
|
||||||
|
print(f"[L32CostSummary] recipe={recipe} pairs=5 median_l31_s={l31:.3f} "
|
||||||
|
f"median_l32_s={l32:.3f} median_change_pct={(l32/l31-1)*100.0:+.2f} "
|
||||||
|
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}")
|
||||||
|
|
||||||
|
print(f"[L32Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}",
|
||||||
|
flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check the L25 hash command and 24 m crossing across compilers and workers."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
RUN_TAG = os.environ.get("VOXELFORGE_DETERMINISM_TAG", "").strip()
|
||||||
|
RUN_RESUME = os.environ.get("VOXELFORGE_DETERMINISM_RESUME", "0") == "1"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": Path(os.environ.get("VOXELFORGE_L32_CLANG_BINARY",
|
||||||
|
str(OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20"))),
|
||||||
|
"gcc": Path(os.environ.get("VOXELFORGE_L32_GCC_BINARY",
|
||||||
|
str(OUT / "bin/VoxelForgeCoreHeadless-l32-gcc-v20"))),
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
candidate_reference = {}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
prior_elapsed = {}
|
||||||
|
if RUN_RESUME and RUN_TAG:
|
||||||
|
prior_log = OUT / f"determinism-{RUN_TAG}-runner.log"
|
||||||
|
if prior_log.exists():
|
||||||
|
for line in prior_log.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||||
|
match = re.match(r"^(\S+) elapsed=([0-9.]+)s .* PASS$", line)
|
||||||
|
if match:
|
||||||
|
prior_elapsed[match.group(1)] = float(match.group(2))
|
||||||
|
|
||||||
|
|
||||||
|
def run(kind, compiler, recipe, workers):
|
||||||
|
global failed
|
||||||
|
tag = f"-{RUN_TAG}" if RUN_TAG else ""
|
||||||
|
name = f"{kind}{tag}-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
str(BINARIES[compiler]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--far-horizon-lod8-step=0", f"--workers={workers}", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
if kind == "crossing24":
|
||||||
|
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||||
|
if RUN_RESUME and name in prior_elapsed and logfile.exists():
|
||||||
|
result = subprocess.CompletedProcess(command, 0)
|
||||||
|
elapsed = prior_elapsed[name]
|
||||||
|
else:
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
hashes = HASH_PATTERN.findall(text)
|
||||||
|
key = (kind, recipe)
|
||||||
|
reference = candidate_reference.get(key)
|
||||||
|
stable = reference is None or hashes == reference
|
||||||
|
if reference is None and hashes:
|
||||||
|
candidate_reference[key] = hashes
|
||||||
|
ok = (result is not None and result.returncode == 0
|
||||||
|
and stamps == [(recipe, recipe, "PASS")]
|
||||||
|
and "hash=on" in text and len(hashes) == 12 and stable)
|
||||||
|
rows.append((kind, recipe, compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", len(hashes), ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"hashes={len(hashes)} stable_across_matrix={'yes' if stable else 'no'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
for kind in ("l25", "crossing24"):
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
run(kind, compiler, recipe, workers)
|
||||||
|
|
||||||
|
summary = OUT / (f"determinism-{RUN_TAG}.csv" if RUN_TAG else "determinism.csv")
|
||||||
|
summary.write_text(
|
||||||
|
"kind,recipe,compiler,workers,elapsed_seconds,exit_code,hash_rows,status\n" +
|
||||||
|
"".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code},{count},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for kind, recipe, compiler, workers, elapsed, code, count, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L32Determinism] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 24 else 0)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run L31 route/proof and transition invariants with recipe-stamped logs."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l32"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l32-clang-v20",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l32-gcc-v20",
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, binary, data, recipe, extra, checks):
|
||||||
|
global failed
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||||
|
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", *extra,
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
checks_ok = all(check(text) for check in checks)
|
||||||
|
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||||
|
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
owner = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for mode in ("hard", "gradient", "interleaved"):
|
||||||
|
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||||
|
text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||||
|
|
||||||
|
summary = OUT / "invariant-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L32InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stream one VFMSH tile at a time to locate LOD/interface patches."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import resource
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("--lod", type=int, required=True)
|
||||||
|
parser.add_argument("--surface", choices=("floor", "ceiling"), required=True)
|
||||||
|
parser.add_argument("--height-min-m", type=float, required=True)
|
||||||
|
parser.add_argument("--height-max-m", type=float, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
candidates = []
|
||||||
|
scanned = 0
|
||||||
|
with open(args.scene, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit("invalid VFMSH001 scene")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.seek(40, 1)
|
||||||
|
if version != 1:
|
||||||
|
raise SystemExit("unsupported mesh scene")
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
payload = vertex_count * 24 + index_count * 4 + mask_count
|
||||||
|
if key[3] != args.lod:
|
||||||
|
stream.seek(payload, 1)
|
||||||
|
continue
|
||||||
|
scanned += 1
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
hits = 0
|
||||||
|
sum_x = sum_y = sum_z = 0.0
|
||||||
|
min_z, max_z = float("inf"), float("-inf")
|
||||||
|
want_up = args.surface == "floor"
|
||||||
|
for offset in range(0, index_count, 3):
|
||||||
|
if masks and masks[offset // 3]:
|
||||||
|
continue
|
||||||
|
a, b, c = indices[offset:offset + 3]
|
||||||
|
if max(a, b, c) >= surface_count:
|
||||||
|
continue
|
||||||
|
nz = (normals[a * 3 + 2] + normals[b * 3 + 2] + normals[c * 3 + 2]) / 3.0
|
||||||
|
if (nz > 0) != want_up:
|
||||||
|
continue
|
||||||
|
za, zb, zc = positions[a * 3 + 2], positions[b * 3 + 2], positions[c * 3 + 2]
|
||||||
|
if ((za + zb + zc) / 3.0 < args.height_min_m
|
||||||
|
or (za + zb + zc) / 3.0 > args.height_max_m):
|
||||||
|
continue
|
||||||
|
hits += 1
|
||||||
|
sum_x += (positions[a * 3] + positions[b * 3] + positions[c * 3]) / 3.0
|
||||||
|
sum_y += (positions[a * 3 + 1] + positions[b * 3 + 1] + positions[c * 3 + 1]) / 3.0
|
||||||
|
z = (za + zb + zc) / 3.0
|
||||||
|
sum_z += z
|
||||||
|
min_z, max_z = min(min_z, za, zb, zc), max(max_z, za, zb, zc)
|
||||||
|
if hits:
|
||||||
|
extent = 32.0 * (1 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
candidates.append((hits, key, sum_x / hits, sum_y / hits,
|
||||||
|
sum_z / hits, min_z, max_z, extent))
|
||||||
|
candidates.sort(reverse=True, key=lambda item: item[0])
|
||||||
|
print(f"[VFMeshTileScan] lod={args.lod} surface={args.surface} scanned_tiles={scanned} "
|
||||||
|
f"candidate_tiles={len(candidates)} peak_rss_kib="
|
||||||
|
f"{resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
||||||
|
for hits, key, x, y, z, low, high, extent in candidates[:40]:
|
||||||
|
print(f"[VFMeshTile] key={key} triangles={hits} centroid_m=({x:.3f},{y:.3f},{z:.3f}) "
|
||||||
|
f"z_m={low:.3f}..{high:.3f} extent_m={extent:.3f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -296,6 +296,7 @@ uniform float uHeightRange;
|
|||||||
uniform float uTileEdgeWidth;
|
uniform float uTileEdgeWidth;
|
||||||
uniform vec3 uLightDirection;
|
uniform vec3 uLightDirection;
|
||||||
uniform int uHeadlamp;
|
uniform int uHeadlamp;
|
||||||
|
uniform int uGrazingLight;
|
||||||
uniform int uClipEnabled;
|
uniform int uClipEnabled;
|
||||||
uniform vec4 uClipPlane;
|
uniform vec4 uClipPlane;
|
||||||
void main()
|
void main()
|
||||||
@@ -315,10 +316,11 @@ void main()
|
|||||||
float heightT=clamp(0.5+(vHeight-uHeightCenter)/max(uHeightRange,0.001),0.0,1.0);
|
float heightT=clamp(0.5+(vHeight-uHeightCenter)/max(uHeightRange,0.001),0.0,1.0);
|
||||||
float smoothHeightT=heightT*heightT*(3.0-2.0*heightT);
|
float smoothHeightT=heightT*heightT*(3.0-2.0*heightT);
|
||||||
vec3 heightTint=uHeightTint!=0
|
vec3 heightTint=uHeightTint!=0
|
||||||
?mix(vec3(0.80,0.91,1.0),vec3(1.0,0.91,0.77),smoothHeightT)
|
?mix(vec3(0.62,0.78,1.0),vec3(1.0,0.82,0.62),smoothHeightT)
|
||||||
:vec3(1.0);
|
:vec3(1.0);
|
||||||
float diffuse=max(dot(n,normalize(uLightDirection)),0.0);
|
float diffuse=max(dot(n,normalize(uLightDirection)),0.0);
|
||||||
float illumination=uHeadlamp!=0?0.78+0.34*diffuse:0.62+0.38*diffuse;
|
float illumination=uGrazingLight!=0?0.32+1.20*diffuse:
|
||||||
|
uHeadlamp!=0?0.78+0.34*diffuse:0.62+0.38*diffuse;
|
||||||
color=vec3(0.64,0.62,0.56)*heightTint*illumination;
|
color=vec3(0.64,0.62,0.56)*heightTint*illumination;
|
||||||
}
|
}
|
||||||
if(uTileEdges!=0&&vTileData.z>0.0)
|
if(uTileEdges!=0&&vTileData.z>0.0)
|
||||||
@@ -380,8 +382,11 @@ void main()
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height,
|
bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height,
|
||||||
double HorizontalFovDegrees,bool ReadableMode,bool HeightTint,bool Headlamp,bool TileEdges,
|
double HorizontalFovDegrees,double HeightRangeMeters,
|
||||||
double TileEdgeWidthMeters,bool ClipPlaneEnabled,
|
double HeightCenterMeters,bool ReadableMode,
|
||||||
|
bool HeightTint,bool Headlamp,bool TileEdges,
|
||||||
|
double TileEdgeWidthMeters,bool HasLightDirection,
|
||||||
|
const std::array<double,3>& LightDirection,bool ClipPlaneEnabled,
|
||||||
const std::array<double,4>& ClipPlane)
|
const std::array<double,4>& ClipPlane)
|
||||||
{
|
{
|
||||||
EGLDisplay Display=MakeDisplay();
|
EGLDisplay Display=MakeDisplay();
|
||||||
@@ -478,19 +483,24 @@ void main()
|
|||||||
glUniform1i(glGetUniformLocation(Program,"uTileEdges"),TileEdges?1:0);
|
glUniform1i(glGetUniformLocation(Program,"uTileEdges"),TileEdges?1:0);
|
||||||
glUniform1i(glGetUniformLocation(Program,"uHeightTint"),HeightTint?1:0);
|
glUniform1i(glGetUniformLocation(Program,"uHeightTint"),HeightTint?1:0);
|
||||||
glUniform1f(glGetUniformLocation(Program,"uHeightCenter"),
|
glUniform1f(glGetUniformLocation(Program,"uHeightCenter"),
|
||||||
static_cast<float>(Scene.CameraMeters[2]));
|
static_cast<float>(HeightCenterMeters));
|
||||||
glUniform1f(glGetUniformLocation(Program,"uHeightRange"),64.0f);
|
glUniform1f(glGetUniformLocation(Program,"uHeightRange"),
|
||||||
|
static_cast<float>(HeightRangeMeters));
|
||||||
glUniform1f(glGetUniformLocation(Program,"uTileEdgeWidth"),
|
glUniform1f(glGetUniformLocation(Program,"uTileEdgeWidth"),
|
||||||
static_cast<float>(TileEdgeWidthMeters));
|
static_cast<float>(TileEdgeWidthMeters));
|
||||||
glUniform1i(glGetUniformLocation(Program,"uClipEnabled"),ClipPlaneEnabled?1:0);
|
glUniform1i(glGetUniformLocation(Program,"uClipEnabled"),ClipPlaneEnabled?1:0);
|
||||||
glUniform1i(glGetUniformLocation(Program,"uHeadlamp"),Headlamp?1:0);
|
glUniform1i(glGetUniformLocation(Program,"uHeadlamp"),Headlamp?1:0);
|
||||||
|
glUniform1i(glGetUniformLocation(Program,"uGrazingLight"),HasLightDirection?1:0);
|
||||||
glUniform4f(glGetUniformLocation(Program,"uClipPlane"),
|
glUniform4f(glGetUniformLocation(Program,"uClipPlane"),
|
||||||
static_cast<float>(ClipPlane[0]),static_cast<float>(ClipPlane[1]),
|
static_cast<float>(ClipPlane[0]),static_cast<float>(ClipPlane[1]),
|
||||||
static_cast<float>(ClipPlane[2]),static_cast<float>(ClipPlane[3]));
|
static_cast<float>(ClipPlane[2]),static_cast<float>(ClipPlane[3]));
|
||||||
|
const std::array<double,3> ActiveLightDirection=HasLightDirection?LightDirection:
|
||||||
|
Headlamp?std::array<double,3>{-Forward[0],-Forward[1],-Forward[2]}:
|
||||||
|
std::array<double,3>{-0.42,-0.58,0.70};
|
||||||
glUniform3f(glGetUniformLocation(Program,"uLightDirection"),
|
glUniform3f(glGetUniformLocation(Program,"uLightDirection"),
|
||||||
Headlamp?static_cast<float>(-Forward[0]):-0.42f,
|
static_cast<float>(ActiveLightDirection[0]),
|
||||||
Headlamp?static_cast<float>(-Forward[1]):-0.58f,
|
static_cast<float>(ActiveLightDirection[1]),
|
||||||
Headlamp?static_cast<float>(-Forward[2]):0.70f);
|
static_cast<float>(ActiveLightDirection[2]));
|
||||||
glDrawElements(GL_TRIANGLES,static_cast<GLsizei>(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr);
|
glDrawElements(GL_TRIANGLES,static_cast<GLsizei>(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr);
|
||||||
glFinish();
|
glFinish();
|
||||||
std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4);
|
std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4);
|
||||||
@@ -527,12 +537,14 @@ int main(int argc,char** argv)
|
|||||||
{
|
{
|
||||||
if(argc<3)
|
if(argc<3)
|
||||||
{
|
{
|
||||||
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--height-tint=on|off] [--headlamp=on|off] [--tile-edges=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D]\n",argv[0]);
|
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--height-range-m=N] [--height-center-m=N] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--height-tint=on|off] [--headlamp=on|off] [--light-direction=X,Y,Z] [--tile-edges=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D]\n",argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
int Width=2560,Height=1440;double Fov=90.0;
|
int Width=2560,Height=1440;double Fov=90.0,HeightRangeMeters=64.0;
|
||||||
|
double HeightCenterMeters=0.0;bool HasHeightCenter=false;
|
||||||
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
||||||
bool HasCamera=false;std::array<double,3> Camera{};
|
bool HasCamera=false;std::array<double,3> Camera{};
|
||||||
|
bool HasLightDirection=false;std::array<double,3> LightDirection{};
|
||||||
bool ReadableMode=true,HeightTint=true,TileEdges=false,Headlamp=false;double TileEdgeWidthMeters=0.15;
|
bool ReadableMode=true,HeightTint=true,TileEdges=false,Headlamp=false;double TileEdgeWidthMeters=0.15;
|
||||||
bool ClipPlaneEnabled=false;std::array<double,4> ClipPlane{};
|
bool ClipPlaneEnabled=false;std::array<double,4> ClipPlane{};
|
||||||
for(int I=3;I<argc;++I)
|
for(int I=3;I<argc;++I)
|
||||||
@@ -550,6 +562,22 @@ int main(int argc,char** argv)
|
|||||||
if(End==Value.c_str()||*End!='\0'||Fov<=1.0||Fov>=179.0)
|
if(End==Value.c_str()||*End!='\0'||Fov<=1.0||Fov>=179.0)
|
||||||
{std::fprintf(stderr,"Invalid horizontal FOV\n");return 2;}
|
{std::fprintf(stderr,"Invalid horizontal FOV\n");return 2;}
|
||||||
}
|
}
|
||||||
|
else if(Arg.starts_with("--height-range-m="))
|
||||||
|
{
|
||||||
|
char* End=nullptr;const std::string Value(Arg.substr(17));
|
||||||
|
HeightRangeMeters=std::strtod(Value.c_str(),&End);
|
||||||
|
if(End==Value.c_str()||*End!='\0'||!std::isfinite(HeightRangeMeters)
|
||||||
|
||HeightRangeMeters<=0.0||HeightRangeMeters>1000000.0)
|
||||||
|
{std::fprintf(stderr,"Invalid height tint range\n");return 2;}
|
||||||
|
}
|
||||||
|
else if(Arg.starts_with("--height-center-m="))
|
||||||
|
{
|
||||||
|
char* End=nullptr;const std::string Value(Arg.substr(18));
|
||||||
|
HeightCenterMeters=std::strtod(Value.c_str(),&End);
|
||||||
|
if(End==Value.c_str()||*End!='\0'||!std::isfinite(HeightCenterMeters))
|
||||||
|
{std::fprintf(stderr,"Invalid height tint center\n");return 2;}
|
||||||
|
HasHeightCenter=true;
|
||||||
|
}
|
||||||
else if(Arg.starts_with("--camera-m="))
|
else if(Arg.starts_with("--camera-m="))
|
||||||
{
|
{
|
||||||
const std::string Value(Arg.substr(11));
|
const std::string Value(Arg.substr(11));
|
||||||
@@ -589,6 +617,17 @@ int main(int argc,char** argv)
|
|||||||
else if(Mode=="off")Headlamp=false;
|
else if(Mode=="off")Headlamp=false;
|
||||||
else {std::fprintf(stderr,"Invalid headlamp mode\n");return 2;}
|
else {std::fprintf(stderr,"Invalid headlamp mode\n");return 2;}
|
||||||
}
|
}
|
||||||
|
else if(Arg.starts_with("--light-direction="))
|
||||||
|
{
|
||||||
|
const std::string Value(Arg.substr(18));
|
||||||
|
if(std::sscanf(Value.c_str(),"%lf,%lf,%lf",&LightDirection[0],
|
||||||
|
&LightDirection[1],&LightDirection[2])!=3
|
||||||
|
||!std::isfinite(LightDirection[0])||!std::isfinite(LightDirection[1])
|
||||||
|
||!std::isfinite(LightDirection[2])
|
||||||
|
||std::hypot(LightDirection[0],LightDirection[1],LightDirection[2])<=0.0)
|
||||||
|
{std::fprintf(stderr,"Invalid light direction; use X,Y,Z with non-zero length\n");return 2;}
|
||||||
|
HasLightDirection=true;
|
||||||
|
}
|
||||||
else if(Arg.starts_with("--tile-edges="))
|
else if(Arg.starts_with("--tile-edges="))
|
||||||
{
|
{
|
||||||
const std::string_view Mode=Arg.substr(13);
|
const std::string_view Mode=Arg.substr(13);
|
||||||
@@ -623,13 +662,16 @@ int main(int argc,char** argv)
|
|||||||
if(HasCamera)Scene.CameraMeters=Camera;
|
if(HasCamera)Scene.CameraMeters=Camera;
|
||||||
if(HasYaw)Scene.YawDegrees=Yaw;
|
if(HasYaw)Scene.YawDegrees=Yaw;
|
||||||
if(HasPitch)Scene.PitchDegrees=Pitch;
|
if(HasPitch)Scene.PitchDegrees=Pitch;
|
||||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f shade=%s height_tint=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
if(!HasHeightCenter)HeightCenterMeters=Scene.CameraMeters[2];
|
||||||
|
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f height_center_m=%.3f shade=%s height_tint=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
||||||
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
||||||
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
|
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
|
||||||
|
HeightCenterMeters,
|
||||||
ReadableMode?"readable":"normal",HeightTint?"on":"off",
|
ReadableMode?"readable":"normal",HeightTint?"on":"off",
|
||||||
TileEdges?"on":"off",Scene.Vertices.size(),
|
TileEdges?"on":"off",Scene.Vertices.size(),
|
||||||
static_cast<unsigned long long>(Scene.Triangles));
|
static_cast<unsigned long long>(Scene.Triangles));
|
||||||
return Render(Scene,argv[2],Width,Height,Fov,ReadableMode,HeightTint,Headlamp,TileEdges,
|
return Render(Scene,argv[2],Width,Height,Fov,HeightRangeMeters,HeightCenterMeters,ReadableMode,
|
||||||
TileEdgeWidthMeters,ClipPlaneEnabled,ClipPlane)?0:3;
|
HeightTint,Headlamp,TileEdges,
|
||||||
|
TileEdgeWidthMeters,HasLightDirection,LightDirection,ClipPlaneEnabled,ClipPlane)?0:3;
|
||||||
}
|
}
|
||||||
|
|
||||||