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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
d0f97bf79a
commit
9d3c50ca95
14 files changed
+1376
-34
No files matched your search
@@ -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
|
||||
|
||||
+13
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<double>(Ground)/SubvoxelsPerVoxel;
|
||||
if(GroundVoxel<Bounds7[2]||GroundVoxel>Bounds7[5]
|
||||
@@ -15263,7 +15264,8 @@ bool RunCoreFarHorizonAgreementAudit(VoxelForgeCore::World& World,
|
||||
const std::int64_t YSub=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(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<double>(Ground)/SubvoxelsPerVoxel;
|
||||
if(GroundVoxel<Bounds7[2]||GroundVoxel>Bounds7[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<std::int32_t,4>;
|
||||
using FXYLod=std::array<std::int32_t,3>;
|
||||
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<double>(WalkStart.positionSubvoxels[0])/SubvoxelsPerVoxel,
|
||||
static_cast<double>(WalkStart.positionSubvoxels[1])/SubvoxelsPerVoxel,
|
||||
static_cast<double>(WalkStart.positionSubvoxels[2])/SubvoxelsPerVoxel};
|
||||
const std::vector<VoxelForgeCore::TileKey> WalkKeys=World.desiredTiles(Camera);
|
||||
std::set<FTileId> Desired;
|
||||
std::set<FXYLod> FarXYTiles;
|
||||
double ClipMaxZ=-std::numeric_limits<double>::infinity();
|
||||
for(const auto& Key:WalkKeys)
|
||||
{
|
||||
Desired.insert(IdFor(Key));
|
||||
const double Extent=static_cast<double>(32<<Key.lod);
|
||||
ClipMaxZ=std::max(ClipMaxZ,(static_cast<double>(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<FTileId> CapBands;
|
||||
std::uint64_t ProfileGridSamples=0, ProfileSamplesWithoutRoof=0;
|
||||
std::set<FXYLod> FarXYTilesWithoutRoof;
|
||||
for(const FXYLod& XY:FarXYTiles)
|
||||
{
|
||||
const std::int32_t Lod=XY[2];
|
||||
const std::int64_t Extent=static_cast<std::int64_t>(32)<<Lod;
|
||||
const std::int64_t OriginX=static_cast<std::int64_t>(XY[0])*Extent;
|
||||
const std::int64_t OriginY=static_cast<std::int64_t>(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<std::int32_t>(Band),Lod});
|
||||
if(FarCap%(Extent*SubvoxelsPerVoxel)==0
|
||||
&&Band>MIN_int32)
|
||||
CapBands.insert({XY[0],XY[1],static_cast<std::int32_t>(Band-1),Lod});
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t RequestedCapBands=0, EmittedCapBands=0, RequestedWithoutTriangles=0;
|
||||
std::uint64_t CulledCapBands=0;
|
||||
std::map<std::int32_t,std::array<std::uint64_t,4>> 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::int64_t,std::int64_t>;
|
||||
std::set<FCapXYKey> CapAgreementSeen;
|
||||
std::vector<FCapAgreementSample> CapAgreementSamples;
|
||||
CapAgreementSamples.reserve(128);
|
||||
const double VoxelSizeCm=static_cast<double>(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<std::size_t>(Mesh->diagnostics.horizonCapTriangles)*3u;
|
||||
for(std::size_t Index=CapIndexStart;Index<Mesh->indices.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::int64_t>(std::llround(
|
||||
Vertex.position.x/VoxelSizeCm*SubvoxelsPerVoxel));
|
||||
const std::int64_t YSub=static_cast<std::int64_t>(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<double>(XSub)/SubvoxelsPerVoxel;
|
||||
const double Y=static_cast<double>(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<double>(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<unsigned long long>(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<unsigned long long>(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<double,8> AzimuthDegrees={0.0,45.0,90.0,135.0,180.0,225.0,270.0,315.0};
|
||||
constexpr std::array<double,6> ElevationDegrees={15.0,30.0,45.0,60.0,75.0,90.0};
|
||||
constexpr double RayStepVoxels=128.0;
|
||||
double ClipMinX=std::numeric_limits<double>::infinity();
|
||||
double ClipMinY=std::numeric_limits<double>::infinity();
|
||||
double ClipMaxX=-std::numeric_limits<double>::infinity();
|
||||
double ClipMaxY=-std::numeric_limits<double>::infinity();
|
||||
for(const FXYLod& XY:FarXYTiles)
|
||||
{
|
||||
const double Extent=static_cast<double>(32<<XY[2]);
|
||||
ClipMinX=std::min(ClipMinX,static_cast<double>(XY[0])*Extent);
|
||||
ClipMinY=std::min(ClipMinY,static_cast<double>(XY[1])*Extent);
|
||||
ClipMaxX=std::max(ClipMaxX,(static_cast<double>(XY[0])+1.0)*Extent);
|
||||
ClipMaxY=std::max(ClipMaxY,(static_cast<double>(XY[1])+1.0)*Extent);
|
||||
}
|
||||
enum class ECapRayTileResult : std::uint8_t
|
||||
{ EmittedHit, CulledByClipmap, RequestedWithoutTriangles, EmittedWithoutHit };
|
||||
struct FCapRayTileGeometry
|
||||
{
|
||||
std::vector<std::array<VoxelForgeCore::Vec3d,3>> 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<std::array<std::uint64_t,6>,6> RayResultsByElevation{};
|
||||
std::vector<double> RayHitDistancesMeters;
|
||||
RayHitDistancesMeters.reserve(Plan->route.size()*AzimuthDegrees.size()*ElevationDegrees.size());
|
||||
std::map<FTileId,FCapRayTileGeometry> 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+2<Mesh->indices.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<VoxelForgeCore::Vec3d,3>& 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(Distance<MinimumDistance-1.0e-6||Distance>MaximumDistance+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<double>(32<<Id[3]);
|
||||
const std::array<double,3> BoundsMin={static_cast<double>(Id[0])*Extent,
|
||||
static_cast<double>(Id[1])*Extent,static_cast<double>(Id[2])*Extent};
|
||||
const std::array<double,3> 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(RayOrigin<BoundsMin[Axis]||RayOrigin>BoundsMax[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||HitDistance<OutHitDistance)OutHitDistance=HitDistance;
|
||||
bFoundHit=true;
|
||||
}
|
||||
if(!OutError.IsEmpty())return ECapRayTileResult::RequestedWithoutTriangles;
|
||||
}
|
||||
if(bFoundHit)return ECapRayTileResult::EmittedHit;
|
||||
if(bFoundRequestedKey&&bFoundGeometryWithoutHit)return ECapRayTileResult::EmittedWithoutHit;
|
||||
if(bFoundRequestedKey&&bFoundEmptyRequestedKey)return ECapRayTileResult::RequestedWithoutTriangles;
|
||||
return ECapRayTileResult::CulledByClipmap;
|
||||
};
|
||||
for(std::size_t RouteIndex=0;RouteIndex<Plan->route.size();++RouteIndex)
|
||||
{
|
||||
const auto& RoutePoint=Plan->route[RouteIndex];
|
||||
const double StartX=static_cast<double>(RoutePoint.positionSubvoxels[0])/SubvoxelsPerVoxel;
|
||||
const double StartY=static_cast<double>(RoutePoint.positionSubvoxels[1])/SubvoxelsPerVoxel;
|
||||
const double StartZ=static_cast<double>(RoutePoint.positionSubvoxels[2])/SubvoxelsPerVoxel;
|
||||
const auto StartColumn=Field.point(RoutePoint.positionSubvoxels[0],
|
||||
RoutePoint.positionSubvoxels[1],0);
|
||||
const double StartCap=static_cast<double>(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::size_t>(
|
||||
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<double>::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<double>::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::int64_t>(std::llround(X*SubvoxelsPerVoxel)),
|
||||
static_cast<std::int64_t>(std::llround(Y*SubvoxelsPerVoxel)));
|
||||
const double Cap=static_cast<double>(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::int64_t>(std::llround(HitX*SubvoxelsPerVoxel)),
|
||||
static_cast<std::int64_t>(std::llround(HitY*SubvoxelsPerVoxel)),0);
|
||||
const std::int64_t HitCapSubvoxels=Field.farSurfaceCapHeightSubvoxels(
|
||||
static_cast<std::int64_t>(std::llround(HitX*SubvoxelsPerVoxel)),
|
||||
static_cast<std::int64_t>(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<double>::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<std::int64_t>(32)<<Lod;
|
||||
const std::int64_t TileX=FloorDiv64(static_cast<std::int64_t>(std::floor(ProfileHitX)),Extent);
|
||||
const std::int64_t TileY=FloorDiv64(static_cast<std::int64_t>(std::floor(ProfileHitY)),Extent);
|
||||
const std::int64_t TileZ=FloorDiv64(ProfileHitCapSubvoxels,Extent*SubvoxelsPerVoxel);
|
||||
const FTileId Id{static_cast<std::int32_t>(TileX),static_cast<std::int32_t>(TileY),
|
||||
static_cast<std::int32_t>(TileZ),Lod};
|
||||
if(Desired.find(Id)==Desired.end())continue;
|
||||
RootRequestedMask|=1u<<static_cast<std::uint32_t>(Lod);
|
||||
const FCapRayTileGeometry* Geometry=GetCapRayTileGeometry(Id);
|
||||
if(Geometry!=nullptr)
|
||||
{
|
||||
RootDownwardTriangles+=Geometry->Triangles.size();
|
||||
if(!Geometry->Triangles.empty())
|
||||
RootEmittedMask|=1u<<static_cast<std::uint32_t>(Lod);
|
||||
}
|
||||
if(Lod>=5)
|
||||
{
|
||||
FarRequestedMask|=1u<<static_cast<std::uint32_t>(Lod-5);
|
||||
if(Geometry!=nullptr&&!Geometry->Triangles.empty())
|
||||
FarEmittedMask|=1u<<static_cast<std::uint32_t>(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<unsigned long long>(RouteIndex),Elevation,Azimuth,
|
||||
bProfileCrossing?1:0,bProfileRoofAtCrossing?1:0,
|
||||
ProfileHitDistance,EdgeDistance,ProfileHitX,ProfileHitY,ProfileHitZ,
|
||||
RootRequestedMask,RootEmittedMask,
|
||||
static_cast<unsigned long long>(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<unsigned long long>(Plan->route.size()),
|
||||
static_cast<unsigned long long>(WalkKeys.size()),
|
||||
static_cast<unsigned long long>(FarXYTiles.size()),
|
||||
static_cast<unsigned long long>(CapBands.size()),
|
||||
static_cast<unsigned long long>(RequestedCapBands),
|
||||
static_cast<unsigned long long>(EmittedCapBands),
|
||||
static_cast<unsigned long long>(RequestedWithoutTriangles),
|
||||
static_cast<unsigned long long>(CulledCapBands),
|
||||
static_cast<unsigned long long>(ProfileGridSamples),
|
||||
static_cast<unsigned long long>(ProfileSamplesWithoutRoof),
|
||||
static_cast<unsigned long long>(FarXYTilesWithoutRoof.size()),
|
||||
Camera.z,static_cast<double>(WalkColumn.groundHeightSubvoxels)/SubvoxelsPerVoxel,
|
||||
static_cast<double>(WalkFarCap)/SubvoxelsPerVoxel);
|
||||
const double CoveragePercent=RayCount>0?100.0*static_cast<double>(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<unsigned long long>(RayCount),static_cast<unsigned long long>(RayHits),CoveragePercent,
|
||||
static_cast<unsigned long long>(RayMissedClipEdge),
|
||||
static_cast<unsigned long long>(RayMissedNoRoof),
|
||||
static_cast<unsigned long long>(RayMissedCulledByClipmap),
|
||||
static_cast<unsigned long long>(RayMissedRequestedWithoutTriangles),
|
||||
static_cast<unsigned long long>(RayMissedEmittedWithoutHit),
|
||||
static_cast<unsigned long long>(RayStartedAboveCap),
|
||||
static_cast<unsigned long long>(RayProfileCrossings),
|
||||
static_cast<unsigned long long>(RayProfileRoofCrossings),
|
||||
static_cast<unsigned long long>(RayProfileRoofWithoutMeshHit),
|
||||
static_cast<unsigned long long>(RayTileGeometryCache.size()),
|
||||
static_cast<unsigned long long>(RayTriangleCount),HitDistanceP50Meters,
|
||||
HitDistanceP95Meters,HitDistanceMaximumMeters);
|
||||
for(std::size_t Index=0;Index<ElevationDegrees.size();++Index)
|
||||
UE_LOG(LogTemp,Display,
|
||||
TEXT("[VoxelForgeFarCapRayElevation] degrees=%.0f rays=%llu mesh_hits=%llu key_culled=%llu requested_empty=%llu emitted_without_mesh_hit=%llu no_roof_or_above=%llu missed_clip_edge=%llu"),
|
||||
ElevationDegrees[Index],
|
||||
static_cast<unsigned long long>(AzimuthDegrees.size()*Plan->route.size()),
|
||||
static_cast<unsigned long long>(RayResultsByElevation[Index][0]),
|
||||
static_cast<unsigned long long>(RayResultsByElevation[Index][1]),
|
||||
static_cast<unsigned long long>(RayResultsByElevation[Index][2]),
|
||||
static_cast<unsigned long long>(RayResultsByElevation[Index][3]),
|
||||
static_cast<unsigned long long>(RayResultsByElevation[Index][4]),
|
||||
static_cast<unsigned long long>(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<unsigned long long>(RayCount),static_cast<unsigned long long>(RayHits),
|
||||
static_cast<unsigned long long>(RayMissedClipEdge),
|
||||
static_cast<unsigned long long>(RayMissedNoRoof),
|
||||
static_cast<unsigned long long>(RayMissedCulledByClipmap),
|
||||
static_cast<unsigned long long>(RayMissedRequestedWithoutTriangles),
|
||||
static_cast<unsigned long long>(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<unsigned long long>(Counts[0]),
|
||||
static_cast<unsigned long long>(Counts[1]),
|
||||
static_cast<unsigned long long>(Counts[2]),
|
||||
static_cast<unsigned long long>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<std::int64_t, 2> capHeightBounds() const noexcept;
|
||||
WorldAssemblyFeatureTileListV1 buildFeatureTileList(
|
||||
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
|
||||
const std::array<std::int64_t, 3>& 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
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <set>
|
||||
|
||||
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<int64>(Plan.bottomChunkZ)*CHUNK_SIZE*Scale;
|
||||
const int64 StratumMaxZ=(static_cast<int64>(Plan.topChunkZ)+1)*CHUNK_SIZE*Scale;
|
||||
if(TileMaxZ<StratumMinZ||TileMinZ>StratumMaxZ)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<<Key.Level):128;
|
||||
constexpr int64 Scale=WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr int64 GridStepSubvoxels=GridStepVoxels*Scale;
|
||||
const int64 GridStepSubvoxels=static_cast<int64>(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<int64>(Plan.bottomChunkZ)*CHUNK_SIZE*Scale;
|
||||
const int64 StratumMaxZ=(static_cast<int64>(Plan.topChunkZ)+1)*CHUNK_SIZE*Scale;
|
||||
if(TileMaxZ<StratumMinZ||TileMinZ>StratumMaxZ)continue;
|
||||
const VoxelForgeCore::Heightfield2D Block(Manifest.recipe,Plan);
|
||||
if(!Block.isValid())continue;
|
||||
const auto CapHeightBounds=Block.capHeightBounds();
|
||||
if(TileMaxZ<CapHeightBounds[0]||TileMinZ>CapHeightBounds[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<double>(Column.groundHeightSubvoxels);
|
||||
Sample.Cap=static_cast<double>(Column.capHeightSubvoxels);
|
||||
Sample.Cap=static_cast<double>(Block.farSurfaceCapHeightSubvoxels(XSub,YSub));
|
||||
int32 Dominant=0,Neighbor=0;float Blend=0.0f;
|
||||
Owner.Generator->GetBiomeMaterialAt(static_cast<float>(XSub)/Scale,
|
||||
static_cast<float>(YSub)/Scale,static_cast<float>(Sample.Cap/Scale),
|
||||
@@ -1160,6 +1160,71 @@ namespace VoxelForgeCore
|
||||
VoxelClipmapDesiredTiles::Build(center,minZ,maxZ,parameters,nativeTiles);
|
||||
std::vector<TileKey> tiles;tiles.reserve(static_cast<std::size_t>(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<std::array<std::int32_t,3>> FarSurfaceColumns;
|
||||
std::set<std::array<std::int32_t,4>> 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<std::int64_t>(32)<<Lod;
|
||||
const std::int64_t TileHeightSubvoxels=Extent*SubvoxelsPerVoxel;
|
||||
const std::int64_t MinimumBand=FloorDiv64(CapBounds[0],TileHeightSubvoxels);
|
||||
const std::int64_t MaximumBand=FloorDiv64(CapBounds[1],TileHeightSubvoxels);
|
||||
for(std::int64_t Band=MinimumBand;Band<=MaximumBand;++Band)
|
||||
{
|
||||
if(Band<MIN_int32||Band>MAX_int32)continue;
|
||||
const std::array<std::int32_t,4> Identity={Column[0],Column[1],
|
||||
static_cast<std::int32_t>(Band),Lod};
|
||||
if(!ExistingKeys.insert(Identity).second)continue;
|
||||
const std::int64_t CandidateMinX=static_cast<std::int64_t>(Identity[0])*Extent;
|
||||
const std::int64_t CandidateMinY=static_cast<std::int64_t>(Identity[1])*Extent;
|
||||
const std::int64_t CandidateMinZ=
|
||||
static_cast<std::int64_t>(Identity[2])*Extent;
|
||||
bool bOverlapsClipmapTile=false;
|
||||
for(const TileKey& Existing:tiles)
|
||||
{
|
||||
const std::int64_t ExistingExtent=static_cast<std::int64_t>(32)<<Existing.lod;
|
||||
const std::int64_t ExistingMinX=static_cast<std::int64_t>(Existing.coordinate.x)*ExistingExtent;
|
||||
const std::int64_t ExistingMinY=static_cast<std::int64_t>(Existing.coordinate.y)*ExistingExtent;
|
||||
const std::int64_t ExistingMinZ=static_cast<std::int64_t>(Existing.coordinate.z)*ExistingExtent;
|
||||
const bool bOverlapX=ExistingMinX<CandidateMinX+Extent
|
||||
&&CandidateMinX<ExistingMinX+ExistingExtent;
|
||||
const bool bOverlapY=ExistingMinY<CandidateMinY+Extent
|
||||
&&CandidateMinY<ExistingMinY+ExistingExtent;
|
||||
const bool bOverlapZ=ExistingMinZ<CandidateMinZ+Extent
|
||||
&&CandidateMinZ<ExistingMinZ+ExistingExtent;
|
||||
if(bOverlapX&&bOverlapY&&bOverlapZ)
|
||||
{bOverlapsClipmapTile=true;break;}
|
||||
}
|
||||
// Preserve the clipmap's non-overlapping 3D partition. Where another
|
||||
// requested tile already occupies this cap volume, that tile owns the
|
||||
// shared roof surface; never add an overlapping LOD sibling.
|
||||
// Garder la partition 3D sans recouvrement. Une tuile deja presente
|
||||
// possede la surface de toit partagee; ne pas ajouter un voisin LOD superpose.
|
||||
if(bOverlapsClipmapTile)continue;
|
||||
tiles.push_back({{Identity[0],Identity[1],Identity[2]},Identity[3]});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(tiles.begin(),tiles.end(),[&](const TileKey& a,const TileKey& b)
|
||||
{
|
||||
const double da=TileDistanceSquared(a,{center.X,center.Y,center.Z});
|
||||
@@ -1201,7 +1266,7 @@ namespace VoxelForgeCore
|
||||
&&options.mesherRecipe!=MesherRecipe::worldAssemblerHybridV1
|
||||
&&options.mesherRecipe!=MesherRecipe::worldAssemblerHybridV2)
|
||||
{error="tile mesher recipe version is unsupported";return std::nullopt;}
|
||||
if(level>=5&&options.farHorizonVersion==0)
|
||||
if(level>=4&&options.farHorizonVersion==0)
|
||||
{error="far-horizon recipe version must be nonzero";return std::nullopt;}
|
||||
TArray<FVoxelHybridSharpFeatureBox> 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<double,std::milli>(
|
||||
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<double,std::milli>(
|
||||
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<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-HorizonStart).count();
|
||||
@@ -1612,7 +1678,7 @@ namespace VoxelForgeCore
|
||||
{ densityHash^=std::bit_cast<std::uint32_t>(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);
|
||||
|
||||
@@ -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<std::int64_t,2> Heightfield2D::capHeightBounds() const noexcept
|
||||
{
|
||||
if(!isValid())return {0,0};
|
||||
long double Minimum=std::numeric_limits<long double>::infinity();
|
||||
long double Maximum=-std::numeric_limits<long double>::infinity();
|
||||
const auto IncludeBiome=[&](const WorldAssemblyBiomeProfileV1& Biome)
|
||||
{
|
||||
const long double Base=Biome.hasCapProfile
|
||||
?static_cast<long double>(Biome.capHeightSubvoxels)
|
||||
:static_cast<long double>(plan_->capHeightSubvoxels);
|
||||
const long double Undulation=Biome.hasCapProfile
|
||||
?static_cast<long double>(Biome.capUndulationSubvoxels)
|
||||
:static_cast<long double>(plan_->capUndulationSubvoxels);
|
||||
const long double Detail=Biome.hasCapProfile
|
||||
?static_cast<long double>(Biome.capDetailAmplitudeSubvoxels)
|
||||
:static_cast<long double>(plan_->capDetailAmplitudeSubvoxels);
|
||||
const long double Ridge=Biome.hasCapProfile
|
||||
?static_cast<long double>(Biome.capRidgeAmplitudeSubvoxels)
|
||||
:static_cast<long double>(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<long double>(Feature.parametersSubvoxels[0]));
|
||||
Minimum-=FeatureAmplitude;Maximum+=FeatureAmplitude;
|
||||
if(recipe_->evaluatorVersion>=5)
|
||||
{
|
||||
const long double TopSubvoxels=(static_cast<long double>(plan_->topChunkZ)+1.0L)
|
||||
*CoreTileVoxelCount*WorldAssemblySubvoxelsPerVoxel;
|
||||
Minimum=std::min(Minimum,TopSubvoxels);
|
||||
Maximum=std::min(Maximum,TopSubvoxels);
|
||||
}
|
||||
const long double Int64Minimum=static_cast<long double>(std::numeric_limits<std::int64_t>::min());
|
||||
const long double Int64Maximum=static_cast<long double>(std::numeric_limits<std::int64_t>::max());
|
||||
return {static_cast<std::int64_t>(std::clamp(Minimum,Int64Minimum,Int64Maximum)),
|
||||
static_cast<std::int64_t>(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<std::int64_t>(plan_->topChunkZ)+1)
|
||||
*CoreTileVoxelCount*WorldAssemblySubvoxelsPerVoxel;
|
||||
return std::min(column.capHeightSubvoxels,ownedUpperBoundary);
|
||||
}
|
||||
|
||||
WorldAssemblyFeatureTileListV1 Heightfield2D::buildFeatureTileList(
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum) const
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -22,6 +22,7 @@
|
||||
#include <windowsx.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <complex>
|
||||
#include <cstdio>
|
||||
@@ -37,6 +38,7 @@
|
||||
#include <mutex>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
@@ -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<std::int32_t,4>;
|
||||
using FXYLod=std::array<std::int32_t,3>;
|
||||
const double VoxelSizeCm=static_cast<double>(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<VoxelForgeCore::TileKey> Keys;
|
||||
std::set<FTileId> KeySet;
|
||||
VoxelForgeCore::TileGenerationOptions Options;
|
||||
double ClipMinX=std::numeric_limits<double>::infinity();
|
||||
double ClipMinY=std::numeric_limits<double>::infinity();
|
||||
double ClipMaxX=-std::numeric_limits<double>::infinity();
|
||||
double ClipMaxY=-std::numeric_limits<double>::infinity();
|
||||
double ClipMaxZ=-std::numeric_limits<double>::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<double>(CHUNK_SIZE<<key.lod);
|
||||
view.ClipMaxZ=std::max(view.ClipMaxZ,(key.coordinate.z+1.0)*extent);
|
||||
if(key.lod<4||key.lod>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<FXYLod> 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<FTileId> 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<std::int64_t>(CHUNK_SIZE)<<lod;
|
||||
const std::int64_t originX=static_cast<std::int64_t>(xy[0])*extent;
|
||||
const std::int64_t originY=static_cast<std::int64_t>(xy[1])*extent;
|
||||
const std::int64_t gridStep=lod<5?std::min<std::int64_t>(128,1ll<<lod):128;
|
||||
for(std::int64_t y=0;y<=extent;y+=gridStep)
|
||||
for(std::int64_t x=0;x<=extent;x+=gridStep)
|
||||
{
|
||||
++profileSamples;
|
||||
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)
|
||||
{++profileSamplesWithoutRoof;continue;}
|
||||
const std::int64_t band=FloorDiv64(farCap,extent*Scale);
|
||||
if(band>=MIN_int32&&band<=MAX_int32)
|
||||
capBands.insert({xy[0],xy[1],static_cast<std::int32_t>(band),lod});
|
||||
if(farCap%(extent*Scale)==0&&band>MIN_int32)
|
||||
capBands.insert({xy[0],xy[1],static_cast<std::int32_t>(band-1),lod});
|
||||
}
|
||||
}
|
||||
|
||||
struct FAgreement {std::int64_t X=0,Y=0;double FarZ=0.0;};
|
||||
struct FGeometry
|
||||
{
|
||||
std::vector<std::array<VoxelForgeCore::Vec3d,3>> DownwardTriangles;
|
||||
std::uint64_t CapTriangles=0;
|
||||
std::vector<FAgreement> AgreementVertices;
|
||||
};
|
||||
constexpr std::int64_t AgreementGrid=128*Scale;
|
||||
std::map<FTileId,FGeometry> 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<std::size_t>(geometry.CapTriangles)*3u;
|
||||
for(std::size_t index=0;index+2<mesh->indices.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;index<mesh->indices.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::int64_t>(std::llround(
|
||||
vertex.position.x/VoxelSizeCm*Scale));
|
||||
const std::int64_t y=static_cast<std::int64_t>(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<std::int32_t,std::array<std::uint64_t,4>> byLod;
|
||||
std::set<std::pair<std::int64_t,std::int64_t>> agreementXY;
|
||||
std::vector<FAgreement> 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<std::int64_t>(VoxelForgeCore::CoreTileVoxelCount)<<id[3];
|
||||
const std::int64_t step=id[3]<5?std::min<std::int64_t>(128,1ll<<id[3]):128;
|
||||
const std::int64_t minZ=static_cast<std::int64_t>(id[2])*extent*Scale;
|
||||
const std::int64_t maxZ=minZ+extent*Scale;
|
||||
const std::int64_t originX=static_cast<std::int64_t>(id[0])*extent;
|
||||
const std::int64_t originY=static_cast<std::int64_t>(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||farCap<minZ||farCap>maxZ)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<unsigned long long>(validSamples),
|
||||
static_cast<unsigned long long>(boundarySamples),static_cast<unsigned long long>(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<double>(sample.X)/Scale,y=static_cast<double>(sample.Y)/Scale;
|
||||
const double capVoxel=static_cast<double>(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<double>(field.farSurfaceCapHeightSubvoxels(
|
||||
maximumAgreementSample.X,maximumAgreementSample.Y))/Scale;
|
||||
const double x=static_cast<double>(maximumAgreementSample.X)/Scale;
|
||||
const double y=static_cast<double>(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<unsigned long long>(profileSamples),
|
||||
static_cast<unsigned long long>(profileSamplesWithoutRoof),capBands.size(),
|
||||
static_cast<unsigned long long>(requested),static_cast<unsigned long long>(emitted),
|
||||
static_cast<unsigned long long>(requestedWithoutTriangles),
|
||||
static_cast<unsigned long long>(zeroAreaBoundaryTouches),
|
||||
static_cast<unsigned long long>(unexplainedEmptyBands),static_cast<unsigned long long>(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<unsigned long long>(count[0]),static_cast<unsigned long long>(count[1]),
|
||||
static_cast<unsigned long long>(count[2]),static_cast<unsigned long long>(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<unsigned long long>(agreementCompared),maximumAgreementCm,AgreementToleranceCm,
|
||||
maximumNegativeControlCm,negativeControlDetected?1:0);
|
||||
|
||||
constexpr std::array<double,8> azimuths={0.0,45.0,90.0,135.0,180.0,225.0,270.0,315.0};
|
||||
constexpr std::array<double,9> 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<VoxelForgeCore::Vec3d,3>& 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<VoxelForgeCore::Vec3d> walkOrigins;
|
||||
walkOrigins.reserve(plan.route.size()+1);
|
||||
for(const auto& point:plan.route)
|
||||
walkOrigins.push_back({static_cast<double>(point.positionSubvoxels[0])/Scale,
|
||||
static_cast<double>(point.positionSubvoxels[1])/Scale,
|
||||
static_cast<double>(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<std::uint64_t,9> raysByElevation{},hitsByElevation{};
|
||||
std::vector<std::string> firstMisses;firstMisses.reserve(8);
|
||||
for(std::size_t viewIndex=0;viewIndex<walkOrigins.size();++viewIndex)
|
||||
{
|
||||
const std::uint64_t hitsBeforeView=rayHits;
|
||||
const std::uint64_t raysBeforeView=rayCount;
|
||||
const auto view=MakeView(walkOrigins[viewIndex]);
|
||||
if(view.ClipMinX>view.ClipMaxX||view.ClipMinY>view.ClipMaxY)continue;
|
||||
const auto startColumn=field.point(
|
||||
static_cast<std::int64_t>(std::llround(view.Origin.x*Scale)),
|
||||
static_cast<std::int64_t>(std::llround(view.Origin.y*Scale)),0);
|
||||
const double startCap=static_cast<double>(field.farSurfaceCapHeightSubvoxels(
|
||||
static_cast<std::int64_t>(std::llround(view.Origin.x*Scale)),
|
||||
static_cast<std::int64_t>(std::llround(view.Origin.y*Scale))))/Scale;
|
||||
for(std::size_t elevationIndex=0;elevationIndex<elevations.size();++elevationIndex)
|
||||
for(const double azimuth:azimuths)
|
||||
{
|
||||
++rayCount;++raysByElevation[elevationIndex];
|
||||
const double elevation=elevations[elevationIndex]*Pi/180.0;
|
||||
const double radians=azimuth*Pi/180.0;
|
||||
const VoxelForgeCore::Vec3d direction{
|
||||
std::cos(elevation)*std::cos(radians),std::cos(elevation)*std::sin(radians),std::sin(elevation)};
|
||||
double edgeDistance=std::numeric_limits<double>::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::int64_t>(std::llround((view.Origin.x+direction.x*distance)*Scale)),
|
||||
static_cast<std::int64_t>(std::llround((view.Origin.y+direction.y*distance)*Scale)),0);
|
||||
const double cap=static_cast<double>(field.farSurfaceCapHeightSubvoxels(
|
||||
static_cast<std::int64_t>(std::llround((view.Origin.x+direction.x*distance)*Scale)),
|
||||
static_cast<std::int64_t>(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::int64_t>(std::llround(hitX*Scale)),
|
||||
static_cast<std::int64_t>(std::llround(hitY*Scale)),0);
|
||||
profileCrossing=true;++profileCrossings;
|
||||
profileRoof=field.farSurfaceCapHeightSubvoxels(
|
||||
static_cast<std::int64_t>(std::llround(hitX*Scale)),
|
||||
static_cast<std::int64_t>(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<double>(CHUNK_SIZE<<key.lod);
|
||||
const double minBounds[3]={key.coordinate.x*extent,key.coordinate.y*extent,key.coordinate.z*extent};
|
||||
const double maxBounds[3]={minBounds[0]+extent,minBounds[1]+extent,minBounds[2]+extent};
|
||||
const double origins[3]={rayOrigin.x,rayOrigin.y,rayOrigin.z};
|
||||
const double directions[3]={direction.x,direction.y,direction.z};
|
||||
double enter=0.0,exit=edgeDistance;bool intersects=true;
|
||||
for(int axis=0;axis<3;++axis)
|
||||
{
|
||||
if(std::abs(directions[axis])<1.0e-12)
|
||||
{if(origins[axis]<minBounds[axis]||origins[axis]>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="<<viewIndex<<" elevation="<<elevations[elevationIndex]
|
||||
<<" azimuth="<<azimuth<<" profile_crossing="<<(profileCrossing?1:0)
|
||||
<<" profile_roof="<<(profileRoof?1:0)<<" requested_tile="<<(intersectedRequestedTile?1:0)
|
||||
<<" empty_tile="<<(intersectedEmptyTile?1:0);
|
||||
firstMisses.push_back(message.str());
|
||||
}
|
||||
}
|
||||
const char* viewLabel=viewIndex<plan.route.size()?"route_point":
|
||||
(viewIndex==farStraightUpIndex?"far_straight_up":"owner_camera");
|
||||
std::printf("[WorldAssemblerFarCapCoverageView] view=%s index=%zu origin_vox=(%.1f,%.1f,%.1f) rays=%llu hits=%llu coverage_pct=%.3f\n",
|
||||
viewLabel,viewIndex,walkOrigins[viewIndex].x,walkOrigins[viewIndex].y,
|
||||
walkOrigins[viewIndex].z,static_cast<unsigned long long>(rayCount-raysBeforeView),
|
||||
static_cast<unsigned long long>(rayHits-hitsBeforeView),
|
||||
rayCount>raysBeforeView?100.0*static_cast<double>(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<unsigned long long>(rayCount),static_cast<unsigned long long>(rayHits),
|
||||
rayCount?100.0*static_cast<double>(rayHits)/rayCount:0.0,
|
||||
static_cast<unsigned long long>(profileCrossings),static_cast<unsigned long long>(profileRoofCrossings),
|
||||
static_cast<unsigned long long>(missesNoProfileRoof),static_cast<unsigned long long>(missesMesh),
|
||||
static_cast<unsigned long long>(missesClipEdge),walkOrigins.size());
|
||||
for(std::size_t i=0;i<elevations.size();++i)
|
||||
std::printf("[WorldAssemblerFarCapCoverageElevation] elevation_deg=%.0f rays=%llu hits=%llu\n",
|
||||
elevations[i],static_cast<unsigned long long>(raysByElevation[i]),
|
||||
static_cast<unsigned long long>(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)
|
||||
|
||||
Reference in new issue
Block a user