From 9d3c50ca95cd85856584a0e615ec2de53ba1efa3 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 19 Oct 2026 00:41:13 +0200 Subject: [PATCH] wip(assembler): round S4 -- continuous far ceiling at the strate's upper boundary S3's far cap sat up to 8.2 m above the near roof and read as floating pieces. The far render cap now projects onto the owning strate's upper boundary (density unchanged), on a global XY lattice, LOD4 16-voxel grid + LOD5-8 128-voxel. Cap agreement 0.05 cm (100 cm control detected), 2520/2520 upward rays hit roof on the owner walk. Far recipe v2; LOD0-3 hashes equal S2/S3. Tile p95 flat; cold clipmap +4.4%. Far-cliff fluting left open (coarse MC step). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- ARCHITECTURE.md | 40 ++ CODEMAP.md | 13 + Source/VoxelForge/Private/VoxelWorld.cpp | 18 +- .../Private/VoxelForgeExploreCommandlet.cpp | 565 +++++++++++++++++- .../include/VoxelForgeCore/VoxelForgeCore.h | 19 +- Standalone/Core/src/VoxelForgeCore.cpp | 96 ++- Standalone/Core/src/WorldAssemblerInternal.h | 62 +- .../gpu_field_port/LUNA_DIALOGUE.md | 19 + .../WorldAssemblerM3/Round8/REPORT.md | 137 +++++ .../Round8/captures/s4_cap_distance.jpg | Bin 0 -> 12927 bytes .../Round8/captures/s4_far_straight_up.jpg | Bin 0 -> 2335 bytes .../Round8/captures/s4_far_y_minus_1800.jpg | Bin 0 -> 17767 bytes .../Round8/captures/s4_walk_horizon.jpg | Bin 0 -> 18218 bytes Standalone/Source/StandaloneViewer.cpp | 441 +++++++++++++- 14 files changed, 1376 insertions(+), 34 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Round8/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Round8/captures/s4_cap_distance.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Round8/captures/s4_far_straight_up.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Round8/captures/s4_far_y_minus_1800.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Round8/captures/s4_walk_horizon.jpg diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index cceeb06..ca39dcc 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1395,6 +1395,46 @@ measure eight warmed keys per LOD and compare the shared LOD7/8 surface lattice density roots, including a 100 cm datum-offset negative control. See `Standalone/Research/WorldAssemblerM3/Round7/REPORT.md` for results and the visual evidence limits. +### 8.25 Continuous planar ceiling — World Assembler S4 / M3 Round 8 + + The cap remains a family declaration in `WorldAssemblyPlanV1::strates` and is evaluated by the shared + Core heightfield path. `Heightfield2D::farSurfaceCapHeightSubvoxels()` clips only the rendered far + surface to `(topChunkZ + 1) * 32` voxels, the owning strate's upper boundary; it leaves + `Heightfield2D::point()` and its density values unchanged. `capHeightBounds()` reports conservative + bounds for that clipped surface. Evaluator v5 opts into this render-surface contract; older serialized + evaluators keep their recorded cap behavior. The clip joins the exact near density root at the owning + boundary without changing LOD0–3 density hashes. Future families with planar roofs must declare their + cap height and bounds through the family plan/provider contract; the shared tile generator consumes + that declaration. FloatingIslands declares no planar roof, so open sky above a surface island is the + expected view. + +For each requested far SurfaceWorld XY column at LOD4–8, desired-tile selection adds cap-height + bands only when they do not overlap an existing or already accepted 3D clipmap tile. The original + clipmap remains a non-overlapping partition. Cap samples use a global XY lattice, are clipped to the + owned strate boundary, tile Z bounds and `cap >= ground`, and share edges across tiles. LOD4 uses a 16-voxel cap step; +LOD5–8 use 128 voxels. Core-off stays on its legacy path, and edits continue through the authoritative +edited field. Runtime material sections are explicitly created/updated from their ground/cap index +ranges so a cap-only far tile cannot rely on an auto-created section being present. + +The far recipe version is 2. It salts mesh, payload and topology hashes for LOD4–8; density hashes +and LOD0–LOD3 identities do not change. The owner audit counts requested cap bands and emitted +positive-area bands, compares 128 shared cap points with the near density root at 2 cm tolerance, +checks a 100 cm datum-offset negative control, and traces upward rays from route and camera points +against emitted triangles. Round 8 records these counters, the matched captures, warmed BuildHost +timings, clipmap overlap validation and remaining failures in +`Standalone/Research/WorldAssemblerM3/Round8/REPORT.md`. + + The Round 8 owner walk audit emitted every requested cap band at LOD4–8 (385/385), agreed with 128 + near roots to 0.0504 cm (2 cm gate), detected a 100 cm offset control, and hit the cap on all 2,520 + upward rays from 35 walk/camera points. No route ray missed the cap or clipmap boundary. + + The far-cliff fluting is coarse marching-cubes geometry on steep faces: with 16 coarse cells per tile, +LOD7 samples every 256 voxels (64 m at 25 cm) and LOD8 every 512 voxels (128 m). The native ground +replacement removes only upward-facing coarse faces; near-vertical faces remain on that lattice. +LOD7/8 shell repair is skipped for certified SurfaceWorld tiles, the renderer does not cull +backfaces, and the strips predate S4 in S2/S3. Refining all steep faces would multiply far mesh work; +S4 leaves that cost/quality tradeoff open. + ## 9. Multiplayer model (listen-server first, dedicated-friendly) > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a diff --git a/CODEMAP.md b/CODEMAP.md index ba9f4e7..5fe1570 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -979,6 +979,19 @@ time, repair field calls, skipped repairs and replaced triangles. `farHorizonVer mesh/payload/topology hashes only. The BuildHost commandlet owns warmed eight-key timings and the exact-density-root LOD7/8 agreement audit with its datum-offset negative control. + World Assembler S4 repairs the planar cap datum and far tile selection. `Heightfield2D::farSurfaceCapHeightSubvoxels` + clips the rendered cap to `(topChunkZ + 1) * 32` voxels while preserving `Heightfield2D::point` density; + `capHeightBounds` provides the matching conservative bands. `World::desiredTiles` adds only non-overlapping + cap-height keys at LOD4–8, and `RefineFarNativeCap` emits the globally aligned cap surface (16 voxels at + LOD4, 128 at LOD5–8). Evaluator v5 opts into this render-surface contract; older serialized evaluator + versions retain their cap behavior. Far recipe v2 salts mesh/payload/topology from LOD4 upward. The + standalone viewer audit measures cap-band census, near-root agreement and upward-ray coverage; the editor + BuildHost check measures the staged owner actor and W1/W8/W16. `VoxelWorld::ApplyMeshToTile` uses explicit + section ranges for ground and cap. Likely compile spots are the Core public/internal heightfield API and +`VoxelForgeCore.cpp`, `VoxelWorld.cpp`, the editor commandlet, and the standalone viewer. See +ARCHITECTURE §8.25 and +[`Standalone/Research/WorldAssemblerM3/Round8/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round8/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index f21ce3c..2c5ae08 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -24,6 +24,7 @@ #include "VoxelClipmapDesiredTiles.h" #include "VoxelCollisionDesiredTiles.h" #include "VoxelPreEditDensityCache.h" +#include "RealtimeMeshLibrary.h" // IWYU (FPSemantics = Precise ⇒ plus de PCH partagé) : GetPlayerPosition déréférence le pawn, donc // APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController n'est // complet que par transitivité : on l'inclut explicitement. @@ -12471,21 +12472,30 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result) // (default config already maps material slot = polygroup index); only config sections that // exist — the bHas* flags come from the worker. Collision at level 0 only (T1.c), both groups. const uint64 CollisionConfigStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0; + const int32 NumIndices = Result.NumTriangles * 3; + const int32 FirstCapIndex = FMath::Clamp( + NumIndices - Result.NumCeilingTriangles * 3, 0, NumIndices); if (bHasGroundTris) { FRealtimeMeshSectionConfig GroundConfig(0); GroundConfig.bCastsShadow = bCastShadow; - RTMesh->UpdateSectionConfig( + const FRealtimeMeshStreamRange GroundRange = + URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange( + 0, Result.NumVertices, 0, FirstCapIndex); + RTMesh->CreateSection( FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 0), - GroundConfig, /*bShouldCreateCollision*/ bWantsCollision); + GroundConfig, GroundRange, /*bShouldCreateCollision*/ bWantsCollision); } if (bHasCeilingTris) { FRealtimeMeshSectionConfig CapConfig(1); CapConfig.bCastsShadow = false; // the cap never casts (see above) - RTMesh->UpdateSectionConfig( + const FRealtimeMeshStreamRange CapRange = + URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange( + 0, Result.NumVertices, FirstCapIndex, NumIndices); + RTMesh->CreateSection( FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 1), - CapConfig, /*bShouldCreateCollision*/ bWantsCollision); + CapConfig, CapRange, /*bShouldCreateCollision*/ bWantsCollision); } const double CollisionConfigSeconds = bProfileApply ? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - CollisionConfigStartCycles) diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 52bf2d3..2fe6973 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -15221,7 +15221,8 @@ bool RunCoreFarHorizonAgreementAudit(VoxelForgeCore::World& World, if(Other==Ground8.ByXY.end())continue; const auto Column=Field.point(Pair.first.first,Pair.first.second,0); const std::int64_t Ground=Column.groundHeightSubvoxels; - const std::int64_t Cap=Column.capHeightSubvoxels; + const std::int64_t Cap=Field.farSurfaceCapHeightSubvoxels( + Pair.first.first,Pair.first.second); if(Ground>=Cap)continue; const double GroundVoxel=static_cast(Ground)/SubvoxelsPerVoxel; if(GroundVoxelBounds7[5] @@ -15263,7 +15264,8 @@ bool RunCoreFarHorizonAgreementAudit(VoxelForgeCore::World& World, const std::int64_t YSub=static_cast(std::llround( static_cast(RouteYSub)/GridPeriod))*GridPeriod; const auto WalkColumn=Field.point(XSub,YSub,0); - if(WalkColumn.groundHeightSubvoxels>=WalkColumn.capHeightSubvoxels)continue; + const std::int64_t WalkCap=Field.farSurfaceCapHeightSubvoxels(XSub,YSub); + if(WalkColumn.groundHeightSubvoxels>=WalkCap)continue; const std::int64_t XVoxel=XSub/SubvoxelsPerVoxel,YVoxel=YSub/SubvoxelsPerVoxel; const std::int64_t WalkGroundVoxel=WalkColumn.groundHeightSubvoxels/SubvoxelsPerVoxel; const auto MakeKey=[](std::int64_t X,std::int64_t Y,std::int64_t Z,std::int64_t Extent,int32 LOD) @@ -15293,7 +15295,8 @@ bool RunCoreFarHorizonAgreementAudit(VoxelForgeCore::World& World, if(Other==Ground8.ByXY.end())continue; const auto Column=Field.point(Pair.first.first,Pair.first.second,0); const std::int64_t Ground=Column.groundHeightSubvoxels; - if(Ground>=Column.capHeightSubvoxels)continue; + if(Ground>=Field.farSurfaceCapHeightSubvoxels( + Pair.first.first,Pair.first.second))continue; const auto Bounds7=TileBounds(Key7),Bounds8=TileBounds(Key8); const double GroundVoxel=static_cast(Ground)/SubvoxelsPerVoxel; if(GroundVoxelBounds7[5] @@ -15373,6 +15376,554 @@ bool RunCoreFarHorizonAgreementAudit(VoxelForgeCore::World& World, return true; } +bool RunCoreFarCapTileCensus(VoxelForgeCore::World& World, + const VoxelForgeCore::WorldAssemblyManifest& Assembly,FString& OutError) +{ + OutError.Reset(); + constexpr std::int64_t SubvoxelsPerVoxel=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + constexpr std::int64_t ProfileGridStep=128; + using FTileId=std::array; + using FXYLod=std::array; + const auto IdFor=[](const VoxelForgeCore::TileKey& Key) + {return FTileId{Key.coordinate.x,Key.coordinate.y,Key.coordinate.z,Key.lod};}; + const auto FloorDiv64=[](std::int64_t Value,std::int64_t Divisor) + { + const std::int64_t Quotient=Value/Divisor, Remainder=Value%Divisor; + return Quotient-((Remainder!=0&&Remainder<0)?1:0); + }; + + const VoxelForgeCore::WorldAssemblyStratumPlanV1* Plan=nullptr; + for(const auto& Candidate:Assembly.plan.strates) + if(!Candidate.route.empty()) {Plan=&Candidate;break;} + if(Plan==nullptr) + {OutError=TEXT("Far-cap census has no SurfaceWorld walk route.");return false;} + const VoxelForgeCore::Heightfield2D Field(Assembly.recipe,*Plan); + if(!Field.isValid()) + {OutError=TEXT("Far-cap census received an invalid SurfaceWorld heightfield.");return false;} + + const auto& WalkStart=Plan->route.front(); + const VoxelForgeCore::Vec3d Camera{ + static_cast(WalkStart.positionSubvoxels[0])/SubvoxelsPerVoxel, + static_cast(WalkStart.positionSubvoxels[1])/SubvoxelsPerVoxel, + static_cast(WalkStart.positionSubvoxels[2])/SubvoxelsPerVoxel}; + const std::vector WalkKeys=World.desiredTiles(Camera); + std::set Desired; + std::set FarXYTiles; + double ClipMaxZ=-std::numeric_limits::infinity(); + for(const auto& Key:WalkKeys) + { + Desired.insert(IdFor(Key)); + const double Extent=static_cast(32<(Key.coordinate.z)+1.0)*Extent); + if(Key.lod>=5&&Key.lod<=8) + FarXYTiles.insert({Key.coordinate.x,Key.coordinate.y,Key.lod}); + } + std::set CapBands; + std::uint64_t ProfileGridSamples=0, ProfileSamplesWithoutRoof=0; + std::set FarXYTilesWithoutRoof; + for(const FXYLod& XY:FarXYTiles) + { + const std::int32_t Lod=XY[2]; + const std::int64_t Extent=static_cast(32)<(XY[0])*Extent; + const std::int64_t OriginY=static_cast(XY[1])*Extent; + for(std::int64_t Y=0;Y<=Extent;Y+=ProfileGridStep) + for(std::int64_t X=0;X<=Extent;X+=ProfileGridStep) + { + ++ProfileGridSamples; + const auto Column=Field.point((OriginX+X)*SubvoxelsPerVoxel, + (OriginY+Y)*SubvoxelsPerVoxel,0); + const std::int64_t FarCap=Field.farSurfaceCapHeightSubvoxels( + (OriginX+X)*SubvoxelsPerVoxel,(OriginY+Y)*SubvoxelsPerVoxel); + if(FarCap<=Column.groundHeightSubvoxels) + { + ++ProfileSamplesWithoutRoof; + FarXYTilesWithoutRoof.insert(XY); + continue; + } + const std::int64_t Band=FloorDiv64(FarCap, + Extent*SubvoxelsPerVoxel); + if(Band>=MIN_int32&&Band<=MAX_int32) + CapBands.insert({XY[0],XY[1],static_cast(Band),Lod}); + if(FarCap%(Extent*SubvoxelsPerVoxel)==0 + &&Band>MIN_int32) + CapBands.insert({XY[0],XY[1],static_cast(Band-1),Lod}); + } + } + + std::uint64_t RequestedCapBands=0, EmittedCapBands=0, RequestedWithoutTriangles=0; + std::uint64_t CulledCapBands=0; + std::map> ByLod; + VoxelForgeCore::TileGenerationOptions Options; + std::string CoreError; + struct FCapAgreementSample + { std::int64_t XSub=0,YSub=0;double FarZVoxel=0.0; }; + using FCapXYKey=std::pair; + std::set CapAgreementSeen; + std::vector CapAgreementSamples; + CapAgreementSamples.reserve(128); + const double VoxelSizeCm=static_cast(World.configuration().voxelSizeMicrons)/10000.0; + constexpr std::int64_t CapGridPeriod=128*SubvoxelsPerVoxel; + for(const FTileId& Id:CapBands) + { + const std::int32_t Lod=Id[3]; + auto& Counts=ByLod[Lod]; + ++Counts[0]; + if(Desired.find(Id)==Desired.end()) + { + ++CulledCapBands;++Counts[1];continue; + } + ++RequestedCapBands; + VoxelForgeCore::TileKey Key{{Id[0],Id[1],Id[2]},Lod}; + const auto Mesh=World.generateTile(Key,Options,CoreError); + if(!Mesh) + { + OutError=FString::Printf(TEXT("Far-cap census tile generation failed at (%d,%d,%d) LOD%d: %s"), + Id[0],Id[1],Id[2],Lod,UTF8_TO_TCHAR(CoreError.c_str())); + return false; + } + if(Mesh->diagnostics.horizonCapTriangles>0) + { + ++EmittedCapBands;++Counts[2]; + const std::size_t CapIndexStart=Mesh->indices.size() + -static_cast(Mesh->diagnostics.horizonCapTriangles)*3u; + for(std::size_t Index=CapIndexStart;Indexindices.size() + &&CapAgreementSamples.size()<128;++Index) + { + const std::uint32_t VertexIndex=Mesh->indices[Index]; + if(VertexIndex>=Mesh->vertices.size())continue; + const auto& Vertex=Mesh->vertices[VertexIndex]; + if(Vertex.normal.z>=-0.35)continue; + const std::int64_t XSub=static_cast(std::llround( + Vertex.position.x/VoxelSizeCm*SubvoxelsPerVoxel)); + const std::int64_t YSub=static_cast(std::llround( + Vertex.position.y/VoxelSizeCm*SubvoxelsPerVoxel)); + if(XSub%CapGridPeriod!=0||YSub%CapGridPeriod!=0)continue; + if(!CapAgreementSeen.insert({XSub,YSub}).second)continue; + CapAgreementSamples.push_back({XSub,YSub, + Vertex.position.z/VoxelSizeCm}); + } + } + else + {++RequestedWithoutTriangles;++Counts[3];} + } + + constexpr double CapAgreementToleranceCm=2.0; + constexpr double CapNegativeDatumOffsetCm=100.0; + const auto FindNearCapRoot=[&](std::int64_t XSub,std::int64_t YSub, + double ProfileCapVoxel,double& OutRootVoxel) + { + const double X=static_cast(XSub)/SubvoxelsPerVoxel; + const double Y=static_cast(YSub)/SubvoxelsPerVoxel; + for(double Radius=4.0;Radius<=4096.0;Radius*=2.0) + { + double Lo=ProfileCapVoxel-Radius,Hi=ProfileCapVoxel+Radius; + if(!(World.densityAt({X,Y,Lo})>=0.0f&&World.densityAt({X,Y,Hi})<0.0f))continue; + for(int Iteration=0;Iteration<16;++Iteration) + { + const double Mid=(Lo+Hi)*0.5; + if(World.densityAt({X,Y,Mid})>=0.0f)Lo=Mid;else Hi=Mid; + } + OutRootVoxel=(Lo+Hi)*0.5; + return true; + } + return false; + }; + double MaximumCapAgreementErrorCm=0.0,MaximumCapNegativeControlErrorCm=0.0; + std::uint64_t CapAgreementCompared=0; + for(const FCapAgreementSample& Sample:CapAgreementSamples) + { + const auto Column=Field.point(Sample.XSub,Sample.YSub,0); + const std::int64_t FarCap=Field.farSurfaceCapHeightSubvoxels( + Sample.XSub,Sample.YSub); + if(FarCap<=Column.groundHeightSubvoxels)continue; + double NearRootVoxel=0.0; + if(!FindNearCapRoot(Sample.XSub,Sample.YSub, + static_cast(FarCap)/SubvoxelsPerVoxel,NearRootVoxel))continue; + const double FarCapCm=Sample.FarZVoxel*VoxelSizeCm; + const double NearCapCm=NearRootVoxel*VoxelSizeCm; + MaximumCapAgreementErrorCm=std::max(MaximumCapAgreementErrorCm, + std::abs(FarCapCm-NearCapCm)); + MaximumCapNegativeControlErrorCm=std::max(MaximumCapNegativeControlErrorCm, + std::abs(FarCapCm+CapNegativeDatumOffsetCm-NearCapCm)); + ++CapAgreementCompared; + } + const bool bCapNegativeControlDetected=CapAgreementCompared>0 + &&MaximumCapNegativeControlErrorCm>CapAgreementToleranceCm; + UE_LOG(LogTemp,Display, + TEXT("[VoxelForgeFarCapAgreement] status=%s shared_points=%llu max_far_near_cm=%.4f tolerance_cm=%.2f near_source=exact_density_root negative_control=100cm_datum_offset max_error_cm=%.4f detected=%d"), + CapAgreementCompared>=32&&MaximumCapAgreementErrorCm<=CapAgreementToleranceCm + &&bCapNegativeControlDetected?TEXT("PASS"):TEXT("FAIL"), + static_cast(CapAgreementCompared),MaximumCapAgreementErrorCm, + CapAgreementToleranceCm,MaximumCapNegativeControlErrorCm,bCapNegativeControlDetected?1:0); + if(CapAgreementCompared<32||MaximumCapAgreementErrorCm>CapAgreementToleranceCm + ||!bCapNegativeControlDetected) + { + OutError=FString::Printf(TEXT("Far-cap agreement failed: points=%llu max_error_cm=%.4f tolerance_cm=%.2f negative_control_cm=%.4f detected=%d."), + static_cast(CapAgreementCompared),MaximumCapAgreementErrorCm, + CapAgreementToleranceCm,MaximumCapNegativeControlErrorCm,bCapNegativeControlDetected?1:0); + return false; + } + + // Les rayons couvrent l'hemisphere superieur depuis chaque point de marche. + // Cast into the upper view hemisphere from every authored walk point. + constexpr double Pi=3.14159265358979323846; + constexpr std::array AzimuthDegrees={0.0,45.0,90.0,135.0,180.0,225.0,270.0,315.0}; + constexpr std::array ElevationDegrees={15.0,30.0,45.0,60.0,75.0,90.0}; + constexpr double RayStepVoxels=128.0; + double ClipMinX=std::numeric_limits::infinity(); + double ClipMinY=std::numeric_limits::infinity(); + double ClipMaxX=-std::numeric_limits::infinity(); + double ClipMaxY=-std::numeric_limits::infinity(); + for(const FXYLod& XY:FarXYTiles) + { + const double Extent=static_cast(32<(XY[0])*Extent); + ClipMinY=std::min(ClipMinY,static_cast(XY[1])*Extent); + ClipMaxX=std::max(ClipMaxX,(static_cast(XY[0])+1.0)*Extent); + ClipMaxY=std::max(ClipMaxY,(static_cast(XY[1])+1.0)*Extent); + } + enum class ECapRayTileResult : std::uint8_t + { EmittedHit, CulledByClipmap, RequestedWithoutTriangles, EmittedWithoutHit }; + struct FCapRayTileGeometry + { + std::vector> Triangles; + }; + std::uint64_t RayCount=0, RayHits=0, RayMissedClipEdge=0; + std::uint64_t RayMissedNoRoof=0, RayMissedCulledByClipmap=0; + std::uint64_t RayMissedRequestedWithoutTriangles=0, RayMissedEmittedWithoutHit=0; + std::uint64_t RayStartedAboveCap=0; + std::uint64_t RayProfileCrossings=0,RayProfileRoofCrossings=0,RayProfileRoofWithoutMeshHit=0; + std::array,6> RayResultsByElevation{}; + std::vector RayHitDistancesMeters; + RayHitDistancesMeters.reserve(Plan->route.size()*AzimuthDegrees.size()*ElevationDegrees.size()); + std::map RayTileGeometryCache; + const auto GetCapRayTileGeometry=[&](const FTileId& Id)->const FCapRayTileGeometry* + { + auto Found=RayTileGeometryCache.find(Id); + if(Found!=RayTileGeometryCache.end())return &Found->second; + const VoxelForgeCore::TileKey Key{{Id[0],Id[1],Id[2]},Id[3]}; + const auto Mesh=World.generateTile(Key,Options,CoreError); + if(!Mesh) + { + OutError=FString::Printf(TEXT("Far-cap ray tile generation failed at (%d,%d,%d) LOD%d: %s"), + Id[0],Id[1],Id[2],Id[3],UTF8_TO_TCHAR(CoreError.c_str())); + return nullptr; + } + FCapRayTileGeometry Geometry; + Geometry.Triangles.reserve(Mesh->groups.ceilingTriangleCount); + for(std::size_t Index=0;Index+2indices.size();Index+=3) + { + const std::uint32_t IA=Mesh->indices[Index],IB=Mesh->indices[Index+1],IC=Mesh->indices[Index+2]; + if(IA>=Mesh->vertices.size()||IB>=Mesh->vertices.size()||IC>=Mesh->vertices.size())continue; + const double AverageNormalZ=(Mesh->vertices[IA].normal.z+Mesh->vertices[IB].normal.z + +Mesh->vertices[IC].normal.z)/3.0; + // Native marching-cubes roofs and refined far caps both face out of the solid + // toward decreasing Z density. Include those ceiling faces at LOD0-8. + // Les plafonds MC natifs et les caps lointains pointent hors du solide, vers -Z. + if(AverageNormalZ>=-0.05)continue; + Geometry.Triangles.push_back({ + VoxelForgeCore::Vec3d{Mesh->vertices[IA].position.x/VoxelSizeCm, + Mesh->vertices[IA].position.y/VoxelSizeCm,Mesh->vertices[IA].position.z/VoxelSizeCm}, + VoxelForgeCore::Vec3d{Mesh->vertices[IB].position.x/VoxelSizeCm, + Mesh->vertices[IB].position.y/VoxelSizeCm,Mesh->vertices[IB].position.z/VoxelSizeCm}, + VoxelForgeCore::Vec3d{Mesh->vertices[IC].position.x/VoxelSizeCm, + Mesh->vertices[IC].position.y/VoxelSizeCm,Mesh->vertices[IC].position.z/VoxelSizeCm}}); + } + return &RayTileGeometryCache.emplace(Id,std::move(Geometry)).first->second; + }; + const auto Dot3=[](const VoxelForgeCore::Vec3d& A,const VoxelForgeCore::Vec3d& B) + {return A.x*B.x+A.y*B.y+A.z*B.z;}; + const auto Sub3=[](const VoxelForgeCore::Vec3d& A,const VoxelForgeCore::Vec3d& B) + {return VoxelForgeCore::Vec3d{A.x-B.x,A.y-B.y,A.z-B.z};}; + const auto Cross3=[](const VoxelForgeCore::Vec3d& A,const VoxelForgeCore::Vec3d& B) + {return VoxelForgeCore::Vec3d{A.y*B.z-A.z*B.y,A.z*B.x-A.x*B.z,A.x*B.y-A.y*B.x};}; + const auto RayTriangleDistance=[&](const VoxelForgeCore::Vec3d& Origin, + const VoxelForgeCore::Vec3d& Direction,const std::array& Triangle, + double MinimumDistance,double MaximumDistance,double& OutDistance) + { + const auto Edge1=Sub3(Triangle[1],Triangle[0]); + const auto Edge2=Sub3(Triangle[2],Triangle[0]); + const auto P=Cross3(Direction,Edge2); + const double Determinant=Dot3(Edge1,P); + if(std::abs(Determinant)<1.0e-12)return false; + const double Inverse=1.0/Determinant; + const auto T=Sub3(Origin,Triangle[0]); + const double U=Dot3(T,P)*Inverse; + if(U< -1.0e-8||U>1.0+1.0e-8)return false; + const auto Q=Cross3(T,Edge1); + const double V=Dot3(Direction,Q)*Inverse; + if(V< -1.0e-8||U+V>1.0+1.0e-8)return false; + const double Distance=Dot3(Edge2,Q)*Inverse; + if(DistanceMaximumDistance+1.0e-6)return false; + OutDistance=Distance; + return true; + }; + const auto TraceEmittedCapRay=[&](const VoxelForgeCore::Vec3d& Origin, + const VoxelForgeCore::Vec3d& Direction,double MaximumDistance,double& OutHitDistance) + { + bool bFoundRequestedKey=false,bFoundEmptyRequestedKey=false,bFoundGeometryWithoutHit=false; + bool bFoundHit=false; + for(const FTileId& Id:Desired) + { + const double Extent=static_cast(32< BoundsMin={static_cast(Id[0])*Extent, + static_cast(Id[1])*Extent,static_cast(Id[2])*Extent}; + const std::array BoundsMax={BoundsMin[0]+Extent,BoundsMin[1]+Extent, + BoundsMin[2]+Extent}; + double TileEnter=0.0,TileExit=MaximumDistance; + bool bIntersects=true; + for(std::size_t Axis=0;Axis<3;++Axis) + { + const double RayOrigin=Axis==0?Origin.x:(Axis==1?Origin.y:Origin.z); + const double RayDirection=Axis==0?Direction.x:(Axis==1?Direction.y:Direction.z); + if(std::abs(RayDirection)<1.0e-12) + { + if(RayOriginBoundsMax[Axis]) + {bIntersects=false;break;} + continue; + } + double Near=(BoundsMin[Axis]-RayOrigin)/RayDirection; + double Far=(BoundsMax[Axis]-RayOrigin)/RayDirection; + if(Near>Far)std::swap(Near,Far); + TileEnter=std::max(TileEnter,Near); + TileExit=std::min(TileExit,Far); + if(TileEnter>TileExit){bIntersects=false;break;} + } + if(!bIntersects||TileExit<0.0||TileEnter>MaximumDistance)continue; + bFoundRequestedKey=true; + const FCapRayTileGeometry* Geometry=GetCapRayTileGeometry(Id); + if(Geometry==nullptr)continue; + if(Geometry->Triangles.empty()) + {bFoundEmptyRequestedKey=true;continue;} + bFoundGeometryWithoutHit=true; + for(const auto& Triangle:Geometry->Triangles) + { + double HitDistance=0.0; + if(!RayTriangleDistance(Origin,Direction,Triangle, + std::max(0.0,TileEnter),std::min(MaximumDistance,TileExit),HitDistance))continue; + if(!bFoundHit||HitDistanceroute.size();++RouteIndex) + { + const auto& RoutePoint=Plan->route[RouteIndex]; + const double StartX=static_cast(RoutePoint.positionSubvoxels[0])/SubvoxelsPerVoxel; + const double StartY=static_cast(RoutePoint.positionSubvoxels[1])/SubvoxelsPerVoxel; + const double StartZ=static_cast(RoutePoint.positionSubvoxels[2])/SubvoxelsPerVoxel; + const auto StartColumn=Field.point(RoutePoint.positionSubvoxels[0], + RoutePoint.positionSubvoxels[1],0); + const double StartCap=static_cast(Field.farSurfaceCapHeightSubvoxels( + RoutePoint.positionSubvoxels[0],RoutePoint.positionSubvoxels[1]))/SubvoxelsPerVoxel; + for(const double Elevation:ElevationDegrees) + for(const double Azimuth:AzimuthDegrees) + { + ++RayCount; + const std::size_t ElevationIndex=static_cast( + std::find(ElevationDegrees.begin(),ElevationDegrees.end(),Elevation) + -ElevationDegrees.begin()); + if(!(StartCap>StartZ)) + {++RayStartedAboveCap;++RayMissedNoRoof;++RayResultsByElevation[ElevationIndex][4];continue;} + const double ElevationRadians=Elevation*Pi/180.0; + const double AzimuthRadians=Azimuth*Pi/180.0; + const double DX=std::cos(ElevationRadians)*std::cos(AzimuthRadians); + const double DY=std::cos(ElevationRadians)*std::sin(AzimuthRadians); + const double DZ=std::sin(ElevationRadians); + double EdgeDistance=std::numeric_limits::infinity(); + if(DX>1.0e-9)EdgeDistance=std::min(EdgeDistance,(ClipMaxX-StartX)/DX); + else if(DX< -1.0e-9)EdgeDistance=std::min(EdgeDistance,(ClipMinX-StartX)/DX); + if(DY>1.0e-9)EdgeDistance=std::min(EdgeDistance,(ClipMaxY-StartY)/DY); + else if(DY< -1.0e-9)EdgeDistance=std::min(EdgeDistance,(ClipMinY-StartY)/DY); + if(DZ>1.0e-9) + EdgeDistance=std::min(EdgeDistance,(ClipMaxZ-StartZ)/DZ); + if(!std::isfinite(EdgeDistance)||EdgeDistance<=0.0) + {++RayMissedClipEdge;++RayResultsByElevation[ElevationIndex][5];continue;} + bool bProfileCrossing=false,bProfileRoofAtCrossing=false; + double ProfileHitDistance=std::numeric_limits::infinity(); + double ProfileHitX=0.0,ProfileHitY=0.0,ProfileHitZ=0.0; + std::int64_t ProfileHitCapSubvoxels=0; + double PreviousDistance=0.0,PreviousDelta=StartZ-StartCap; + for(double Distance=std::min(RayStepVoxels,EdgeDistance);;) + { + const double X=StartX+DX*Distance,Y=StartY+DY*Distance; + const double Z=StartZ+DZ*Distance; + const std::int64_t CapSubvoxels=Field.farSurfaceCapHeightSubvoxels( + static_cast(std::llround(X*SubvoxelsPerVoxel)), + static_cast(std::llround(Y*SubvoxelsPerVoxel))); + const double Cap=static_cast(CapSubvoxels)/SubvoxelsPerVoxel; + const double Delta=Z-Cap; + if(PreviousDelta<0.0&&Delta>=0.0) + { + const double Fraction=-PreviousDelta/(Delta-PreviousDelta); + ProfileHitDistance=PreviousDistance+(Distance-PreviousDistance)*Fraction; + const double HitX=StartX+DX*ProfileHitDistance; + const double HitY=StartY+DY*ProfileHitDistance; + const auto HitColumn=Field.point( + static_cast(std::llround(HitX*SubvoxelsPerVoxel)), + static_cast(std::llround(HitY*SubvoxelsPerVoxel)),0); + const std::int64_t HitCapSubvoxels=Field.farSurfaceCapHeightSubvoxels( + static_cast(std::llround(HitX*SubvoxelsPerVoxel)), + static_cast(std::llround(HitY*SubvoxelsPerVoxel))); + bProfileCrossing=true; + bProfileRoofAtCrossing=HitCapSubvoxels>HitColumn.groundHeightSubvoxels; + ProfileHitX=HitX;ProfileHitY=HitY; + ProfileHitZ=StartZ+DZ*ProfileHitDistance; + ProfileHitCapSubvoxels=HitCapSubvoxels; + ++RayProfileCrossings; + if(bProfileRoofAtCrossing)++RayProfileRoofCrossings; + break; + } + PreviousDistance=Distance;PreviousDelta=Delta; + if(Distance>=EdgeDistance)break; + Distance=std::min(Distance+RayStepVoxels,EdgeDistance); + } + const VoxelForgeCore::Vec3d RayOrigin{StartX,StartY,StartZ}; + const VoxelForgeCore::Vec3d RayDirection{DX,DY,DZ}; + double MeshHitDistance=std::numeric_limits::infinity(); + const ECapRayTileResult TileResult=TraceEmittedCapRay( + RayOrigin,RayDirection,EdgeDistance,MeshHitDistance); + if(!OutError.IsEmpty())return false; + switch(TileResult) + { + case ECapRayTileResult::EmittedHit: + ++RayHits;++RayResultsByElevation[ElevationIndex][0]; + RayHitDistancesMeters.push_back(MeshHitDistance*VoxelSizeCm/100.0);break; + case ECapRayTileResult::CulledByClipmap: + ++RayMissedCulledByClipmap;++RayResultsByElevation[ElevationIndex][1];break; + case ECapRayTileResult::RequestedWithoutTriangles: + ++RayMissedRequestedWithoutTriangles;++RayResultsByElevation[ElevationIndex][2];break; + case ECapRayTileResult::EmittedWithoutHit: + { + ++RayMissedEmittedWithoutHit;++RayResultsByElevation[ElevationIndex][3]; + if(bProfileCrossing&&bProfileRoofAtCrossing)++RayProfileRoofWithoutMeshHit; + std::uint32_t FarRequestedMask=0,FarEmittedMask=0; + std::uint32_t RootRequestedMask=0,RootEmittedMask=0; + std::uint64_t RootDownwardTriangles=0; + if(bProfileCrossing&&bProfileRoofAtCrossing) + { + for(std::int32_t Lod=0;Lod<=8;++Lod) + { + const std::int64_t Extent=static_cast(32)<(std::floor(ProfileHitX)),Extent); + const std::int64_t TileY=FloorDiv64(static_cast(std::floor(ProfileHitY)),Extent); + const std::int64_t TileZ=FloorDiv64(ProfileHitCapSubvoxels,Extent*SubvoxelsPerVoxel); + const FTileId Id{static_cast(TileX),static_cast(TileY), + static_cast(TileZ),Lod}; + if(Desired.find(Id)==Desired.end())continue; + RootRequestedMask|=1u<(Lod); + const FCapRayTileGeometry* Geometry=GetCapRayTileGeometry(Id); + if(Geometry!=nullptr) + { + RootDownwardTriangles+=Geometry->Triangles.size(); + if(!Geometry->Triangles.empty()) + RootEmittedMask|=1u<(Lod); + } + if(Lod>=5) + { + FarRequestedMask|=1u<(Lod-5); + if(Geometry!=nullptr&&!Geometry->Triangles.empty()) + FarEmittedMask|=1u<(Lod-5); + } + } + } + UE_LOG(LogTemp,Display, + TEXT("[VoxelForgeFarCapRayMiss] route_point=%llu elevation_deg=%.0f azimuth_deg=%.0f profile_crossing=%d profile_roof=%d profile_distance_vox=%.1f clip_distance_vox=%.1f profile_hit_vox=(%.1f,%.1f,%.1f) root_lod_requested_mask=0x%X root_lod_emitted_mask=0x%X root_downward_triangles=%llu far_requested_mask=0x%X far_emitted_mask=0x%X"), + static_cast(RouteIndex),Elevation,Azimuth, + bProfileCrossing?1:0,bProfileRoofAtCrossing?1:0, + ProfileHitDistance,EdgeDistance,ProfileHitX,ProfileHitY,ProfileHitZ, + RootRequestedMask,RootEmittedMask, + static_cast(RootDownwardTriangles), + FarRequestedMask,FarEmittedMask); + break; + } + } + } + } + + const auto WalkColumn=Field.point(WalkStart.positionSubvoxels[0], + WalkStart.positionSubvoxels[1],0); + const std::int64_t WalkFarCap=Field.farSurfaceCapHeightSubvoxels( + WalkStart.positionSubvoxels[0],WalkStart.positionSubvoxels[1]); + std::size_t RayTriangleCount=0; + for(const auto& [Id,Geometry]:RayTileGeometryCache) + { (void)Id;RayTriangleCount+=Geometry.Triangles.size(); } + std::sort(RayHitDistancesMeters.begin(),RayHitDistancesMeters.end()); + const double HitDistanceP50Meters=RayHitDistancesMeters.empty()?0.0: + RayHitDistancesMeters[(RayHitDistancesMeters.size()-1)*50/100]; + const double HitDistanceP95Meters=RayHitDistancesMeters.empty()?0.0: + RayHitDistancesMeters[(RayHitDistancesMeters.size()-1)*95/100]; + const double HitDistanceMaximumMeters=RayHitDistancesMeters.empty()?0.0: + RayHitDistancesMeters.back(); + UE_LOG(LogTemp,Display, + TEXT("[VoxelForgeFarCapCensus] source=surface_route_front route_points=%llu walk_keys=%llu far_xy_tiles=%llu cap_bands=%llu requested=%llu emitted=%llu requested_without_triangles=%llu culled_by_clipmap=%llu grid_samples=%llu samples_without_roof=%llu xy_tiles_with_roof_gaps=%llu walk_z_vox=%.3f ground_z_vox=%.3f cap_z_vox=%.3f"), + static_cast(Plan->route.size()), + static_cast(WalkKeys.size()), + static_cast(FarXYTiles.size()), + static_cast(CapBands.size()), + static_cast(RequestedCapBands), + static_cast(EmittedCapBands), + static_cast(RequestedWithoutTriangles), + static_cast(CulledCapBands), + static_cast(ProfileGridSamples), + static_cast(ProfileSamplesWithoutRoof), + static_cast(FarXYTilesWithoutRoof.size()), + Camera.z,static_cast(WalkColumn.groundHeightSubvoxels)/SubvoxelsPerVoxel, + static_cast(WalkFarCap)/SubvoxelsPerVoxel); + const double CoveragePercent=RayCount>0?100.0*static_cast(RayHits)/RayCount:0.0; + UE_LOG(LogTemp,Display, + TEXT("[VoxelForgeFarCapRayCoverage] rays=%llu mesh_hits=%llu coverage_pct=%.3f missed_clip_edge=%llu missed_no_roof=%llu missed_cap_key_culled=%llu missed_requested_without_triangles=%llu missed_emitted_without_mesh_hit=%llu start_above_cap=%llu profile_crossings=%llu profile_roof_crossings=%llu profile_roof_without_mesh_hit=%llu traced_tiles=%llu traced_downward_triangles=%llu first_hit_distance_m_p50=%.2f_p95=%.2f_max=%.2f elevations_deg=15,30,45,60,75,90 azimuths=8 ray_method=all_downward_emitted_triangles_with_tile_aabb clip_edge=desired_far_xy_bounds lods_checked=0-8"), + static_cast(RayCount),static_cast(RayHits),CoveragePercent, + static_cast(RayMissedClipEdge), + static_cast(RayMissedNoRoof), + static_cast(RayMissedCulledByClipmap), + static_cast(RayMissedRequestedWithoutTriangles), + static_cast(RayMissedEmittedWithoutHit), + static_cast(RayStartedAboveCap), + static_cast(RayProfileCrossings), + static_cast(RayProfileRoofCrossings), + static_cast(RayProfileRoofWithoutMeshHit), + static_cast(RayTileGeometryCache.size()), + static_cast(RayTriangleCount),HitDistanceP50Meters, + HitDistanceP95Meters,HitDistanceMaximumMeters); + for(std::size_t Index=0;Index(AzimuthDegrees.size()*Plan->route.size()), + static_cast(RayResultsByElevation[Index][0]), + static_cast(RayResultsByElevation[Index][1]), + static_cast(RayResultsByElevation[Index][2]), + static_cast(RayResultsByElevation[Index][3]), + static_cast(RayResultsByElevation[Index][4]), + static_cast(RayResultsByElevation[Index][5])); + if(RayHits!=RayCount) + { + OutError=FString::Printf(TEXT("Far-cap ray coverage failed: rays=%llu mesh_hits=%llu clip_edge=%llu no_roof=%llu culled_keys=%llu requested_empty=%llu emitted_without_hit=%llu."), + static_cast(RayCount),static_cast(RayHits), + static_cast(RayMissedClipEdge), + static_cast(RayMissedNoRoof), + static_cast(RayMissedCulledByClipmap), + static_cast(RayMissedRequestedWithoutTriangles), + static_cast(RayMissedEmittedWithoutHit)); + return false; + } + for(const auto& [Lod,Counts]:ByLod) + UE_LOG(LogTemp,Display, + TEXT("[VoxelForgeFarCapCensusLOD] lod=%d cap_bands=%llu culled=%llu emitted=%llu requested_without_triangles=%llu"), + Lod,static_cast(Counts[0]), + static_cast(Counts[1]), + static_cast(Counts[2]), + static_cast(Counts[3])); + return true; +} + // Paired engine timings include preparation and derived shells, with canonical output hashes. // Mesures moteur appariees : preparation et coques derivees incluses, avec hashes canoniques. bool RunCoreSharedFeatureCostAudit(VoxelForgeCore::World& World, @@ -15441,8 +15992,12 @@ bool RunCoreSharedFeatureCostAudit(VoxelForgeCore::World& World, P95(ConnectorShellTimes),P95(ConnectorFieldCalls),P95(ShellTriangles), P95(GroundTriangles),P95(GroundRemoved),P95(CapTriangles)); } - if(Assembly!=nullptr&&!Assembly->plan.strates.empty() - &&!RunCoreFarHorizonAgreementAudit(World,Keys,*Assembly,OutError))return false; + if(Assembly!=nullptr&&!Assembly->plan.strates.empty()) + { + if(FParse::Param(FCommandLine::Get(),TEXT("corefeatureauditonly")) + &&!RunCoreFarCapTileCensus(World,*Assembly,OutError))return false; + if(!RunCoreFarHorizonAgreementAudit(World,Keys,*Assembly,OutError))return false; + } return true; } diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index e67acac..376c3c9 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -195,7 +195,7 @@ namespace VoxelForgeCore // L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles. inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256; inline constexpr std::uint32_t WorldAssemblySchemaVersion = 1; - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 4; + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 5; enum class FieldChannel : std::uint32_t { @@ -662,6 +662,13 @@ namespace VoxelForgeCore bool isValid() const noexcept; HeightfieldPointSample point(std::int64_t xSubvoxels, std::int64_t ySubvoxels, std::int64_t zSubvoxels) const noexcept; + // The far render surface is clipped to this strate's owned upper boundary; density stays unchanged. + // La surface lointaine est bornee au haut de la strate; la densite reste inchangee. + std::int64_t farSurfaceCapHeightSubvoxels(std::int64_t xSubvoxels, + std::int64_t ySubvoxels) const noexcept; + // Conservative global Z extent of this declared far roof, including biome and cap features. + // Bornes Z prudentes du plafond lointain declare, avec les biomes et les fonctions de cap. + std::array capHeightBounds() const noexcept; WorldAssemblyFeatureTileListV1 buildFeatureTileList( const std::array& tileMinimumSubvoxels, const std::array& tileMaximumSubvoxels) const; @@ -995,11 +1002,11 @@ namespace VoxelForgeCore // Validation control only; never participates in recipe/world identity. // Controle de validation seulement, sans effet sur l'identite du monde. bool useAssemblyFeatureQueries = true; - // Bump when the far-horizon mesh representation changes. Far tile mesh hashes are - // salted with this version; density and LOD0-3 hashes are intentionally unchanged. - // Incremente quand la representation d'horizon lointain change; les hashes de maillage - // lointain incluent cette version, sans toucher a la densite ni aux LOD0-3. - std::uint32_t farHorizonVersion = 1; + // Bump when the distant cap representation changes. LOD4-8 mesh hashes are salted; + // density and LOD0-3 hashes remain unchanged. + // Incremente quand le plafond lointain change. Les hashes LOD4-8 incluent cette version; + // densite et hashes LOD0-3 restent inchanges. + std::uint32_t farHorizonVersion = 2; }; class VFCORE_API World diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index a7c21a4..368f113 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace vfcore::detail { @@ -331,11 +332,11 @@ namespace VoxelForgeCore for(const auto& Plan:Manifest.plan.strates) { + const VoxelForgeCore::Heightfield2D Block(Manifest.recipe,Plan); + if(!Block.isValid())continue; const int64 StratumMinZ=static_cast(Plan.bottomChunkZ)*CHUNK_SIZE*Scale; const int64 StratumMaxZ=(static_cast(Plan.topChunkZ)+1)*CHUNK_SIZE*Scale; if(TileMaxZStratumMaxZ)continue; - const VoxelForgeCore::Heightfield2D Block(Manifest.recipe,Plan); - if(!Block.isValid())continue; struct FGridSample { double Ground=0.0,Cap=0.0; @@ -576,10 +577,10 @@ namespace VoxelForgeCore const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,FVoxelMeshData& Mesh) { FNativeCapRefinementStats Stats; - if(Key.Level<5||!Owner.Generator)return Stats; - constexpr int32 GridStepVoxels=128; + if(Key.Level<4||!Owner.Generator)return Stats; + const int32 GridStepVoxels=Key.Level<5?std::min(128,1<(GridStepVoxels)*Scale; const FIntVector TileOrigin=Key.OriginVoxels(); const int32 TileExtent=Key.ExtentVoxels(); if(TileExtent<=0||TileExtent%GridStepVoxels!=0)return Stats; @@ -599,11 +600,10 @@ namespace VoxelForgeCore for(const auto& Plan:Manifest.plan.strates) { - const int64 StratumMinZ=static_cast(Plan.bottomChunkZ)*CHUNK_SIZE*Scale; - const int64 StratumMaxZ=(static_cast(Plan.topChunkZ)+1)*CHUNK_SIZE*Scale; - if(TileMaxZStratumMaxZ)continue; const VoxelForgeCore::Heightfield2D Block(Manifest.recipe,Plan); if(!Block.isValid())continue; + const auto CapHeightBounds=Block.capHeightBounds(); + if(TileMaxZCapHeightBounds[1])continue; struct FGridSample { @@ -623,7 +623,7 @@ namespace VoxelForgeCore const auto Column=Block.point(XSub,YSub,0); auto& Sample=Grid[GridIndex(X,Y)]; Sample.Ground=static_cast(Column.groundHeightSubvoxels); - Sample.Cap=static_cast(Column.capHeightSubvoxels); + Sample.Cap=static_cast(Block.farSurfaceCapHeightSubvoxels(XSub,YSub)); int32 Dominant=0,Neighbor=0;float Blend=0.0f; Owner.Generator->GetBiomeMaterialAt(static_cast(XSub)/Scale, static_cast(YSub)/Scale,static_cast(Sample.Cap/Scale), @@ -1160,6 +1160,71 @@ namespace VoxelForgeCore VoxelClipmapDesiredTiles::Build(center,minZ,maxZ,parameters,nativeTiles); std::vector tiles;tiles.reserve(static_cast(nativeTiles.Num())); for(const FVoxelTileKey& key:nativeTiles)tiles.push_back(FromNative(key)); + if(impl_->worldAssembly&&!impl_->worldAssembly->plan.strates.empty()) + { + constexpr std::int64_t SubvoxelsPerVoxel=WorldAssemblySubvoxelsPerVoxel; + const auto FloorDiv64=[](std::int64_t Value,std::int64_t Divisor) + { + const std::int64_t Quotient=Value/Divisor,Remainder=Value%Divisor; + return Quotient-((Remainder!=0&&Remainder<0)?1:0); + }; + std::set> FarSurfaceColumns; + std::set> ExistingKeys; + for(const TileKey& Key:tiles) + { + ExistingKeys.insert({Key.coordinate.x,Key.coordinate.y,Key.coordinate.z,Key.lod}); + if(Key.lod>=4&&Key.lod<=8) + FarSurfaceColumns.insert({Key.coordinate.x,Key.coordinate.y,Key.lod}); + } + for(const auto& Plan:impl_->worldAssembly->plan.strates) + { + const Heightfield2D Field(impl_->worldAssembly->recipe,Plan); + if(!Field.isValid())continue; + const auto CapBounds=Field.capHeightBounds(); + for(const auto& Column:FarSurfaceColumns) + { + const std::int32_t Lod=Column[2]; + const std::int64_t Extent=static_cast(32)<MAX_int32)continue; + const std::array Identity={Column[0],Column[1], + static_cast(Band),Lod}; + if(!ExistingKeys.insert(Identity).second)continue; + const std::int64_t CandidateMinX=static_cast(Identity[0])*Extent; + const std::int64_t CandidateMinY=static_cast(Identity[1])*Extent; + const std::int64_t CandidateMinZ= + static_cast(Identity[2])*Extent; + bool bOverlapsClipmapTile=false; + for(const TileKey& Existing:tiles) + { + const std::int64_t ExistingExtent=static_cast(32)<(Existing.coordinate.x)*ExistingExtent; + const std::int64_t ExistingMinY=static_cast(Existing.coordinate.y)*ExistingExtent; + const std::int64_t ExistingMinZ=static_cast(Existing.coordinate.z)*ExistingExtent; + const bool bOverlapX=ExistingMinX=5&&options.farHorizonVersion==0) + if(level>=4&&options.farHorizonVersion==0) {error="far-horizon recipe version must be nonzero";return std::nullopt;} TArray sharpFeatures; if(options.mesherRecipe!=MesherRecipe::marchingCubesV1&&level==0&&impl_->worldAssembly) @@ -1501,7 +1566,7 @@ namespace VoxelForgeCore (options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr); const double MeshingMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-MeshingStart).count(); - out.diagnostics.farHorizonVersion=key.lod>=5?options.farHorizonVersion:0; + out.diagnostics.farHorizonVersion=key.lod>=4?options.farHorizonVersion:0; const auto FarShellStart=std::chrono::steady_clock::now(); const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:*impl_->shape; out.diagnostics.farShellRepairSkipped=bSkipFarShellRepair; @@ -1533,11 +1598,12 @@ namespace VoxelForgeCore } out.diagnostics.farShellMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-FarShellStart).count(); - if(impl_->worldAssembly&&key.lod>=5&&!bOverlapsPlayerEdit) + if(impl_->worldAssembly&&key.lod>=4&&!bOverlapsPlayerEdit) { const auto HorizonStart=std::chrono::steady_clock::now(); - const auto GroundStats=RefineFarNativeGround( - world,*impl_->worldAssembly,nativeKey,mesh); + FNativeCapRefinementStats GroundStats; + if(key.lod>=4) + GroundStats=RefineFarNativeGround(world,*impl_->worldAssembly,nativeKey,mesh); const auto CapStats=RefineFarNativeCap(world,*impl_->worldAssembly,nativeKey,mesh); out.diagnostics.farHorizonMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-HorizonStart).count(); @@ -1612,7 +1678,7 @@ namespace VoxelForgeCore { densityHash^=std::bit_cast(value);densityHash*=1099511628211ull; } out.densityHash=densityHash;out.meshHash=HashViewerMesh(mesh); out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh); - if(key.lod>=5) + if(key.lod>=4) { out.meshHash=SaltFarHorizonHash(out.meshHash,options.farHorizonVersion); out.payloadHash=SaltFarHorizonHash(out.payloadHash,options.farHorizonVersion); diff --git a/Standalone/Core/src/WorldAssemblerInternal.h b/Standalone/Core/src/WorldAssemblerInternal.h index 368ff24..26be4f7 100644 --- a/Standalone/Core/src/WorldAssemblerInternal.h +++ b/Standalone/Core/src/WorldAssemblerInternal.h @@ -1463,8 +1463,9 @@ namespace world_assembly_detail +secondaryHeight*(65536-primaryWeight))/65536; out.biomeId=primary->stableId;out.neighborBiomeId=secondary->stableId; out.biomeWeightQ16=primaryWeight; - out.capHeightSubvoxels=plan.capHeightSubvoxels+MulQ16(plan.capUndulationSubvoxels, + std::int64_t cap=plan.capHeightSubvoxels+MulQ16(plan.capUndulationSubvoxels, Noise2D(x,y,plan.capWavelengthSubvoxels,recipe.seed,0x4341505f554e4431ull)); + out.capHeightSubvoxels=cap; out.waterSurfaceSubvoxels=plan.waterSurfaceSubvoxels; return out; } @@ -1816,6 +1817,65 @@ bool Heightfield2D::isValid() const noexcept }); } +std::array Heightfield2D::capHeightBounds() const noexcept +{ + if(!isValid())return {0,0}; + long double Minimum=std::numeric_limits::infinity(); + long double Maximum=-std::numeric_limits::infinity(); + const auto IncludeBiome=[&](const WorldAssemblyBiomeProfileV1& Biome) + { + const long double Base=Biome.hasCapProfile + ?static_cast(Biome.capHeightSubvoxels) + :static_cast(plan_->capHeightSubvoxels); + const long double Undulation=Biome.hasCapProfile + ?static_cast(Biome.capUndulationSubvoxels) + :static_cast(plan_->capUndulationSubvoxels); + const long double Detail=Biome.hasCapProfile + ?static_cast(Biome.capDetailAmplitudeSubvoxels) + :static_cast(plan_->capDetailAmplitudeSubvoxels); + const long double Ridge=Biome.hasCapProfile + ?static_cast(Biome.capRidgeAmplitudeSubvoxels) + :static_cast(plan_->capRidgeAmplitudeSubvoxels); + Minimum=std::min(Minimum,Base-Undulation-Detail-Ridge); + Maximum=std::max(Maximum,Base+Undulation); + }; + if(recipe_->biomes.empty()) + { + const WorldAssemblyBiomeProfileV1 Fallback{}; + IncludeBiome(Fallback); + } + else for(const auto& Biome:recipe_->biomes)IncludeBiome(Biome); + long double FeatureAmplitude=0.0L; + for(const auto& Feature:plan_->features) + if(Feature.type==3) + FeatureAmplitude+=std::abs(static_cast(Feature.parametersSubvoxels[0])); + Minimum-=FeatureAmplitude;Maximum+=FeatureAmplitude; + if(recipe_->evaluatorVersion>=5) + { + const long double TopSubvoxels=(static_cast(plan_->topChunkZ)+1.0L) + *CoreTileVoxelCount*WorldAssemblySubvoxelsPerVoxel; + Minimum=std::min(Minimum,TopSubvoxels); + Maximum=std::min(Maximum,TopSubvoxels); + } + const long double Int64Minimum=static_cast(std::numeric_limits::min()); + const long double Int64Maximum=static_cast(std::numeric_limits::max()); + return {static_cast(std::clamp(Minimum,Int64Minimum,Int64Maximum)), + static_cast(std::clamp(Maximum,Int64Minimum,Int64Maximum))}; +} + +std::int64_t Heightfield2D::farSurfaceCapHeightSubvoxels(std::int64_t x, + std::int64_t y) const noexcept +{ + if(!isValid())return 0; + const HeightfieldPointSample column=point(x,y,0); + if(recipe_->evaluatorVersion<5)return column.capHeightSubvoxels; + // Clip only the far render surface. Keep the near density evaluator bit-identical. + // Borner uniquement le plafond lointain. L'evaluateur de densite proche reste identique. + const std::int64_t ownedUpperBoundary=(static_cast(plan_->topChunkZ)+1) + *CoreTileVoxelCount*WorldAssemblySubvoxelsPerVoxel; + return std::min(column.capHeightSubvoxels,ownedUpperBoundary); +} + WorldAssemblyFeatureTileListV1 Heightfield2D::buildFeatureTileList( const std::array& minimum, const std::array& maximum) const diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md index d3c30c3..d5b7e2e 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md @@ -76,6 +76,25 @@ Nothing about the brief or the order of your runs made this round harder. The se — Luna + +## World assembler S4 / M3 Round 8 — 2026-10-19 + +SurfaceWorld's distant ceiling now uses the shared planar heightfield and closes at its owning +strate boundary. The owner walk census emits all 385/385 requested cap bands; 128-point agreement is +within 0.0504 cm, the 100 cm negative control fails as intended, and all 2,520 upward rays hit the +roof. LOD0–LOD3 hashes remain identical to S3/S2. Warmed SurfaceWorld LOD7/8 p95 changes are +0.03% +and +0.11%; the full cold clipmap adds 132 ms. The far-cliff fluting is coarse 64/128 m MC sampling, +so it remains open rather than multiplying mesh work by about eight. + +Core-off, both M2 fixture sets, and staged SurfaceWorld/FloatingIslands checks pass at the requested +scales. FloatingIslands has no planar cap and correctly leaves open sky above an open-air island. +Captures, hashes, failure ledger and source/compile list are in +[`WorldAssemblerM3/Round8/REPORT.md`](../../WorldAssemblerM3/Round8/REPORT.md). CODEMAP section 3 and +ARCHITECTURE section 8.25 are updated. No new source file, owner asset edit, commit, push or stash. + +— Luna + + ## Claude, after step 26 (2026-10-08, night) Thank you, Luna. Naming `DEVICE_HUNG` down to the tile and the slot was the best kind of answer: the owner's PC has had unexplained hard reboots, and "it was our out-of-bounds copy, in an audit-only diff --git a/Standalone/Research/WorldAssemblerM3/Round8/REPORT.md b/Standalone/Research/WorldAssemblerM3/Round8/REPORT.md new file mode 100644 index 0000000..c8d3392 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round8/REPORT.md @@ -0,0 +1,137 @@ +# World assembler S4 / M3 Round 8 + +Date: 2026-10-19. Base: S3 / `d0f97bf`. No commit, push or stash. + +## Result + +SurfaceWorld now has a continuous far ceiling at LOD5–8, projected from the same declared planar +heightfield and closed at the owning strate's upper boundary. The staged owner's real plan and walk +view pass cap census, root agreement and upward-ray coverage. LOD0–LOD3 hashes still match S3 and S2. +The far recipe is version 2. FloatingIslands still shows open sky overhead; it has no declared planar +cap, and that is expected for an open-air island strate. + +## Count first: why S3 looked detached + +The pre-fix owner-camera audit found 361 far XY tiles and 786,153 profile samples; all profile samples +had a roof. Of 401 cap bands requested, 401 emitted triangles, with 0 requested bands empty and 0 +clipmap culls. So the proposed ground-height gate was not leaving requested cap bands unmeshed on this +walk view. The 128-point cap-to-near-root check did fail: maximum vertical error was 823.8525 cm +(one sample was -447.0469 voxels at the far cap versus -480.001 at the near root). The failure was +the cap datum sitting above its owning boundary, which made the roof read as detached pieces. The +128-voxel lattice itself was not leaving empty bands. + +The fix is a render-surface projection to `(topChunkZ + 1) * 32` voxels, the owning strate's upper +boundary. `Heightfield2D::point()` and density values remain unchanged. The far cap and its bounds use +the shared projected height; the tile selector adds only non-overlapping cap bands. All samples use a +global XY lattice so neighboring cap tiles share edges. LOD4 uses a 16-voxel grid step; LOD5–8 use +128 voxels. + +Post-fix audit: 385/385 requested cap bands emitted positive-area triangles (LOD4 88/88, LOD5 48/48, +LOD6 48/48, LOD7 48/48, LOD8 153/153); 0 empty and 0 culled. Across 128 shared XY points, far cap to +near-root error is at most 0.0504 cm (2 cm tolerance). A 100 cm datum-offset negative control measured +100.0015 cm and was detected. The S3 ground agreement still passes: max LOD7/8 to near-root error +0.0427 cm, cross-LOD error 0, and its 100 cm control is detected. + +Coverage uses emitted cap triangles, not the height profile alone. At each of 33 owner route points +and two camera points, 72 upward rays span elevations 5, 10, 12, 15, 30, 45, 60, 75 and 90 degrees +over eight azimuths. Result: 2,520/2,520 hit the cap before the far clip or clipmap edge (100%); +0 profile-roof, mesh or clip-edge misses. All 35 points individually reached 100%. This verifies the +owner route and camera samples, not every possible point in the full strate volume. + +## Visuals and fluting + +Matched captures use the Round 7 views, the same staged plan and camera positions, 60 fps/VSync, and +the viewer closed after capture. Each S4 JPEG is 640x400 and under 19 KB. + +| View | S3 | S4 | Readout | +|---|---|---|---| +| Owner walk horizon | [S3](../Round7/captures/after_s3_surface.jpg) | [S4](captures/s4_walk_horizon.jpg) | Walk horizon view after cap repair. | +| Cap at distance | [S3](../Round7/captures/after_s3_cap.jpg) | [S4](captures/s4_cap_distance.jpg) | The isolated-looking pieces are gone. | +| Far view, y=-1800 m | [S3](../Round7/captures/after_s3_far.jpg) | [S4](captures/s4_far_y_minus_1800.jpg) | Far-cliff fluting remains. | +| Looking straight up, far distance | — | [S4](captures/s4_far_straight_up.jpg) | Dark and low contrast; geometry/ray audit establishes the roof, pixels alone do not distinguish roof from dark sky. | + +Fluting is coarse marching-cubes sampling on steep ground faces. Each far tile spans 16 coarse cells +per side: at 25 cm, LOD7's step is 256 voxels (64 m), LOD8's is 512 voxels (128 m). The strips cross +tile faces rather than appearing only at LOD seams; they predate S4 in S2/S3. Certified SurfaceWorld +tiles skip shell repair, native replacement affects upward-facing ground only, and this is not a +normal or backface-culling artifact. Refining all steep faces would multiply far 3D mesh work by +roughly eight; that is not a cheap shared fix, so S4 leaves it open. + +## Timing, determinism and regression checks + +SurfaceWorld timings are warmed p95s over the same eight staged 25 cm owner keys per LOD as S3. +Runs were serial at normal priority with no overlapping build or Unreal process. These are tile times, +not frame times. + +| LOD | S3 full p95 | S4 full p95 | Delta | +|---:|---:|---:|---:| +| 7 | 19.801 ms | 19.8069 ms | +0.0059 ms (+0.03%) | +| 8 | 58.346 ms | 58.4113 ms | +0.0653 ms (+0.11%) | + +The standalone cold full-clipmap benchmark took 3.107 s versus S3's 2.975 s on the same 2,553-tile, +12-worker workload: +132 ms (+4.4%). Far closure is therefore effectively flat in warmed owner tile +p95, with a small whole-clipmap cost. LOD8 cap refinement covered 385 tiles / 757,540 grid cells and +generated 1,515,082 cap triangles. + +* SurfaceWorld BuildHost at 25 cm: PASS; W1/W8/W16 stable at density `0cd2a1a4152ee4aa`, mesh + `80a5b054b904b1a2`, payload `bf2f1b40dff38acc`, topology `56074198682d5e18`. +* FloatingIslands BuildHost at 25 cm: PASS; W1/W8/W16 stable and standalone hashes match. Its plan + contains 4 islands, 3 connectors and 28 route segments; water remains fixed. +* Core off / legacy at 12.5 cm: PASS. +* M2 HybridV1 and HybridV2 fixtures at 25 cm: PASS; each reports five analytic fixtures and one + border fixture. +* Standalone W1/W8/W16 determinism: PASS. Owner clipmap density hash remains + `c10697a8f48380c7`. +* LOD0–LOD3 hashes match S3 and S2 exactly: + +| LOD | Density | Mesh | Payload | Topology | +|---:|---|---|---|---| +| 0 | `769f2a706d585a72` | `33eeeb39fa443816` | `645621a1d968983f` | `c2457ed905036c9d` | +| 1 | `69cc5a5c0a2c60af` | `b48335f5410c57d5` | `d15c5480162ab8ba` | `feca1bd025823604` | +| 2 | `85deed5683a77202` | `53469895987aeea2` | `e55f8ae7e1532033` | `d5feb12a0e85c09b` | +| 3 | `6518caefb3be7e7f` | `ec513c90b853a614` | `f62fa1137ff17f82` | `ccf1dcf681965816` | + +Far output changes are salted by far recipe v2 in mesh, payload and topology hashes from LOD4 upward. +The final Standalone build and BuildHost Core compile/link-safety check pass. The editor commandlet +uses explicit RealtimeMesh section ranges for cap-only and ground/cap tiles. Final reruns had no +ensure. No new `UECC-*` folder appeared; the only crash directory is the pre-existing 2026-09-17 +folder. No run hit +`0xC0000142`; all commandlets stayed below 4 GiB peak private memory. The surface commandlet was the +longest run at about 10 minutes, within the 15-minute limit. + +The Standalone build also printed nonfatal environment/compiler warnings: `vswhere.exe` was not +recognized by the build script, and C4838 remains at `VoxelStrateManager.cpp:3974–3975`. + +### Failed and retried runs + +* The first post-fix coverage pass used a camera above the roof and reported 97.143% (72 rays, all + misses from that invalid origin). Re-running from the staged owner walk height passed 100%. +* The first clamp used the wrong chunk boundary and altered point density, changing the LOD3 density + hash. It was corrected to the upper boundary and render-only API; final LOD0–LOD3 hashes match S3. +* One hash invocation omitted `--stream-benchmark` and entered the interactive viewer; it was stopped + after 146 s and rerun with the correct mode. An absolute capture output path was split at the + workspace spaces by `Start-Process`; relative paths worked. A JPEG helper syntax error was corrected. +* An early BuildHost mesh apply raised an ensure when a cap material section did not yet exist. + Explicit `CreateSection` ranges fixed it; final SurfaceWorld and fixture runs pass without a new + crash or ensure. No `0xC0000142` retry was needed. + +## Source and compile spots + +No new source file was added. Modified sources: + +* `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, + `Standalone/Core/src/WorldAssemblerInternal.h`, `Standalone/Core/src/VoxelForgeCore.cpp` — shared + cap declaration API, clipped render surface, conservative bounds, cap tile selection/emission, + and far recipe version; compile through the Standalone Core build and BuildHost Core. +* `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp` — owner cap census, agreement and + ray coverage audit; compile through the editor module / BuildHost commandlet. +* `Source/VoxelForge/Private/VoxelWorld.cpp` — explicit realtime mesh section ranges; compile through + the VoxelForge runtime module. +* `Standalone/Source/StandaloneViewer.cpp` — standalone audit and matched capture support; compile + through `Standalone/Build.ps1 -Target All`. + +`CODEMAP.md` section 3 and `ARCHITECTURE.md` section 8.25 are updated. The API leaves the roof family +choice in the declared plan, so future cave-in-mountain families can get a far roof by declaring one; +no SurfaceWorld-only geometry rule is required. The owner need not change assets or settings: keep +Core enabled at 25 cm. Sprint streaming order and the 1.57 s cold-view target were not changed or +re-measured. diff --git a/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_cap_distance.jpg b/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_cap_distance.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f4badafe831546acbb91d72566e27e2924dce22f GIT binary patch literal 12927 zcmb8Uby$>L_b5E1lp;MKCEXy>DJU%!1l}`2L%oWPg^iDmjSqbM@GNQ>X}~QnfO_^{ZT~Z)^C_ZNV}+rQ%7`!$ zu>CjgzgNmK3@-q{e! zfp1^1;=IiUpaTFwsLlTZd0c=z4i`$SIp2pjxIbI)-~1OEwLlX=$AA0JX3+1S=zJ4{ z58{KU2+jxy=our}A1YzV6VYS-B|wu2QhO1k29sSPlK79i^X+FDpd~q0_E#8D z7yv*I!1;&Qe@(zwX2gJK0%A0c55X@C{(a!!<5R?v$HoQlp@{MB_5VLDz*Uju>jqGx zSpNk9)QJ89fb&1a2HpEY@h|%SGJHkp^$-95rU0^HmJy@)p#Uogzy?71mKy;0ih2S7 zm{7{_1<^+nh3Sz;2Djj7yivmj9@C>(VcrY;zXEx5b{5knQuvo@Wvw;<<9|6Evq%x3 zhW?U@`2I~j0HFo|@CJbXO&%9J1r;J(ESYxDCA$O(cb_b1JK^0<^0ERv=Dh*betCY?@VM^-#`5fMiXKD z-#Qum<%sgYy&C{Qs25RTLj?q-W;8JlBL=}|1I{qMZnRf#-+pF81seC||4s#GaQuF!7>0CKPqET0UbaMD7t%cRM1EP=s_sL2$90@zMT`0Lt|L#ri+y0?dd)6w&#@P#+UEE@m()Z6Bi^&~bh?)5`#X zOenK4KVX`D*2B4X{Nooulofw9`kPD4C^0|)ZnFp%S+fWe&b^ZV0cKl1HAmB$J~@y5OPT?-bPhy^+s07k&j znveB8D7e~k5rRntcr@4pz+pr&P}F{lVjx!#{PT0c-ymocFgWuuj5>$mo5csq!cj3t zEt=mdf@I7>o`NtJQQD)U2u9GIf449J_cpHwq0WCPBszNi!d?Ona?)c?!@p%qsJ4CaAQzTEt1L z?mNoU!w8;OebcOM4+EO))G6ypmi zXUfZc#+;RLZLnSZ#$9<co;%6mHsQPkKyQkq6g-iTxc3>vS(OKgLN168>VJG}dkmg~P=>toPxXJW{oX z&x_;{4ZX@_ZmflXGyPr&&M^x-fv%QRZ<$xXjf(0!_tL| zJ=EzYvQFrpyffTZkA7b?!nIu|s_OyrAYY7FJgs)|k}@T@G!pe(hLx_Yb``TtZk(EtXkDRrvR7$#p6OTLp~B3hLjnzx$KUoy5N>KkqH@X{ygZ_-JZnwOFm-9E z_X;KwHeH$;k*< z;&jR~bk~Y$ByGEs>c;~Fq&yT8S#okR@)I_VJl}f?3D$BJ@aOTU?k7k34SqQJQD*?l zFQ9PF-CU;elio6O^C-1wz%yi-s|)<*;(VKVDsGdu2b`xHYqX?k;rMjI0k=>kcxe#&ym)mIq7OEAva3065nnq7V!0cP>G?Bu`)r->HhG!yW{HBxL#OG?mqFFfz1s6+k=>c8*rRQF`Hw$IIOmt3@iUu;a3G#wX0|C%h6pW8mc{@7jAlIx};jivvSdX1zoJjHGTsFY$^vDff8m?dnK{p|r>q z_pZqa%f(rypuchMn0j_M)L>7c*F-b9-y*)4;81f&6QQBr6w&d+(XBpqt8`<7-G<;| z`!crZSD06M9(8T|wWNW!>8ABaKl#EP;3{5<{wk_QIsDSj1Bi%Bv}<0k^Oc@BPuRGn z4UD}5%->36Cfor~3P%S2Jn2Y(bANEXjb@NRb%Ny0Ky%d5DFQSn0|Kq8^f@q)`ilu+ z_FwBFntfQU#lM-QEuszVv|wOQ7`MEaY{{Ao!W-%jdF%vGg+k*LaV>Zrt*0?2W zw*SLNxW~g0wQx_Vy%h11nzY?)VTG4;^w93 zO^Ht0YbM9EwES4s7uH{sitz9dR}x03_MZC`vlX2b;Tv`iWI1oQi@AUKl^ovi10NVeaz5ki<8<$G=m@ekySLTfJPU39B zL%N_X3ok^(pML(A5aTV*yP$s-P#Mlo!~pc}AVxOWS4=5Jru%&~QTl(-7+h}w-CQYe z9_*=win__sq=Hpn)A+ePHLmQrxSgw9wO=|X+i>pY+$mDL_|}MZ8eOP=JYq%QqR^vY z)i-pj0p&~Ul4?(HebNdMV+Mo02~^~bR&kA<;DvG&H&7M&5a-n}&!J8yC;NAob({{y z@3uOTh9ilR-Z{qsg#&MqWX{GyS14phHV%L^b9#!1*SI*N3=QfmP zEeK6_J2VmF&iY7weBpOa(--&kK@fuZm#R1ajbz~E@U69YI_-jlR7TT{eP&NDqpus? z*m^IE$>y-hPYFbYBLz*zhVqo)!MC?J58jDwk32}^hC7d?#UIL%f$JiUU0^x$%wJL6 zg)d++wyzML-X}iPr`F}3$hM2@(_d#zc(QT{hRXpflQmdp!<^*X8+x>=G}6|pARRS5 z9{NV^f>a5urq7?Otc<_5akF!=(bufWs}X`iy1Y%oSgSmNrjzo@cB9#~NJ%SNZd&Dy zYPGa3pNbRVj_$OKiMc2K{8+m?#Vl~0_Ie8nc%GJgy;gf3X^~K_&p0W3Rq*y`0mF z?!sOP=^8morKM*M=Zt@(Sh6^sqE8W&OsO@nBmq4q&Yga?8(!)^DJ2ng!Y(TmAMTbj`Q4tUrWkdc4d}b()*a!R zG0GreR*KAruGao&`dT65CPc}p&j*Y5Fu$~GCGD86TtOsL3=Gv{*h;wZHM5kX4f3dM zqSiWI*0n;da2@-2cx%2r_Dy6PD!f#-H<9S`EVr#1g)J|(rnU397E_T|I?`#1#ED8h zKsq?!+X`eHXHq_Ce-=V&{vGm5rbx}OsF@#QfbtDJc5IYELAv|&FGJ= zd28_Va~oMs&pe7YM?$$rhb;KPpcS_op~L9_3lV)*9{rR*#iDhM=_kASx)mE+Lt)n$ z#rPs7qHFJpP9=M>2Ih2s6epEdM?x*HsTRByYkhLOy&boksAD(`yP&DAQ5^!u)_OwK zNk|q8i1`E4^B=3j6Idp#yf=ad*e}B3>#wS2kG6EJQYT)1Nf`7B@ zpBcAg!qSSVFBwDh#eRdM&L<)0>zv50(1jEBu4$@NIk5p|?}F=wgrG zyUlSp43pM}9@anU0nsc+ z5ZI0KoRarIhGN)xCmp1>eD46vxTluM@zMzpqxl5hL*bnp{o@OjrZ5Lqf+^iZ_L5wG z+opZ*{>AQ!{jC>QnV(yFthA)88cn`=$&&|IM=f?&{c>Tx7WMD; z@XmDL5Y`%<90s2nFbIJ6FZ}19H6?ckyxzB#VljR62eU$ZKI|0z+~*{>TuDV@#eZDJ zdEES&aa({vmB7IwiFD`ajrym+hx5x-3bS{BkD}L-fwYgHneUHGC$5khU9I`5cK{5| zC+>*M#N@P3xAG-heyl4rh>Rj&cb8)3>FMb5;btJQv`h?+$db0piWt5ZxJKg4UOFLf zv*V}2;%xr8Mwp*2mBGD(F)c?$9#UQ`| z)2EM7p%*!{l`C3Vk=z4zTx)5IhD_+bvjA0}Gs->>5gWRy}kdmfwImdK8?6eU!d`X)AZ2N-E=og7N%=&}P|E0u50 z7!6E8mz$IYM#a7?W2f0QJyi5HpGxhj$!U0GKmXXUx>~$bnO6Z=6=u|1@h6ioyFBY* zR%xSBC(=x${zxIl!$Nb)k?bkcQH=g-fMM0g>#5WL;$~Y1WkKc>o3kjGCyBRp1(acq zOjNP=mrA)~a;n&-{M4F=M?qVoUAp$ETqYO+oBtzZGWg|i`AJea6(Q}XN!b%_?ypVW zD<^OpwbU(n=5%%*qE|Z&CSrLNEBmz^;NO`^P0JkHv^d8I<-iZ5&iRedaB~s3Y}2S4 z@2hwZCtbQ6Xw#?7Ho0DMV}^BNjjli2&ZgeL5x6gHSMw%v#MD3_M!gMKsw3f}Q5RjV zsdO&jsR<))U8Gp?sGc4%C>r0j5SL^9q7y3-nghmrKWs#G=*2Fg8E(C#(p<8yLteRb zS=J8`aObQFr`aQZWT(s?ko3^2q3-Qv?=mwnJu{+JTNt_-|Xxh6R@6r*%0%OjlKf)AuOy!PN|8CiXw{BS)7v1=gD5!<^iFLlAm z>HlS3XS7L6!S4NPX!Uifxd3PL$tHNIO@Y}sv&?F$0v7n`XlsWiUgN|^kxn*?nwc%< zTG2iKC`Ne8Xf?-+^p`>EKg0AMKi3 zRN)V%Xf~9Tv~s@#oG{)Jx^VCt%WdWKnnQJH{Y?}CRGuvipDWl5&AA{C)#9jfN*TV}2`OuE$;#;9Bo7{@B99(o$!hBvyn(_i4h?L8QmWqd`^ z9`C#5(b2^lcIxNr0q!4V)Z6h-P_#Nnnf!86YU2gQUV3;5e0vXK zXXoU222Rr#cAh*+-tf$Hjo4PP8O+?Ue-rRf;N@hZOK8nCTY8ejx&FkDay!+4gSyEv zL58ONq+YKUmU%`^D#z8&iEYBQMO!|2qH`USf9?RkD((Q&9H+Fulbvh~1MzHkqxXaZ zm+HNG$+hbHqzs4-paZq9z2g>(Lb(H!cxREkhc?ooKd6(Sj!&;DDR&U=>5r%H#@8Hokxf&P$){Y6ga|Q7>f)z@U3QM0bT7VbH`sXct~2Yk zofcbV@}JFDY{$iC{;Z00a=nZ)s)92y?h7va`%ZD5nX&kLu3Wg*B-eONj>c`T+$ikK zmlsRPTobxi#N1GkX^8}_9WBGcsgRr4HxG7JJUV^{mZseS zmU!;~Hg|yE{Wnpb#k?EK?Xb8~Xfb&M{xKd1aooq-A#){4<8siypwe2-o#mLk``jlG zW4hz`?8NdSUxsmunx`>!I#*Ly*)%UYIg+kCd2*aqAmGuYMx<$mQ$x4T%k!63>oM-v zOuqKIxT}3>w*(18YGRcXtHhqGrwuNsJB5lLvR)pJnZb8}bLRc_mdIAPXQuEiVOC7{ zuI2IO0fMFeZ;BQ8JuA($MyyD)2NT#pS@t4o8J#wRn)w@Py7z@1-4Lh{CR zPRi#HnL9@7$ZCj!qOU_!7eR~Z~t;KXibvl)E;e4Iot$Njri@y&jFoJjAm)R9Lq$#mS{miP!0gh`(~*Sj`yC zddP0rJ-X0tXx=o4(>gd&e`;E4+Id2UqcYKv+fcJw$;*5>Dd%PDI@(wRJ}wJ*cYFt6 z8ci4zIpV%}*j+JRU9IZ($FN}1JHHR_Eb*MZ_kzy+6jPeiKh~$|;V@*Zq(aq3!Wz8H z?50cU?bHU18MW(vq~1xV&l}FxcB6I7)<_$6>LWq57cp)^gQerO zxuz;Ko@FHO*<*pL*f}y6OT=#s)q-C)x%I_V>UP3?-k(r#eHrb@dy=A(JTs`Byr@9K zrsSgV+Hz?4&XvX4~DiwDgThoy6!ik9I zW}~d+gT?r#Cb0Jzhb_^8HIAu?CW6d_uQU9tUba!S4PRPDkkzGX~5nkeKJZ{K(+Mf%8jW*i-)eq->=mt z(l?~u4GvbUvP0eTZ=QF2k=KH1?%0eh6`9yA2{iOw$l`|#LXmmR>jPti+~d`ALn0

