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 0000000..f4badaf Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_cap_distance.jpg differ 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 0000000..24f40f6 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_far_straight_up.jpg differ 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 0000000..7427215 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_far_y_minus_1800.jpg differ 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 0000000..9e3e35c Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round8/captures/s4_walk_horizon.jpg differ diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index 1357076..51abdb8 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -22,6 +22,7 @@ #include #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)