diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 5dd3949..508d7ad 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -250,9 +250,8 @@ namespace VoxelForgeCore inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256; inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2; // Version 14 adds shared passage columns; version 16 smooths owner Gradient faces; - // version 18 limits FloatingIslands Gradient face displacement to one voxel; - // version 19 reduces it to one quarter voxel to remove the residual floor contour. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 19; + // version 20 restores full Gradient amplitude and uses a continuous signed-distance band union. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 20; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/FarShellsInternal.h b/Standalone/Core/src/FarShellsInternal.h index 38c92c9..cc73f7d 100644 --- a/Standalone/Core/src/FarShellsInternal.h +++ b/Standalone/Core/src/FarShellsInternal.h @@ -1217,6 +1217,82 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta 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 Cells; +}; + +inline int32 RemoveCoarseTransitionFaceTriangles(FVoxelMeshData& Mesh, + const FIntVector& Origin,int32 Step,int32 GridSide,double VoxelSizeCm, + const std::vector& 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 OldMasks=Mesh.TriangleSkirtFaceMasks; + OldMasks.SetNum(TriangleCount); + TArray KeptTriangles; + TArray KeptMasks; + KeptTriangles.Reserve(Mesh.Triangles.Num()); + KeptMasks.Reserve(TriangleCount); + int32 Removed=0,RemovedCeiling=0; + const int32 CellSide=GridSide-1; + for(int32 Triangle=0;Triangle=0&&IB>=0&&IC>=0 + &&IA1.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(std::floor((X-Origin.X)/Step)); + const int32 CellY=static_cast(std::floor((Y-Origin.Y)/Step)); + if(CellX>=0&&CellX=0&&CellY(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, 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 Thickness=0.0; + double MaximumDisplacementVoxels=0.0; + std::vector FaceHeights; std::vector SolidAbove; + const VoxelForgeCore::ResolvedLayoutStratumV2* Stratum=nullptr; + bool bTransition=false; }; std::vector Candidates; + std::vector Replacements; const double CoreMicrons=std::max(1,World.CoreVoxelSizeMicrons); for(const FStrateSlot& Slot:World.Manager->GetLayout()) { @@ -1243,19 +1324,34 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World, Candidate.FaceZ=static_cast(Stratum->ceiling.bottomMicrometres)/CoreMicrons; Candidate.Thickness=static_cast(Stratum->ceiling.topMicrometres -Stratum->ceiling.bottomMicrometres)/CoreMicrons; - if(Candidate.FaceZOrigin.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( + Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres(*transitionUpper,*Stratum)) + /CoreMicrons:0.0; + if(Candidate.FaceZOrigin.Z+Extent+displacement+1.0e-6)continue; + Candidate.bTransition=bTransition; + Candidate.MaximumDisplacementVoxels=displacement; const double Epsilon=std::min(0.5,Candidate.Thickness*0.25); if(Epsilon<=0.0)continue; - Candidate.SolidAbove.resize(static_cast(GridSide)*GridSide,0); + const std::size_t GridCount=static_cast(GridSide)*GridSide; + Candidate.FaceHeights.resize(GridCount,Candidate.FaceZ); + Candidate.SolidAbove.resize(GridCount,0); bool AnySolidAbove=false; for(int32 Y=0;Y(Y*GridSide+X); + Candidate.bTransition=Shape.ResolvedCeilingBottomAt(*Candidate.Stratum, + WorldX,WorldY,Candidate.FaceHeights[GridIndex])||Candidate.bTransition; const float Above=Shape.DensityAt(static_cast(WorldX),static_cast(WorldY), - static_cast(Candidate.FaceZ+Epsilon)); + static_cast(Candidate.FaceHeights[GridIndex]+Epsilon)); const bool bSolidAbove=std::isfinite(Above)&&Above<0.0f; - Candidate.SolidAbove[static_cast(Y*GridSide+X)]=bSolidAbove?1:0; + Candidate.SolidAbove[GridIndex]=bSolidAbove?1:0; AnySolidAbove=AnySolidAbove||bSolidAbove; } if(AnySolidAbove)Candidates.push_back(std::move(Candidate)); @@ -1277,8 +1373,19 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World, std::vector NewTriangles; for(const FCandidate& Candidate:Candidates) { - std::vector VertexMap(static_cast(GridSide)*GridSide,INDEX_NONE); - const double CeilingZ=Candidate.FaceZ; + const std::size_t GridCount=static_cast(GridSide)*GridSide; + std::vector VertexMap(GridCount,INDEX_NONE); + FTransitionFaceReplacement Replacement; + Replacement.BaseZVoxels=Candidate.FaceZ; + Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels; + Replacement.Cells.resize(static_cast(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 std::size_t GridIndex=static_cast(Y*GridSide+X); @@ -1286,8 +1393,20 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World, if(Mapped!=INDEX_NONE)return Mapped; Mapped=static_cast(NewVertices.size()); const double WorldX=Origin.X+X*GridStep,WorldY=Origin.Y+Y*GridStep; - NewVertices.emplace_back(WorldX*VoxelSizeCm,WorldY*VoxelSizeCm,CeilingZ*VoxelSizeCm); - NewNormals.emplace_back(0.0,0.0,-1.0); + const double FaceZ=Candidate.FaceHeights[GridIndex]; + FVector Normal(0.0,0.0,-1.0); + if(Candidate.bTransition) + { + const double Delta=std::max(1.0,std::min(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(WorldX),static_cast(WorldY)); if(bHasColors)NewColors.push_back(Color); return Mapped; @@ -1300,15 +1419,40 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World, ||!Candidate.SolidAbove[I01]||!Candidate.SolidAbove[I11])continue; const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*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 - // center of a curved shell triangle is not enough to leave the rest of this cap cell open. - if(MeshIndex.HasDownwardCellCoverage(CellX0,CellY0,CellX1,CellY1,CeilingZ,0.001))continue; + bool fullyCovered=false; + if(!Candidate.bTransition) + 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(Y)*(GridSide-1)+X]=1; const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y); const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1); 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; + const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,GridStep, + GridSide,VoxelSizeCm,Replacements,false); const int32 AddedVertices=static_cast(NewVertices.size()); auto InsertVertices=[&](auto& Existing,const auto& Added) @@ -1347,9 +1491,8 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World, Mesh.NumCeilingTriangles+=static_cast(NewTriangles.size()/3); } -// Add the interior-facing top of each bedrock band. The density composition already -// makes this band solid and editable; this planar closure keeps its visible floor -// continuous when a coarse marching-cubes step would otherwise skip a sub-voxel band. +// Fill only top cells that marching cubes missed. Active transitions use the same +// displaced face and passage-mouth fade as the signed-distance field. inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World, 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(); if(Extent<=0||Extent%Step!=0)return; 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 Candidates; + std::vector Replacements; const double CoreMicrons=std::max(1,World.CoreVoxelSizeMicrons); for(const FStrateSlot& Slot:World.Manager->GetLayout()) { @@ -1367,9 +1518,17 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World, if(!Stratum||!Stratum->bedrock.presentInCurrentField ||Stratum->bedrock.topMicrometres<=Stratum->bedrock.bottomMicrometres)continue; const double FloorZ=static_cast(Stratum->bedrock.topMicrometres)/CoreMicrons; - if(FloorZOrigin.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(Shape.ResolvedTransitionMaximumFaceDisplacementMicrometres( + *Stratum,*transitionLower))/CoreMicrons + :0.0; + if(FloorZOrigin.Z+Extent+displacement+1.0e-6)continue; Candidates.push_back({FloorZ,static_cast(Stratum->bedrock.topMicrometres - -Stratum->bedrock.bottomMicrometres)/CoreMicrons}); + -Stratum->bedrock.bottomMicrometres)/CoreMicrons,displacement,Stratum,bTransition}); } 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); if(Epsilon<=0.0)continue; - std::vector SolidBelow(static_cast(GridSide)*GridSide,0); + const std::size_t GridCount=static_cast(GridSide)*GridSide; + std::vector faceHeights(GridCount,Candidate.FloorZ); + std::vector solidBelow(GridCount,0); + FTransitionFaceReplacement Replacement; + Replacement.BaseZVoxels=Candidate.FloorZ; + Replacement.MaximumDisplacementVoxels=Candidate.MaximumDisplacementVoxels; + Replacement.Cells.resize(static_cast(GridSide-1)*(GridSide-1),0); for(int32 Y=0;Y(Y*GridSide+X); + if(Candidate.bTransition) + Shape.ResolvedBedrockTopAt(*Candidate.Stratum,WorldX,WorldY, + faceHeights[GridIndex]); const float Density=Shape.DensityAt(static_cast(WorldX),static_cast(WorldY), - static_cast(Candidate.FloorZ-Epsilon)); - SolidBelow[static_cast(Y*GridSide+X)]= + static_cast(faceHeights[GridIndex]-Epsilon)); + solidBelow[GridIndex]= std::isfinite(Density)&&Density<0.0f?1:0; } - std::vector VertexMap(static_cast(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 VertexMap(GridCount,INDEX_NONE); const auto AddVertex=[&](int32 X,int32 Y) { const std::size_t GridIndex=static_cast(Y*GridSide+X); @@ -1407,9 +1584,20 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World, if(Mapped!=INDEX_NONE)return Mapped; Mapped=static_cast(NewVertices.size()); 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(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, - Candidate.FloorZ*VoxelSizeCm); - NewNormals.emplace_back(0.0,0.0,1.0); + faceHeights[GridIndex]*VoxelSizeCm); + NewNormals.push_back(Normal); if(bHasUVs)NewUVs.emplace_back(static_cast(WorldX),static_cast(WorldY)); if(bHasColors)NewColors.push_back(Color); return Mapped; @@ -1418,16 +1606,43 @@ inline void AppendStratumBoundaryFloors(const FStandaloneOwnerWorld& World, { const std::size_t I00=static_cast(Y*GridSide+X); 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 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(Y)*(GridSide-1)+X]=1; const int32 A=AddVertex(X,Y),B=AddVertex(X+1,Y); const int32 C=AddVertex(X+1,Y+1),D=AddVertex(X,Y+1); 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; + const int32 removedCoarse=RemoveCoarseTransitionFaceTriangles(Mesh,Origin,Step, + GridSide,VoxelSizeCm,Replacements,true); const int32 AddedVertices=static_cast(NewVertices.size()); auto InsertVertices=[&](auto& Existing,const auto& Added) diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index e0844e7..faa30c8 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -71,8 +71,22 @@ namespace 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 ty = TransitionFade(((y - iy * cell) * TransitionQ16) / cell); + std::uint64_t prefix=14695981039346656037ull; + TransitionHashWord(prefix,static_cast(seed)); + TransitionHashWord(prefix,static_cast(interfaceIndex)); 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(ix+dx)); + TransitionHashWord(hash,static_cast(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(hash>>32); + return static_cast(bits>>15)-TransitionQ16; + }; return TransitionLerp(TransitionLerp(at(0, 0), at(1, 0), tx), TransitionLerp(at(0, 1), at(1, 1), tx), ty); } @@ -359,10 +373,7 @@ ResolvedLayoutTransitionBandsV1 EvaluateResolvedLayoutTransitionBandsV1( return result; } - // FloatingIslands floor faces retain the same deterministic Gradient field but - // 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 displacement = (noise * (width / 8)) / TransitionQ16; const std::int64_t interfaceZ = seam + displacement; result.upperBedrockBottomMicrometres = interfaceZ; result.upperBedrockTopMicrometres = interfaceZ + upperThickness; diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 8c58763..74b2771 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -270,7 +270,7 @@ public: { const std::int64_t width=std::max(0,Upper.transitionWidthMicrometres); if(Upper.transitionMode=="Gradient") - return width/(Upper.generatorKind=="FloatingIslands"?256:8); + return width/8; if(Upper.transitionMode!="Interleaved")return 0; const std::int64_t upperThickness=std::max(0, Upper.bedrock.topMicrometres-Upper.bedrock.bottomMicrometres); @@ -385,6 +385,70 @@ public: 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(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::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(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::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(bands.lowerCeilingBottomMicrometres) + /Owner->CoreVoxelSizeMicrons; + return std::isfinite(BottomZ); + } + bool IsInsideResolvedStrateBoundaryBand(float Z) const { #if defined(VF_L30_ENABLE_COST_COUNTERS) @@ -471,12 +535,72 @@ public: 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(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&&zMicrometresmatches(*this) + &&!tileQueries->mayContainTransitionPassageFade(); +#if defined(VF_L30_ENABLE_COST_COUNTERS) + if(skipPassageFade) + ++ThreadClassificationStats().TransitionPassageTileSkips; +#endif + const auto xMicrometres=static_cast(std::llround( + static_cast(X)*Owner->CoreVoxelSizeMicrons)); + const auto yMicrometres=static_cast(std::llround( + static_cast(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-zMicrometres) + :zMicrometres>solidTop + ?static_cast(zMicrometres-solidTop) + :-static_cast(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=bands.upperBedrockTopMicrometres + &&zMicrometres0.0&&density<0.0f + &&!floatingIslandAirGap)) + return static_cast(signedDistanceMicrometres + /Owner->CoreVoxelSizeMicrons); + } + } + } const double worldZCentimetres=static_cast(Z)*GetCoreVoxelSizeCm(); const int32 strateIndex=Owner->Manager->GetStrateIndex(static_cast(worldZCentimetres)); const auto* stratum=FindResolvedLayoutStratum(strateIndex); if(!stratum)return density; - const double zMicrometres=static_cast(Z)*Owner->CoreVoxelSizeMicrons; const auto solidAt=[&](const VoxelForgeCore::ResolvedLayoutBandV2& band) {return band.presentInCurrentField&&zMicrometres>=band.bottomMicrometres &&zMicrometres(std::llround( static_cast(X)*Owner->CoreVoxelSizeMicrons)); const std::int64_t yMicrometres=static_cast(std::llround( static_cast(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) &&!tileQueries->mayContainTransitionPassageFade(); - const std::int64_t transitionWeight=skipPassageFade?TransitionQ16 - :ResolvedTransitionPassageWeightQ16(*upper,*lower,xMicrometres,yMicrometres); #if defined(VF_L30_ENABLE_COST_COUNTERS) if(skipPassageFade)++ThreadClassificationStats().TransitionPassageTileSkips; #endif - if(transitionWeightbedrock.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*transitionWeight)/TransitionQ16;}; - resolvedBands.upperBedrockBottomMicrometres=seam+scaled( - bands.upperBedrockBottomMicrometres-seam); - resolvedBands.upperBedrockTopMicrometres= - resolvedBands.upperBedrockBottomMicrometres+upperThickness; - resolvedBands.lowerCeilingTopMicrometres=seam+scaled( - bands.lowerCeilingTopMicrometres-seam); - resolvedBands.lowerCeilingBottomMicrometres= - resolvedBands.lowerCeilingTopMicrometres-lowerThickness; - resolvedBands.crossingDepthMicrometres=std::max(0, - resolvedBands.lowerCeilingTopMicrometres - -resolvedBands.upperBedrockBottomMicrometres); - } - const bool upperBedrock=zMicrometres>=resolvedBands.upperBedrockBottomMicrometres - &&zMicrometres=resolvedBands.lowerCeilingBottomMicrometres - &&zMicrometresbedrock.bottomMicrometres; - if(upper->generatorKind=="FloatingIslands" - &&upper->transitionMode=="Gradient" - &&resolvedBands.upperBedrockTopMicrometres=resolvedBands.upperBedrockTopMicrometres - &&zMicrometres(zMicrometres - -resolvedBands.upperBedrockTopMicrometres) - /std::max(1,Owner->CoreVoxelSizeMicrons); - density=std::max(0.05f,static_cast(distanceVoxels)); - return true; - } + const auto resolvedBands=ResolvedTransitionBandsAtXY(*upper,*lower, + xMicrometres,yMicrometres,skipPassageFade); + // Treat the two authored bands as one continuous solid interval. Its + // exterior faces remain the lower ceiling bottom and upper bedrock top; + // measuring signed distance from those faces also clears native generator + // density on both air sides of the displaced interface. + const std::int64_t solidBottom=std::min( + resolvedBands.upperBedrockBottomMicrometres, + resolvedBands.lowerCeilingBottomMicrometres); + const std::int64_t solidTop=std::max( + resolvedBands.upperBedrockTopMicrometres, + resolvedBands.lowerCeilingTopMicrometres); + const double signedDistanceMicrometres=zMicrometres(solidBottom-zMicrometres) + :zMicrometres>solidTop + ?static_cast(zMicrometres-solidTop) + :-static_cast(std::min(zMicrometres-solidBottom, + solidTop-zMicrometres)); + const std::int64_t seam=upper->bedrock.bottomMicrometres; + const bool floatingIslandAirGap=upper->generatorKind=="FloatingIslands" + &&upper->transitionMode=="Gradient" + &&resolvedBands.upperBedrockTopMicrometres=resolvedBands.upperBedrockTopMicrometres + &&zMicrometres0.0&&density<0.0f&&!floatingIslandAirGap) return false; - } - const auto& band=upperBedrock?upper->bedrock:lower->ceiling; - if(upperBedrock&&upper->generatorKind=="FloatingIslands" - &&upper->transitionMode=="Gradient") - { - const double distanceVoxels=static_cast( - resolvedBands.upperBedrockTopMicrometres-zMicrometres) - /std::max(1,Owner->CoreVoxelSizeMicrons); - density=-std::max(0.05f,static_cast(distanceVoxels)); - } - else density=-std::max(0.05f,static_cast(band.solidDensityQ16)/65536.0f); + density=static_cast(signedDistanceMicrometres + /std::max(1,Owner->CoreVoxelSizeMicrons)); return true; }; diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/AuditPassageFadeRingL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/AuditPassageFadeRingL32.py new file mode 100644 index 0000000..2fcb73b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/AuditPassageFadeRingL32.py @@ -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() diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py new file mode 100644 index 0000000..6b25463 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py @@ -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() diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py new file mode 100644 index 0000000..b797c73 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py @@ -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() diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/EncodeCapturesL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/EncodeCapturesL32.py new file mode 100644 index 0000000..2b147e7 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/EncodeCapturesL32.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/ExtractMeshLodWindowL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/ExtractMeshLodWindowL32.py new file mode 100644 index 0000000..67da5a2 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/ExtractMeshLodWindowL32.py @@ -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(" 1_000_000: + raise SystemExit("source has an unsupported scene header") + struct.pack_into(" 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(" 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() diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/FindRingWindowL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/FindRingWindowL32.py new file mode 100644 index 0000000..df3025b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/FindRingWindowL32.py @@ -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() diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/FindSurfaceWorldAirL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/FindSurfaceWorldAirL32.py new file mode 100644 index 0000000..36bdfad --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/FindSurfaceWorldAirL32.py @@ -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()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/ProbeGradientDensity.cpp b/Standalone/Research/WorldAssemblerM3/Linux32/ProbeGradientDensity.cpp new file mode 100644 index 0000000..00d8e79 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/ProbeGradientDensity.cpp @@ -0,0 +1,118 @@ +#include "Json.h" +#include "OwnerWorldLoader.h" +#include "StandaloneCoreScheduler.h" +#include "WorldAssemblyPrototype.h" +#include "StandaloneSnapshotAdapter.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +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(input),std::istreambuf_iterator{}}; + 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(); + 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(&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(stratum.transitionWidthMicrometres), + static_cast(stratum.bedrock.bottomMicrometres), + static_cast(stratum.bedrock.topMicrometres), + static_cast(stratum.ceiling.bottomMicrometres), + static_cast(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::floor((x-32.0)*4.0)), + static_cast(std::floor((y-32.0)*4.0)), + static_cast(std::floor((zmin-4.0)*4.0))}; + request.maxVoxel={static_cast(std::ceil((x+32.0)*4.0)), + static_cast(std::ceil((y+32.0)*4.0)), + static_cast(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(interface->transitionWidthMicrometres), + static_cast(interface->bedrock.bottomMicrometres), + static_cast(interface->bedrock.topMicrometres), + static_cast(interface->bedrock.bottomMicrometres),x,y); + std::printf("z_m,density_vox,lattice_vox,unclamped_vox,dispatch\n"); + const auto count=static_cast(std::floor((zmax-zmin)/step+1.0e-9)); + for(std::size_t i=0;i<=count;++i) + { + const double z=zmin+static_cast(i)*step; + const VoxelForgeCore::Vec3d p{x*4.0,y*4.0,z*4.0}; + const VoxelForgeCore::Vec3i lattice{ + static_cast(std::llround(x*4.0)), + static_cast(std::llround(y*4.0)), + static_cast(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; +} diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/ProbeSurfaceProfileL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/ProbeSurfaceProfileL32.py new file mode 100644 index 0000000..aa04e14 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/ProbeSurfaceProfileL32.py @@ -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() diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/ProbeTransitionCacheL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/ProbeTransitionCacheL32.py new file mode 100644 index 0000000..ce80266 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/ProbeTransitionCacheL32.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux32/REPORT.md new file mode 100644 index 0000000..14a1626 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/REPORT.md @@ -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%. diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/RunAuditsL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/RunAuditsL32.py new file mode 100644 index 0000000..76bed8a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/RunAuditsL32.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/RunCostL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/RunCostL32.py new file mode 100644 index 0000000..8a1c9e5 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/RunCostL32.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/RunDeterminismL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/RunDeterminismL32.py new file mode 100644 index 0000000..c8b951f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/RunDeterminismL32.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/RunInvariantAuditsL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/RunInvariantAuditsL32.py new file mode 100644 index 0000000..de05c79 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/RunInvariantAuditsL32.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/ScanMeshSurfaceTilesL32.py b/Standalone/Research/WorldAssemblerM3/Linux32/ScanMeshSurfaceTilesL32.py new file mode 100644 index 0000000..aef2aa8 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux32/ScanMeshSurfaceTilesL32.py @@ -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() diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-far-tint-off.jpg b/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-far-tint-off.jpg new file mode 100644 index 0000000..47ad2a6 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-far-tint-off.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-far-tint-on.jpg b/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-far-tint-on.jpg new file mode 100644 index 0000000..d8e8e83 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-far-tint-on.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-near-tint-off.jpg b/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-near-tint-off.jpg new file mode 100644 index 0000000..c0e0667 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-near-tint-off.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-near-tint-on.jpg b/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-near-tint-on.jpg new file mode 100644 index 0000000..f4c7f82 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux32/captures/fi-floor-near-tint-on.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-far-tint-off.jpg b/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-far-tint-off.jpg new file mode 100644 index 0000000..5c1c8d3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-far-tint-off.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-far-tint-on.jpg b/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-far-tint-on.jpg new file mode 100644 index 0000000..4fdefbf Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-far-tint-on.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-near-tint-off.jpg b/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-near-tint-off.jpg new file mode 100644 index 0000000..5c1cc12 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-near-tint-off.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-near-tint-on.jpg b/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-near-tint-on.jpg new file mode 100644 index 0000000..5b0c673 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux32/captures/sw-ceiling-near-tint-on.jpg differ diff --git a/Standalone/Source/LinuxOffscreenCapture.cpp b/Standalone/Source/LinuxOffscreenCapture.cpp index 1a1ca34..df470f3 100644 --- a/Standalone/Source/LinuxOffscreenCapture.cpp +++ b/Standalone/Source/LinuxOffscreenCapture.cpp @@ -296,6 +296,7 @@ uniform float uHeightRange; uniform float uTileEdgeWidth; uniform vec3 uLightDirection; uniform int uHeadlamp; +uniform int uGrazingLight; uniform int uClipEnabled; uniform vec4 uClipPlane; void main() @@ -315,10 +316,11 @@ void main() float heightT=clamp(0.5+(vHeight-uHeightCenter)/max(uHeightRange,0.001),0.0,1.0); float smoothHeightT=heightT*heightT*(3.0-2.0*heightT); 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); 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; } 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, - double HorizontalFovDegrees,bool ReadableMode,bool HeightTint,bool Headlamp,bool TileEdges, - double TileEdgeWidthMeters,bool ClipPlaneEnabled, + double HorizontalFovDegrees,double HeightRangeMeters, + double HeightCenterMeters,bool ReadableMode, + bool HeightTint,bool Headlamp,bool TileEdges, + double TileEdgeWidthMeters,bool HasLightDirection, + const std::array& LightDirection,bool ClipPlaneEnabled, const std::array& ClipPlane) { EGLDisplay Display=MakeDisplay(); @@ -478,19 +483,24 @@ void main() glUniform1i(glGetUniformLocation(Program,"uTileEdges"),TileEdges?1:0); glUniform1i(glGetUniformLocation(Program,"uHeightTint"),HeightTint?1:0); glUniform1f(glGetUniformLocation(Program,"uHeightCenter"), - static_cast(Scene.CameraMeters[2])); - glUniform1f(glGetUniformLocation(Program,"uHeightRange"),64.0f); + static_cast(HeightCenterMeters)); + glUniform1f(glGetUniformLocation(Program,"uHeightRange"), + static_cast(HeightRangeMeters)); glUniform1f(glGetUniformLocation(Program,"uTileEdgeWidth"), static_cast(TileEdgeWidthMeters)); glUniform1i(glGetUniformLocation(Program,"uClipEnabled"),ClipPlaneEnabled?1:0); glUniform1i(glGetUniformLocation(Program,"uHeadlamp"),Headlamp?1:0); + glUniform1i(glGetUniformLocation(Program,"uGrazingLight"),HasLightDirection?1:0); glUniform4f(glGetUniformLocation(Program,"uClipPlane"), static_cast(ClipPlane[0]),static_cast(ClipPlane[1]), static_cast(ClipPlane[2]),static_cast(ClipPlane[3])); + const std::array ActiveLightDirection=HasLightDirection?LightDirection: + Headlamp?std::array{-Forward[0],-Forward[1],-Forward[2]}: + std::array{-0.42,-0.58,0.70}; glUniform3f(glGetUniformLocation(Program,"uLightDirection"), - Headlamp?static_cast(-Forward[0]):-0.42f, - Headlamp?static_cast(-Forward[1]):-0.58f, - Headlamp?static_cast(-Forward[2]):0.70f); + static_cast(ActiveLightDirection[0]), + static_cast(ActiveLightDirection[1]), + static_cast(ActiveLightDirection[2])); glDrawElements(GL_TRIANGLES,static_cast(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr); glFinish(); std::vector Pixels(static_cast(Width)*Height*4); @@ -527,12 +537,14 @@ int main(int argc,char** argv) { 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; } - 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 HasCamera=false;std::array Camera{}; + bool HasLightDirection=false;std::array LightDirection{}; bool ReadableMode=true,HeightTint=true,TileEdges=false,Headlamp=false;double TileEdgeWidthMeters=0.15; bool ClipPlaneEnabled=false;std::array ClipPlane{}; for(int I=3;I=179.0) {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=")) { const std::string Value(Arg.substr(11)); @@ -589,6 +617,17 @@ int main(int argc,char** argv) else if(Mode=="off")Headlamp=false; 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=")) { const std::string_view Mode=Arg.substr(13); @@ -623,13 +662,16 @@ int main(int argc,char** argv) if(HasCamera)Scene.CameraMeters=Camera; if(HasYaw)Scene.YawDegrees=Yaw; 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], Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight, + HeightCenterMeters, ReadableMode?"readable":"normal",HeightTint?"on":"off", TileEdges?"on":"off",Scene.Vertices.size(), static_cast(Scene.Triangles)); - return Render(Scene,argv[2],Width,Height,Fov,ReadableMode,HeightTint,Headlamp,TileEdges, - TileEdgeWidthMeters,ClipPlaneEnabled,ClipPlane)?0:3; + return Render(Scene,argv[2],Width,Height,Fov,HeightRangeMeters,HeightCenterMeters,ReadableMode, + HeightTint,Headlamp,TileEdges, + TileEdgeWidthMeters,HasLightDirection,LightDirection,ClipPlaneEnabled,ClipPlane)?0:3; }