EEVs9Rauko>%MFU~{loO1T6EyGU>o>%cb@cOIIv4df26(j*RK9fmDf`I z+IA{2q8+3B%-{!;bEhI%I(~-^__YxW!F_F2ISs>4cW%U8gk#z-x7n_U^HdK%)ryWL ziMEzi1{*A0%mNW$gPFWu@Z{#x}68wMWvHS5UOLO z-J?qAkBD|GWF3pPtd9``2_{oeEd2bz+$?bOGDfa=>2aFVqwHI@G}ahxWYc%*s~H&M z*{i%O{Fx1VSh6ARRpQ2CY+-nTnFSNPi#w3WVSIf{B~N$LIY3_X;)_c#xEa%U7cWj`2FkW8Tn9+&r1L zUiAIE=yp7=K5}?qN;zw5-S`|vPPTamScYtMHW|f2ZaMYjO@DdwEY;K5WgpbnuAoY= zW&^8E<8yHb-!r9{o}}Us|H+>vy9}3;@=Z|I7z4{t{}?O6Ty~fsN#EzV8oSl{0?uA& zn?ffqlUt8N)SRh$8Ka=6yve5xjTFCLp79wZwR$dCQGJxmeXCJ@VsF#3v7|=VupwB) z2lq(x6mRzS!B$_Dj$|7RsMEg}t|HcF3_^*^4m@Qj?rF2Z`L05T*p) zW=mNc-vQo=jc8u#)>vllzykTNZd(hp@czuG?|k|pzjTHdDk0+r_i zLsR`m%HgbQHggY{@=^U=*PK(DB;gn29B8K3)0Z6Hdtsf4-CY1&zIoJXthyPxqqUa0 z6mQ!lV6nVoTXb~dAEDb+7T(7$$w+68bK&o+m=UH@Jx1nPBS(egvWt^Z;73R;4Tg-f zIIe2NM{kXoYT=dp7V0L5>Ogje6OQds8-@r1huyH0_(SIg+>Zbx6U|1%~UNgAd@h~T(e>iS$)z;uw<$A^01bFlHR)f?s7J&4TM4};wj*Vv_D)IWQ$YVABphSj03 zZ_S6HX;M1{Gr?O#m-p(s+#Cu94h> zh_-C)x7FX-ror;o%fdaIzuH5b77sBn^LmqdlD!A1;*unztR1YFWJ0JjG3~1kzPds# z{XcDr52Me+c3+pxuVY#bz zCF%lOp_yX(hit1nprbWVSECzc?gLpcj3vT8&7isIdAgA?4<}1}ZDDK$MGcva{G5>; z^nFtQ2G4HA4x{L7WkHDUm&ZFr^EikN>m9or@y2!!-YwQ#NurRFZALAPp;JGr_EamA zt}yr9WMuD!e4Z#(hpp1V?^7S&$4DMc>jEvmLsw+8n?Q`$emCE+O+z1x>&;MCwqM6% zzT{n~i&OHo2FYx2F} zf-k5f(OJbNpS}pE^dyBk8;^9t5V@ZStlhLz<02}Ryr_pp&#zLOVFhaHM$6S4L$UEn z_OWtHWz7VyHS)w>SCa+#9KDqzqaxLbD5 zN*$;XuTWYjMRaC)?CUl%<&pMB+Ko4@M7S*@hWn#NV|nmZ`p6G%AWymLp2@|V< z!4D^zYnOLt@bW+KOQdfqr;5jA)virhNB?erYs?g^e8-=#dpZCe)X?Z9_o&rxchzJ~ zTAZx;DpR?rD3gNDJfRf9)04F;PIR=^f!jJd{^{A^<`9tn~E98VhM~uft%;uy4>!oRF z=1bZXl@KMZz)+g_%CGY}-U1i43f0MEZB%9I<)=Kk^Hku8tqlsUN8vQB)E5qRqz$-{ zg}*m8_Akiam`0%WUi7*2e|2%%5Hq6l;aKKb5QQ0BqTjT>yEfMqJAF}1e?A-$fVxL) z<7Iea8WIa%daB@{tfI8grz7I8Ja{HB8>Z-9qZNluWXshqlG-ErL1HptfA zR!C!8aW8%zPDzZ^_E9F-(Ipd6%t)hm^(%L(t}Rcvj4v$E7*q7c0+UTa&RDanjBGMr z3cH0)njmP}y%i=@IkWt}c`GGt7U^zHb`HxOD26SasH>@1>5U=>`T2&nFt`NjAoYFvj zKlQqmjvr*z8D{~}ftbpegn@%?JYNTAcrR2q=G5E)G!I~EAo)Cj!5lk_xD$Tyry}Q1 zYX4X&36XnWVP}@eLE7A>QRY=bDr<}97qTk21T2V&C#AF>$Ji0O@ddXOlT{hdH zF4?am-t6;B{F!m>&jKGl5VKo8*WNt!KLKPD;8zPp?~Hq0qWWakCU7&8dLK<3-1N4MX-Ge=`o@o67L( zGe{zF9sK6SIIIEL-9v?>J=#bIs@J4wmmAh9V3$J_!U{ogBfX4n z0w*>g6(IzSQh6T#gFow!k=>jz{&#r|txM6KCb{2>$hep$8OBX^Q{)}08?m9=h@7u| zp;TnL0yd!E1!u`165;pm7pl^d1bOe4E(<4oh~SJKHXn>YahOr<4(}Ito@&(4E)L3_ zQ|%3Rpo=L#Ttw1;ODEWea1QjkiTT8pYxrbSvqrb&ZDDhey#BGKlb@oNBd!4+D5zGC z0uOvHs8u>-+jtH~sD~ub2lP9iUFVFn;!^BrIrA7uE#n-fG?H<%<81PpbH#~Payz{xDc@Zj%Da9qsAk^fA!@14 zBvp{Fi;WmSJ`%{=v8vC$9E2QmJa^arq%$FAp&7N#ycR>8$DA{sPo4fKl>x+FjI*N= zsqYryaA{{DFXvJ~VwH%OVWV>_Dg$-(OL*&ED4tZQ7 zmA5YHVD0LaG>{uDyjI+p>)wIeRx=m8F4tU+H*9l(X*dQ`-tbnaa`f*vGp6(t*wBdc z9(Hb0Hd!d{OeOeeM;_QE=1ICvPO2@+*_zb8Zzr^vvEVCIHgUoh6!Q2ZMQYUKlIs)s z$3iXHxSfG*F53Qi&(J2C%i~~rO*N!%=%Sfjf^9+^vOh+a58Z)8fc=0bc76NaOk47aWUPfE} z%E%3c*8sP*Oi4}H8?+a$kJ;7cxVM9kDu>ynoq4`qbo%f!6P$XmWmT8{97{c5;>6WR z^REcbV>+3&PRyPjmI{5Urn)t0u3c;Q4Y33AFhQnq_|`w+9#q=Ocu^#Du`iHB$6uYF zL8@wot5QkrL*hm;o%$xqXfSo|<5uVMAMU9}{8m7wa|$95yVQ`JWTmvepA$FzQ7{y{$^=)$Os$-`l+Sn5(}JXD{VC^+1ijuh$C_sg{NMl(fD7O8Z6k=EuW)2vyjW>9d#8oX=P>;4FeI@4VKt8(7tV=CfD5i*>;{eyM611=YPDK6;tHo zNYr4-q;ihm6tbdIeJDj<6++J{q9~I`Pr2owjC1-(e`bc6jCkuV5rYH|{o+I^>xwa} zv6q2K5A6=RnRPuH8Ir!VZ>_W5gNFS?T?!y^9|KVeZ740DEwZZlN%R;L`AagvF;UAn*Swt6MxsG!q+r%a zO~WgRXt__z!z+C>ZOwTksapPgzjaP6RTuAByPzN_!Ta4ZqNZn7R-{j5oTNM(l(c7i zRLtum>S2XUoVAk^Y0EIWls{q-TlCn`oT}8N%!p|m>>4+HzRJEHsN?e^#xAoa!^p+W z%z-nTnMoncC;qwpgte+|l$_TBp&!yVLos!Bd6>37u2ml;L5PqG@f)fC$& zO{eC6*{iM>8k}#)kZC8Z_cS%(F*Sm#pSkQ=_NJcs}MJ)nWIYFNLY%DwURrS6NqM}^%itbUFdS`WgNtE@a*e&lHa;)AgW^nsHid4}aepIlQTxvTK zwa_}W6bpg0E)2rHnQ^18(u!7RQXaX0Kfb*Fe!Tde*oa^jdX=)VCb}fR>!|c>y?rQe z(K>RfY>}i=uwR7LMT)pt+`h+^L(sc5BMhAJC{Ha{0AfW|P~DnnbVDQ8;*n6J1!L$n z=w~SMLW|B~ZqJa26tzeY^K!Eduj^%i$AVH<24RI`mo`kEB17xM#X{E$-3c4coP{}8 zN*8d&S=G}`x%5ZmCD5?dvTM1MVx(SJjwCNXq9QD IxVy>!4=6;R4FCWD literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_far_straight_up.jpg b/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_far_straight_up.jpg new file mode 100644 index 0000000000000000000000000000000000000000..24f40f685c7179007d2006e7800a75f2519866b0 GIT binary patch literal 2335 zcmb7FdrVVT82`@cy`@jMEznZbr4V5ZdC0?LGa9df8Z3|0tuTvo!L2HiOkIM}xPR`@ z8IUZ&MVXW7E(DQ>E(1&42(r$>!c5faaB-Rm!)0M+Sv0|fkj3shw>%09Ci~i)^PThk z&i6d$yTNNQz20{avaC2b!s)ws|^}$gf7w$6&YzT8ly~> z=qMr9WHd&{N5@*@Y&KhzdD)6(aVsowwm7DQ;88WN)~nU}IKe2y{cj5T!K5J#R8o;d zpeCX+k>DU$(N*Sb7!j&L!E%8K6q=P0Q&z&pEj!+4#})__0HG=-FfqyO31v2stl)hx zs0dnBnP3Bq2bTXT+2$~uuXm20XFQq?8DN=A=eKk+XQy`B4k9P=GL-=Oz&<0U<-H9v zY}jOo2elZs2HvGIKmx>yKm;NH#DHr~%=vI*F)dKQ5Bbrui-??`51lJ$05D;x<(YHl zStd-+&jh?-OEgN(g9sB4!Y1Nz4Xm7X#CcI#0A`CGI;^tK?&^U9i~|B($c1tMOa*o7 z7+_dIQFo@OF!KZPQb$-Sh|G%|@bhxyDhp4hNxZIN5Mb+9XdZwo!sHb| z$|4jU!?0o`2>F}ytm_AHAtHKX$KVME>lchTR^Wtf}~q={bzT1RxZSRSKndMw%wc zsF#k9QC5H#6Acg#BJ}?Lup)pb#y!NLb`4&t@cqTQR24VVPC8|!!agVy(dv^-(l86- z&t~n%ONs4MiOPz1sw@ zRRAK#E2y2VLDr$sO>aWW=Y`dtb(0!7b$2K37$%a&r4j|T^q@*ls;SES;+Vbu(9uiP zZ?^?OxJ=?h8Gu56Ar=-Rp+8RE85K%Lhc?H!`gdI%J321OjTax;QIGoyNzmTPY4ycsjTe~W3LQO)4}P!)aELF``v=(x@Y;LVynBOrRlN5LDK)>wSNvs zpB_J%d&=X%RvG-BV;42HT z2ZDb_{Frvf+OE0)FDgrbqBQq)jehTk=bDZd-gcJM+fyxa##DVReem?P@_X9996m|P zu0Ll!k?Gi`sfS*w>3jX!2fSls`kR!Ln`?O0`T$vfS5Yle&`4B5t2!Ui!FfoOa6gU7 ztR4{AjKp<*c7U@c{$8Hlcj3EtH!B^f=CDAdaC#D9*=X$*xn_K;9bU?;1-h3#HHh$&gNLPzRA*;^y<#NxRS_7|g>5n)N*P-U z&Xc5dE)E0Ou6owx+zAwBdDpho9j{U)j@M@7(d3`X+MpowblmP|3YL@b(+hgCQ;d%; zj>obyvUx+ZGRBH%4j}X{Li>K9XiJKv`QwyNUO9BjS9(Hpe}Atde`KP2AI;9NOFJZh muTSecM261s!}B9=97)$>?t$6?64805(px1a$Ho^C{N-QX9*(U5 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_far_y_minus_1800.jpg b/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_far_y_minus_1800.jpg new file mode 100644 index 0000000000000000000000000000000000000000..742721524803951e792d4898a5600a590208710c GIT binary patch literal 17767 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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_walk_horizon.jpg b/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_walk_horizon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9e3e35c41e53aacfb78542906f144aa479d02cdb GIT binary patch literal 18218 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 #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -8311,8 +8313,435 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; return true; } + bool RunWorldAssemblerCapAudit(FStandaloneOwnerWorld& owner, + const VoxelForgeCore::ResolvedWorldSnapshot& snapshot,VoxelForgeCore::World& world, + VoxelForgeCore::Vec3d walkCameraVoxels) + { + // This audit follows the shipped owner manifest and Core clipmap, not the transient BuildHost fixture. + // Cet audit suit le manifeste owner livre et le clipmap Core, pas le fixture BuildHost transitoire. + constexpr std::int64_t Scale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + constexpr double Pi=3.14159265358979323846; + constexpr double RayStepVoxels=128.0; + constexpr double AgreementToleranceCm=2.0; + constexpr double NegativeDatumOffsetCm=100.0; + using FTileId=std::array; + using FXYLod=std::array; + const double VoxelSizeCm=static_cast(world.configuration().voxelSizeMicrons)/10000.0; + const auto IdFor=[](const VoxelForgeCore::TileKey& key) + {return FTileId{key.coordinate.x,key.coordinate.y,key.coordinate.z,key.lod};}; + const auto FloorDiv64=[](std::int64_t value,std::int64_t divisor) + { + const std::int64_t quotient=value/divisor,remainder=value%divisor; + return quotient-((remainder!=0&&remainder<0)?1:0); + }; + const auto* manifest=snapshot.worldAssembly(); + if(!manifest||manifest->plan.strates.empty()) + {std::fprintf(stderr,"[WorldAssemblerFarCapAudit] no SurfaceWorld plan; status=FAIL\n");return false;} + const auto planIt=std::find_if(manifest->plan.strates.begin(),manifest->plan.strates.end(), + [](const auto& plan){return !plan.route.empty();}); + if(planIt==manifest->plan.strates.end()) + {std::fprintf(stderr,"[WorldAssemblerFarCapAudit] no authored walk route; status=FAIL\n");return false;} + const auto& plan=*planIt; + const VoxelForgeCore::Heightfield2D field(manifest->recipe,plan); + if(!field.isValid()) + {std::fprintf(stderr,"[WorldAssemblerFarCapAudit] invalid Heightfield2D; status=FAIL\n");return false;} + + struct FView + { + VoxelForgeCore::Vec3d Origin{}; + std::vector Keys; + std::set KeySet; + VoxelForgeCore::TileGenerationOptions Options; + double ClipMinX=std::numeric_limits::infinity(); + double ClipMinY=std::numeric_limits::infinity(); + double ClipMaxX=-std::numeric_limits::infinity(); + double ClipMaxY=-std::numeric_limits::infinity(); + double ClipMaxZ=-std::numeric_limits::infinity(); + }; + const auto MakeView=[&](VoxelForgeCore::Vec3d origin) + { + FView view;view.Origin=origin; + const FStandaloneDesiredTileSet native=StandaloneWorldMesher::BuildDesiredSet(owner, + FVector(origin.x,origin.y,origin.z)); + view.Options.bandMinZVoxel=native.bHasStrate + ?native.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32; + view.Options.bandMaxZVoxel=native.bHasStrate + ?(native.StrateTopChunkZ+1)*CHUNK_SIZE:MAX_int32; + view.Keys=world.desiredTiles(origin); + for(const auto& key:view.Keys) + { + view.KeySet.insert(IdFor(key)); + const double extent=static_cast(CHUNK_SIZE<8)continue; + view.ClipMinX=std::min(view.ClipMinX,key.coordinate.x*extent); + view.ClipMinY=std::min(view.ClipMinY,key.coordinate.y*extent); + view.ClipMaxX=std::max(view.ClipMaxX,(key.coordinate.x+1.0)*extent); + view.ClipMaxY=std::max(view.ClipMaxY,(key.coordinate.y+1.0)*extent); + } + return view; + }; + const FView cameraView=MakeView(walkCameraVoxels); + std::set farXY; + for(const auto& key:cameraView.Keys) + if(key.lod>=4&&key.lod<=8)farXY.insert({key.coordinate.x,key.coordinate.y,key.lod}); + std::set capBands; + std::uint64_t profileSamples=0,profileSamplesWithoutRoof=0; + for(const FXYLod& xy:farXY) + { + const std::int32_t lod=xy[2]; + const std::int64_t extent=static_cast(CHUNK_SIZE)<(xy[0])*extent; + const std::int64_t originY=static_cast(xy[1])*extent; + const std::int64_t gridStep=lod<5?std::min(128,1ll<=MIN_int32&&band<=MAX_int32) + capBands.insert({xy[0],xy[1],static_cast(band),lod}); + if(farCap%(extent*Scale)==0&&band>MIN_int32) + capBands.insert({xy[0],xy[1],static_cast(band-1),lod}); + } + } + + struct FAgreement {std::int64_t X=0,Y=0;double FarZ=0.0;}; + struct FGeometry + { + std::vector> DownwardTriangles; + std::uint64_t CapTriangles=0; + std::vector AgreementVertices; + }; + constexpr std::int64_t AgreementGrid=128*Scale; + std::map geometryCache; + std::string coreError; + const auto GetGeometry=[&](const FTileId& id,const VoxelForgeCore::TileGenerationOptions& options) + ->const FGeometry* + { + if(const auto found=geometryCache.find(id);found!=geometryCache.end())return &found->second; + const VoxelForgeCore::TileKey key{{id[0],id[1],id[2]},id[3]}; + const auto mesh=world.generateTile(key,options,coreError); + if(!mesh)return nullptr; + FGeometry geometry; + geometry.CapTriangles=mesh->diagnostics.horizonCapTriangles; + const std::size_t capIndexStart=mesh->indices.size() + -static_cast(geometry.CapTriangles)*3u; + for(std::size_t index=0;index+2indices.size();index+=3) + { + const auto ia=mesh->indices[index],ib=mesh->indices[index+1],ic=mesh->indices[index+2]; + if(ia>=mesh->vertices.size()||ib>=mesh->vertices.size()||ic>=mesh->vertices.size())continue; + const auto& a=mesh->vertices[ia];const auto& b=mesh->vertices[ib];const auto& c=mesh->vertices[ic]; + if((a.normal.z+b.normal.z+c.normal.z)/3.0>=-0.05)continue; + geometry.DownwardTriangles.push_back({ + VoxelForgeCore::Vec3d{a.position.x/VoxelSizeCm,a.position.y/VoxelSizeCm,a.position.z/VoxelSizeCm}, + VoxelForgeCore::Vec3d{b.position.x/VoxelSizeCm,b.position.y/VoxelSizeCm,b.position.z/VoxelSizeCm}, + VoxelForgeCore::Vec3d{c.position.x/VoxelSizeCm,c.position.y/VoxelSizeCm,c.position.z/VoxelSizeCm}}); + } + for(std::size_t index=capIndexStart;indexindices.size();++index) + { + const std::uint32_t vertexIndex=mesh->indices[index]; + if(vertexIndex>=mesh->vertices.size())continue; + const auto& vertex=mesh->vertices[vertexIndex]; + if(vertex.normal.z>=-0.35)continue; + const std::int64_t x=static_cast(std::llround( + vertex.position.x/VoxelSizeCm*Scale)); + const std::int64_t y=static_cast(std::llround( + vertex.position.y/VoxelSizeCm*Scale)); + if(x%AgreementGrid!=0||y%AgreementGrid!=0)continue; + geometry.AgreementVertices.push_back({x,y,vertex.position.z/VoxelSizeCm}); + } + return &geometryCache.emplace(id,std::move(geometry)).first->second; + }; + + std::uint64_t requested=0,emitted=0,requestedWithoutTriangles=0,culled=0; + std::uint64_t zeroAreaBoundaryTouches=0,unexplainedEmptyBands=0; + std::map> byLod; + std::set> agreementXY; + std::vector agreementSamples;agreementSamples.reserve(128); + for(const FTileId& id:capBands) + { + auto& count=byLod[id[3]];++count[0]; + if(!cameraView.KeySet.contains(id)){++culled;++count[1];continue;} + ++requested; + const FGeometry* geometry=GetGeometry(id,cameraView.Options); + if(geometry==nullptr) + { + std::fprintf(stderr,"[WorldAssemblerFarCapAudit] tile generation failed key=(%d,%d,%d,lod%d): %s\n", + id[0],id[1],id[2],id[3],coreError.c_str());return false; + } + if(geometry->CapTriangles==0) + { + ++requestedWithoutTriangles;++count[3]; + const std::int64_t extent=static_cast(VoxelForgeCore::CoreTileVoxelCount)<(128,1ll<(id[2])*extent*Scale; + const std::int64_t maxZ=minZ+extent*Scale; + const std::int64_t originX=static_cast(id[0])*extent; + const std::int64_t originY=static_cast(id[1])*extent; + std::uint64_t validSamples=0,boundarySamples=0,interiorSamples=0; + for(std::int64_t y=0;y<=extent;y+=step) + for(std::int64_t x=0;x<=extent;x+=step) + { + const auto column=field.point((originX+x)*Scale,(originY+y)*Scale,0); + const std::int64_t farCap=field.farSurfaceCapHeightSubvoxels( + (originX+x)*Scale,(originY+y)*Scale); + if(farCap<=column.groundHeightSubvoxels||farCapmaxZ)continue; + ++validSamples; + if(farCap==minZ||farCap==maxZ)++boundarySamples; + else ++interiorSamples; + } + const bool zeroAreaTouch=validSamples>0&&boundarySamples==validSamples + &&interiorSamples==0; + if(zeroAreaTouch)++zeroAreaBoundaryTouches;else ++unexplainedEmptyBands; + std::printf("[WorldAssemblerFarCapEmptyBand] key=(%d,%d,%d,lod%d) valid_profile_samples=%llu boundary_samples=%llu interior_samples=%llu classification=%s\n", + id[0],id[1],id[2],id[3],static_cast(validSamples), + static_cast(boundarySamples),static_cast(interiorSamples), + zeroAreaTouch?"zero_area_boundary_touch":"UNEXPLAINED"); + continue; + } + ++emitted;++count[2]; + for(const FAgreement& sample:geometry->AgreementVertices) + { + if(agreementSamples.size()>=128)break; + if(!agreementXY.insert({sample.X,sample.Y}).second)continue; + agreementSamples.push_back(sample); + } + } + std::uint64_t agreementCompared=0; + double maximumAgreementCm=0.0,maximumNegativeControlCm=0.0; + FAgreement maximumAgreementSample{};double maximumAgreementNearRoot=0.0; + for(const FAgreement& sample:agreementSamples) + { + const auto column=field.point(sample.X,sample.Y,0); + const std::int64_t farCap=field.farSurfaceCapHeightSubvoxels(sample.X,sample.Y); + if(farCap<=column.groundHeightSubvoxels)continue; + const double x=static_cast(sample.X)/Scale,y=static_cast(sample.Y)/Scale; + const double capVoxel=static_cast(farCap)/Scale; + bool foundRoot=false;double nearRoot=0.0; + for(double radius=4.0;radius<=4096.0;radius*=2.0) + { + double lo=capVoxel-radius,hi=capVoxel+radius; + if(!(world.densityAt({x,y,lo})>=0.0f&&world.densityAt({x,y,hi})<0.0f))continue; + for(int iteration=0;iteration<16;++iteration) + {const double mid=(lo+hi)*0.5;if(world.densityAt({x,y,mid})>=0.0f)lo=mid;else hi=mid;} + nearRoot=(lo+hi)*0.5;foundRoot=true;break; + } + if(!foundRoot)continue; + const double sampleErrorCm=std::abs(sample.FarZ-nearRoot)*VoxelSizeCm; + if(sampleErrorCm>maximumAgreementCm) + {maximumAgreementCm=sampleErrorCm;maximumAgreementSample=sample;maximumAgreementNearRoot=nearRoot;} + maximumNegativeControlCm=std::max(maximumNegativeControlCm, + std::abs(sample.FarZ+NegativeDatumOffsetCm/VoxelSizeCm-nearRoot)*VoxelSizeCm); + ++agreementCompared; + } + if(agreementCompared>0) + { + const double capVoxel=static_cast(field.farSurfaceCapHeightSubvoxels( + maximumAgreementSample.X,maximumAgreementSample.Y))/Scale; + const double x=static_cast(maximumAgreementSample.X)/Scale; + const double y=static_cast(maximumAgreementSample.Y)/Scale; + std::printf("[WorldAssemblerFarCapAgreementMax] xy_vox=(%.1f,%.1f) profile_cap_vox=%.4f far_cap_vox=%.4f near_root_vox=%.4f density_at_cap=%.4f density_below=%.4f density_above=%.4f\n", + x,y,capVoxel,maximumAgreementSample.FarZ,maximumAgreementNearRoot, + world.densityAt({x,y,capVoxel}),world.densityAt({x,y,capVoxel-4.0}), + world.densityAt({x,y,capVoxel+4.0})); + } + const bool negativeControlDetected=agreementCompared>0 + &&maximumNegativeControlCm>AgreementToleranceCm; + std::printf("[WorldAssemblerFarCapCensus] view=owner_camera far_xy_tiles=%zu profile_samples=%llu samples_without_roof=%llu cap_bands=%zu requested=%llu emitted=%llu requested_without_triangles=%llu zero_area_boundary_touches=%llu unexplained_empty_bands=%llu culled_by_clipmap=%llu\n", + farXY.size(),static_cast(profileSamples), + static_cast(profileSamplesWithoutRoof),capBands.size(), + static_cast(requested),static_cast(emitted), + static_cast(requestedWithoutTriangles), + static_cast(zeroAreaBoundaryTouches), + static_cast(unexplainedEmptyBands),static_cast(culled)); + for(const auto& [lod,count]:byLod) + std::printf("[WorldAssemblerFarCapLod] lod=%d cap_bands=%llu culled=%llu emitted=%llu requested_without_triangles=%llu\n", + lod,static_cast(count[0]),static_cast(count[1]), + static_cast(count[2]),static_cast(count[3])); + std::printf("[WorldAssemblerFarCapAgreement] status=%s shared_points=%llu max_far_near_cm=%.4f tolerance_cm=%.2f negative_control=100cm_datum_offset max_error_cm=%.4f detected=%d\n", + agreementCompared>=32&&maximumAgreementCm<=AgreementToleranceCm&&negativeControlDetected?"PASS":"FAIL", + static_cast(agreementCompared),maximumAgreementCm,AgreementToleranceCm, + maximumNegativeControlCm,negativeControlDetected?1:0); + + constexpr std::array azimuths={0.0,45.0,90.0,135.0,180.0,225.0,270.0,315.0}; + constexpr std::array elevations={5.0,10.0,12.0,15.0,30.0,45.0,60.0,75.0,90.0}; + const auto dot3=[](const VoxelForgeCore::Vec3d& a,const VoxelForgeCore::Vec3d& b) + {return a.x*b.x+a.y*b.y+a.z*b.z;}; + const auto sub3=[](const VoxelForgeCore::Vec3d& a,const VoxelForgeCore::Vec3d& b) + {return VoxelForgeCore::Vec3d{a.x-b.x,a.y-b.y,a.z-b.z};}; + const auto cross3=[](const VoxelForgeCore::Vec3d& a,const VoxelForgeCore::Vec3d& b) + {return VoxelForgeCore::Vec3d{a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x};}; + const auto rayTriangle=[&](const VoxelForgeCore::Vec3d& origin,const VoxelForgeCore::Vec3d& direction, + const std::array& triangle,double minDistance,double maxDistance) + { + const auto edge1=sub3(triangle[1],triangle[0]),edge2=sub3(triangle[2],triangle[0]); + const auto p=cross3(direction,edge2);const double determinant=dot3(edge1,p); + if(std::abs(determinant)<1.0e-12)return false; + const double inverse=1.0/determinant;const auto t=sub3(origin,triangle[0]); + const double u=dot3(t,p)*inverse;if(u< -1.0e-8||u>1.0+1.0e-8)return false; + const auto q=cross3(t,edge1);const double v=dot3(direction,q)*inverse; + if(v< -1.0e-8||u+v>1.0+1.0e-8)return false; + const double distance=dot3(edge2,q)*inverse; + return distance>=minDistance-1.0e-6&&distance<=maxDistance+1.0e-6; + }; + + std::vector walkOrigins; + walkOrigins.reserve(plan.route.size()+1); + for(const auto& point:plan.route) + walkOrigins.push_back({static_cast(point.positionSubvoxels[0])/Scale, + static_cast(point.positionSubvoxels[1])/Scale, + static_cast(point.positionSubvoxels[2])/Scale}); + const auto sameCamera=std::find_if(walkOrigins.begin(),walkOrigins.end(),[&](const auto& point) + {return std::abs(point.x-walkCameraVoxels.x)<1.0&&std::abs(point.y-walkCameraVoxels.y)<1.0 + &&std::abs(point.z-walkCameraVoxels.z)<1.0;}); + if(sameCamera==walkOrigins.end())walkOrigins.push_back(walkCameraVoxels); + const VoxelForgeCore::Vec3d farStraightUpCamera{0.0,-1800.0*4.0,walkCameraVoxels.z}; + const std::size_t farStraightUpIndex=walkOrigins.size(); + walkOrigins.push_back(farStraightUpCamera); + std::uint64_t rayCount=0,rayHits=0,profileCrossings=0,profileRoofCrossings=0; + std::uint64_t missesNoProfileRoof=0,missesMesh=0,missesClipEdge=0; + std::array raysByElevation{},hitsByElevation{}; + std::vector firstMisses;firstMisses.reserve(8); + for(std::size_t viewIndex=0;viewIndexview.ClipMaxX||view.ClipMinY>view.ClipMaxY)continue; + const auto startColumn=field.point( + static_cast(std::llround(view.Origin.x*Scale)), + static_cast(std::llround(view.Origin.y*Scale)),0); + const double startCap=static_cast(field.farSurfaceCapHeightSubvoxels( + static_cast(std::llround(view.Origin.x*Scale)), + static_cast(std::llround(view.Origin.y*Scale))))/Scale; + for(std::size_t elevationIndex=0;elevationIndex::infinity(); + if(direction.x>1.0e-9)edgeDistance=std::min(edgeDistance,(view.ClipMaxX-view.Origin.x)/direction.x); + else if(direction.x< -1.0e-9)edgeDistance=std::min(edgeDistance,(view.ClipMinX-view.Origin.x)/direction.x); + if(direction.y>1.0e-9)edgeDistance=std::min(edgeDistance,(view.ClipMaxY-view.Origin.y)/direction.y); + else if(direction.y< -1.0e-9)edgeDistance=std::min(edgeDistance,(view.ClipMinY-view.Origin.y)/direction.y); + if(direction.z>1.0e-9)edgeDistance=std::min(edgeDistance,(view.ClipMaxZ-view.Origin.z)/direction.z); + if(!std::isfinite(edgeDistance)||edgeDistance<=0.0) + {++missesClipEdge;continue;} + bool profileCrossing=false,profileRoof=false; + double previousDistance=0.0,previousDelta=view.Origin.z-startCap; + for(double distance=std::min(RayStepVoxels,edgeDistance);;) + { + const auto column=field.point( + static_cast(std::llround((view.Origin.x+direction.x*distance)*Scale)), + static_cast(std::llround((view.Origin.y+direction.y*distance)*Scale)),0); + const double cap=static_cast(field.farSurfaceCapHeightSubvoxels( + static_cast(std::llround((view.Origin.x+direction.x*distance)*Scale)), + static_cast(std::llround((view.Origin.y+direction.y*distance)*Scale))))/Scale; + const double delta=view.Origin.z+direction.z*distance-cap; + if(previousDelta<0.0&&delta>=0.0) + { + const double fraction=-previousDelta/(delta-previousDelta); + const double hitX=view.Origin.x+direction.x*(previousDistance+(distance-previousDistance)*fraction); + const double hitY=view.Origin.y+direction.y*(previousDistance+(distance-previousDistance)*fraction); + const auto hitColumn=field.point(static_cast(std::llround(hitX*Scale)), + static_cast(std::llround(hitY*Scale)),0); + profileCrossing=true;++profileCrossings; + profileRoof=field.farSurfaceCapHeightSubvoxels( + static_cast(std::llround(hitX*Scale)), + static_cast(std::llround(hitY*Scale))) + >hitColumn.groundHeightSubvoxels; + if(profileRoof)++profileRoofCrossings; + break; + } + previousDistance=distance;previousDelta=delta; + if(distance>=edgeDistance)break; + distance=std::min(distance+RayStepVoxels,edgeDistance); + } + bool meshHit=false,intersectedRequestedTile=false,intersectedEmptyTile=false; + const auto& rayOrigin=view.Origin; + for(const auto& key:view.Keys) + { + const double extent=static_cast(CHUNK_SIZE<maxBounds[axis]){intersects=false;break;}continue;} + double tNear=(minBounds[axis]-origins[axis])/directions[axis]; + double tFar=(maxBounds[axis]-origins[axis])/directions[axis]; + if(tNear>tFar)std::swap(tNear,tFar); + enter=std::max(enter,tNear);exit=std::min(exit,tFar); + if(enter>exit){intersects=false;break;} + } + if(!intersects||exit<0.0||enter>edgeDistance)continue; + intersectedRequestedTile=true; + const FGeometry* geometry=GetGeometry(IdFor(key),view.Options); + if(geometry==nullptr) + {std::fprintf(stderr,"[WorldAssemblerFarCapAudit] ray tile generation failed: %s\n",coreError.c_str());return false;} + if(geometry->DownwardTriangles.empty()){intersectedEmptyTile=true;continue;} + for(const auto& triangle:geometry->DownwardTriangles) + if(rayTriangle(rayOrigin,direction,triangle,std::max(0.0,enter),std::min(edgeDistance,exit))) + {meshHit=true;break;} + if(meshHit)break; + } + if(meshHit){++rayHits;++hitsByElevation[elevationIndex];continue;} + if(!profileCrossing||!profileRoof)++missesNoProfileRoof; + else ++missesMesh; + if(firstMisses.size()<8) + { + std::ostringstream message; + message<<"view="<(rayCount-raysBeforeView), + static_cast(rayHits-hitsBeforeView), + rayCount>raysBeforeView?100.0*static_cast(rayHits-hitsBeforeView)/(rayCount-raysBeforeView):0.0); + } + std::printf("[WorldAssemblerFarCapCoverage] rays=%llu hits=%llu coverage_pct=%.3f profile_crossings=%llu profile_roof_crossings=%llu misses_no_profile_roof=%llu misses_mesh=%llu misses_clip_edge=%llu view_points=%zu\n", + static_cast(rayCount),static_cast(rayHits), + rayCount?100.0*static_cast(rayHits)/rayCount:0.0, + static_cast(profileCrossings),static_cast(profileRoofCrossings), + static_cast(missesNoProfileRoof),static_cast(missesMesh), + static_cast(missesClipEdge),walkOrigins.size()); + for(std::size_t i=0;i(raysByElevation[i]), + static_cast(hitsByElevation[i])); + for(const auto& miss:firstMisses)std::printf("[WorldAssemblerFarCapCoverageMiss] %s\n",miss.c_str()); + const bool censusPassing=requested>0&&unexplainedEmptyBands==0&&culled==0; + const bool agreementPassing=agreementCompared>=32&&maximumAgreementCm<=AgreementToleranceCm + &&negativeControlDetected; + const bool coveragePassing=rayCount>0&&rayHits==rayCount; + std::printf("[WorldAssemblerFarCapAudit] census=%s agreement=%s coverage=%s status=%s\n", + censusPassing?"PASS":"FAIL",agreementPassing?"PASS":"FAIL", + coveragePassing?"PASS":"FAIL",censusPassing&&agreementPassing&&coveragePassing?"PASS":"FAIL"); + return censusPassing&&agreementPassing&&coveragePassing; + } + int RunWorldAssemblerAudit(FStandaloneOwnerWorld& owner, - const VoxelForgeCore::ResolvedWorldSnapshot& snapshot,const std::string& captureDirectory) + const VoxelForgeCore::ResolvedWorldSnapshot& snapshot,const std::string& captureDirectory, + VoxelForgeCore::World* compiledWorld,VoxelForgeCore::Vec3d walkCameraVoxels) { const auto* manifest=snapshot.worldAssembly(); if(!manifest||manifest->plan.strates.empty()) @@ -8858,9 +9287,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; std::printf("[WorldAssemblerReadiness] local_field_query=%s analytic_collision_validation=%s physics_collision_cooking=deferred_to_Unreal_adapter render_mesh_readiness=reported_by_matched_view_runs\n", coreQueryPass?"ready_without_render_meshes":"not_ready", routeAudit.passed?"route_sweep_ready":"not_ready"); + const bool farCapAuditPassing=compiledWorld!=nullptr + &&RunWorldAssemblerCapAudit(owner,snapshot,*compiledWorld,walkCameraVoxels); return bounds.passed&&shelfThickness.passed&&routeAudit.passed &&routeSupportNegativeControl&&cliffRegressionPassing&&seamClassificationPassing - &&deterministic&&coreQueryPass?0:11; + &&deterministic&&coreQueryPass&&farCapAuditPassing?0:11; } } @@ -9254,7 +9685,11 @@ int main(int Argc,char** Argv) App.bRoundTunnelEnhanced,Error); if(!App.Snapshot) {std::fprintf(stderr,"World assembler snapshot setup failed: %s\n",Error.c_str());return 2;} - return RunWorldAssemblerAudit(*App.World,App.Snapshot,WorldAssemblyCaptureDir); + App.bShapeWorld=true; + if(!App.BuildShapeWorldForCamera(Error,true,false,true)) + {std::fprintf(stderr,"World assembler cap audit shape build failed: %s\n",Error.c_str());return 2;} + return RunWorldAssemblerAudit(*App.World,App.Snapshot,WorldAssemblyCaptureDir, + App.ShapeWorld.get(),{App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0}); } const bool bNeedFarShapeWorld=bStreamBenchmark ?(StreamBenchmarkMaxLevel<0||StreamBenchmarkMaxLevel>=App.World->FullResClipLevels)