Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6185b1a3f |
No files matched your search
+18
-20
@@ -570,6 +570,15 @@ ApplyModification` (BlueprintCallable) + `EditorCarveSphere/EditorFillSphere` (C
|
|||||||
driven by `EditorBrush*` props.
|
driven by `EditorBrush*` props.
|
||||||
|
|
||||||
### 8.10 Performance invariants (DON'T regress)
|
### 8.10 Performance invariants (DON'T regress)
|
||||||
|
|
||||||
|
- **Far display mesh budget (L15)** (`WorldAssemblyEvaluatorVersion` 7): recipes may explicitly
|
||||||
|
declare `far_display_mesh` for LOD3+ display generation. With no declaration, the L14 mesh path
|
||||||
|
remains byte-for-byte selected. This setting must never alter LOD0–2, exact field queries,
|
||||||
|
collision, edits, or server state. A declared reduction must pass the same-LOD screen gate at the
|
||||||
|
closest displayed distance: at most 1.0 px geometry displacement, at most 1/255 normalized
|
||||||
|
shading/material delta, no worse swap pop, and no new holes, cracks, or floating pieces. Connector
|
||||||
|
wall repairs are safety geometry: retain every station/ray needed to close the existing gaps. Keep
|
||||||
|
the setting in the shared Core path so all recipe routes use the same declaration and evaluator.
|
||||||
- **Streaming** (`UpdateChunksAroundPosition`): rebuild/cull the desired set only when the player
|
- **Streaming** (`UpdateChunksAroundPosition`): rebuild/cull the desired set only when the player
|
||||||
crosses a level-0 chunk boundary, an anchor moves, or a rebuild is forced; use the stamped
|
crosses a level-0 chunk boundary, an anchor moves, or a rebuild is forced; use the stamped
|
||||||
desired map for the cull and idle via `bAllChunksLoaded`. On every rebuild, pending requests whose
|
desired map for the cull and idle via `bAllChunksLoaded`. On every rebuild, pending requests whose
|
||||||
@@ -1840,27 +1849,16 @@ fill and check page tiles against the evaluator they actually sample. This is th
|
|||||||
the Unreal adapter and its asynchronous MPSC/Epoch flow are unchanged. See
|
the Unreal adapter and its asynchronous MPSC/Epoch flow are unchanged. See
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
[`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
||||||
|
|
||||||
### 8.37 Owner/Core stratum coordinates and FloatingIslands boundaries — World Assembler M3 L17
|
### 8.37 Screen-gated far display budget — World Assembler M3 L15
|
||||||
|
|
||||||
Manager chunk coordinates are in owner chunks, while Core density positions are Core voxels. Convert
|
Evaluator version 7 adds the optional, serialized `far_display_mesh` recipe declaration for
|
||||||
between them with `32 * OwnerVoxelSizeMicrons / CoreVoxelSizeMicrons`; do not divide Core positions
|
display-only LOD3+ refinement and connector repair. A missing declaration selects the existing L14
|
||||||
by `CHUNK_SIZE` directly. The L17 owner snapshot uses 0.125 m owner voxels and 0.25 m Core voxels,
|
path; exact queries, LOD0–2, collision, edits, and route binding stay on their existing shared Core
|
||||||
so one owner chunk is 16 Core voxels (4 m), not 32 (8 m). The old hard-coded divisor doubled the
|
evaluators. The acceptance gate compares against today's same-LOD display mesh, reports geometry and
|
||||||
manifest's vertical stratum bounds and misplaced native-only readiness queries.
|
fixed-light/material shading deltas, checks every displayed tile, and measures each adjacent LOD
|
||||||
|
swap before and after. It rejects any new hole, crack, or floating piece, including lost connector
|
||||||
FloatingIslands is the sparse `FloatingIslandFieldV1` manifest route. Its 0.5 m `boundarySeal`
|
panels. No tested L15 candidate met the full budget, so no declaration is enabled by default. See
|
||||||
limits feature influence; it is not a signed solid cap across the whole XY plane. Applying it as a
|
[`Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md).
|
||||||
solid cap produced a false top at +3.5 m, while the doubled legacy layer bound also produced a cap at
|
|
||||||
+8 m. SurfaceWorld remains the planar `Heightfield2D` field with its independent ground and cap
|
|
||||||
surfaces. Adjacent native FI/SW strata now clip their signed distances to the same physical owner
|
|
||||||
boundary, preserving exact surface signs and height through the mesh sample lattice.
|
|
||||||
|
|
||||||
The output change increments `WorldAssemblyEvaluatorVersion` from 6 to 7; the plan schema and
|
|
||||||
recipe geometry version stay unchanged because their inputs did not change. The L17 probes found
|
|
||||||
zero cave/floor influence at the false square and at the sampled island ground. The shared Core
|
|
||||||
coordinate conversion also changes SurfaceWorld native readiness and hashes where the previous
|
|
||||||
mapping selected the wrong route. See
|
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux17/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux17/REPORT.md).
|
|
||||||
|
|
||||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||||
|
|
||||||
|
|||||||
+9
-7
@@ -1132,18 +1132,20 @@ Implementation and evidence: [`Standalone/Research/WorldAssemblerM3/Linux9/REPOR
|
|||||||
|
|
||||||
| Symbol | Canonical file | Role |
|
| Symbol | Canonical file | Role |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `FStandaloneShapeWorld::SetSharedDensitySource`, `SharedDensitySource` | `Standalone/Core/src/ShapeWorldInternal.h`, bound before publication in `Standalone/Core/src/VoxelForgeCore.cpp` | Makes compiled analytic pages delegate field, lattice, detail, classification, and solid queries to the Core world's base shape; `WorldAssemblyEvaluatorVersion` is 7. |
|
| `FStandaloneShapeWorld::SetSharedDensitySource`, `SharedDensitySource` | `Standalone/Core/src/ShapeWorldInternal.h`, bound before publication in `Standalone/Core/src/VoxelForgeCore.cpp` | Makes compiled analytic pages delegate field, lattice, detail, classification, and solid queries to the Core world's base shape; `WorldAssemblyEvaluatorVersion` is 7, including optional far display-mesh recipe settings. |
|
||||||
|
|
||||||
The base manifest field is authoritative for tile generation and exact queries. See ARCHITECTURE
|
The base manifest field is authoritative for tile generation and exact queries. See ARCHITECTURE
|
||||||
§8.36 and [`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
§8.36 and [`Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux13/REPORT.md).
|
||||||
|
|
||||||
### 3.23 Owner/Core stratum coordinates — World Assembler M3 L17
|
### 3.23 Screen-gated far display mesh — World Assembler M3 L15
|
||||||
|
|
||||||
`FStandaloneShapeWorld::GetCoreVoxelsPerOwnerChunk` is the shared conversion for manager chunk
|
`ApplyFarDisplayMeshDeclaration` in `Standalone/Source/WorldAssemblyPrototype.h` parses the optional
|
||||||
lookups, manifest layer bounds, readiness proofs, and adjacent native strata. FloatingIslands uses
|
`far_display_mesh` recipe block; `WorldAssemblyEvaluatorVersion` 7 serializes and hashes it.
|
||||||
`FloatingIslandFieldV1`; its boundary seal bounds feature influence and does not fill an XY-wide
|
`VoxelForgeCore::World::generateTile` consumes its display-only refinement and connector settings.
|
||||||
solid cap. SurfaceWorld remains the `Heightfield2D` path. See ARCHITECTURE §8.37 and
|
No declaration keeps the L14 output path. `Standalone/Source/LinuxScreenSpaceGate.h` compares every
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux17/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux17/REPORT.md).
|
desired LOD3–8 tile against today's same-LOD mesh and audits geometry, shading, swaps, and connector
|
||||||
|
coverage. The measured candidates and null result are in
|
||||||
|
[`Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md).
|
||||||
|
|
||||||
## 4. The density pipeline (most-edited hot path)
|
## 4. The density pipeline (most-edited hot path)
|
||||||
|
|
||||||
|
|||||||
@@ -184,6 +184,12 @@ namespace VoxelForgeCore
|
|||||||
std::uint64_t farConnectorColumnEvaluations = 0;
|
std::uint64_t farConnectorColumnEvaluations = 0;
|
||||||
double farStratumColumnComputeMilliseconds = 0.0;
|
double farStratumColumnComputeMilliseconds = 0.0;
|
||||||
double farConnectorColumnComputeMilliseconds = 0.0;
|
double farConnectorColumnComputeMilliseconds = 0.0;
|
||||||
|
// Opt-in quality audit at the L14 route-wall stations and four axial rays.
|
||||||
|
std::uint64_t farConnectorAuditRays = 0;
|
||||||
|
std::uint64_t farConnectorAuditHoles = 0;
|
||||||
|
std::uint64_t farConnectorAuditFloating = 0;
|
||||||
|
std::vector<std::uint64_t> farConnectorAuditHoleIds;
|
||||||
|
std::vector<std::uint64_t> farConnectorAuditFloatingIds;
|
||||||
std::uint64_t farShellTrianglesAdded = 0;
|
std::uint64_t farShellTrianglesAdded = 0;
|
||||||
std::uint64_t horizonGroundTriangles = 0;
|
std::uint64_t horizonGroundTriangles = 0;
|
||||||
std::uint64_t horizonGroundTrianglesRemoved = 0;
|
std::uint64_t horizonGroundTrianglesRemoved = 0;
|
||||||
@@ -249,7 +255,8 @@ namespace VoxelForgeCore
|
|||||||
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
|
// 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::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
||||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 1;
|
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 1;
|
||||||
// Version 7 fixes owner/Core chunk mapping and native adjacent-stratum boundaries.
|
// Version 6 binds every compiled tile page to the world's base density evaluator.
|
||||||
|
// Version 7 adds optional, display-only far-mesh quality declarations.
|
||||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 7;
|
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 7;
|
||||||
|
|
||||||
enum class FieldChannel : std::uint32_t
|
enum class FieldChannel : std::uint32_t
|
||||||
@@ -312,6 +319,17 @@ namespace VoxelForgeCore
|
|||||||
std::int64_t biomeClimateReliefWavelengthSubvoxels = 256 * 3333;
|
std::int64_t biomeClimateReliefWavelengthSubvoxels = 256 * 3333;
|
||||||
std::int64_t biomeClimateMoistureWavelengthSubvoxels = 256 * 2500;
|
std::int64_t biomeClimateMoistureWavelengthSubvoxels = 256 * 2500;
|
||||||
std::int64_t biomeClimateReliefContrastQ16 = 2ll << 16;
|
std::int64_t biomeClimateReliefContrastQ16 = 2ll << 16;
|
||||||
|
// Optional display-only mesh budget. Zero leaves the L14 far-mesh path unchanged.
|
||||||
|
// Budget is in 1/1000 pixel at the closest display distance.
|
||||||
|
std::uint32_t farDisplayGeometryBudgetMillipixels = 0;
|
||||||
|
// Scale/mask change only display refinement density at the selected LODs.
|
||||||
|
// A zero scale or mask leaves the current refinement grids intact.
|
||||||
|
std::uint32_t farRefinementGridScale = 0;
|
||||||
|
std::uint32_t farRefinementLodMask = 0;
|
||||||
|
// Connector scale 0 and ray mask 0 preserve the current station/ray pattern.
|
||||||
|
std::uint32_t farConnectorStationScale = 0;
|
||||||
|
std::uint32_t farConnectorRayMask = 0;
|
||||||
|
bool farConnectorReuseStationRadii = false;
|
||||||
std::vector<WorldAssemblyBiomeProfileV1> biomes;
|
std::vector<WorldAssemblyBiomeProfileV1> biomes;
|
||||||
std::uint64_t contentHash = 0;
|
std::uint64_t contentHash = 0;
|
||||||
};
|
};
|
||||||
@@ -1099,6 +1117,16 @@ namespace VoxelForgeCore
|
|||||||
// Incremente quand le plafond lointain change. Les hashes LOD4-8 incluent cette version;
|
// Incremente quand le plafond lointain change. Les hashes LOD4-8 incluent cette version;
|
||||||
// densite et hashes LOD0-3 restent inchanges.
|
// densite et hashes LOD0-3 restent inchanges.
|
||||||
std::uint32_t farHorizonVersion = 3;
|
std::uint32_t farHorizonVersion = 3;
|
||||||
|
// Research-only override of the recipe declaration; used by same-LOD controls.
|
||||||
|
bool overrideFarDisplayMesh = false;
|
||||||
|
std::uint32_t farDisplayGeometryBudgetMillipixels = 0;
|
||||||
|
std::uint32_t farRefinementGridScale = 0;
|
||||||
|
std::uint32_t farRefinementLodMask = 0;
|
||||||
|
std::uint32_t farConnectorStationScale = 0;
|
||||||
|
std::uint32_t farConnectorRayMask = 0;
|
||||||
|
bool farConnectorReuseStationRadii = false;
|
||||||
|
bool diagnosticRemoveFarConnectorPanels = false;
|
||||||
|
bool diagnosticAuditFarConnectorCoverage = false;
|
||||||
// Linux research controls for reproducing L5 negative controls. Defaults preserve output.
|
// Linux research controls for reproducing L5 negative controls. Defaults preserve output.
|
||||||
std::uint32_t diagnosticGlobalOctaveDrop = 0;
|
std::uint32_t diagnosticGlobalOctaveDrop = 0;
|
||||||
std::uint32_t diagnosticCellStride = 1;
|
std::uint32_t diagnosticCellStride = 1;
|
||||||
|
|||||||
@@ -242,7 +242,8 @@ inline bool FindFarConnectorRadiusNearNominal(const FStandaloneShapeWorld& Shape
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,
|
inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,
|
||||||
int32 Step,int32 Cells,FVoxelMeshData& Mesh)
|
int32 Step,int32 Cells,FVoxelMeshData& Mesh,int32 StationScale=1,
|
||||||
|
uint32 RayMask=0x0fu,bool bReuseStationRadii=false)
|
||||||
{
|
{
|
||||||
if(Key.Level<6||Step<=0)return;
|
if(Key.Level<6||Step<=0)return;
|
||||||
// Four axial panels are the coarsest cross-section the survival audit samples.
|
// Four axial panels are the coarsest cross-section the survival audit samples.
|
||||||
@@ -313,7 +314,8 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
|||||||
if(!bIntersects||TMax-TMin<1.0e-6)continue;
|
if(!bIntersects||TMax-TMin<1.0e-6)continue;
|
||||||
// LOD7 and LOD8 share a 256-voxel connector station grid; near LOD keeps 128-voxel checks.
|
// LOD7 and LOD8 share a 256-voxel connector station grid; near LOD keeps 128-voxel checks.
|
||||||
// LOD7 et LOD8 partagent des stations aux 256 voxels; pres du joueur, garder les pas de 128.
|
// LOD7 et LOD8 partagent des stations aux 256 voxels; pres du joueur, garder les pas de 128.
|
||||||
const double ConnectorStationStep=Key.Level>=7?std::max(256.0,static_cast<double>(Step)):128.0;
|
const double ConnectorStationStep=(Key.Level>=7?std::max(256.0,static_cast<double>(Step)):128.0)
|
||||||
|
*std::max(1,StationScale);
|
||||||
const int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/ConnectorStationStep)));
|
const int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/ConnectorStationStep)));
|
||||||
std::vector<uint8> MissingMasks(static_cast<size_t>(AxialSampleCount),0);
|
std::vector<uint8> MissingMasks(static_cast<size_t>(AxialSampleCount),0);
|
||||||
std::vector<std::array<double,4>> SourceRadii(static_cast<size_t>(AxialSampleCount));
|
std::vector<std::array<double,4>> SourceRadii(static_cast<size_t>(AxialSampleCount));
|
||||||
@@ -332,6 +334,7 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
|||||||
const double DetectionTolerance=MatchTolerance*0.98;
|
const double DetectionTolerance=MatchTolerance*0.98;
|
||||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||||
{
|
{
|
||||||
|
if((RayMask&(1u<<DirectionIndex))==0)continue;
|
||||||
const FVector& Direction=RadialDirections[DirectionIndex];
|
const FVector& Direction=RadialDirections[DirectionIndex];
|
||||||
if(!ContainsFarTilePoint(Key,Center+Direction*RayLength))continue;
|
if(!ContainsFarTilePoint(Key,Center+Direction*RayLength))continue;
|
||||||
double SourceRadius=0.0,SourceUncertainty=0.0;
|
double SourceRadius=0.0,SourceUncertainty=0.0;
|
||||||
@@ -388,7 +391,13 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
|||||||
{Ring.Radii[DirectionIndex]=Connector.Radius;continue;}
|
{Ring.Radii[DirectionIndex]=Connector.Radius;continue;}
|
||||||
double Radius=Connector.Radius;
|
double Radius=Connector.Radius;
|
||||||
double RadiusUncertainty=0.0;
|
double RadiusUncertainty=0.0;
|
||||||
if(!FindFarConnectorRadiusNearNominal(Shape,Center,
|
const int32 NearestStation=std::clamp(static_cast<int32>(std::lround(
|
||||||
|
T*AxialSampleCount-0.5)),0,AxialSampleCount-1);
|
||||||
|
const double ReusedRadius=bReuseStationRadii
|
||||||
|
?SourceRadii[static_cast<size_t>(NearestStation)][DirectionIndex]:0.0;
|
||||||
|
if(ReusedRadius>0.0)
|
||||||
|
Radius=ReusedRadius;
|
||||||
|
else if(!FindFarConnectorRadiusNearNominal(Shape,Center,
|
||||||
RadialDirections[DirectionIndex],Connector.Radius,Radius,RadiusUncertainty)
|
RadialDirections[DirectionIndex],Connector.Radius,Radius,RadiusUncertainty)
|
||||||
&&!FindFarConnectorSurfaceRadius(Shape,Center,RadialDirections[DirectionIndex],
|
&&!FindFarConnectorSurfaceRadius(Shape,Center,RadialDirections[DirectionIndex],
|
||||||
Connector.Radius,Radius))
|
Connector.Radius,Radius))
|
||||||
@@ -432,7 +441,8 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::vector<FRing> Rings;
|
std::vector<FRing> Rings;
|
||||||
const double MaxRingStep=Key.Level>=7?std::max(256.0,static_cast<double>(Step)):128.0;
|
const double MaxRingStep=(Key.Level>=7?std::max(256.0,static_cast<double>(Step)):128.0)
|
||||||
|
*std::max(1,StationScale);
|
||||||
double CurrentT=RunMin;
|
double CurrentT=RunMin;
|
||||||
Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask));
|
Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask));
|
||||||
while(CurrentT<RunMax-1.0e-7)
|
while(CurrentT<RunMax-1.0e-7)
|
||||||
@@ -814,6 +824,112 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
|||||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FFarConnectorCoverageAudit
|
||||||
|
{
|
||||||
|
std::uint64_t Rays=0;
|
||||||
|
std::uint64_t Holes=0;
|
||||||
|
std::uint64_t Floating=0;
|
||||||
|
std::vector<std::uint64_t> HoleIds;
|
||||||
|
std::vector<std::uint64_t> FloatingIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::uint64_t FarConnectorAuditRayId(const FVoxelTileKey& Key,uint32 Connector,
|
||||||
|
int32 Station,int32 Direction)
|
||||||
|
{
|
||||||
|
std::uint64_t Hash=14695981039346656037ull;
|
||||||
|
const auto Add=[&](std::uint64_t Value,int Bytes)
|
||||||
|
{
|
||||||
|
for(int Byte=0;Byte<Bytes;++Byte)
|
||||||
|
{Hash^=static_cast<uint8>(Value>>(Byte*8));Hash*=1099511628211ull;}
|
||||||
|
};
|
||||||
|
Add(static_cast<uint32>(Key.Coord.X),4);Add(static_cast<uint32>(Key.Coord.Y),4);
|
||||||
|
Add(static_cast<uint32>(Key.Coord.Z),4);Add(static_cast<uint32>(Key.Level),4);
|
||||||
|
Add(Connector,4);Add(static_cast<uint32>(Station),4);Add(static_cast<uint32>(Direction),4);
|
||||||
|
return Hash?Hash:1;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FFarConnectorCoverageAudit AuditFarConnectorCoverage(const FStandaloneShapeWorld& Shape,
|
||||||
|
const FVoxelTileKey& Key,int32 Step,int32 Cells,const FVoxelMeshData& Mesh)
|
||||||
|
{
|
||||||
|
FFarConnectorCoverageAudit Audit;
|
||||||
|
if(Key.Level<6||Step<=0||Mesh.Triangles.Num()<3)return Audit;
|
||||||
|
const FIntVector TileOrigin=Key.OriginVoxels();
|
||||||
|
const int32 TileExtent=Key.ExtentVoxels();
|
||||||
|
FFarMeshSegmentIndex MeshIndex(Mesh,TileOrigin,TileExtent,Cells,Step);
|
||||||
|
const auto& Connectors=Shape.GetFarConnectorSegments();
|
||||||
|
std::vector<uint32> CandidateIndices;
|
||||||
|
Shape.FindFarConnectorCandidates(
|
||||||
|
FIntVector(TileOrigin.X-64,TileOrigin.Y-64,TileOrigin.Z-64),
|
||||||
|
FIntVector(TileOrigin.X+TileExtent+64,TileOrigin.Y+TileExtent+64,TileOrigin.Z+TileExtent+64),
|
||||||
|
CandidateIndices);
|
||||||
|
for(const uint32 ConnectorIndex:CandidateIndices)
|
||||||
|
{
|
||||||
|
if(ConnectorIndex>=Connectors.size())continue;
|
||||||
|
const auto& Connector=Connectors[ConnectorIndex];
|
||||||
|
const FVector Delta=Connector.End-Connector.Start;
|
||||||
|
const double Length=std::sqrt(Delta.X*Delta.X+Delta.Y*Delta.Y+Delta.Z*Delta.Z);
|
||||||
|
if(Length<=1.0e-6)continue;
|
||||||
|
const FVector Axis=Delta/Length;
|
||||||
|
FVector U=FVector::CrossProduct(Axis,FVector::UpVector);
|
||||||
|
if(U.SizeSquared()<0.01)U=FVector::CrossProduct(Axis,FVector::RightVector);
|
||||||
|
U.Normalize();
|
||||||
|
FVector V=FVector::CrossProduct(Axis,U);V.Normalize();
|
||||||
|
const FVector Directions[4]={U,U*-1.0,V,V*-1.0};
|
||||||
|
const double Deltas[3]={Delta.X,Delta.Y,Delta.Z};
|
||||||
|
const double Starts[3]={Connector.Start.X,Connector.Start.Y,Connector.Start.Z};
|
||||||
|
const double Low[3]={TileOrigin.X-Connector.Radius,TileOrigin.Y-Connector.Radius,
|
||||||
|
TileOrigin.Z-Connector.Radius};
|
||||||
|
const double High[3]={TileOrigin.X+TileExtent+Connector.Radius,
|
||||||
|
TileOrigin.Y+TileExtent+Connector.Radius,TileOrigin.Z+TileExtent+Connector.Radius};
|
||||||
|
double TMin=0.0,TMax=1.0;
|
||||||
|
bool bIntersects=true;
|
||||||
|
for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex)
|
||||||
|
{
|
||||||
|
if(std::abs(Deltas[AxisIndex])<1.0e-9)
|
||||||
|
{if(Starts[AxisIndex]<Low[AxisIndex]||Starts[AxisIndex]>High[AxisIndex])bIntersects=false;continue;}
|
||||||
|
double A=(Low[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex];
|
||||||
|
double B=(High[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex];
|
||||||
|
if(A>B)std::swap(A,B);
|
||||||
|
TMin=std::max(TMin,A);TMax=std::min(TMax,B);
|
||||||
|
if(TMin>TMax){bIntersects=false;break;}
|
||||||
|
}
|
||||||
|
if(!bIntersects||TMax-TMin<1.0e-6)continue;
|
||||||
|
const double StationStep=Key.Level>=7?std::max(256.0,static_cast<double>(Step)):128.0;
|
||||||
|
const int32 StationCount=std::max(1,static_cast<int32>(std::ceil(Length/StationStep)));
|
||||||
|
const double RayLength=Connector.Radius*2.0+32.0;
|
||||||
|
const double MatchTolerance=std::max(8.0,static_cast<double>(Connector.Radius)*0.35);
|
||||||
|
const double DetectionTolerance=MatchTolerance*0.98;
|
||||||
|
const double SourceRadiusBand=std::max(16.0,static_cast<double>(Connector.Radius)*0.75);
|
||||||
|
for(int32 Station=0;Station<StationCount;++Station)
|
||||||
|
{
|
||||||
|
const FVector Center=Connector.Start+Delta*((static_cast<double>(Station)+0.5)/StationCount);
|
||||||
|
if(!ContainsFarTilePoint(Key,Center))continue;
|
||||||
|
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||||
|
{
|
||||||
|
if(!ContainsFarTilePoint(Key,Center+Directions[DirectionIndex]*RayLength))continue;
|
||||||
|
double SourceRadius=0.0,Uncertainty=0.0;
|
||||||
|
const bool bSource=FindFarConnectorRadiusNearNominal(Shape,Center,
|
||||||
|
Directions[DirectionIndex],Connector.Radius,SourceRadius,Uncertainty)
|
||||||
|
||FindFarConnectorSurfaceRadius(Shape,Center,Directions[DirectionIndex],
|
||||||
|
Connector.Radius,SourceRadius);
|
||||||
|
const double MeshDifference=MeshIndex.NearestHitDifference(Center,
|
||||||
|
Directions[DirectionIndex],RayLength,bSource?SourceRadius:Connector.Radius);
|
||||||
|
if(bSource&&std::abs(SourceRadius-Connector.Radius)<=SourceRadiusBand)
|
||||||
|
{
|
||||||
|
++Audit.Rays;
|
||||||
|
if(!std::isfinite(MeshDifference)||MeshDifference>DetectionTolerance)
|
||||||
|
Audit.HoleIds.push_back(FarConnectorAuditRayId(Key,ConnectorIndex,Station,DirectionIndex));
|
||||||
|
}
|
||||||
|
else if(std::isfinite(MeshDifference)&&MeshDifference<=DetectionTolerance)
|
||||||
|
Audit.FloatingIds.push_back(FarConnectorAuditRayId(Key,ConnectorIndex,Station,DirectionIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Audit.Holes=Audit.HoleIds.size();
|
||||||
|
Audit.Floating=Audit.FloatingIds.size();
|
||||||
|
return Audit;
|
||||||
|
}
|
||||||
|
|
||||||
struct FFarMeshColumnIndex
|
struct FFarMeshColumnIndex
|
||||||
{
|
{
|
||||||
FFarMeshColumnIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent,
|
FFarMeshColumnIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent,
|
||||||
|
|||||||
@@ -47,18 +47,6 @@ public:
|
|||||||
return static_cast<double>(CHUNK_SIZE) * OwnerVoxelSizeCm / GetCoreVoxelSizeCm();
|
return static_cast<double>(CHUNK_SIZE) * OwnerVoxelSizeCm / GetCoreVoxelSizeCm();
|
||||||
}
|
}
|
||||||
|
|
||||||
int32 OwnerChunkLowerBoundaryCoreVoxel(int32 ChunkZ) const
|
|
||||||
{
|
|
||||||
return static_cast<int32>(std::floor(
|
|
||||||
static_cast<double>(ChunkZ) * GetCoreVoxelsPerOwnerChunk()));
|
|
||||||
}
|
|
||||||
|
|
||||||
int32 OwnerChunkUpperBoundaryCoreVoxel(int32 ChunkZ) const
|
|
||||||
{
|
|
||||||
return static_cast<int32>(std::ceil(
|
|
||||||
static_cast<double>(ChunkZ) * GetCoreVoxelsPerOwnerChunk()));
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
|
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
|
||||||
struct FBoxClassification
|
struct FBoxClassification
|
||||||
{
|
{
|
||||||
@@ -724,21 +712,14 @@ public:
|
|||||||
if(Owner&&Owner->Manager)
|
if(Owner&&Owner->Manager)
|
||||||
{
|
{
|
||||||
const double TileZ0=Origin.Z,TileZ1=Origin.Z+static_cast<double>(Cells)*Step;
|
const double TileZ0=Origin.Z,TileZ1=Origin.Z+static_cast<double>(Cells)*Step;
|
||||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
|
||||||
for(const FStrateSlot& Slot:Owner->Manager->GetLayout())
|
for(const FStrateSlot& Slot:Owner->Manager->GetLayout())
|
||||||
{
|
{
|
||||||
const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2;
|
|
||||||
if(bUseNativeManifestFields
|
|
||||||
&&Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,MidChunkZ))
|
|
||||||
==ECaveGeneratorType::FloatingIslands)
|
|
||||||
continue;
|
|
||||||
const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(
|
const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(
|
||||||
FIntVector(0,0,MidChunkZ));
|
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
|
||||||
const float HalfWidth=static_cast<float>(std::max(8.0,
|
const float HalfWidth=static_cast<float>(std::max(8.0,
|
||||||
static_cast<double>(Params.BoundarySealThickness)+8.0));
|
static_cast<double>(Params.BoundarySealThickness)+8.0));
|
||||||
const float Boundaries[2]={
|
const float Boundaries[2]={static_cast<float>(Slot.BottomChunkZ)*CHUNK_SIZE,
|
||||||
static_cast<float>(static_cast<double>(Slot.BottomChunkZ)*CoreVoxelsPerOwnerChunk),
|
static_cast<float>(Slot.TopChunkZ+1)*CHUNK_SIZE};
|
||||||
static_cast<float>(static_cast<double>(Slot.TopChunkZ+1)*CoreVoxelsPerOwnerChunk)};
|
|
||||||
for(const float Z:Boundaries)
|
for(const float Z:Boundaries)
|
||||||
if(Z+HalfWidth>=TileZ0&&Z-HalfWidth<=TileZ1)
|
if(Z+HalfWidth>=TileZ0&&Z-HalfWidth<=TileZ1)
|
||||||
Out.stratumBoundaries.push_back({Z,HalfWidth});
|
Out.stratumBoundaries.push_back({Z,HalfWidth});
|
||||||
@@ -1047,21 +1028,19 @@ public:
|
|||||||
float NativeDensity=0.0f;
|
float NativeDensity=0.0f;
|
||||||
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||||
{RecordNativeOnlyViolation();return 0.0f;}
|
{RecordNativeOnlyViolation();return 0.0f;}
|
||||||
return ApplyAdjacentStratumBoundary(X,Y,Z,NativeDensity,false);
|
return NativeDensity;
|
||||||
}
|
}
|
||||||
float IslandDensity=0.0f;
|
float IslandDensity=0.0f;
|
||||||
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
||||||
{
|
{
|
||||||
++ThreadClassificationStats().NativeFloatFieldCalls;
|
++ThreadClassificationStats().NativeFloatFieldCalls;
|
||||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
return ApplyEditBrushes(IslandDensity,X,Y,Z);
|
||||||
X,Y,Z,IslandDensity,true),X,Y,Z);
|
|
||||||
}
|
}
|
||||||
float NativeDensity=0.0f;
|
float NativeDensity=0.0f;
|
||||||
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||||
{
|
{
|
||||||
++ThreadClassificationStats().NativeFloatFieldCalls;
|
++ThreadClassificationStats().NativeFloatFieldCalls;
|
||||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
return ApplyEditBrushes(NativeDensity,X,Y,Z);
|
||||||
X,Y,Z,NativeDensity,false),X,Y,Z);
|
|
||||||
}
|
}
|
||||||
if(UsesLegacySurfaceField(X,Y,Z))
|
if(UsesLegacySurfaceField(X,Y,Z))
|
||||||
return LegacySurfaceDensityAt(X,Y,Z);
|
return LegacySurfaceDensityAt(X,Y,Z);
|
||||||
@@ -1085,27 +1064,21 @@ public:
|
|||||||
if(!NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
if(!NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||||
static_cast<float>(Z),NativeDensity))
|
static_cast<float>(Z),NativeDensity))
|
||||||
{RecordNativeOnlyViolation();return 0.0f;}
|
{RecordNativeOnlyViolation();return 0.0f;}
|
||||||
return ApplyAdjacentStratumBoundary(
|
return NativeDensity;
|
||||||
static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z),
|
|
||||||
NativeDensity,false);
|
|
||||||
}
|
}
|
||||||
float IslandDensity=0.0f;
|
float IslandDensity=0.0f;
|
||||||
if(NativeFloatingIslandDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
if(NativeFloatingIslandDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||||
static_cast<float>(Z),IslandDensity))
|
static_cast<float>(Z),IslandDensity))
|
||||||
{
|
{
|
||||||
++ThreadClassificationStats().NativeLatticeFieldCalls;
|
++ThreadClassificationStats().NativeLatticeFieldCalls;
|
||||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
return ApplyEditBrushes(IslandDensity,X,Y,Z);
|
||||||
static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z),
|
|
||||||
IslandDensity,true),X,Y,Z);
|
|
||||||
}
|
}
|
||||||
float NativeDensity=0.0f;
|
float NativeDensity=0.0f;
|
||||||
if(NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
if(NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||||
static_cast<float>(Z),NativeDensity))
|
static_cast<float>(Z),NativeDensity))
|
||||||
{
|
{
|
||||||
++ThreadClassificationStats().NativeLatticeFieldCalls;
|
++ThreadClassificationStats().NativeLatticeFieldCalls;
|
||||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
return ApplyEditBrushes(NativeDensity,X,Y,Z);
|
||||||
static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z),
|
|
||||||
NativeDensity,false),X,Y,Z);
|
|
||||||
}
|
}
|
||||||
if(UsesLegacySurfaceField(static_cast<float>(X),static_cast<float>(Y),
|
if(UsesLegacySurfaceField(static_cast<float>(X),static_cast<float>(Y),
|
||||||
static_cast<float>(Z)))
|
static_cast<float>(Z)))
|
||||||
@@ -1129,21 +1102,19 @@ public:
|
|||||||
float NativeDensity=0.0f;
|
float NativeDensity=0.0f;
|
||||||
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
if(!NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||||
{RecordNativeOnlyViolation();return 0.0f;}
|
{RecordNativeOnlyViolation();return 0.0f;}
|
||||||
return ApplyAdjacentStratumBoundary(X,Y,Z,NativeDensity,false);
|
return NativeDensity;
|
||||||
}
|
}
|
||||||
float IslandDensity=0.0f;
|
float IslandDensity=0.0f;
|
||||||
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
||||||
{
|
{
|
||||||
++ThreadClassificationStats().NativeUnclampedFieldCalls;
|
++ThreadClassificationStats().NativeUnclampedFieldCalls;
|
||||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
return ApplyEditBrushes(IslandDensity,X,Y,Z);
|
||||||
X,Y,Z,IslandDensity,true),X,Y,Z);
|
|
||||||
}
|
}
|
||||||
float NativeDensity=0.0f;
|
float NativeDensity=0.0f;
|
||||||
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||||
{
|
{
|
||||||
++ThreadClassificationStats().NativeUnclampedFieldCalls;
|
++ThreadClassificationStats().NativeUnclampedFieldCalls;
|
||||||
return ApplyEditBrushes(ApplyAdjacentStratumBoundary(
|
return ApplyEditBrushes(NativeDensity,X,Y,Z);
|
||||||
X,Y,Z,NativeDensity,false),X,Y,Z);
|
|
||||||
}
|
}
|
||||||
if(UsesLegacySurfaceField(X,Y,Z))
|
if(UsesLegacySurfaceField(X,Y,Z))
|
||||||
return LegacySurfaceDensityAt(X,Y,Z);
|
return LegacySurfaceDensityAt(X,Y,Z);
|
||||||
@@ -1289,18 +1260,12 @@ private:
|
|||||||
if(OutFloorInfluence)*OutFloorInfluence=0.0f;
|
if(OutFloorInfluence)*OutFloorInfluence=0.0f;
|
||||||
float IslandDensity=0.0f;
|
float IslandDensity=0.0f;
|
||||||
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity))
|
||||||
{
|
|
||||||
IslandDensity=ApplyAdjacentStratumBoundary(X,Y,Z,IslandDensity,true);
|
|
||||||
return bApplyEdits?ApplyEditBrushes(IslandDensity,X,Y,Z):IslandDensity;
|
return bApplyEdits?ApplyEditBrushes(IslandDensity,X,Y,Z):IslandDensity;
|
||||||
}
|
|
||||||
if(UsesLegacySurfaceField(X,Y,Z))
|
if(UsesLegacySurfaceField(X,Y,Z))
|
||||||
{
|
{
|
||||||
float NativeDensity=0.0f;
|
float NativeDensity=0.0f;
|
||||||
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
|
||||||
{
|
|
||||||
NativeDensity=ApplyAdjacentStratumBoundary(X,Y,Z,NativeDensity,false);
|
|
||||||
return bApplyEdits?ApplyEditBrushes(NativeDensity,X,Y,Z):NativeDensity;
|
return bApplyEdits?ApplyEditBrushes(NativeDensity,X,Y,Z):NativeDensity;
|
||||||
}
|
|
||||||
const float Density=Owner->Generator->GetDensityAt(X,Y,Z);
|
const float Density=Owner->Generator->GetDensityAt(X,Y,Z);
|
||||||
return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density;
|
return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density;
|
||||||
}
|
}
|
||||||
@@ -1356,11 +1321,8 @@ public:
|
|||||||
return SharedDensitySource->GetNativeSurfaceHeightsAt(X,Y,ChunkZ,OutGroundZ,OutCeilingZ);
|
return SharedDensitySource->GetNativeSurfaceHeightsAt(X,Y,ChunkZ,OutGroundZ,OutCeilingZ);
|
||||||
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
||||||
||!std::isfinite(X)||!std::isfinite(Y))return false;
|
||!std::isfinite(X)||!std::isfinite(Y))return false;
|
||||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
const std::int64_t chunkMinimum=static_cast<std::int64_t>(ChunkZ)*CHUNK_SIZE;
|
||||||
const std::int64_t chunkMinimum=static_cast<std::int64_t>(std::floor(
|
const std::int64_t chunkMaximum=chunkMinimum+CHUNK_SIZE;
|
||||||
static_cast<double>(ChunkZ)*CoreVoxelsPerOwnerChunk));
|
|
||||||
const std::int64_t chunkMaximum=static_cast<std::int64_t>(std::ceil(
|
|
||||||
static_cast<double>(ChunkZ+1)*CoreVoxelsPerOwnerChunk));
|
|
||||||
if(bNativeOnly&&(!NativeOnlyContainsColumn(X,Y)
|
if(bNativeOnly&&(!NativeOnlyContainsColumn(X,Y)
|
||||||
||chunkMaximum<NativeOnlyMinimum.Z||chunkMinimum>NativeOnlyMaximum.Z
|
||chunkMaximum<NativeOnlyMinimum.Z||chunkMinimum>NativeOnlyMaximum.Z
|
||||||
||Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||||||
@@ -1382,8 +1344,7 @@ public:
|
|||||||
const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*Plan);
|
const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*Plan);
|
||||||
if(!Block.isValid())return false;
|
if(!Block.isValid())return false;
|
||||||
const auto Column=Block.point(ToFixed(X),ToFixed(Y),
|
const auto Column=Block.point(ToFixed(X),ToFixed(Y),
|
||||||
static_cast<int64>(std::llround(static_cast<double>(ChunkZ)
|
static_cast<int64>(ChunkZ)*CHUNK_SIZE*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||||
*CoreVoxelsPerOwnerChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)));
|
|
||||||
OutGroundZ=static_cast<float>(Column.groundHeightSubvoxels/Scale);
|
OutGroundZ=static_cast<float>(Column.groundHeightSubvoxels/Scale);
|
||||||
OutCeilingZ=static_cast<float>(Column.capHeightSubvoxels/Scale);
|
OutCeilingZ=static_cast<float>(Column.capHeightSubvoxels/Scale);
|
||||||
return std::isfinite(OutGroundZ)&&std::isfinite(OutCeilingZ);
|
return std::isfinite(OutGroundZ)&&std::isfinite(OutCeilingZ);
|
||||||
@@ -1399,11 +1360,10 @@ public:
|
|||||||
const int32 Pad=std::max(Step,1);
|
const int32 Pad=std::max(Step,1);
|
||||||
const int32 BMin=Origin.Z-Pad;
|
const int32 BMin=Origin.Z-Pad;
|
||||||
const int32 BMax=Origin.Z+(Cells+1)*Step;
|
const int32 BMax=Origin.Z+(Cells+1)*Step;
|
||||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
|
||||||
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin)
|
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin)
|
||||||
/static_cast<float>(CoreVoxelsPerOwnerChunk));
|
/static_cast<float>(CHUNK_SIZE));
|
||||||
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax)
|
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax)
|
||||||
/static_cast<float>(CoreVoxelsPerOwnerChunk));
|
/static_cast<float>(CHUNK_SIZE));
|
||||||
const VoxelForgeCore::WorldAssemblyStratumPlanV1* Plan=nullptr;
|
const VoxelForgeCore::WorldAssemblyStratumPlanV1* Plan=nullptr;
|
||||||
for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ)
|
for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ)
|
||||||
{
|
{
|
||||||
@@ -1447,13 +1407,12 @@ public:
|
|||||||
bool bIslandOtherStrateOverlap=false,bBadPlan=false;
|
bool bIslandOtherStrateOverlap=false,bBadPlan=false;
|
||||||
if(Owner&&Owner->Manager)
|
if(Owner&&Owner->Manager)
|
||||||
{
|
{
|
||||||
// All manifest strata use the same resolved owner-chunk to Core-voxel scale.
|
// Keep the delivered SurfaceWorld chunk mapping unchanged.
|
||||||
// Toutes les strates du manifeste partagent l'echelle owner/Core resolue.
|
// Conserver le mapping de chunks SurfaceWorld deja livre.
|
||||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
|
||||||
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin[2])
|
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin[2])
|
||||||
/static_cast<float>(CoreVoxelsPerOwnerChunk));
|
/static_cast<float>(CHUNK_SIZE));
|
||||||
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax[2])
|
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax[2])
|
||||||
/static_cast<float>(CoreVoxelsPerOwnerChunk));
|
/static_cast<float>(CHUNK_SIZE));
|
||||||
for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ)
|
for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ)
|
||||||
{
|
{
|
||||||
const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk(
|
const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk(
|
||||||
@@ -1585,6 +1544,19 @@ public:
|
|||||||
{
|
{
|
||||||
if(!IslandPlan)return Finish(EVoxelTileClass::Mixed,true);
|
if(!IslandPlan)return Finish(EVoxelTileClass::Mixed,true);
|
||||||
constexpr std::int64_t Scale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
constexpr std::int64_t Scale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||||
|
const std::int64_t Bottom=IslandPlan->manifest.recipe.layoutMinimumZSubvoxels;
|
||||||
|
const std::int64_t Top=IslandPlan->manifest.recipe.layoutMaximumZSubvoxels;
|
||||||
|
const std::int64_t Seal=IslandPlan->manifest.recipe.boundarySealSubvoxels;
|
||||||
|
const std::int64_t BoxBottom=static_cast<std::int64_t>(BMin[2])*Scale;
|
||||||
|
const std::int64_t BoxTop=static_cast<std::int64_t>(BMax[2])*Scale;
|
||||||
|
const bool bBottomSealOverlap=BoxBottom<Bottom+Seal&&BoxTop>=Bottom;
|
||||||
|
const bool bTopSealOverlap=BoxBottom<=Top&&BoxTop>Top-Seal;
|
||||||
|
if(bBottomSealOverlap)
|
||||||
|
return Finish(BoxBottom>=Bottom&&BoxTop<=Bottom+Seal
|
||||||
|
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
||||||
|
if(bTopSealOverlap)
|
||||||
|
return Finish(BoxBottom>=Top-Seal&&BoxTop<=Top
|
||||||
|
?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed);
|
||||||
const VoxelForgeCore::FloatingIslandFieldV1 Block(
|
const VoxelForgeCore::FloatingIslandFieldV1 Block(
|
||||||
IslandPlan->manifest.recipe,IslandPlan->manifest.plan);
|
IslandPlan->manifest.recipe,IslandPlan->manifest.plan);
|
||||||
if(!Block.isValid())return Finish(EVoxelTileClass::Mixed,true);
|
if(!Block.isValid())return Finish(EVoxelTileClass::Mixed,true);
|
||||||
@@ -2107,8 +2079,7 @@ private:
|
|||||||
{
|
{
|
||||||
if(!bUseNativeManifestFields||!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
if(!bUseNativeManifestFields||!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator
|
||||||
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return false;
|
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return false;
|
||||||
const int32 ChunkZ=FMath::FloorToInt(Z
|
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(CHUNK_SIZE));
|
||||||
/static_cast<float>(GetCoreVoxelsPerOwnerChunk()));
|
|
||||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
|
||||||
!=ECaveGeneratorType::SurfaceWorld)return false;
|
!=ECaveGeneratorType::SurfaceWorld)return false;
|
||||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
||||||
@@ -2154,79 +2125,23 @@ private:
|
|||||||
const VoxelForgeCore::FloatingIslandFieldV1 Field(
|
const VoxelForgeCore::FloatingIslandFieldV1 Field(
|
||||||
Plan->manifest.recipe,Plan->manifest.plan);
|
Plan->manifest.recipe,Plan->manifest.plan);
|
||||||
if(!Field.isValid())return false;
|
if(!Field.isValid())return false;
|
||||||
// The recipe's boundary seal is a feature margin, not a horizontal solid cap.
|
std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
|
||||||
// La marge de scellement du plan borne ses formes, sans remplir tout le plan XY.
|
const std::int64_t Bottom=Plan->manifest.recipe.layoutMinimumZSubvoxels;
|
||||||
const std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z));
|
const std::int64_t Top=Plan->manifest.recipe.layoutMaximumZSubvoxels;
|
||||||
|
const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels;
|
||||||
|
const std::int64_t Zq=ToFixed(Z);
|
||||||
|
if(Zq>=Bottom&&Zq<Bottom+Seal)Density=std::min(Density,-std::max<std::int64_t>(1,Zq-Bottom));
|
||||||
|
if(Zq<=Top&&Zq>Top-Seal)Density=std::min(Density,-std::max<std::int64_t>(1,Top-Zq));
|
||||||
OutDensity=static_cast<float>(static_cast<double>(Density)/Scale);
|
OutDensity=static_cast<float>(static_cast<double>(Density)/Scale);
|
||||||
return std::isfinite(OutDensity);
|
return std::isfinite(OutDensity);
|
||||||
}
|
}
|
||||||
|
|
||||||
float ApplyAdjacentStratumBoundary(float X,float Y,float Z,float Density,
|
|
||||||
bool bFloatingIsland) const
|
|
||||||
{
|
|
||||||
if(!bUseNativeManifestFields||!WorldAssembly||!Owner||!Owner->Manager
|
|
||||||
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return Density;
|
|
||||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
|
||||||
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(CoreVoxelsPerOwnerChunk));
|
|
||||||
const ECaveGeneratorType CurrentType=Owner->Manager->GetGeneratorTypeForChunk(
|
|
||||||
FIntVector(0,0,ChunkZ));
|
|
||||||
if(bFloatingIsland&&CurrentType==ECaveGeneratorType::FloatingIslands)
|
|
||||||
{
|
|
||||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
|
||||||
static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
|
|
||||||
const auto* Plan=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ);
|
|
||||||
if(!Plan)return Density;
|
|
||||||
const int32 LowerChunk=Plan->bottomChunkZ-1;
|
|
||||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,LowerChunk))
|
|
||||||
!=ECaveGeneratorType::SurfaceWorld)return Density;
|
|
||||||
const double Boundary=static_cast<double>(Plan->bottomChunkZ)
|
|
||||||
*CoreVoxelsPerOwnerChunk;
|
|
||||||
float LowerDensity=0.0f,UpperDensity=0.0f;
|
|
||||||
if(!NativeSurfaceDensityAt(X,Y,static_cast<float>(Boundary-0.5),LowerDensity)
|
|
||||||
||!NativeFloatingIslandDensityAt(X,Y,static_cast<float>(Boundary+0.5),UpperDensity)
|
|
||||||
||(LowerDensity<0.0f)==(UpperDensity<0.0f))return Density;
|
|
||||||
|
|
||||||
// Clip the two signed-distance fields to their shared owner-stratum plane.
|
|
||||||
// This keeps a solid-below/air-above seam continuous at every mesh sample step.
|
|
||||||
// Les deux champs sont limites au plan commun, quel que soit le pas de maillage.
|
|
||||||
const float Plane=static_cast<float>(static_cast<double>(Z)-Boundary);
|
|
||||||
return LowerDensity<0.0f?std::min(Density,Plane)
|
|
||||||
:std::max(Density,-Plane);
|
|
||||||
}
|
|
||||||
if(!bFloatingIsland&&CurrentType==ECaveGeneratorType::SurfaceWorld)
|
|
||||||
{
|
|
||||||
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
|
|
||||||
static_cast<float>((static_cast<double>(ChunkZ)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE));
|
|
||||||
const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
|
|
||||||
if(!Plan)return Density;
|
|
||||||
const int32 UpperChunk=Plan->topChunkZ+1;
|
|
||||||
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,UpperChunk))
|
|
||||||
!=ECaveGeneratorType::FloatingIslands)return Density;
|
|
||||||
const auto* UpperPlan=FindWorldAssemblyFloatingIslandStratum(
|
|
||||||
Owner->Manager->GetStrateIndex(static_cast<float>((
|
|
||||||
static_cast<double>(UpperChunk)*CHUNK_SIZE+CHUNK_SIZE/2)*VOXEL_SIZE)),UpperChunk);
|
|
||||||
if(!UpperPlan)return Density;
|
|
||||||
const double Boundary=static_cast<double>(UpperPlan->bottomChunkZ)
|
|
||||||
*CoreVoxelsPerOwnerChunk;
|
|
||||||
float LowerDensity=0.0f,UpperDensity=0.0f;
|
|
||||||
if(!NativeSurfaceDensityAt(X,Y,static_cast<float>(Boundary-0.5),LowerDensity)
|
|
||||||
||!NativeFloatingIslandDensityAt(X,Y,static_cast<float>(Boundary+0.5),UpperDensity)
|
|
||||||
||(LowerDensity<0.0f)==(UpperDensity<0.0f))return Density;
|
|
||||||
|
|
||||||
const float Plane=static_cast<float>(static_cast<double>(Z)-Boundary);
|
|
||||||
return LowerDensity<0.0f?std::max(Density,Plane)
|
|
||||||
:std::min(Density,-Plane);
|
|
||||||
}
|
|
||||||
return Density;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool UsesLegacySurfaceField(float X,float Y,float Z) const
|
bool UsesLegacySurfaceField(float X,float Y,float Z) const
|
||||||
{
|
{
|
||||||
if(!Owner||!Owner->Manager||!Owner->Generator)return false;
|
if(!Owner||!Owner->Manager||!Owner->Generator)return false;
|
||||||
const float CoreVoxelsPerOwnerChunk=static_cast<float>(GetCoreVoxelsPerOwnerChunk());
|
const FIntVector ChunkCoord(FMath::FloorToInt(X/static_cast<float>(CHUNK_SIZE)),
|
||||||
const FIntVector ChunkCoord(FMath::FloorToInt(X/CoreVoxelsPerOwnerChunk),
|
FMath::FloorToInt(Y/static_cast<float>(CHUNK_SIZE)),
|
||||||
FMath::FloorToInt(Y/CoreVoxelsPerOwnerChunk),
|
FMath::FloorToInt(Z/static_cast<float>(CHUNK_SIZE)));
|
||||||
FMath::FloorToInt(Z/CoreVoxelsPerOwnerChunk));
|
|
||||||
return Owner->Manager->GetGeneratorTypeForChunk(ChunkCoord)
|
return Owner->Manager->GetGeneratorTypeForChunk(ChunkCoord)
|
||||||
==ECaveGeneratorType::SurfaceWorld;
|
==ECaveGeneratorType::SurfaceWorld;
|
||||||
}
|
}
|
||||||
@@ -2436,26 +2351,19 @@ private:
|
|||||||
int32 Top=MIN_int32,Bottom=MAX_int32;
|
int32 Top=MIN_int32,Bottom=MAX_int32;
|
||||||
for(const FStrateSlot& Slot:Layout)
|
for(const FStrateSlot& Slot:Layout)
|
||||||
{
|
{
|
||||||
const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2;
|
|
||||||
// A manifest-backed FloatingIslands stratum supplies its own sparse island
|
|
||||||
// field; it must not extend the legacy solid envelope across the open sky.
|
|
||||||
if(bUseNativeManifestFields
|
|
||||||
&&Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,MidChunkZ))
|
|
||||||
==ECaveGeneratorType::FloatingIslands)
|
|
||||||
continue;
|
|
||||||
Top=std::max(Top,Slot.TopChunkZ);Bottom=std::min(Bottom,Slot.BottomChunkZ);
|
Top=std::max(Top,Slot.TopChunkZ);Bottom=std::min(Bottom,Slot.BottomChunkZ);
|
||||||
if(!Slot.Definition)continue;
|
if(!Slot.Definition)continue;
|
||||||
|
const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2;
|
||||||
FLayerField Field;
|
FLayerField Field;
|
||||||
Field.Lo=OwnerChunkLowerBoundaryCoreVoxel(Slot.BottomChunkZ);
|
Field.Lo=Slot.BottomChunkZ*CHUNK_SIZE;
|
||||||
Field.Hi=OwnerChunkUpperBoundaryCoreVoxel(Slot.TopChunkZ+1);
|
Field.Hi=(Slot.TopChunkZ+1)*CHUNK_SIZE;
|
||||||
Field.StrateIndex=Slot.StrateIndex;
|
Field.StrateIndex=Slot.StrateIndex;
|
||||||
Field.Params=Owner->Manager->GetGenerationParams(FIntVector(0,0,MidChunkZ));
|
Field.Params=Owner->Manager->GetGenerationParams(FIntVector(0,0,MidChunkZ));
|
||||||
LayerFields.push_back(std::move(Field));
|
LayerFields.push_back(std::move(Field));
|
||||||
}
|
}
|
||||||
if(Top>=Bottom)
|
if(Top>=Bottom)
|
||||||
{
|
{
|
||||||
LayerRanges.push_back({OwnerChunkLowerBoundaryCoreVoxel(Bottom),
|
LayerRanges.push_back({Bottom*CHUNK_SIZE,(Top+1)*CHUNK_SIZE});
|
||||||
OwnerChunkUpperBoundaryCoreVoxel(Top+1)});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3566,9 +3474,8 @@ private:
|
|||||||
auto Unit=[&](uint32 H){return static_cast<double>(Hash(H)>>8)*(1.0/16777216.0);};
|
auto Unit=[&](uint32 H){return static_cast<double>(Hash(H)>>8)*(1.0/16777216.0);};
|
||||||
for(const FStrateSlot& Slot:Layout)
|
for(const FStrateSlot& Slot:Layout)
|
||||||
{
|
{
|
||||||
const double CoreVoxelsPerOwnerChunk=GetCoreVoxelsPerOwnerChunk();
|
const double ZBottom=static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE;
|
||||||
const double ZBottom=static_cast<double>(Slot.BottomChunkZ)*CoreVoxelsPerOwnerChunk;
|
const double ZTop=static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE;
|
||||||
const double ZTop=static_cast<double>(Slot.TopChunkZ+1)*CoreVoxelsPerOwnerChunk;
|
|
||||||
for(int32 GY=GY0;GY<=GY1;++GY)for(int32 GX=GX0;GX<=GX1;++GX)
|
for(int32 GY=GY0;GY<=GY1;++GY)for(int32 GX=GX0;GX<=GX1;++GX)
|
||||||
{
|
{
|
||||||
uint32 H=Hash(Seed^static_cast<uint32>(GX)*0x9E3779B9u
|
uint32 H=Hash(Seed^static_cast<uint32>(GX)*0x9E3779B9u
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ namespace VoxelForgeCore
|
|||||||
|
|
||||||
FNativeCapRefinementStats RefineFarNativeGround(const FStandaloneOwnerWorld& Owner,
|
FNativeCapRefinementStats RefineFarNativeGround(const FStandaloneOwnerWorld& Owner,
|
||||||
const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,int32 MeshStepVoxels,
|
const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,int32 MeshStepVoxels,
|
||||||
int32 InteriorStepOverride,FVoxelMeshData& Mesh)
|
int32 InteriorStepOverride,int32 RefinementGridScale,FVoxelMeshData& Mesh)
|
||||||
{
|
{
|
||||||
FNativeCapRefinementStats Stats;
|
FNativeCapRefinementStats Stats;
|
||||||
if(Key.Level<5||!Owner.Generator)return Stats;
|
if(Key.Level<5||!Owner.Generator)return Stats;
|
||||||
@@ -354,11 +354,12 @@ namespace VoxelForgeCore
|
|||||||
const int32 RequestedStep=Key.Level==8
|
const int32 RequestedStep=Key.Level==8
|
||||||
?(InteriorStepOverride>0?InteriorStepOverride:MeshStepVoxels)
|
?(InteriorStepOverride>0?InteriorStepOverride:MeshStepVoxels)
|
||||||
:std::min(BoundaryStepVoxels,1<<Key.Level);
|
:std::min(BoundaryStepVoxels,1<<Key.Level);
|
||||||
|
const int32 ScaledRequestedStep=RequestedStep*std::max(1,RefinementGridScale);
|
||||||
const int32 GridStepVoxels=Key.Level==8
|
const int32 GridStepVoxels=Key.Level==8
|
||||||
?((RequestedStep<BoundaryStepVoxels||RequestedStep%BoundaryStepVoxels!=0
|
?((ScaledRequestedStep<BoundaryStepVoxels||ScaledRequestedStep%BoundaryStepVoxels!=0
|
||||||
||TileExtent<=0||TileExtent%RequestedStep!=0)
|
||TileExtent<=0||TileExtent%ScaledRequestedStep!=0)
|
||||||
?BoundaryStepVoxels:std::max(BoundaryStepVoxels,RequestedStep))
|
?BoundaryStepVoxels:std::max(BoundaryStepVoxels,ScaledRequestedStep))
|
||||||
:RequestedStep;
|
:ScaledRequestedStep;
|
||||||
if(TileExtent<=0||TileExtent%GridStepVoxels!=0)return Stats;
|
if(TileExtent<=0||TileExtent%GridStepVoxels!=0)return Stats;
|
||||||
const std::vector<int32> GridCoordinates=
|
const std::vector<int32> GridCoordinates=
|
||||||
BuildNativeHorizonGridCoordinates(TileExtent,GridStepVoxels);
|
BuildNativeHorizonGridCoordinates(TileExtent,GridStepVoxels);
|
||||||
@@ -644,7 +645,7 @@ namespace VoxelForgeCore
|
|||||||
|
|
||||||
FNativeCapRefinementStats RefineFarNativeCap(const FStandaloneOwnerWorld& Owner,
|
FNativeCapRefinementStats RefineFarNativeCap(const FStandaloneOwnerWorld& Owner,
|
||||||
const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,int32 MeshStepVoxels,
|
const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,int32 MeshStepVoxels,
|
||||||
int32 InteriorStepOverride,FVoxelMeshData& Mesh)
|
int32 InteriorStepOverride,int32 RefinementGridScale,FVoxelMeshData& Mesh)
|
||||||
{
|
{
|
||||||
FNativeCapRefinementStats Stats;
|
FNativeCapRefinementStats Stats;
|
||||||
if(Key.Level<4||!Owner.Generator)return Stats;
|
if(Key.Level<4||!Owner.Generator)return Stats;
|
||||||
@@ -654,11 +655,12 @@ namespace VoxelForgeCore
|
|||||||
:(Key.Level<5?std::min(BoundaryStepVoxels,1<<Key.Level):BoundaryStepVoxels);
|
:(Key.Level<5?std::min(BoundaryStepVoxels,1<<Key.Level):BoundaryStepVoxels);
|
||||||
const FIntVector TileOrigin=Key.OriginVoxels();
|
const FIntVector TileOrigin=Key.OriginVoxels();
|
||||||
const int32 TileExtent=Key.ExtentVoxels();
|
const int32 TileExtent=Key.ExtentVoxels();
|
||||||
|
const int32 ScaledRequestedStep=RequestedStep*std::max(1,RefinementGridScale);
|
||||||
const int32 EffectiveGridStep=Key.Level==8
|
const int32 EffectiveGridStep=Key.Level==8
|
||||||
?((RequestedStep<BoundaryStepVoxels||RequestedStep%BoundaryStepVoxels!=0
|
?((ScaledRequestedStep<BoundaryStepVoxels||ScaledRequestedStep%BoundaryStepVoxels!=0
|
||||||
||TileExtent<=0||TileExtent%RequestedStep!=0)
|
||TileExtent<=0||TileExtent%ScaledRequestedStep!=0)
|
||||||
?BoundaryStepVoxels:std::max(BoundaryStepVoxels,RequestedStep))
|
?BoundaryStepVoxels:std::max(BoundaryStepVoxels,ScaledRequestedStep))
|
||||||
:RequestedStep;
|
:ScaledRequestedStep;
|
||||||
constexpr int64 Scale=WorldAssemblySubvoxelsPerVoxel;
|
constexpr int64 Scale=WorldAssemblySubvoxelsPerVoxel;
|
||||||
if(TileExtent<=0||TileExtent%EffectiveGridStep!=0)return Stats;
|
if(TileExtent<=0||TileExtent%EffectiveGridStep!=0)return Stats;
|
||||||
const std::vector<int32> GridCoordinates=
|
const std::vector<int32> GridCoordinates=
|
||||||
@@ -1162,13 +1164,8 @@ namespace VoxelForgeCore
|
|||||||
|
|
||||||
const std::int64_t tileMinimum[3]={boundsMin.X,boundsMin.Y,boundsMin.Z};
|
const std::int64_t tileMinimum[3]={boundsMin.X,boundsMin.Y,boundsMin.Z};
|
||||||
const std::int64_t tileMaximum[3]={boundsMax.X,boundsMax.Y,boundsMax.Z};
|
const std::int64_t tileMaximum[3]={boundsMax.X,boundsMax.Y,boundsMax.Z};
|
||||||
const double coreVoxelsPerOwnerChunk=static_cast<double>(CHUNK_SIZE)
|
const int32 firstSurfaceChunk=FloorDiv(boundsMin.Z,CHUNK_SIZE);
|
||||||
*static_cast<double>(impl_->owner->OwnerVoxelSizeMicrons)
|
const int32 lastSurfaceChunk=FloorDiv(boundsMax.Z,CHUNK_SIZE);
|
||||||
/static_cast<double>(std::max(1u,impl_->owner->CoreVoxelSizeMicrons));
|
|
||||||
const int32 firstSurfaceChunk=FMath::FloorToInt(
|
|
||||||
static_cast<double>(boundsMin.Z)/coreVoxelsPerOwnerChunk);
|
|
||||||
const int32 lastSurfaceChunk=FMath::FloorToInt(
|
|
||||||
static_cast<double>(boundsMax.Z)/coreVoxelsPerOwnerChunk);
|
|
||||||
const WorldAssemblyStratumPlanV1* selectedPlan=nullptr;
|
const WorldAssemblyStratumPlanV1* selectedPlan=nullptr;
|
||||||
for(int32 chunkZ=firstSurfaceChunk;chunkZ<=lastSurfaceChunk;++chunkZ)
|
for(int32 chunkZ=firstSurfaceChunk;chunkZ<=lastSurfaceChunk;++chunkZ)
|
||||||
{
|
{
|
||||||
@@ -1188,6 +1185,9 @@ namespace VoxelForgeCore
|
|||||||
if(!selectedPlan||!Heightfield2D(impl_->worldAssembly->recipe,*selectedPlan).isValid())
|
if(!selectedPlan||!Heightfield2D(impl_->worldAssembly->recipe,*selectedPlan).isValid())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
const double coreVoxelsPerOwnerChunk=static_cast<double>(CHUNK_SIZE)
|
||||||
|
*static_cast<double>(impl_->owner->OwnerVoxelSizeMicrons)
|
||||||
|
/static_cast<double>(std::max(1u,impl_->owner->CoreVoxelSizeMicrons));
|
||||||
const int32 firstIslandChunk=FMath::FloorToInt(
|
const int32 firstIslandChunk=FMath::FloorToInt(
|
||||||
static_cast<double>(boundsMin.Z)/coreVoxelsPerOwnerChunk);
|
static_cast<double>(boundsMin.Z)/coreVoxelsPerOwnerChunk);
|
||||||
const int32 lastIslandChunk=FMath::FloorToInt(
|
const int32 lastIslandChunk=FMath::FloorToInt(
|
||||||
@@ -1608,6 +1608,37 @@ namespace VoxelForgeCore
|
|||||||
options.legacyDirectMappedColumnCache||key.lod<=2);
|
options.legacyDirectMappedColumnCache||key.lod<=2);
|
||||||
if(!impl_||!impl_->owner)
|
if(!impl_||!impl_->owner)
|
||||||
{ error="resolved world snapshot is empty";return std::nullopt; }
|
{ error="resolved world snapshot is empty";return std::nullopt; }
|
||||||
|
const WorldAssemblyRecipeV1* FarRecipe=impl_->worldAssembly?&impl_->worldAssembly->recipe:nullptr;
|
||||||
|
std::uint32_t FarDisplayBudget=FarRecipe?FarRecipe->farDisplayGeometryBudgetMillipixels:0;
|
||||||
|
std::uint32_t FarRefinementScale=FarRecipe?FarRecipe->farRefinementGridScale:0;
|
||||||
|
std::uint32_t FarRefinementLodMask=FarRecipe?FarRecipe->farRefinementLodMask:0;
|
||||||
|
std::uint32_t FarConnectorStationScale=FarRecipe?FarRecipe->farConnectorStationScale:0;
|
||||||
|
std::uint32_t FarConnectorRayMask=FarRecipe?FarRecipe->farConnectorRayMask:0;
|
||||||
|
bool bFarConnectorReuseStationRadii=FarRecipe&&FarRecipe->farConnectorReuseStationRadii;
|
||||||
|
if(options.overrideFarDisplayMesh)
|
||||||
|
{
|
||||||
|
FarDisplayBudget=options.farDisplayGeometryBudgetMillipixels;
|
||||||
|
FarRefinementScale=options.farRefinementGridScale;
|
||||||
|
FarRefinementLodMask=options.farRefinementLodMask;
|
||||||
|
FarConnectorStationScale=options.farConnectorStationScale;
|
||||||
|
FarConnectorRayMask=options.farConnectorRayMask;
|
||||||
|
bFarConnectorReuseStationRadii=options.farConnectorReuseStationRadii;
|
||||||
|
}
|
||||||
|
const auto ValidFarScale=[](std::uint32_t Value)
|
||||||
|
{return Value==0||Value==2||Value==4||Value==8;};
|
||||||
|
if((FarDisplayBudget!=0&&FarDisplayBudget!=1000)
|
||||||
|
||!ValidFarScale(FarRefinementScale)
|
||||||
|
||!ValidFarScale(FarConnectorStationScale)
|
||||||
|
||(FarRefinementLodMask&~0x1f0u)!=0||FarConnectorRayMask>0x0fu
|
||||||
|
||(FarRefinementScale!=0&&FarRefinementLodMask==0))
|
||||||
|
{error="far display mesh override is outside the recipe's supported 1 px settings";return std::nullopt;}
|
||||||
|
const std::uint32_t ActiveFarRefinementScale=FarDisplayBudget==1000
|
||||||
|
&&(FarRefinementLodMask&(1u<<std::clamp(key.lod,0,31)))!=0
|
||||||
|
?FarRefinementScale:0;
|
||||||
|
const int32 ActiveConnectorStationScale=FarDisplayBudget==1000
|
||||||
|
&&FarConnectorStationScale!=0?static_cast<int32>(FarConnectorStationScale):1;
|
||||||
|
const uint32 ActiveConnectorRayMask=FarDisplayBudget==1000
|
||||||
|
&&FarConnectorRayMask!=0?FarConnectorRayMask:0x0fu;
|
||||||
const bool bShapeReady=impl_->shapeReady.load(std::memory_order_acquire);
|
const bool bShapeReady=impl_->shapeReady.load(std::memory_order_acquire);
|
||||||
const bool bNativeOnly=!bShapeReady||options.diagnosticForceNativeOnly;
|
const bool bNativeOnly=!bShapeReady||options.diagnosticForceNativeOnly;
|
||||||
if(!bShapeReady&&!options.diagnosticForceNativeOnly&&!isTileShapeFree(key))
|
if(!bShapeReady&&!options.diagnosticForceNativeOnly&&!isTileShapeFree(key))
|
||||||
@@ -2137,7 +2168,10 @@ namespace VoxelForgeCore
|
|||||||
BeforeConnectorColumnComputeMilliseconds=Columns.columnComputeMilliseconds;
|
BeforeConnectorColumnComputeMilliseconds=Columns.columnComputeMilliseconds;
|
||||||
}
|
}
|
||||||
const auto ConnectorStart=std::chrono::steady_clock::now();
|
const auto ConnectorStart=std::chrono::steady_clock::now();
|
||||||
AppendFarConnectorShells(farShape,nativeKey,step,world.CoarseTileCells,mesh);
|
if(!options.diagnosticRemoveFarConnectorPanels)
|
||||||
|
AppendFarConnectorShells(farShape,nativeKey,step,world.CoarseTileCells,mesh,
|
||||||
|
ActiveConnectorStationScale,ActiveConnectorRayMask,
|
||||||
|
FarDisplayBudget==1000&&bFarConnectorReuseStationRadii);
|
||||||
const auto AfterConnector=FStandaloneShapeWorld::GetThreadClassificationStats();
|
const auto AfterConnector=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||||
out.diagnostics.farConnectorShellMilliseconds=std::chrono::duration<double,std::milli>(
|
out.diagnostics.farConnectorShellMilliseconds=std::chrono::duration<double,std::milli>(
|
||||||
std::chrono::steady_clock::now()-ConnectorStart).count();
|
std::chrono::steady_clock::now()-ConnectorStart).count();
|
||||||
@@ -2163,6 +2197,16 @@ namespace VoxelForgeCore
|
|||||||
}
|
}
|
||||||
out.diagnostics.farShellTrianglesAdded+=static_cast<std::uint64_t>(
|
out.diagnostics.farShellTrianglesAdded+=static_cast<std::uint64_t>(
|
||||||
std::max(0,mesh.Triangles.Num()/3-BeforeConnectorTriangles));
|
std::max(0,mesh.Triangles.Num()/3-BeforeConnectorTriangles));
|
||||||
|
if(options.diagnosticAuditFarConnectorCoverage)
|
||||||
|
{
|
||||||
|
const auto Audit=AuditFarConnectorCoverage(farShape,nativeKey,step,
|
||||||
|
world.CoarseTileCells,mesh);
|
||||||
|
out.diagnostics.farConnectorAuditRays=Audit.Rays;
|
||||||
|
out.diagnostics.farConnectorAuditHoles=Audit.Holes;
|
||||||
|
out.diagnostics.farConnectorAuditFloating=Audit.Floating;
|
||||||
|
out.diagnostics.farConnectorAuditHoleIds=Audit.HoleIds;
|
||||||
|
out.diagnostics.farConnectorAuditFloatingIds=Audit.FloatingIds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out.diagnostics.farShellMilliseconds=std::chrono::duration<double,std::milli>(
|
out.diagnostics.farShellMilliseconds=std::chrono::duration<double,std::milli>(
|
||||||
std::chrono::steady_clock::now()-FarShellStart).count();
|
std::chrono::steady_clock::now()-FarShellStart).count();
|
||||||
@@ -2170,12 +2214,14 @@ namespace VoxelForgeCore
|
|||||||
{
|
{
|
||||||
const auto HorizonStart=std::chrono::steady_clock::now();
|
const auto HorizonStart=std::chrono::steady_clock::now();
|
||||||
FNativeCapRefinementStats GroundStats;
|
FNativeCapRefinementStats GroundStats;
|
||||||
|
const int32 ActiveRefinementScale=ActiveFarRefinementScale>1
|
||||||
|
?static_cast<int32>(ActiveFarRefinementScale):1;
|
||||||
const double ColumnMillisecondsBeforeGround=
|
const double ColumnMillisecondsBeforeGround=
|
||||||
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
||||||
const auto GroundStart=std::chrono::steady_clock::now();
|
const auto GroundStart=std::chrono::steady_clock::now();
|
||||||
if(key.lod>=4)
|
if(key.lod>=4)
|
||||||
GroundStats=RefineFarNativeGround(world,*impl_->worldAssembly,nativeKey,step,
|
GroundStats=RefineFarNativeGround(world,*impl_->worldAssembly,nativeKey,step,
|
||||||
options.lod8HorizonInteriorStepVoxels,mesh);
|
options.lod8HorizonInteriorStepVoxels,ActiveRefinementScale,mesh);
|
||||||
const double GroundWallMilliseconds=std::chrono::duration<double,std::milli>(
|
const double GroundWallMilliseconds=std::chrono::duration<double,std::milli>(
|
||||||
std::chrono::steady_clock::now()-GroundStart).count();
|
std::chrono::steady_clock::now()-GroundStart).count();
|
||||||
const double ColumnMillisecondsAfterGround=
|
const double ColumnMillisecondsAfterGround=
|
||||||
@@ -2188,7 +2234,7 @@ namespace VoxelForgeCore
|
|||||||
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
||||||
const auto CapStart=std::chrono::steady_clock::now();
|
const auto CapStart=std::chrono::steady_clock::now();
|
||||||
const auto CapStats=RefineFarNativeCap(world,*impl_->worldAssembly,nativeKey,step,
|
const auto CapStats=RefineFarNativeCap(world,*impl_->worldAssembly,nativeKey,step,
|
||||||
options.lod8HorizonInteriorStepVoxels,mesh);
|
options.lod8HorizonInteriorStepVoxels,ActiveRefinementScale,mesh);
|
||||||
const double CapWallMilliseconds=std::chrono::duration<double,std::milli>(
|
const double CapWallMilliseconds=std::chrono::duration<double,std::milli>(
|
||||||
std::chrono::steady_clock::now()-CapStart).count();
|
std::chrono::steady_clock::now()-CapStart).count();
|
||||||
const double ColumnMillisecondsAfterCap=
|
const double ColumnMillisecondsAfterCap=
|
||||||
|
|||||||
@@ -244,6 +244,24 @@ namespace world_assembly_detail
|
|||||||
|| recipe.biomeBorderBlendSubvoxels < 0
|
|| recipe.biomeBorderBlendSubvoxels < 0
|
||||||
|| recipe.biomeBorderBlendSubvoxels > recipe.biomeCellSizeSubvoxels)
|
|| recipe.biomeBorderBlendSubvoxels > recipe.biomeCellSizeSubvoxels)
|
||||||
{ error = "world assembly biome-map parameters are outside the supported fixed-scale range"; return false; }
|
{ error = "world assembly biome-map parameters are outside the supported fixed-scale range"; return false; }
|
||||||
|
if (recipe.evaluatorVersion >= 7)
|
||||||
|
{
|
||||||
|
constexpr std::uint32_t FarLodMask = 0x1f0u; // LOD4..8.
|
||||||
|
const bool anyFarSetting = recipe.farDisplayGeometryBudgetMillipixels != 0
|
||||||
|
|| recipe.farRefinementGridScale != 0 || recipe.farRefinementLodMask != 0
|
||||||
|
|| recipe.farConnectorStationScale != 0 || recipe.farConnectorRayMask != 0
|
||||||
|
|| recipe.farConnectorReuseStationRadii;
|
||||||
|
const auto validScale = [](std::uint32_t value)
|
||||||
|
{ return value == 0 || value == 2 || value == 4 || value == 8; };
|
||||||
|
if ((anyFarSetting && recipe.farDisplayGeometryBudgetMillipixels != 1000)
|
||||||
|
|| recipe.farDisplayGeometryBudgetMillipixels > 1000
|
||||||
|
|| !validScale(recipe.farRefinementGridScale)
|
||||||
|
|| (recipe.farRefinementLodMask & ~FarLodMask) != 0
|
||||||
|
|| !validScale(recipe.farConnectorStationScale)
|
||||||
|
|| recipe.farConnectorRayMask > 0x0fu
|
||||||
|
|| (recipe.farRefinementGridScale != 0 && recipe.farRefinementLodMask == 0))
|
||||||
|
{ error = "far display mesh declaration must use the 1 px budget and supported LOD, scale, and ray values"; return false; }
|
||||||
|
}
|
||||||
std::vector<std::uint32_t> ids;
|
std::vector<std::uint32_t> ids;
|
||||||
ids.reserve(recipe.biomes.size());
|
ids.reserve(recipe.biomes.size());
|
||||||
for (const auto& biome : recipe.biomes)
|
for (const auto& biome : recipe.biomes)
|
||||||
@@ -431,6 +449,16 @@ namespace world_assembly_detail
|
|||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeClimateMoistureWavelengthSubvoxels), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeClimateMoistureWavelengthSubvoxels), 8);
|
||||||
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeClimateReliefContrastQ16), 8);
|
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeClimateReliefContrastQ16), 8);
|
||||||
}
|
}
|
||||||
|
if (recipe.evaluatorVersion >= 7)
|
||||||
|
{
|
||||||
|
HashWord(hash, 0x4641524449535037ull, 8);
|
||||||
|
HashWord(hash, recipe.farDisplayGeometryBudgetMillipixels, 4);
|
||||||
|
HashWord(hash, recipe.farRefinementGridScale, 4);
|
||||||
|
HashWord(hash, recipe.farRefinementLodMask, 4);
|
||||||
|
HashWord(hash, recipe.farConnectorStationScale, 4);
|
||||||
|
HashWord(hash, recipe.farConnectorRayMask, 4);
|
||||||
|
HashWord(hash, recipe.farConnectorReuseStationRadii ? 1u : 0u, 1);
|
||||||
|
}
|
||||||
HashWord(hash, recipe.biomes.size(), 4);
|
HashWord(hash, recipe.biomes.size(), 4);
|
||||||
for (const auto& biome : recipe.biomes)
|
for (const auto& biome : recipe.biomes)
|
||||||
{
|
{
|
||||||
@@ -641,6 +669,16 @@ namespace world_assembly_detail
|
|||||||
|| !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels)
|
|| !ReadInteger(value.Get("climate_moisture_wavelength_q"), recipe.biomeClimateMoistureWavelengthSubvoxels)
|
||||||
|| !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16)))
|
|| !ReadInteger(value.Get("climate_relief_contrast_q16"), recipe.biomeClimateReliefContrastQ16)))
|
||||||
{ error = "world assembly recipe is missing evaluator-2 climate parameters"; return false; }
|
{ error = "world assembly recipe is missing evaluator-2 climate parameters"; return false; }
|
||||||
|
if (recipe.evaluatorVersion >= 7
|
||||||
|
&& (!ReadU32(value.Get("far_display_budget_millipixels"), recipe.farDisplayGeometryBudgetMillipixels)
|
||||||
|
|| !ReadU32(value.Get("far_refinement_grid_scale"), recipe.farRefinementGridScale)
|
||||||
|
|| !ReadU32(value.Get("far_refinement_lod_mask"), recipe.farRefinementLodMask)
|
||||||
|
|| !ReadU32(value.Get("far_connector_station_scale"), recipe.farConnectorStationScale)
|
||||||
|
|| !ReadU32(value.Get("far_connector_ray_mask"), recipe.farConnectorRayMask)
|
||||||
|
|| !std::holds_alternative<bool>(value.Get("far_connector_reuse_station_radii").Data)))
|
||||||
|
{ error = "world assembly recipe is missing evaluator-7 far display mesh parameters"; return false; }
|
||||||
|
if (recipe.evaluatorVersion >= 7)
|
||||||
|
recipe.farConnectorReuseStationRadii = std::get<bool>(value.Get("far_connector_reuse_station_radii").Data);
|
||||||
for (const auto& item : value.Get("biomes").AsArray())
|
for (const auto& item : value.Get("biomes").AsArray())
|
||||||
{
|
{
|
||||||
WorldAssemblyBiomeProfileV1 biome;
|
WorldAssemblyBiomeProfileV1 biome;
|
||||||
@@ -702,6 +740,14 @@ namespace world_assembly_detail
|
|||||||
out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels
|
out << ",\"climate_relief_wavelength_q\":" << recipe.biomeClimateReliefWavelengthSubvoxels
|
||||||
<< ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels
|
<< ",\"climate_moisture_wavelength_q\":" << recipe.biomeClimateMoistureWavelengthSubvoxels
|
||||||
<< ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16;
|
<< ",\"climate_relief_contrast_q16\":" << recipe.biomeClimateReliefContrastQ16;
|
||||||
|
if (recipe.evaluatorVersion >= 7)
|
||||||
|
out << ",\"far_display_budget_millipixels\":" << recipe.farDisplayGeometryBudgetMillipixels
|
||||||
|
<< ",\"far_refinement_grid_scale\":" << recipe.farRefinementGridScale
|
||||||
|
<< ",\"far_refinement_lod_mask\":" << recipe.farRefinementLodMask
|
||||||
|
<< ",\"far_connector_station_scale\":" << recipe.farConnectorStationScale
|
||||||
|
<< ",\"far_connector_ray_mask\":" << recipe.farConnectorRayMask
|
||||||
|
<< ",\"far_connector_reuse_station_radii\":"
|
||||||
|
<< (recipe.farConnectorReuseStationRadii ? "true" : "false");
|
||||||
out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":[";
|
out << ",\"content_hash\":\"" << HashText(recipe.contentHash) << "\",\"biomes\":[";
|
||||||
for (std::size_t i = 0; i < recipe.biomes.size(); ++i)
|
for (std::size_t i = 0; i < recipe.biomes.size(); ++i)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
# Linux16 — terrain cache design and Core prototype
|
|
||||||
|
|
||||||
This is a persistence feature. The measurements judge exactness, storage, and worker responsiveness;
|
|
||||||
first-time terrain generation remains a separate cost. The prototype is headless and opt-in. Unreal
|
|
||||||
integration remains for the owner's PC.
|
|
||||||
|
|
||||||
## Design
|
|
||||||
|
|
||||||
- **Cache identity.** Production keys should include a stable world ID; season ID and canonical
|
|
||||||
season-content hash; seed and RNG contract; recipe, plan, biome/manifest and owner-content hashes;
|
|
||||||
schema and `WorldAssemblyEvaluatorVersion` (6 here); tile coordinate and LOD; voxel size and tile
|
|
||||||
dimensions; generation options; material/payload-section schema; mesher contract; and a generator
|
|
||||||
build/FP-contract identity. Include a platform or compiler identity unless cross-build output has
|
|
||||||
been proven equivalent. Clang and GCC matched this round's full hash matrix, but that alone does
|
|
||||||
not make every future build interchangeable. Use canonical content hashes so whitespace or JSON
|
|
||||||
key order does not change the world identity.
|
|
||||||
- **Prototype identity.** The header and file name include the raw owner-snapshot hash, Core
|
|
||||||
configuration, recipe/plan/manifest hashes, seed, schema, evaluator, voxel size, tile size,
|
|
||||||
mesher/FP contract, generation-options hash, tile key and LOD. The edited-tile probe also includes
|
|
||||||
edit sequence and tile edit digest. The prototype uses 64-bit FNV for identity inputs and CRC32
|
|
||||||
for accidental file corruption. Production should use a 256-bit content hash and a stable world
|
|
||||||
ID; the prototype hashes are evidence machinery, not a security boundary.
|
|
||||||
- **Edits.** Keep the reusable cache pristine. On load, apply the authoritative versioned diff layer
|
|
||||||
in sequence, then build the edited render/collision result. A derived edited-tile cache, if useful,
|
|
||||||
must key on the last applied sequence and a digest of edits intersecting that tile. A newer edit
|
|
||||||
invalidates only derived entries for touched tiles; it leaves pristine entries valid. The edit
|
|
||||||
probe wrote and reread sequence 1 with digest `3cd5603ebf707d0c` and matched every array and hash.
|
|
||||||
- **Server.** Never accept a client's cached mesh or collision as authoritative. The server may use
|
|
||||||
its own locally generated cache for pristine meshes, then apply its ordered edit layer and build
|
|
||||||
collision from that result. Client cache contents never cross the network; only validated edits
|
|
||||||
do. A server restart can reuse its own cache after identity and payload validation.
|
|
||||||
- **Invalidation and verification.** Season/content, seed/RNG, recipe, plan, evaluator, generator,
|
|
||||||
mesher, FP, voxel-grid, LOD, options, or payload-schema changes select a different key. Evaluator
|
|
||||||
bump and edit-sequence bump were rejected by the prototype. A canonical uncompressed-payload hash
|
|
||||||
should be stored and verified at load; corruption or a mismatch is a cache miss, followed by
|
|
||||||
regeneration on a worker. The prototype verifies CRCs and structure on every load, compares the
|
|
||||||
complete mesh arrays and Core D/M/P/T hashes after every generated write/read, and regenerates a
|
|
||||||
deliberately corrupted cached tile. This proves the sampled write/read route; a file's own hash
|
|
||||||
cannot independently prove generator semantics on every later hit, so complete identity and
|
|
||||||
version discipline remain essential.
|
|
||||||
- **Stored data.** The prototype stores the final Core mesh: exact double positions/normals, float
|
|
||||||
UVs, RGBA, section/group counts, indices, skirt masks, density sample count, and D/M/P/T hashes.
|
|
||||||
This avoids remeshing on a hit and maps directly to future RMC sections. The indices and section
|
|
||||||
ranges can feed server-side collision construction after edits. Density/sign tiles offer more
|
|
||||||
freedom for future meshers but require meshing, materials, and collision work on load; density
|
|
||||||
sizes were not benchmarked here. Keep collision ownership server-side in either design.
|
|
||||||
- **Worker/smoothness path.** Cache reads, zlib decode, integrity checks, serialization, and writes
|
|
||||||
run in the existing headless Core tile worker/result route. The Unreal game thread was not
|
|
||||||
present on this machine; its later integration should submit cache work through the same worker
|
|
||||||
queue and return the ordinary tile result. The prototype uses that route for all recipes and
|
|
||||||
changes no recipe implementation. It exercises SurfaceWorld for cache round trips; the cache-off
|
|
||||||
matrix and route audits cover SurfaceWorld and FloatingIslands.
|
|
||||||
- **Read versus regenerate.** Track per-LOD generation and load/decode EWMA. If a demanded tile's
|
|
||||||
estimated read/decode time exceeds generation time, schedule generation directly; if an attempted
|
|
||||||
load fails, discard it and regenerate without blocking the result queue. Keep the same tile
|
|
||||||
priority and permit the previous coarser LOD to remain visible while work finishes. On this L1
|
|
||||||
set, warm read/decode averaged about 0.11 ms per LOD0 tile and 0.91–1.61 ms per LOD8/7 tile,
|
|
||||||
compared with 3.97 ms LOD0 and 12.89–24.24 ms LOD8/6 mesh generation in the instrumented fill.
|
|
||||||
- **Layout, crash safety, and eviction.** One file per tile under a world-content directory keeps
|
|
||||||
reads and eviction independent; the prototype has 2,553 L1 records and 2,986 touched records after
|
|
||||||
the 24 m crossing. It writes a same-directory `.tmp`, flushes and `fsync`s it, atomically renames
|
|
||||||
to `.vftile`, then syncs the directory. Readers only open final `.vftile` names and reject bad
|
|
||||||
lengths, versions, identities, CRCs, counts, indices, and padding. A crash before rename leaves a
|
|
||||||
non-loadable temp file. Production startup should remove old temp files. Use a global 2 GB cap
|
|
||||||
with an LRU index by last use, protect entries currently in use, reserve space under one lock,
|
|
||||||
evict oldest unpinned records before writes, and skip the write if space still cannot be made.
|
|
||||||
The prototype enforces the cap and skips writes when full; it does not implement LRU eviction.
|
|
||||||
Its 4 KiB padding enabled `O_DIRECT` measurements and can be removed in production buffered I/O.
|
|
||||||
|
|
||||||
## Compression measurements
|
|
||||||
|
|
||||||
Available system zlib headers/library (zlib 1.3.1) were linked with `-lz`. The existing zstd 1.5.7
|
|
||||||
CLI had no development headers, so it was tested as a per-tile CLI codec in LOD batches; its times
|
|
||||||
include one process start per LOD. No installs or system changes were made. All totals below cover
|
|
||||||
all 2,553 L1 tiles except Q16, whose first run had five route-audit tiles prepopulated before codec
|
|
||||||
sampling. Sizes are payload bytes; the lossless cache also has headers and per-file 4 KiB padding.
|
|
||||||
|
|
||||||
| Encoding | Total bytes | Relative to raw | Exact Core arrays? | L1 gate |
|
|
||||||
|---|---:|---:|---|---|
|
|
||||||
| Raw canonical mesh | 107,342,349 | 100% | Yes | Pass |
|
|
||||||
| zlib level 6 | 47,388,401 | 44.15% | Yes | Pass |
|
|
||||||
| zstd level 1 (CLI, per tile) | 46,513,479 | 43.33% | Yes | Pass; 1.85% smaller than zlib payload |
|
|
||||||
| Q16 positions + oct16 normals + delta-varint indices | 38,672,768 (2,548 tiles sampled) | 36.17% | No | Fail: 11.4482 px at LOD0; 6 degeneracy changes (2 at LOD4, 4 at LOD6) |
|
|
||||||
| Q24 positions + oct16 normals + delta-varint indices | 42,862,996 | 39.94% | No | Pass: 0.0458 px max, 0.000060 Lambert delta max, 0.00361° normal error max, no winding/degeneracy changes |
|
|
||||||
|
|
||||||
Q24 is about 9.5% smaller than zlib's payload, but it changes floating-point positions/normals and
|
|
||||||
cannot satisfy exact loaded-versus-regenerated hashes. The selected cache path is lossless zlib.
|
|
||||||
The zstd CLI decoded every tile byte-for-byte, and was modestly smaller/faster in this test. There
|
|
||||||
are no zstd development headers here, so it was not linked into the worker prototype. The L1 zlib
|
|
||||||
records occupied 55,685,120 bytes on disk, including record framing and page padding.
|
|
||||||
|
|
||||||
Per-tile size averages by LOD:
|
|
||||||
|
|
||||||
| LOD | Tiles | Raw B | zlib B | zstd B | Q16 B* | Q24 B |
|
|
||||||
|---:|---:|---:|---:|---:|---:|---:|
|
|
||||||
| 0 | 512 | 19,232.6 | 7,901.7 | 8,009.4 | 7,115.2 | 7,797.3 |
|
|
||||||
| 1 | 448 | 15,437.8 | 6,715.2 | 6,715.6 | 5,394.2 | 6,244.9 |
|
|
||||||
| 2 | 448 | 11,152.2 | 4,452.4 | 4,516.8 | 4,129.9 | 4,548.7 |
|
|
||||||
| 3 | 384 | 11,750.3 | 4,813.1 | 4,885.5 | 4,361.7 | 4,801.7 |
|
|
||||||
| 4 | 320 | 49,186.9 | 18,865.3 | 18,352.3 | 17,858.1 | 19,738.2 |
|
|
||||||
| 5 | 144 | 83,106.3 | 37,693.0 | 37,004.3 | 30,247.6 | 33,494.7 |
|
|
||||||
| 6 | 96 | 128,102.3 | 59,624.7 | 56,521.2 | 46,390.6 | 51,437.7 |
|
|
||||||
| 7 | 48 | 308,206.8 | 141,387.7 | 139,348.3 | 110,361.7 | 121,997.0 |
|
|
||||||
| 8 | 153 | 171,712.3 | 81,805.0 | 79,662.9 | 60,755.8 | 67,046.4 |
|
|
||||||
|
|
||||||
*Q16 averages are over the 2,548 newly generated tiles; five prepopulated tiles did not enter its
|
|
||||||
codec sample. Raw, zlib, and Q24 averages are from the clean 2,553-tile fill.
|
|
||||||
|
|
||||||
Average codec encode/decode time per tile, microseconds:
|
|
||||||
|
|
||||||
| LOD | Raw E/D | zlib E/D | zstd CLI E/D | Q24 mesh E/D |
|
|
||||||
|---:|---:|---:|---:|---:|
|
|
||||||
| 0 | 38.60 / 12.31 | 736.63 / 79.49 | 72.14 / 47.97 | 16.28 / 18.78 |
|
|
||||||
| 1 | 30.13 / 9.47 | 608.30 / 68.07 | 62.71 / 40.53 | 14.26 / 16.04 |
|
|
||||||
| 2 | 18.94 / 4.64 | 286.44 / 37.86 | 48.50 / 34.65 | 8.20 / 7.55 |
|
|
||||||
| 3 | 20.21 / 5.18 | 313.54 / 41.77 | 50.73 / 35.90 | 8.81 / 8.07 |
|
|
||||||
| 4 | 90.46 / 27.53 | 1,508.23 / 189.40 | 134.50 / 78.66 | 41.58 / 41.93 |
|
|
||||||
| 5 | 163.48 / 55.53 | 2,935.24 / 370.92 | 220.23 / 140.52 | 76.90 / 79.86 |
|
|
||||||
| 6 | 254.98 / 96.01 | 4,900.50 / 611.07 | 361.50 / 184.35 | 127.39 / 136.19 |
|
|
||||||
| 7 | 621.57 / 211.84 | 11,944.70 / 1,475.82 | 861.84 / 442.64 | 327.72 / 391.27 |
|
|
||||||
| 8 | 325.34 / 109.80 | 6,478.56 / 784.32 | 486.54 / 289.96 | 167.25 / 186.01 |
|
|
||||||
|
|
||||||
## Smoothness, prototype, and budgets
|
|
||||||
|
|
||||||
- A clean write generated, losslessly stored, reloaded, and exactly compared all 2,553 L1 tiles.
|
|
||||||
The whole-view D/M/P/T hash matched L14. The warm run hit 2,553/2,553 tiles, returned the same
|
|
||||||
hash, and reached whole-view-ready in 0.450 s including 0.193 s of shape setup; tile streaming
|
|
||||||
took 0.243 s.
|
|
||||||
- The 24 m crossing covered 432 entering tiles. Its D/M/P/T hash was
|
|
||||||
`337ca62131576728 / 7612f48e2ddbbaab / 69a67ed4add83b87 / f11ac1ebd0f7ddb8`, equal to L14.
|
|
||||||
The cache route audit passed 9 pristine pairs, one edited pair, and one movement-history pair,
|
|
||||||
all with exact arrays and zero D/M/P/T mismatches. The crossing fill generated and round-tripped
|
|
||||||
428 tiles; four had already been created by the route audit.
|
|
||||||
- Safety controls passed: evaluator bump rejected, edit-sequence bump rejected, a flipped compressed
|
|
||||||
payload byte rejected by record CRC, and a truncated file rejected by size validation. A separate
|
|
||||||
L1 run with one real tile corrupted regenerated that tile, hit the other 2,552, and restored the
|
|
||||||
exact L14 whole-view hash.
|
|
||||||
- `O_DIRECT` bypassed the kernel page cache for the 55,685,120-byte L1 record set: 0.269 s
|
|
||||||
(197.6 MiB/s). Buffered first touch was 0.0191 s and the repeat was 0.0126 s, but those passes
|
|
||||||
followed file writes/verification and were page-cache warm. No page cache was dropped; device
|
|
||||||
firmware may also cache blocks, so `O_DIRECT` is page-cache-cold evidence rather than proof of a
|
|
||||||
physical cold-device read. The ordinary worker hit path includes buffered read, zlib decode,
|
|
||||||
validation, and tile construction.
|
|
||||||
- The instrumentation measures raw, zlib, and mesh candidates on a miss, so its clean-fill
|
|
||||||
whole-view-ready time was 2.674 s versus 1.718 s for the hash-on no-cache L1 run. This includes
|
|
||||||
benchmark-only extra codec work. Production should encode only its selected lossless format on a
|
|
||||||
miss. Warm disk work and decode stay on workers; the first tile was ready in 0.226 s on fill and
|
|
||||||
0.208 s warm. These headless timings show worker completion; an Unreal frame-time check is still
|
|
||||||
for the integration round.
|
|
||||||
- The cache root peaked at 60,835,840 bytes after L1, the 24 m crossing, edit probe, and corruption
|
|
||||||
controls; the enforced limit is 2,000,000,000 bytes. No eviction was needed. The actual cache
|
|
||||||
implementation is a headless Core-worker prototype, not the Unreal plugin integration.
|
|
||||||
|
|
||||||
## Default-path cost and compatibility
|
|
||||||
|
|
||||||
Five interleaved W12 L1 pairs, hash off, 2,553 tiles each:
|
|
||||||
|
|
||||||
| Pair | L14 seconds | L16 cache-disabled seconds |
|
|
||||||
|---:|---:|---:|
|
|
||||||
| 1 | 1.450 | 1.446 |
|
|
||||||
| 2 | 1.464 | 1.457 |
|
|
||||||
| 3 | 1.458 | 1.453 |
|
|
||||||
| 4 | 1.525 | 1.472 |
|
|
||||||
| 5 | 1.459 | 1.483 |
|
|
||||||
|
|
||||||
Median: 1.459 s L14, 1.457 s L16, −0.14%; no measurable cache-off cost in this run.
|
|
||||||
|
|
||||||
All 28 L14 hash configurations passed (SurfaceWorld and FloatingIslands; Clang 19 FP off and GCC
|
|
||||||
14 default FP; W1/W8/W12 with five W12 repeats). Every one matched all 12 full-detail, whole-view,
|
|
||||||
nine LOD, and exact-query rows. The four default 24 m crossing checks (W1/W12 on both compilers)
|
|
||||||
matched all 13 rows, including the 432-tile crossing hash. Default route parity passed for both
|
|
||||||
compilers across SurfaceWorld seeds 0, 1, and 17 and FloatingIslands seed 0, using the existing
|
|
||||||
96 m history probe. The cache route passed its separate L1 and 24 m crossing audits.
|
|
||||||
|
|
||||||
Representative SurfaceWorld seed-0 hashes (D/M/P/T):
|
|
||||||
|
|
||||||
| View | Density / mesh / payload / topology |
|
|
||||||
|---|---|
|
|
||||||
| Full detail, 960 tiles | `0ee0579bd15cbbe4 / 32e5d4964d610fee / 1932b96ee4b1d910 / 69743c6bad3447dc` |
|
|
||||||
| Whole L1, 2,553 tiles | `c10697a8f48380c7 / e75c9119a1498cd5 / 49a9a8d1835a6c8c / edcb23288b5e8724` |
|
|
||||||
| LOD0, 512 tiles | `769f2a706d585a72 / 33eeeb39fa443816 / 645621a1d968983f / c2457ed905036c9d` |
|
|
||||||
| LOD1, 448 tiles | `69cc5a5c0a2c60af / b48335f5410c57d5 / d15c5480162ab8ba / feca1bd025823604` |
|
|
||||||
| LOD2, 448 tiles | `85deed5683a77202 / 53469895987aeea2 / e55f8ae7e1532033 / d5feb12a0e85c09b` |
|
|
||||||
| LOD3, 384 tiles | `6518caefb3be7e7f / ec513c90b853a614 / f62fa1137ff17f82 / ccf1dcf681965816` |
|
|
||||||
| LOD4, 320 tiles | `62d4be96021992f4 / f1e4c6efcc2615a9 / 9dc5f9c88be02f33 / fe6f55125a07c3bf` |
|
|
||||||
| LOD5, 144 tiles | `5e4a8c38b8cea6fc / 08ee56998aaf21e5 / 541eea8558f26cc8 / 6539374a8b3336d8` |
|
|
||||||
| LOD6, 96 tiles | `e036e95b9006945b / 624a33f941a99b02 / 81b73e14f68bdf86 / 4de4c63ce1f80a56` |
|
|
||||||
| LOD7, 48 tiles | `8ee358af56a30d8d / 46ab3c6184048b7c / 0a6164b7e68e80c5 / 0c26b85bb03bc831` |
|
|
||||||
| LOD8, 153 tiles | `c3458e1cbdb9c471 / 46c03d0994d75192 / 8f89fc5bde12d230 / d16dc0cf62031898` |
|
|
||||||
| Exact queries: density / unclamped / detail / collision | `e4d4a8eb95b35933 / e4d4a8eb95b35933 / efcba20c0c70c415 / e604823a249029bf` |
|
|
||||||
|
|
||||||
FloatingIslands full-detail was `6d842d2878b08ef6 / 4b868a3184f08035 / 2cedf2236788752d / eac17d5e0cb5044a`; whole-view was `5be4d8cde79b2a76 / 96f75041f6861b3a / 3700a516cc89d50c / 6b99948d6eab4f21`. Its exact-query hashes also matched L14: `61daded9c303becb / 61daded9c303becb / efcba20c0c70c415 / 84d37463be9b0573`.
|
|
||||||
|
|
||||||
## Runs and limits
|
|
||||||
|
|
||||||
- First Clang build was interrupted during a silent compile phase. The next build exposed a const
|
|
||||||
error in the prototype file index; making that index mutable fixed it. Final Clang and GCC builds
|
|
||||||
passed.
|
|
||||||
- The first L1 command used the binary's incorrect default data path (`Standalone/bin/data`) and
|
|
||||||
exited before writing. Supplying `Standalone/data/owner_world.json` passed.
|
|
||||||
- The first codec run sampled 2,548 tiles because route parity had prepopulated five; it was kept
|
|
||||||
for Q16 measurements. A clean fill then sampled all 2,553 Q24 tiles.
|
|
||||||
- A route wrapper first required a moving hash when hashing was disabled; the route audit itself
|
|
||||||
passed. Seed 17's 24 m history probe had no departed-tile candidate and failed on both L16 and
|
|
||||||
the L14 control. The same L14 96 m history setup passed on both L16 compilers. The required 24 m
|
|
||||||
crossing was separately checked and passed.
|
|
||||||
- The first crossing-hash comparator used an incorrect old-log filename; correcting the filename
|
|
||||||
let all four comparisons pass. No run exceeded 30 minutes or stalled for 15 minutes.
|
|
||||||
- Final review found that the cache-hit route audit's `exact_arrays` field initially reported hash
|
|
||||||
agreement without comparing the arrays themselves. The audit was changed to compare complete
|
|
||||||
arrays; after rebuilding, both L1 and 24 m crossing route audits passed with zero mismatches.
|
|
||||||
|
|
||||||
Runs used CPUs 0–11, at most 12 build jobs, a 6 GiB address-space cap, and normal priority. The
|
|
||||||
governor stayed `powersave`; peak sampled single-process RSS was 943,844 KiB (FloatingIslands route
|
|
||||||
audit, GCC). No installs, system changes, or page-cache drops were made. The L3 FP-contract preamble
|
|
||||||
and self-test, L4 `FLazy`, L7 shared cache, L9/L12 proofs, L11 readiness/crash fix, L13 route binding,
|
|
||||||
and async worker/result rules remain enabled. Cache logic is recipe-agnostic.
|
|
||||||
|
|
||||||
**Owner note:** Saving generated terrain works exactly and fits well under the round's disk cap.
|
|
||||||
Lossless zlib saved about 56% of the serialized payload. The smaller Q24 mesh encoding passed the
|
|
||||||
screen gate but changed exact hashes, so the feature should use the lossless record format.
|
|
||||||
|
|
||||||
## Orchestrator review (Claude)
|
|
||||||
|
|
||||||
- Design accepted: the key, the pristine cache plus ordered edits, a server that never trusts client
|
|
||||||
caches, corruption/stale rejection, lossless zlib, and atomic writes.
|
|
||||||
- **Not merged into `assembler/world`:** `Standalone/Source/StreamBenchmarkPipeline.h` (included by the
|
|
||||||
Windows viewer `StandaloneViewer.cpp`) now unconditionally includes `TileCachePrototype.h`, which
|
|
||||||
includes `<unistd.h>`, `<fcntl.h>` and `<zlib.h>`. That would break the MSVC build. Kept on side
|
|
||||||
branch `wip/l16-tile-cache`. Before merging, the prototype must be platform-guarded (or the cache
|
|
||||||
given a portable I/O and codec layer), and zlib needs a Windows source (UE ships zlib).
|
|
||||||
- Cache-off cost re-checked by Luna at −0.14% (within noise).
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Measure upward mesh faces around the FI/SW stratum interface."""
|
|
||||||
import array
|
|
||||||
import collections
|
|
||||||
import math
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def read_exact(stream, size):
|
|
||||||
data = stream.read(size)
|
|
||||||
if len(data) != size:
|
|
||||||
raise EOFError(f"wanted {size} bytes, got {len(data)}")
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
path = sys.argv[1]
|
|
||||||
low_z = float(sys.argv[2]) if len(sys.argv) > 2 else -80.0
|
|
||||||
high_z = float(sys.argv[3]) if len(sys.argv) > 3 else -45.0
|
|
||||||
with open(path, "rb") as stream:
|
|
||||||
if read_exact(stream, 8) != b"VFMSH001":
|
|
||||||
raise SystemExit("bad VFMSH001 header")
|
|
||||||
version, voxel_um, tile_count = struct.unpack("<3I", read_exact(stream, 12))
|
|
||||||
camera = struct.unpack("<3d", read_exact(stream, 24))
|
|
||||||
yaw, pitch = struct.unpack("<2d", read_exact(stream, 16))
|
|
||||||
per_lod = collections.defaultdict(lambda: {"tiles": 0, "faces": [], "spans": []})
|
|
||||||
near_tiles = collections.defaultdict(list)
|
|
||||||
for _ in range(tile_count):
|
|
||||||
key = struct.unpack("<4i", read_exact(stream, 16))
|
|
||||||
surf_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
|
||||||
"<5I", read_exact(stream, 20))
|
|
||||||
p = array.array("f")
|
|
||||||
p.frombytes(read_exact(stream, vertex_count * 12))
|
|
||||||
n = array.array("f")
|
|
||||||
n.frombytes(read_exact(stream, vertex_count * 12))
|
|
||||||
if sys.byteorder != "little":
|
|
||||||
p.byteswap(); n.byteswap()
|
|
||||||
indices = array.array("I")
|
|
||||||
indices.frombytes(read_exact(stream, index_count * 4))
|
|
||||||
masks = read_exact(stream, mask_count)
|
|
||||||
if sys.byteorder != "little":
|
|
||||||
indices.byteswap()
|
|
||||||
lod = key[3]
|
|
||||||
z_values = []
|
|
||||||
for tri in range(index_count // 3):
|
|
||||||
if masks and masks[tri] != 0:
|
|
||||||
continue
|
|
||||||
a, b, c = indices[tri * 3:tri * 3 + 3]
|
|
||||||
if max(a, b, c) >= surf_vertices:
|
|
||||||
continue
|
|
||||||
normal_z = (n[a * 3 + 2] + n[b * 3 + 2] + n[c * 3 + 2]) / 3.0
|
|
||||||
if normal_z < 0.65:
|
|
||||||
continue
|
|
||||||
z = (p[a * 3 + 2] + p[b * 3 + 2] + p[c * 3 + 2]) / 3.0
|
|
||||||
if low_z <= z <= high_z:
|
|
||||||
z_values.append(z)
|
|
||||||
if not z_values:
|
|
||||||
continue
|
|
||||||
z_values.sort()
|
|
||||||
row = per_lod[lod]
|
|
||||||
row["tiles"] += 1
|
|
||||||
row["faces"].extend(z_values)
|
|
||||||
span = z_values[math.floor(.95 * (len(z_values) - 1))] - z_values[math.floor(.05 * (len(z_values) - 1))]
|
|
||||||
row["spans"].append(span)
|
|
||||||
extent = 32 * (1 << lod) * voxel_um / 1_000_000.0
|
|
||||||
cx = (key[0] + 0.5) * extent
|
|
||||||
cy = (key[1] + 0.5) * extent
|
|
||||||
near_tiles[lod].append((math.hypot(cx - camera[0], cy - camera[1]), key,
|
|
||||||
z_values[len(z_values) // 2], span, len(z_values)))
|
|
||||||
|
|
||||||
|
|
||||||
def q(values, fraction):
|
|
||||||
return values[math.floor(fraction * (len(values) - 1))] if values else math.nan
|
|
||||||
|
|
||||||
|
|
||||||
print(f"camera_m={camera} yaw={yaw} pitch={pitch} boundary_band_m={low_z}..{high_z}")
|
|
||||||
print("lod tiles_with_boundary_faces faces z_m_p05/median/p95 tile_span_m_p50/p95/max")
|
|
||||||
for lod in sorted(per_lod):
|
|
||||||
row = per_lod[lod]
|
|
||||||
spans = sorted(row["spans"])
|
|
||||||
print(f"{lod} {row['tiles']} {len(row['faces'])} "
|
|
||||||
f"{q(sorted(row['faces']), .05):.4f}/{q(sorted(row['faces']), .5):.4f}/"
|
|
||||||
f"{q(sorted(row['faces']), .95):.4f} "
|
|
||||||
f"{q(spans, .5):.4f}/{q(spans, .95):.4f}/{max(spans):.4f}")
|
|
||||||
print("nearest_xy_boundary_tiles_by_lod:")
|
|
||||||
for lod in sorted(near_tiles):
|
|
||||||
for distance, key, median, span, faces in sorted(near_tiles[lod])[:3]:
|
|
||||||
print(f"lod={lod} key={key} xy_distance_m={distance:.2f} "
|
|
||||||
f"boundary_median_z={median:.4f} within_tile_p05_p95_m={span:.4f} faces={faces}")
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
#include "Json.h"
|
|
||||||
#include "StandaloneCoreScheduler.h"
|
|
||||||
#include "StandaloneSnapshotAdapter.h"
|
|
||||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <fstream>
|
|
||||||
#include <iterator>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
constexpr double CoreVoxelsPerMeter = 4.0;
|
|
||||||
constexpr std::int64_t QPerCoreVoxel = VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
||||||
|
|
||||||
std::int64_t ToQ(double Meters)
|
|
||||||
{
|
|
||||||
return static_cast<std::int64_t>(std::llround(Meters*CoreVoxelsPerMeter*QPerCoreVoxel));
|
|
||||||
}
|
|
||||||
|
|
||||||
void PrintLegacyBoundarySealTerms(const VoxelForgeCore::FloatingIslandManifestV1& Manifest,
|
|
||||||
double X, double Y, double Z)
|
|
||||||
{
|
|
||||||
using namespace VoxelForgeCore;
|
|
||||||
const auto Xq=ToQ(X),Yq=ToQ(Y),Zq=ToQ(Z);
|
|
||||||
const FloatingIslandFieldV1 field(Manifest.recipe,Manifest.plan);
|
|
||||||
const std::int64_t fieldQ=field.densitySubvoxels(Xq,Yq,Zq);
|
|
||||||
const std::int64_t bottom=Manifest.recipe.layoutMinimumZSubvoxels;
|
|
||||||
const std::int64_t top=Manifest.recipe.layoutMaximumZSubvoxels;
|
|
||||||
const std::int64_t seal=Manifest.recipe.boundarySealSubvoxels;
|
|
||||||
std::int64_t bridged=fieldQ;
|
|
||||||
bool applied=false;
|
|
||||||
if(Zq>=bottom&&Zq<bottom+seal)
|
|
||||||
{bridged=std::min(bridged,-std::max<std::int64_t>(1,Zq-bottom));applied=true;}
|
|
||||||
if(Zq<=top&&Zq>top-seal)
|
|
||||||
{bridged=std::min(bridged,-std::max<std::int64_t>(1,top-Zq));applied=true;}
|
|
||||||
std::printf("[PreFixIslandTerms] xyz_m=%.3f,%.3f,%.3f native_field_q=%lld "
|
|
||||||
"legacy_solid_seal_delta_q=%lld legacy_seal_active=%d pre_fix_wrapper_q=%lld\n",
|
|
||||||
X,Y,Z,static_cast<long long>(fieldQ),
|
|
||||||
static_cast<long long>(bridged-fieldQ),applied?1:0,static_cast<long long>(bridged));
|
|
||||||
for(const auto& island:Manifest.plan.islands)
|
|
||||||
{
|
|
||||||
const bool inBounds=Xq>=island.influenceMinimumSubvoxels[0]
|
|
||||||
&&Xq<=island.influenceMaximumSubvoxels[0]
|
|
||||||
&&Yq>=island.influenceMinimumSubvoxels[1]
|
|
||||||
&&Yq<=island.influenceMaximumSubvoxels[1]
|
|
||||||
&&Zq>=island.influenceMinimumSubvoxels[2]
|
|
||||||
&&Zq<=island.influenceMaximumSubvoxels[2];
|
|
||||||
std::printf("[IslandGroup] ordinal=%u center_m=%.3f,%.3f,%.3f "
|
|
||||||
"bounds_contains_sample=%d\n",
|
|
||||||
island.ordinal,island.centerSubvoxels[0]/1024.0,island.centerSubvoxels[1]/1024.0,
|
|
||||||
island.centerSubvoxels[2]/1024.0,inBounds?1:0);
|
|
||||||
}
|
|
||||||
for(const auto& edge:Manifest.plan.clusterEdges)
|
|
||||||
{
|
|
||||||
bool inBounds=true;
|
|
||||||
const std::int64_t q[3]={Xq,Yq,Zq};
|
|
||||||
for(std::size_t axis=0;axis<3;++axis)
|
|
||||||
inBounds=inBounds&&q[axis]>=edge.influenceMinimumSubvoxels[axis]
|
|
||||||
&&q[axis]<=edge.influenceMaximumSubvoxels[axis];
|
|
||||||
std::printf("[IslandConnector] id=%016llx bounds_contains_sample=%d\n",
|
|
||||||
static_cast<unsigned long long>(edge.stableId),inBounds?1:0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PrintDensityLine(VoxelForgeCore::World& World,const char* Label,
|
|
||||||
double X,double Y,double ZMinimum,double ZMaximum,double Step)
|
|
||||||
{
|
|
||||||
using namespace VoxelForgeCore;
|
|
||||||
std::printf("[DensityLine] label=%s xy_m=(%.6f,%.6f) z_m=%.2f..%.2f step_m=%.2f\n",
|
|
||||||
Label,X,Y,ZMinimum,ZMaximum,Step);
|
|
||||||
for(double Z=ZMinimum;Z<=ZMaximum+1.0e-9;Z+=Step)
|
|
||||||
{
|
|
||||||
const Vec3d Point{X*CoreVoxelsPerMeter,Y*CoreVoxelsPerMeter,
|
|
||||||
Z*CoreVoxelsPerMeter};
|
|
||||||
const double Density=World.densityAt(Point);
|
|
||||||
const double Unclamped=World.unclampedDensityAt(Point);
|
|
||||||
const DetailFieldSample Detail=World.detailSampleAt(Point);
|
|
||||||
std::printf("[DensitySample] label=%s xyz_m=%.3f,%.3f,%.3f density_core_vox=%.9f "
|
|
||||||
"density_cm=%.3f unclamped_core_vox=%.9f cave_influence=%.6f "
|
|
||||||
"floor_influence=%.6f detail_displacement=%.6f sign=%s\n",Label,X,Y,Z,
|
|
||||||
Density,Density*25.0,Unclamped,Detail.caveInfluence,Detail.floorInfluence,
|
|
||||||
Detail.displacement,Density<0.0?"solid":"air");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc,char** argv)
|
|
||||||
{
|
|
||||||
if(argc!=2)
|
|
||||||
{
|
|
||||||
std::fprintf(stderr,"usage: %s owner-world-with-world-assembly.json\n",argv[0]);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
VFJson::Value root;
|
|
||||||
std::string error;
|
|
||||||
if(!VFJson::Load(argv[1],root,error))
|
|
||||||
{std::fprintf(stderr,"owner JSON parse failed: %s\n",error.c_str());return 2;}
|
|
||||||
auto snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(root,0,3,8,false,false,error);
|
|
||||||
if(!snapshot)
|
|
||||||
{std::fprintf(stderr,"snapshot resolve failed: %s\n",error.c_str());return 2;}
|
|
||||||
const auto* assembly=snapshot.worldAssembly();
|
|
||||||
if(!assembly||assembly->floatingIslands.empty())
|
|
||||||
{std::fprintf(stderr,"FloatingIslands manifest missing\n");return 2;}
|
|
||||||
|
|
||||||
VoxelForgeCore::World world(std::move(snapshot));
|
|
||||||
VoxelForgeCore::ShapeBuildRequest request;
|
|
||||||
request.minVoxel={-96,-512,-288};
|
|
||||||
request.maxVoxel={96,96,64};
|
|
||||||
request.clipMinVoxel=request.minVoxel;
|
|
||||||
request.clipMaxVoxel=request.maxVoxel;
|
|
||||||
request.workerLimit=12;
|
|
||||||
request.includeFarWorld=false;
|
|
||||||
FStandaloneCoreScheduler scheduler;
|
|
||||||
if(!world.compile(request,scheduler,error))
|
|
||||||
{std::fprintf(stderr,"shape compile failed: %s\n",error.c_str());return 2;}
|
|
||||||
|
|
||||||
constexpr double FlatX=-11.612443923950195;
|
|
||||||
constexpr double FlatY=-118.22123718261719;
|
|
||||||
constexpr double IslandX=-6.3;
|
|
||||||
constexpr double IslandY=-19.057;
|
|
||||||
constexpr double RingX=-4.889;
|
|
||||||
constexpr double RingY=-16.008;
|
|
||||||
std::printf("[DensityRoute] flat_square_xy_m=(%.6f,%.6f) island_ground_xy_m=(%.6f,%.6f) "
|
|
||||||
"compiled=%d core_voxel_size_m=0.25 owner_to_core_chunk_voxels=16\n",
|
|
||||||
FlatX,FlatY,IslandX,IslandY,world.isCompiled()?1:0);
|
|
||||||
PrintDensityLine(world,"flat-square",FlatX,FlatY,2.5,9.0,0.25);
|
|
||||||
PrintDensityLine(world,"fi-sw-boundary",FlatX,FlatY,-62.0,-58.0,0.25);
|
|
||||||
PrintDensityLine(world,"island-center",IslandX,IslandY,-39.0,-20.0,0.5);
|
|
||||||
PrintDensityLine(world,"lod0-lod1-ring-edge",RingX,RingY,-62.0,-20.0,1.0);
|
|
||||||
|
|
||||||
const auto& island=assembly->floatingIslands.front().manifest;
|
|
||||||
for(double z:{2.75,2.99,3.00,3.01,3.25,3.49,3.50,3.51,3.75,3.99,4.00,4.01,7.75,8.00,8.25})
|
|
||||||
PrintLegacyBoundarySealTerms(island,FlatX,FlatY,z);
|
|
||||||
for(double z:{-35.0,-30.0,-25.0})
|
|
||||||
PrintLegacyBoundarySealTerms(island,IslandX,IslandY,z);
|
|
||||||
for(double z:{-60.0,-35.0,-28.0,-25.0})
|
|
||||||
PrintLegacyBoundarySealTerms(island,RingX,RingY,z);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Measure peak per-process RSS for a build process tree under the L17 limits."""
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def descendants(root):
|
|
||||||
pending = [root]
|
|
||||||
seen = set()
|
|
||||||
while pending:
|
|
||||||
pid = pending.pop()
|
|
||||||
if pid in seen:
|
|
||||||
continue
|
|
||||||
seen.add(pid)
|
|
||||||
try:
|
|
||||||
children = Path(f"/proc/{pid}/task/{pid}/children").read_text().split()
|
|
||||||
pending.extend(int(child) for child in children)
|
|
||||||
except (FileNotFoundError, PermissionError, ProcessLookupError):
|
|
||||||
pass
|
|
||||||
return seen
|
|
||||||
|
|
||||||
|
|
||||||
def status(pid):
|
|
||||||
try:
|
|
||||||
fields = {}
|
|
||||||
for line in Path(f"/proc/{pid}/status").read_text().splitlines():
|
|
||||||
if line.startswith(("Name:", "VmRSS:", "VmSize:")):
|
|
||||||
key, *value = line.split()
|
|
||||||
fields[key[:-1]] = value[0] if value else ""
|
|
||||||
return (int(fields.get("VmRSS", "0")), int(fields.get("VmSize", "0")),
|
|
||||||
fields.get("Name", ""))
|
|
||||||
except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
raise SystemExit("usage: MeasureBuild.py command [arguments ...]")
|
|
||||||
if not os.sched_getaffinity(0).issubset(set(range(12))):
|
|
||||||
raise SystemExit("run under taskset -c 0-11")
|
|
||||||
|
|
||||||
started = time.monotonic()
|
|
||||||
process = subprocess.Popen(sys.argv[1:], start_new_session=True)
|
|
||||||
peak_rss = (0, 0, "")
|
|
||||||
peak_vms = (0, 0, "")
|
|
||||||
while process.poll() is None:
|
|
||||||
for pid in descendants(process.pid):
|
|
||||||
measured = status(pid)
|
|
||||||
if measured is None:
|
|
||||||
continue
|
|
||||||
rss, vms, name = measured
|
|
||||||
if rss > peak_rss[0]:
|
|
||||||
peak_rss = (rss, pid, name)
|
|
||||||
if vms > peak_vms[0]:
|
|
||||||
peak_vms = (vms, pid, name)
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
exit_code = process.wait()
|
|
||||||
elapsed = time.monotonic() - started
|
|
||||||
print(f"[BuildTreeResources] exit_code={exit_code} wall_s={elapsed:.3f} "
|
|
||||||
f"peak_process_rss_kib={peak_rss[0]} peak_rss_pid={peak_rss[1]} "
|
|
||||||
f"peak_rss_name={peak_rss[2]} peak_process_vms_kib={peak_vms[0]} "
|
|
||||||
f"peak_vms_pid={peak_vms[1]} peak_vms_name={peak_vms[2]} "
|
|
||||||
"monitor_interval_s=0.1", flush=True)
|
|
||||||
raise SystemExit(exit_code)
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Summarize exported tile geometry and upward-facing ground for L17."""
|
|
||||||
import array
|
|
||||||
import collections
|
|
||||||
import math
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def read_exact(stream, size):
|
|
||||||
data = stream.read(size)
|
|
||||||
if len(data) != size:
|
|
||||||
raise EOFError(f"wanted {size} bytes, got {len(data)}")
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
path = sys.argv[1]
|
|
||||||
with open(path, "rb") as stream:
|
|
||||||
if read_exact(stream, 8) != b"VFMSH001":
|
|
||||||
raise SystemExit("bad VFMSH001 header")
|
|
||||||
version, voxel_um, tile_count = struct.unpack("<3I", read_exact(stream, 12))
|
|
||||||
camera = struct.unpack("<3d", read_exact(stream, 24))
|
|
||||||
yaw, pitch = struct.unpack("<2d", read_exact(stream, 16))
|
|
||||||
per_lod = collections.defaultdict(lambda: {"tiles": 0, "vertices": 0,
|
|
||||||
"triangles": 0, "surface_triangles": 0, "up_triangles": 0,
|
|
||||||
"up_centroids": [], "tile_spans": [], "empty": 0})
|
|
||||||
tile_rows = []
|
|
||||||
for _ in range(tile_count):
|
|
||||||
key = struct.unpack("<4i", read_exact(stream, 16))
|
|
||||||
surf_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
|
||||||
"<5I", read_exact(stream, 20))
|
|
||||||
p = array.array("f")
|
|
||||||
p.frombytes(read_exact(stream, vertex_count * 12))
|
|
||||||
n = array.array("f")
|
|
||||||
n.frombytes(read_exact(stream, vertex_count * 12))
|
|
||||||
if sys.byteorder != "little":
|
|
||||||
p.byteswap(); n.byteswap()
|
|
||||||
indices = array.array("I")
|
|
||||||
indices.frombytes(read_exact(stream, index_count * 4))
|
|
||||||
masks = read_exact(stream, mask_count)
|
|
||||||
if sys.byteorder != "little":
|
|
||||||
indices.byteswap()
|
|
||||||
lod = key[3]
|
|
||||||
row = per_lod[lod]
|
|
||||||
row["tiles"] += 1
|
|
||||||
row["vertices"] += vertex_count
|
|
||||||
row["triangles"] += index_count // 3
|
|
||||||
row["surface_triangles"] += index_count // 3 - skirt_count
|
|
||||||
up = []
|
|
||||||
for tri in range(index_count // 3):
|
|
||||||
if masks and masks[tri] != 0:
|
|
||||||
continue
|
|
||||||
a, b, c = indices[tri * 3:tri * 3 + 3]
|
|
||||||
if max(a, b, c) >= surf_vertices:
|
|
||||||
continue
|
|
||||||
if (n[a * 3 + 2] + n[b * 3 + 2] + n[c * 3 + 2]) / 3.0 < 0.65:
|
|
||||||
continue
|
|
||||||
up.append((p[a * 3 + 2] + p[b * 3 + 2] + p[c * 3 + 2]) / 3.0)
|
|
||||||
row["up_triangles"] += len(up)
|
|
||||||
row["up_centroids"].extend(up)
|
|
||||||
if up:
|
|
||||||
up.sort()
|
|
||||||
span = up[math.floor(0.95 * (len(up) - 1))] - up[math.floor(0.05 * (len(up) - 1))]
|
|
||||||
row["tile_spans"].append(span)
|
|
||||||
tile_rows.append((key, vertex_count, index_count // 3, skirt_count,
|
|
||||||
len(up), up[0], up[len(up) // 2], up[-1], span))
|
|
||||||
else:
|
|
||||||
row["empty"] += 1
|
|
||||||
tile_rows.append((key, vertex_count, index_count // 3, skirt_count,
|
|
||||||
0, math.nan, math.nan, math.nan, math.nan))
|
|
||||||
|
|
||||||
print(f"version={version} voxel_um={voxel_um} tiles={tile_count} camera_m={camera} yaw={yaw} pitch={pitch}")
|
|
||||||
print("lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles "
|
|
||||||
"upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max")
|
|
||||||
for lod in sorted(per_lod):
|
|
||||||
r = per_lod[lod]
|
|
||||||
z = sorted(r["up_centroids"])
|
|
||||||
s = sorted(r["tile_spans"])
|
|
||||||
def q(values, fraction):
|
|
||||||
return values[math.floor(fraction * (len(values) - 1))] if values else math.nan
|
|
||||||
print(f"{lod} {r['tiles']} {r['tiles']-r['empty']} {r['vertices']} {r['triangles']} "
|
|
||||||
f"{sum(row[3] for row in tile_rows if row[0][3] == lod)} {r['up_triangles']} "
|
|
||||||
f"{q(z,.05):.4f}/{q(z,.5):.4f}/{q(z,.95):.4f} "
|
|
||||||
f"{q(s,.5):.4f}/{q(s,.95):.4f}/{max(s) if s else math.nan:.4f}")
|
|
||||||
|
|
||||||
print("near_camera_up_tiles (sorted by XY tile-centre distance):")
|
|
||||||
near = []
|
|
||||||
for row in tile_rows:
|
|
||||||
key = row[0]
|
|
||||||
lod = key[3]
|
|
||||||
extent = 32 * (1 << lod) * voxel_um / 1_000_000.0
|
|
||||||
cx = (key[0] + 0.5) * extent
|
|
||||||
cy = (key[1] + 0.5) * extent
|
|
||||||
near.append((math.hypot(cx-camera[0], cy-camera[1]), row))
|
|
||||||
for distance, row in sorted(near)[:30]:
|
|
||||||
key, verts, tris, skirts, ups, lo, med, hi, span = row
|
|
||||||
if ups:
|
|
||||||
print(f"key={key} xy_distance_m={distance:.2f} vertices={verts} triangles={tris} "
|
|
||||||
f"skirt_triangles={skirts} upward_triangles={ups} z_m={lo:.3f}/{med:.3f}/{hi:.3f} "
|
|
||||||
f"tile_span_p05_p95_m={span:.3f}")
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Owner note — L17 FloatingIslands steps
|
|
||||||
|
|
||||||
The ledges were real generated mesh geometry caused by two boundary mistakes that made empty space
|
|
||||||
look like flat ground: the FloatingIslands seal was applied as a solid cap across XY, and owner
|
|
||||||
chunks were treated as 8 m of Core voxels instead of their actual 4 m. At the measured flat square,
|
|
||||||
the native island field is air and cave/floor influence is zero. The point is about 100 m from the
|
|
||||||
four islands.
|
|
||||||
|
|
||||||
I removed the false cap and corrected the shared owner/Core coordinate conversion. Evaluator version
|
|
||||||
is 7. The before/after captures show the false sheet disappear and the actual island shape remain.
|
|
||||||
SurfaceWorld’s L1 hash changes because it now selects its intended `Heightfield2D` route; its route,
|
|
||||||
readiness and proof audits pass.
|
|
||||||
|
|
||||||
The export has nonempty meshes at LOD0–8, and the standalone viewer enables and draws all levels by
|
|
||||||
default. I cannot identify the screenshot’s dark square as a whole missing LOD from the Linux data;
|
|
||||||
it may be a specific tile or viewer draw/upload issue. I did not change the Windows viewer. Please
|
|
||||||
capture the view below and, if the square remains, repeat with `--capture-only-lod=0` through `8`.
|
|
||||||
|
|
||||||
The low-view camera is `(-22,-19,-22.8) m`, yaw 0°, pitch −4°, matching the Linux island pair.
|
|
||||||
|
|
||||||
```bat
|
|
||||||
VoxelForgeCoreStandalone.exe --world-assembler-prototype --recipe=floating-islands --clip-radius=3 --max-clip-level=8 --workers=12 --camera=-22,-19,-22.8,0,-4 --capture=Standalone\Research\WorldAssemblerM3\Linux17\captures\floating-islands-windows.png --frame-limit=60
|
|
||||||
```
|
|
||||||
|
|
||||||
Use a different `--capture=` filename for each isolated LOD. Send the all-level image plus the
|
|
||||||
isolated image that corresponds to the square.
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
# VoxelForge L17 — FloatingIslands boundary report
|
|
||||||
|
|
||||||
Branch `assembler/world`, starting HEAD `9a7c87e`. No cache code was changed. The shared evaluator
|
|
||||||
version is now 7; the recipe schema stays at 1.
|
|
||||||
|
|
||||||
## Cause and fix
|
|
||||||
|
|
||||||
FloatingIslands uses the sparse `FloatingIslandFieldV1` manifest field. SurfaceWorld uses the
|
|
||||||
separate planar `Heightfield2D` field. The owner voxel is 12.5 cm and the Core voxel is 25 cm, so
|
|
||||||
one 32-voxel owner chunk is **16 Core voxels / 4 m**. Several shared paths divided Core positions
|
|
||||||
by 32 directly, treating that chunk as 8 m. That doubled stratum bounds, selected the wrong
|
|
||||||
generator route near the SurfaceWorld boundary, and made the legacy layer envelope extend over
|
|
||||||
empty FloatingIslands space.
|
|
||||||
|
|
||||||
The FloatingIslands `boundarySeal` also had been applied as a solid cap over all XY. At the owner’s
|
|
||||||
flat-square probe `(-11.612444,-118.221237) m`, the raw island field is air: at Z=3.25 m it was
|
|
||||||
`+81,809` subvoxels. The old seal added `−82,065`, yielding `−256` (`−1` Core voxel, or −25 cm);
|
|
||||||
at Z=3.50 m it yielded `−1` subvoxel (−0.098 cm), then air again at Z=3.75 m. The wrong 8 m
|
|
||||||
legacy layer bound produced a second false top near Z=8 m (old world density at 7.75 m was
|
|
||||||
−0.743 Core voxels; at 8 m it was +0.073). The owner’s 4.5 m LOD0 jump is real mesh geometry
|
|
||||||
created by those bad boundaries, often inside a tile; it is not a renderer artifact or island relief.
|
|
||||||
Nested clipmap rectangles made its placement look like a tile-boundary-only issue.
|
|
||||||
|
|
||||||
The fix uses the resolved owner/Core scale for manager lookups, layer bounds, readiness, and
|
|
||||||
adjacent native strata; removes the XY-wide FloatingIslands seal cap and its all-solid proof; and
|
|
||||||
clips adjacent native FI/SW signed-distance fields at their shared physical boundary. This is in
|
|
||||||
Core and applies to every generator route. SurfaceWorld still uses `Heightfield2D`.
|
|
||||||
|
|
||||||
The cave-floor lead does not match the samples. Cave and floor influence were exactly zero at the
|
|
||||||
flat square, the island center, and the sampled ledge/ring point. The native FI field and its island
|
|
||||||
or connector bounds account for the surface. The owner world compiled 70 rooms, 69 tunnels and 2
|
|
||||||
passages in this run; those legacy primitives do not contribute to the measured FI ledge.
|
|
||||||
|
|
||||||
## Before/after measurements
|
|
||||||
|
|
||||||
`RingProbe.py` samples upward-facing mesh surfaces on adjacent XY tile edges only when the tiles’
|
|
||||||
vertical bands overlap. Values are median / p95 / maximum absolute height difference, in cm. `n/a`
|
|
||||||
means this sparse-island export had no same-band LOD0↔1 surface pair after removing the false cap;
|
|
||||||
it is not a zero-error result. Higher rings include the deep SurfaceWorld boundary and other deep
|
|
||||||
surfaces, so those rows are not all FloatingIslands ground.
|
|
||||||
|
|
||||||
| LOD edge | Before (cm) | After (cm) |
|
|
||||||
|---|---:|---:|
|
|
||||||
| 0→1 | 3.451 / 8.920 / 449.158 (266 samples) | n/a (no same-band pair) |
|
|
||||||
| 1→2 | 26.642 / 148.262 / 149.862 (514) | 0 / 0 / 0 (234) |
|
|
||||||
| 2→3 | 0.134 / 198.730 / 773.318 (596) | 0 / 0 / 0 (207) |
|
|
||||||
| 3→4 | 3.269 / 47.875 / 188.531 (277) | 0 / 0 / 0 (288) |
|
|
||||||
| 4→5 | 29.975 / 130.496 / 445.444 (544) | 0 / 0 / 30.957 (258) |
|
|
||||||
| 5→6 | 12.234 / 127.224 / 536.573 (266) | 12.234 / 127.224 / 536.573 (266) |
|
|
||||||
| 6→7 | 29.013 / 301.752 / 42,624.479 (264) | 28.892 / 291.861 / 945.477 (264) |
|
|
||||||
| 7→8 | 83.843 / 1,225.416 / 3,755.407 (275) | 83.843 / 1,225.416 / 3,755.407 (275) |
|
|
||||||
|
|
||||||
The valid worst pre-fix LOD0↔1 sample is the false cap: 3.500 m vs 7.992 m, a 449.158 cm
|
|
||||||
difference. Upward-face within-tile span p95 / maximum changed from 4.490 / 4.491 m to 1.186 /
|
|
||||||
1.927 m on LOD0. The remaining LOD0 span is the actual island shape. LOD1–3 after-fix upward
|
|
||||||
faces at the measured lower boundary are exactly at −60 m. The island tops around the matched
|
|
||||||
camera are roughly −30 to −25 m.
|
|
||||||
|
|
||||||
A separate exploratory column comparison initially reported a 34.7 m difference by pairing an
|
|
||||||
island top near −25 m with a −60 m SurfaceWorld surface from a vertically disjoint tile. That
|
|
||||||
comparison was rejected: it is not a same-band ring seam. The saved `RingProbe.py` output is the
|
|
||||||
cross-LOD evidence used above.
|
|
||||||
|
|
||||||
## History, viewer and SurfaceWorld
|
|
||||||
|
|
||||||
Prebuilt L7, L9, L12, L13 and L14 binaries/logs all have the same legacy hashes: whole-view D/M/P/T
|
|
||||||
`92ba6a57a8690c00 / 9c6b56dfab6241c9 / 82394dd7c368a061 / 445e40bdb69b0bdb`; full-detail
|
|
||||||
`93ee3b6fb6b446f3 / 3ebc72d2027ab80b / cd5f55a96de12458 / 2f61c29af247b547`. The pattern
|
|
||||||
predates L7; these measurements show no output change at L9 or L13. S9 `e7586a1`, S12 `212814a`
|
|
||||||
and S14 `ec2afe9` do not contain the headless pipeline source, so they cannot be measured this way.
|
|
||||||
No old commit was rebuilt: `git worktree add` failed because `.git/worktrees` is read-only, and the
|
|
||||||
available old binaries were used as-is.
|
|
||||||
|
|
||||||
The headless mesh export has nonempty tiles at **all** LODs 0–8 (matched view counts:
|
|
||||||
15, 64, 40, 48, 48, 96, 48, 48, 153). The standalone viewer initializes all nine `bShowLevel`
|
|
||||||
flags to true and its draw loop visits all streaming batches. There is no evidence that a whole LOD
|
|
||||||
is absent from generated data or globally filtered by the viewer. The screenshot’s dark square
|
|
||||||
cannot be mapped to a particular tile/LOD from this Linux export; a localized viewer draw/upload
|
|
||||||
or camera issue remains unconfirmed. No viewer change was made because the Windows D3D11 build and
|
|
||||||
capture are unavailable here. The Unreal comparison does not identify the cause of that square.
|
|
||||||
|
|
||||||
SurfaceWorld L1 hashes changed for the shared coordinate fix. Old L14 LOD1 D/M/P/T was
|
|
||||||
`5043975b1e5b5a3d / 1c2a5658f10fdeaa / 23b6779ef35c3a0a / 1a6d2e2d152b4f84`; L17 is
|
|
||||||
`393ad79fd8c07132 / 47c9ba17e9f5ceaa / d5d6a948ac880e17 / 8b85ea08b2c440c4`. Before, readiness
|
|
||||||
was 0/27; now it is 27/27 on the near ring. The old mapping selected the wrong route at the
|
|
||||||
SurfaceWorld stratum; the corrected mapping selects its manifest `Heightfield2D` plan. Page/native
|
|
||||||
route parity and exact queries pass, so this is the intended shared-route output change.
|
|
||||||
|
|
||||||
## Gates, captures and limits
|
|
||||||
|
|
||||||
All 28 determinism rows passed: FI and SW, Clang 19 `-ffp-contract=off` and GCC 14 default,
|
|
||||||
W1/W8/W12 with five W12 repeats. Each recipe had one unique full, whole, nine-LOD and exact-query
|
|
||||||
hash tuple across all configurations. FI whole-view D/M/P/T is
|
|
||||||
`8339509b4d5ec231 / 70e5b2b9085b517c / 4e72e53745f20ed0 / 1a2643e5138e0407`; SW whole-view is
|
|
||||||
`b286ee4f4c38b7fc / 6862deb8ab9c2af9 / f23480a4953457f9 / 62977edfd04b3afe`.
|
|
||||||
|
|
||||||
FI route parity: 8 page pairs, 14 higher-LOD exact pairs, 782,026 exact samples, zero mismatches;
|
|
||||||
uniform proof: 19,288 blocks / 25,672,328 samples / zero violations. SW: 5 page and 9 native-only
|
|
||||||
pairs, 814,625 exact samples, zero mismatches; uniform proof: 13,297 blocks / 17,698,307 samples /
|
|
||||||
zero violations. Readiness negative controls were caught. The Clang FP-contraction-off and GCC
|
|
||||||
default builds passed; the Core startup FP self-test did not reject either. No async/MPSC/Epoch
|
|
||||||
code changed; `FLazy`, shared column caches, L9/L12 proof paths and L13 route binding remain in the
|
|
||||||
tested build.
|
|
||||||
|
|
||||||
Readable Linux capture mode adds directional light with soft ambient, gentle height tint, and an
|
|
||||||
optional faint tile-edge overlay. Five 1600×900 JPEGs, all below 300 KB, are in `captures/`:
|
|
||||||
|
|
||||||
- `floating-islands-flat-square-before.jpg` / `floating-islands-flat-square-after.jpg`: same owner
|
|
||||||
probe camera `(-11.6124,-118.2212,-19.8452) m`, yaw 0, pitch 0, before/after.
|
|
||||||
- `floating-islands-island-view-before.jpg` / `floating-islands-island-view-after.jpg`: same low
|
|
||||||
island camera `(-22,-19,-22.8) m`, yaw 0, pitch −4°, before/after.
|
|
||||||
- `surface-world-l1-readable-tiles.jpg`: SurfaceWorld L1 sanity view, camera `(0,-35,30) m`, yaw 0,
|
|
||||||
pitch −60°.
|
|
||||||
|
|
||||||
All are 1600×900 JPEGs; SHA256 is included for byte-level review.
|
|
||||||
|
|
||||||
| Capture | Bytes | SHA256 |
|
|
||||||
|---|---:|---|
|
|
||||||
| `floating-islands-flat-square-before.jpg` | 70,166 | `c41ed7645735ebefea97fbc1aed167b66a3e06c019c25b1c7d8cae9449713999` |
|
|
||||||
| `floating-islands-flat-square-after.jpg` | 47,612 | `f8fb5f937ac65be9db70a41473c7a10d7f35943ba6ef5c94ab03fde9ded5c544` |
|
|
||||||
| `floating-islands-island-view-before.jpg` | 70,813 | `5c7a8eda446f51a51ab22e45ed7286bf4180e69d8e3b2180780349facadacd32` |
|
|
||||||
| `floating-islands-island-view-after.jpg` | 51,429 | `3aeedef903fce62e81ea1f07bb460dd2beb42b893bbd9bf29220973951295b4c` |
|
|
||||||
| `surface-world-l1-readable-tiles.jpg` | 53,351 | `23f1753ed100c87607e22d2adc4f8e8a2856b8af3f1d37aa4f4cec9420a92b93` |
|
|
||||||
|
|
||||||
Clang/GCC compile peak RSS was 435,588 / 902,000 KiB. Determinism peak was 538,140 KiB; FI/SW
|
|
||||||
route-audit peaks were 621,404 / 763,112 KiB. The governor remained `powersave`; runs were limited
|
|
||||||
to CPUs 0–11, at most 12 workers, 6 GiB address space; llvmpipe used 12 threads.
|
|
||||||
|
|
||||||
## Failed or retried attempts
|
|
||||||
|
|
||||||
- The first headless invocation omitted the explicit owner-world input; it was rerun with
|
|
||||||
`Standalone/data/owner_world.json`.
|
|
||||||
- An initial SurfaceWorld export failed the native-only readiness guard; corrected owner/Core
|
|
||||||
mapping and rerun passed.
|
|
||||||
- A SurfaceWorld `--max-clip-level=1` attempt expanded to 35,435,983 tiles and failed the bounded
|
|
||||||
compiler (20.985 s, 3,988,160 KiB peak RSS). Retried with `--max-level=1` (960 tiles), which
|
|
||||||
passed.
|
|
||||||
- `BoundaryProbe.py` was first called with unsupported named Z arguments; rerun with positional
|
|
||||||
bounds passed.
|
|
||||||
- The first XY-only ring comparison mixed vertically disjoint surfaces; replaced by the
|
|
||||||
overlapping-band `RingProbe.py` used above. The exploratory column probe was also rejected for
|
|
||||||
pairing different vertical bands.
|
|
||||||
- The first offscreen SurfaceWorld command reversed scene/output arguments and failed to open the
|
|
||||||
mesh; rerun with the documented order passed.
|
|
||||||
- The first density-probe invocation used `Standalone/data/owner_world.json` without a resolved FI
|
|
||||||
manifest and failed. It was rerun with `Saved/Linux/l17-fi-owner-snapshot.json` and passed.
|
|
||||||
- `/usr/bin/time` was unavailable and an early process monitor mishandled a zombie child; the
|
|
||||||
process-tree monitor in `MeasureBuild.py` replaced both attempts.
|
|
||||||
- `git worktree add` could not write `.git/worktrees`; no worktree was created and no old commit was
|
|
||||||
built outside the repository.
|
|
||||||
- Mesa could not create `/home/jahni/.cache` in the read-only environment, disabled shader caching,
|
|
||||||
and completed all captures. No file was written outside the repository.
|
|
||||||
|
|
||||||
## Windows confirmation
|
|
||||||
|
|
||||||
Run from the repository root on Windows to capture the low island view:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
VoxelForgeCoreStandalone.exe --world-assembler-prototype --recipe=floating-islands --clip-radius=3 --max-clip-level=8 --workers=12 --camera=-22,-19,-22.8,0,-4 --capture=Standalone\Research\WorldAssemblerM3\Linux17\captures\floating-islands-windows.png --frame-limit=60
|
|
||||||
```
|
|
||||||
|
|
||||||
For isolation, repeat with `--capture-only-lod=0` through `--capture-only-lod=8`, changing the
|
|
||||||
capture filename each time. Please send the all-level image and the isolated image matching the
|
|
||||||
dark square; those will identify whether it is a per-tile draw gap or a particular LOD payload.
|
|
||||||
|
|
||||||
## Orchestrator review (Claude) — NOT MERGED
|
|
||||||
|
|
||||||
- **The fix breaks SurfaceWorld.** I exported the L1 view (camera `0,-35,-487.94`) from L14 and from
|
|
||||||
L17 and rendered both in the new readable mode (`Saved/Linux/claude/sw/{before,after}-{l1,hz}.jpg`).
|
|
||||||
After L17, the ground under the spawn camera is gone (black void below), the terrain sits far
|
|
||||||
away, and the mesh export shrank from 58.7 MB to 31.7 MB. Halving the strate chunk size (8 m → 4 m)
|
|
||||||
moves and squashes every stratum: a world change, not a fix.
|
|
||||||
- **What the approved world uses:** the Core world the owner played and approved places strata
|
|
||||||
with 8 m chunks (32 Core voxels of 25 cm). The "owner chunk = 4 m" claim must be checked against
|
|
||||||
what the Unreal Core route actually does (`AVoxelWorld::CoreVoxelSizeCm`, the Core bridge,
|
|
||||||
`ResolvedWorldSnapshot`), not assumed.
|
|
||||||
- **Probably real and separable:** the FloatingIslands `boundarySeal` applied as a solid cap over all
|
|
||||||
XY (the false floors at 3.5/8 m). That needs its own minimal fix with SurfaceWorld unchanged.
|
|
||||||
- **Still open in FloatingIslands after the fix:** dark tile-aligned rectangles on the ground (the L17
|
|
||||||
"after" captures, lower left).
|
|
||||||
- Kept on side branch `wip/l17-fi-scale`. Only the capture tool's readable shading
|
|
||||||
(`LinuxOffscreenCapture.cpp`) is taken into `assembler/world`.
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Compare upward mesh heights on shared XY edges between adjacent LOD rings."""
|
|
||||||
import array
|
|
||||||
import collections
|
|
||||||
import math
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def read_scene(path):
|
|
||||||
tiles = []
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
if f.read(8) != b"VFMSH001":
|
|
||||||
raise SystemExit(f"{path}: invalid mesh header")
|
|
||||||
version, voxel_um, count = struct.unpack("<3I", f.read(12))
|
|
||||||
camera = struct.unpack("<3d", f.read(24))
|
|
||||||
yaw, pitch = struct.unpack("<2d", f.read(16))
|
|
||||||
voxel_m = voxel_um / 1_000_000.0
|
|
||||||
for _ in range(count):
|
|
||||||
key = struct.unpack("<4i", f.read(16))
|
|
||||||
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
|
||||||
"<5I", f.read(20))
|
|
||||||
positions = array.array("f")
|
|
||||||
positions.frombytes(f.read(vertex_count * 12))
|
|
||||||
normals = array.array("f")
|
|
||||||
normals.frombytes(f.read(vertex_count * 12))
|
|
||||||
indices = array.array("I")
|
|
||||||
indices.frombytes(f.read(index_count * 4))
|
|
||||||
masks = f.read(mask_count)
|
|
||||||
if sys.byteorder != "little":
|
|
||||||
positions.byteswap(); normals.byteswap(); indices.byteswap()
|
|
||||||
triangles = []
|
|
||||||
for i in range(0, index_count, 3):
|
|
||||||
if masks and masks[i // 3] != 0:
|
|
||||||
continue
|
|
||||||
a, b, c = indices[i:i + 3]
|
|
||||||
if max(a, b, c) >= surface_vertices:
|
|
||||||
continue
|
|
||||||
nz = (normals[a * 3 + 2] + normals[b * 3 + 2] + normals[c * 3 + 2]) / 3.0
|
|
||||||
if nz < 0.65:
|
|
||||||
continue
|
|
||||||
tri = tuple((positions[j * 3], positions[j * 3 + 1], positions[j * 3 + 2])
|
|
||||||
for j in (a, b, c))
|
|
||||||
triangles.append(tri)
|
|
||||||
if triangles:
|
|
||||||
lod = key[3]
|
|
||||||
extent = 32 * (1 << lod) * voxel_m
|
|
||||||
tiles.append({"key": key, "lod": lod, "extent": extent,
|
|
||||||
"x0": key[0] * extent, "y0": key[1] * extent,
|
|
||||||
"x1": (key[0] + 1) * extent, "y1": (key[1] + 1) * extent,
|
|
||||||
"z0": key[2] * extent, "z1": (key[2] + 1) * extent,
|
|
||||||
"triangles": triangles})
|
|
||||||
return camera, yaw, pitch, tiles
|
|
||||||
|
|
||||||
|
|
||||||
def height_at(tile, x, y):
|
|
||||||
best = None
|
|
||||||
for tri in tile["triangles"]:
|
|
||||||
(x0, y0, z0), (x1, y1, z1), (x2, y2, z2) = tri
|
|
||||||
den = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2)
|
|
||||||
if abs(den) < 1.0e-10:
|
|
||||||
continue
|
|
||||||
a = ((y1 - y2) * (x - x2) + (x2 - x1) * (y - y2)) / den
|
|
||||||
b = ((y2 - y0) * (x - x2) + (x0 - x2) * (y - y2)) / den
|
|
||||||
c = 1.0 - a - b
|
|
||||||
if min(a, b, c) >= -1.0e-5:
|
|
||||||
z = a * z0 + b * z1 + c * z2
|
|
||||||
if best is None or z > best:
|
|
||||||
best = z
|
|
||||||
return best
|
|
||||||
|
|
||||||
|
|
||||||
path = sys.argv[1]
|
|
||||||
camera, yaw, pitch, tiles = read_scene(path)
|
|
||||||
by_lod = collections.defaultdict(list)
|
|
||||||
for tile in tiles:
|
|
||||||
by_lod[tile["lod"]].append(tile)
|
|
||||||
print(f"scene={path} camera_m={camera} yaw={yaw} pitch={pitch} nonempty_mesh_tiles={len(tiles)}")
|
|
||||||
print("lod_pair shared_edges samples_both median_abs_cm p95_abs_cm max_abs_cm median_delta_cm gaps")
|
|
||||||
for lod in range(8):
|
|
||||||
fine = by_lod[lod]
|
|
||||||
coarse = by_lod[lod + 1]
|
|
||||||
deltas = []
|
|
||||||
edge_rows = []
|
|
||||||
edge_count = 0
|
|
||||||
gap_count = 0
|
|
||||||
# Shared XY edges occur only where tile extents touch and overlap on the other axis.
|
|
||||||
for a in fine:
|
|
||||||
for b in coarse:
|
|
||||||
zlo, zhi = max(a["z0"], b["z0"]), min(a["z1"], b["z1"])
|
|
||||||
if zhi - zlo <= 1.0e-4:
|
|
||||||
continue
|
|
||||||
edge = None
|
|
||||||
if abs(a["x1"] - b["x0"]) < 1.0e-4 or abs(b["x1"] - a["x0"]) < 1.0e-4:
|
|
||||||
lo, hi = max(a["y0"], b["y0"]), min(a["y1"], b["y1"])
|
|
||||||
if hi - lo > 1.0e-4:
|
|
||||||
edge = ("x", a["x1"] if abs(a["x1"] - b["x0"]) < 1.0e-4 else a["x0"], lo, hi)
|
|
||||||
if edge is None and (abs(a["y1"] - b["y0"]) < 1.0e-4 or abs(b["y1"] - a["y0"]) < 1.0e-4):
|
|
||||||
lo, hi = max(a["x0"], b["x0"]), min(a["x1"], b["x1"])
|
|
||||||
if hi - lo > 1.0e-4:
|
|
||||||
edge = ("y", a["y1"] if abs(a["y1"] - b["y0"]) < 1.0e-4 else a["y0"], lo, hi)
|
|
||||||
if edge is None:
|
|
||||||
continue
|
|
||||||
edge_count += 1
|
|
||||||
axis, fixed, lo, hi = edge
|
|
||||||
epsilon = min(0.02, a["extent"] * 0.001, b["extent"] * 0.001)
|
|
||||||
samples = 9
|
|
||||||
matched = 0
|
|
||||||
edge_deltas = []
|
|
||||||
for i in range(samples):
|
|
||||||
t = (i + 0.5) / samples
|
|
||||||
varying = lo + (hi - lo) * t
|
|
||||||
if axis == "x":
|
|
||||||
side_a = 1.0 if abs(a["x1"] - fixed) < 1.0e-4 else -1.0
|
|
||||||
side_b = 1.0 if abs(b["x1"] - fixed) < 1.0e-4 else -1.0
|
|
||||||
za = height_at(a, fixed - side_a * epsilon, varying)
|
|
||||||
zb = height_at(b, fixed - side_b * epsilon, varying)
|
|
||||||
else:
|
|
||||||
side_a = 1.0 if abs(a["y1"] - fixed) < 1.0e-4 else -1.0
|
|
||||||
side_b = 1.0 if abs(b["y1"] - fixed) < 1.0e-4 else -1.0
|
|
||||||
za = height_at(a, varying, fixed - side_a * epsilon)
|
|
||||||
zb = height_at(b, varying, fixed - side_b * epsilon)
|
|
||||||
if za is not None and not zlo - 1.0e-3 <= za <= zhi + 1.0e-3:
|
|
||||||
za = None
|
|
||||||
if zb is not None and not zlo - 1.0e-3 <= zb <= zhi + 1.0e-3:
|
|
||||||
zb = None
|
|
||||||
if za is not None and zb is not None:
|
|
||||||
delta = (za - zb) * 100.0
|
|
||||||
deltas.append(delta)
|
|
||||||
edge_deltas.append((delta, za, zb, varying))
|
|
||||||
matched += 1
|
|
||||||
if matched == 0:
|
|
||||||
gap_count += 1
|
|
||||||
elif edge_deltas:
|
|
||||||
edge_rows.append((max(abs(row[0]) for row in edge_deltas),
|
|
||||||
a["key"], b["key"], edge, edge_deltas))
|
|
||||||
if deltas:
|
|
||||||
absolute = sorted(abs(x) for x in deltas)
|
|
||||||
signed = sorted(deltas)
|
|
||||||
p95 = absolute[math.floor(0.95 * (len(absolute) - 1))]
|
|
||||||
median_abs = absolute[len(absolute) // 2]
|
|
||||||
median_delta = signed[len(signed) // 2]
|
|
||||||
print(f"{lod}->{lod+1} {edge_count} {len(deltas)} {median_abs:.3f} {p95:.3f} {max(absolute):.3f} {median_delta:.3f} {gap_count}")
|
|
||||||
for worst, fine_key, coarse_key, edge, rows in sorted(edge_rows, reverse=True)[:2]:
|
|
||||||
row = max(rows, key=lambda item: abs(item[0]))
|
|
||||||
print(f" worst_edge max_abs_cm={worst:.3f} fine={fine_key} coarse={coarse_key} "
|
|
||||||
f"axis={edge[0]} fixed_m={edge[1]:.3f} span_m={edge[2]:.3f}..{edge[3]:.3f} "
|
|
||||||
f"sample_v_m={row[3]:.3f} fine_z_m={row[1]:.3f} "
|
|
||||||
f"coarse_z_m={row[2]:.3f} delta_cm={row[0]:.3f}")
|
|
||||||
else:
|
|
||||||
print(f"{lod}->{lod+1} {edge_count} 0 n/a n/a n/a n/a {gap_count}")
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Run the L17 deterministic W1/W8/W12 x5 matrix on both supported Linux builds."""
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[4]
|
|
||||||
OUT = ROOT / "Saved/Linux/l17-matrix"
|
|
||||||
OUT.mkdir(parents=True, exist_ok=True)
|
|
||||||
if not os.sched_getaffinity(0).issubset(set(range(12))):
|
|
||||||
raise SystemExit("run under taskset -c 0-11")
|
|
||||||
|
|
||||||
BINARIES = {
|
|
||||||
"clang-off": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux17-final-clang",
|
|
||||||
"gcc-default": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux17-final-gcc",
|
|
||||||
}
|
|
||||||
RECIPES = {
|
|
||||||
"floating-islands": "-10,-31,-22,0,-8",
|
|
||||||
"surface-world": "0,-35,-75,0,0",
|
|
||||||
}
|
|
||||||
HASH_PATTERNS = {
|
|
||||||
"full": re.compile(r"^\[PipelineFullDetailHash\].*$", re.M),
|
|
||||||
"whole": re.compile(r"^\[PipelineWholeViewHash\].*$", re.M),
|
|
||||||
"lod": re.compile(r"^\[PipelineLODHash\].*$", re.M),
|
|
||||||
"exact": re.compile(r"^\[PipelineExactQueryHash\].*$", re.M),
|
|
||||||
}
|
|
||||||
rows = []
|
|
||||||
all_pass = True
|
|
||||||
|
|
||||||
for recipe, camera in RECIPES.items():
|
|
||||||
for compiler, binary in BINARIES.items():
|
|
||||||
if not binary.exists():
|
|
||||||
raise SystemExit(f"missing binary: {binary}")
|
|
||||||
for workers, repeat in [(1, 1), (8, 1), (12, 1), (12, 2), (12, 3), (12, 4), (12, 5)]:
|
|
||||||
name = f"{recipe}-{compiler}-w{workers}-r{repeat}"
|
|
||||||
logfile = OUT / f"{name}.log"
|
|
||||||
command = ["prlimit", "--as=6442450944", "--", str(binary),
|
|
||||||
str(ROOT / "Standalone/data/owner_world.json"),
|
|
||||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
|
||||||
"--stream-hash", "--force-generate", "--clip-radius=3",
|
|
||||||
"--max-clip-level=8", f"--workers={workers}", f"--camera={camera}",
|
|
||||||
f"--recipe={recipe}"]
|
|
||||||
started = time.monotonic()
|
|
||||||
peak_rss_kib = 0
|
|
||||||
with logfile.open("w", encoding="utf-8") as stream:
|
|
||||||
process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT,
|
|
||||||
start_new_session=True)
|
|
||||||
while process.poll() is None:
|
|
||||||
pending = [process.pid]
|
|
||||||
seen = set()
|
|
||||||
while pending:
|
|
||||||
pid = pending.pop()
|
|
||||||
if pid in seen:
|
|
||||||
continue
|
|
||||||
seen.add(pid)
|
|
||||||
try:
|
|
||||||
children = Path(f"/proc/{pid}/task/{pid}/children").read_text().split()
|
|
||||||
pending.extend(int(value) for value in children)
|
|
||||||
for line in Path(f"/proc/{pid}/status").read_text().splitlines():
|
|
||||||
if line.startswith("VmRSS:"):
|
|
||||||
peak_rss_kib = max(peak_rss_kib, int(line.split()[1]))
|
|
||||||
break
|
|
||||||
except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError):
|
|
||||||
pass
|
|
||||||
if time.monotonic() - started > 900:
|
|
||||||
os.killpg(process.pid, signal.SIGTERM)
|
|
||||||
try:
|
|
||||||
process.wait(timeout=5)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
os.killpg(process.pid, signal.SIGKILL)
|
|
||||||
process.wait()
|
|
||||||
all_pass = False
|
|
||||||
break
|
|
||||||
time.sleep(0.1)
|
|
||||||
code = process.wait()
|
|
||||||
elapsed = time.monotonic() - started
|
|
||||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
|
||||||
matched = {key: pattern.findall(output) for key, pattern in HASH_PATTERNS.items()}
|
|
||||||
ok = code == 0 and all(matched.values()) and len(matched["lod"]) == 9
|
|
||||||
if not ok:
|
|
||||||
all_pass = False
|
|
||||||
row = {"recipe": recipe, "compiler": compiler, "workers": workers,
|
|
||||||
"repeat": repeat, "exit": code, "seconds": elapsed,
|
|
||||||
"peak_rss_kib": peak_rss_kib, "ok": ok,
|
|
||||||
"full": matched["full"][-1] if matched["full"] else "",
|
|
||||||
"whole": matched["whole"][-1] if matched["whole"] else "",
|
|
||||||
"lod": " || ".join(matched["lod"]),
|
|
||||||
"exact": matched["exact"][-1] if matched["exact"] else ""}
|
|
||||||
rows.append(row)
|
|
||||||
print(f"[MatrixRun] {name} exit={code} seconds={elapsed:.3f} "
|
|
||||||
f"peak_rss_kib={peak_rss_kib} hash_rows={sum(map(len,matched.values()))} "
|
|
||||||
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
|
||||||
|
|
||||||
summary = OUT / "summary.tsv"
|
|
||||||
columns = ["recipe", "compiler", "workers", "repeat", "exit", "seconds", "peak_rss_kib",
|
|
||||||
"ok", "full", "whole", "lod", "exact"]
|
|
||||||
with summary.open("w", encoding="utf-8") as f:
|
|
||||||
f.write("\t".join(columns) + "\n")
|
|
||||||
for row in rows:
|
|
||||||
f.write("\t".join(str(row[key]) for key in columns) + "\n")
|
|
||||||
|
|
||||||
for recipe in RECIPES:
|
|
||||||
reference = None
|
|
||||||
for row in rows:
|
|
||||||
if row["recipe"] != recipe:
|
|
||||||
continue
|
|
||||||
signature = (row["full"], row["whole"], row["lod"], row["exact"])
|
|
||||||
if reference is None:
|
|
||||||
reference = signature
|
|
||||||
elif signature != reference:
|
|
||||||
print(f"[MatrixMismatch] recipe={recipe} workers={row['workers']} "
|
|
||||||
f"compiler={row['compiler']} repeat={row['repeat']}", flush=True)
|
|
||||||
all_pass = False
|
|
||||||
print(f"[MatrixSummary] rows={len(rows)} expected=28 result={'PASS' if all_pass else 'FAIL'} "
|
|
||||||
f"path={summary}", flush=True)
|
|
||||||
raise SystemExit(0 if all_pass and len(rows) == 28 else 1)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
camera_m=(-22.0, -19.0, -22.799999237060547) yaw=0.0 pitch=-4.0 boundary_band_m=-80.0..-45.0
|
|
||||||
lod tiles_with_boundary_faces faces z_m_p05/median/p95 tile_span_m_p50/p95/max
|
|
||||||
1 64 131072 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
|
||||||
2 40 20480 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
|
||||||
3 48 24576 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
|
||||||
4 48 22945 -60.0000/-60.0000/-60.0000 0.0000/0.9167/1.8008
|
|
||||||
5 48 19554 -60.0000/-60.0000/-60.0000 0.0000/1.0819/1.7900
|
|
||||||
6 9 134 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
|
||||||
nearest_xy_boundary_tiles_by_lod:
|
|
||||||
lod=1 key=(-2, -2, -4, 1) xy_distance_m=5.39 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=2048
|
|
||||||
lod=1 key=(-2, -1, -4, 1) xy_distance_m=11.18 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=2048
|
|
||||||
lod=1 key=(-1, -2, -4, 1) xy_distance_m=14.87 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=2048
|
|
||||||
lod=2 key=(-1, 1, -2, 2) xy_distance_m=67.27 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
|
||||||
lod=2 key=(1, -1, -2, 2) xy_distance_m=70.06 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
|
||||||
lod=2 key=(-2, 1, -2, 2) xy_distance_m=71.87 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
|
||||||
lod=3 key=(-3, -1, -1, 3) xy_distance_m=138.61 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
|
||||||
lod=3 key=(-1, -3, -1, 3) xy_distance_m=141.35 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
|
||||||
lod=3 key=(-3, 0, -1, 3) xy_distance_m=147.12 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
|
||||||
lod=4 key=(-3, -1, -1, 4) xy_distance_m=301.38 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
|
||||||
lod=4 key=(-1, -3, -1, 4) xy_distance_m=303.92 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
|
||||||
lod=4 key=(-3, 0, -1, 4) xy_distance_m=309.34 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
|
||||||
lod=5 key=(-3, -1, -1, 5) xy_distance_m=627.54 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=343
|
|
||||||
lod=5 key=(-1, -3, -1, 5) xy_distance_m=629.98 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=512
|
|
||||||
lod=5 key=(-3, 0, -1, 5) xy_distance_m=635.24 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.4903 faces=142
|
|
||||||
lod=6 key=(-3, 0, -1, 6) xy_distance_m=1287.71 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=2
|
|
||||||
lod=6 key=(0, -3, -1, 6) xy_distance_m=1291.28 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=16
|
|
||||||
lod=6 key=(1, 2, -1, 6) xy_distance_m=1520.36 boundary_median_z=-60.0000 within_tile_p05_p95_m=0.0000 faces=3
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
camera_m=(-22.0, -19.0, -22.799999237060547) yaw=0.0 pitch=-4.0 boundary_band_m=-80.0..-45.0
|
|
||||||
lod tiles_with_boundary_faces faces z_m_p05/median/p95 tile_span_m_p50/p95/max
|
|
||||||
1 65 144957 -60.4936/-60.4872/-59.5000 0.0059/0.9906/2.2272
|
|
||||||
2 75 29057 -76.1525/-61.3304/-60.7788 0.8725/2.9863/8.5642
|
|
||||||
3 87 26484 -73.0968/-63.0185/-62.4964 1.3211/11.6236/14.1057
|
|
||||||
4 48 25679 -72.6651/-62.8690/-62.2182 3.2371/15.9700/17.0106
|
|
||||||
5 48 26381 -67.4390/-62.7489/-61.8320 4.2721/11.9903/14.5467
|
|
||||||
6 14 390 -73.7183/-67.0059/-62.3980 4.0318/11.7718/12.7473
|
|
||||||
7 18 483 -74.7834/-71.1169/-67.5754 4.0317/7.3030/8.1412
|
|
||||||
8 22 176 -75.3669/-69.9656/-63.4334 3.5327/3.5370/13.7478
|
|
||||||
nearest_xy_boundary_tiles_by_lod:
|
|
||||||
lod=1 key=(-2, -2, -4, 1) xy_distance_m=5.39 boundary_median_z=-60.4712 within_tile_p05_p95_m=0.0177 faces=2048
|
|
||||||
lod=1 key=(-2, -1, -4, 1) xy_distance_m=11.18 boundary_median_z=-60.4718 within_tile_p05_p95_m=0.0158 faces=2048
|
|
||||||
lod=1 key=(-1, -2, -4, 1) xy_distance_m=14.87 boundary_median_z=-60.4550 within_tile_p05_p95_m=0.0212 faces=2048
|
|
||||||
lod=2 key=(-1, 1, -2, 2) xy_distance_m=67.27 boundary_median_z=-60.7421 within_tile_p05_p95_m=0.2955 faces=496
|
|
||||||
lod=2 key=(1, -1, -3, 2) xy_distance_m=70.06 boundary_median_z=-74.2743 within_tile_p05_p95_m=2.9398 faces=97
|
|
||||||
lod=2 key=(1, -1, -2, 2) xy_distance_m=70.06 boundary_median_z=-60.7387 within_tile_p05_p95_m=1.2725 faces=496
|
|
||||||
lod=3 key=(-3, -1, -1, 3) xy_distance_m=138.61 boundary_median_z=-62.5408 within_tile_p05_p95_m=0.3465 faces=512
|
|
||||||
lod=3 key=(-1, -3, -1, 3) xy_distance_m=141.35 boundary_median_z=-62.5378 within_tile_p05_p95_m=1.6699 faces=512
|
|
||||||
lod=3 key=(-3, 0, -1, 3) xy_distance_m=147.12 boundary_median_z=-62.5837 within_tile_p05_p95_m=0.3165 faces=512
|
|
||||||
lod=4 key=(-3, -1, -1, 4) xy_distance_m=301.38 boundary_median_z=-62.3784 within_tile_p05_p95_m=4.5901 faces=546
|
|
||||||
lod=4 key=(-1, -3, -1, 4) xy_distance_m=303.92 boundary_median_z=-62.3534 within_tile_p05_p95_m=10.4664 faces=596
|
|
||||||
lod=4 key=(-3, 0, -1, 4) xy_distance_m=309.34 boundary_median_z=-62.3696 within_tile_p05_p95_m=6.1327 faces=544
|
|
||||||
lod=5 key=(-3, -1, -1, 5) xy_distance_m=627.54 boundary_median_z=-62.0651 within_tile_p05_p95_m=9.5826 faces=523
|
|
||||||
lod=5 key=(-1, -3, -1, 5) xy_distance_m=629.98 boundary_median_z=-61.9414 within_tile_p05_p95_m=2.3891 faces=526
|
|
||||||
lod=5 key=(-3, 0, -1, 5) xy_distance_m=635.24 boundary_median_z=-62.0996 within_tile_p05_p95_m=9.3777 faces=567
|
|
||||||
lod=6 key=(-3, 0, -1, 6) xy_distance_m=1287.71 boundary_median_z=-69.5358 within_tile_p05_p95_m=4.1243 faces=26
|
|
||||||
lod=6 key=(0, -3, -1, 6) xy_distance_m=1291.28 boundary_median_z=-70.4254 within_tile_p05_p95_m=12.7473 faces=41
|
|
||||||
lod=6 key=(0, 2, -1, 6) xy_distance_m=1328.41 boundary_median_z=-63.9805 within_tile_p05_p95_m=0.0000 faces=1
|
|
||||||
lod=7 key=(0, -3, -1, 7) xy_distance_m=2596.50 boundary_median_z=-70.2015 within_tile_p05_p95_m=4.1663 faces=36
|
|
||||||
lod=7 key=(2, -1, -1, 7) xy_distance_m=2628.64 boundary_median_z=-70.1072 within_tile_p05_p95_m=3.5366 faces=8
|
|
||||||
lod=7 key=(-3, 1, -1, 7) xy_distance_m=2976.49 boundary_median_z=-70.9568 within_tile_p05_p95_m=4.8223 faces=24
|
|
||||||
lod=8 key=(-1, 2, -1, 8) xy_distance_m=5235.77 boundary_median_z=-65.8628 within_tile_p05_p95_m=3.5343 faces=8
|
|
||||||
lod=8 key=(-1, -4, -1, 8) xy_distance_m=7218.88 boundary_median_z=-71.4663 within_tile_p05_p95_m=3.5332 faces=8
|
|
||||||
lod=8 key=(-4, 0, -1, 8) xy_distance_m=7221.71 boundary_median_z=-70.6999 within_tile_p05_p95_m=3.5335 faces=8
|
|
||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB |
@@ -1,13 +0,0 @@
|
|||||||
version=1 voxel_um=250000 tiles=2370 camera_m=(-22.0, -19.0, -22.799999237060547) yaw=0.0 pitch=-4.0
|
|
||||||
lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max
|
|
||||||
0 512 15 30819 54860 9176 12102 -29.7482/-28.0275/-25.2555 0.5335/1.1856/1.9265
|
|
||||||
1 448 64 94272 163840 32768 131072 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
|
||||||
2 320 40 28476 45376 15296 20480 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
|
||||||
3 208 48 58599 93736 30968 24576 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
|
||||||
4 240 48 105724 164240 27638 22945 -60.0000/-60.0000/-60.0000 0.0000/0.9167/1.8008
|
|
||||||
5 144 96 96543 156615 22706 109798 -506.3981/-502.5195/-60.0000 1.7900/110.1491/138.3581
|
|
||||||
6 96 48 114212 157583 28194 91871 -506.2913/-503.3460/-411.9775 78.1123/124.2575/136.4059
|
|
||||||
7 96 48 142940 234358 31690 95029 -506.1172/-503.3633/-412.8203 90.7064/105.1855/106.9902
|
|
||||||
8 306 153 290960 421667 73118 147296 -505.8558/-503.1657/-422.5029 82.1507/93.3314/110.2594
|
|
||||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
|
||||||
key=(-2, -2, -4, 1) xy_distance_m=5.39 vertices=1473 triangles=2560 skirt_triangles=512 upward_triangles=2048 z_m=-60.000/-60.000/-60.000 tile_span_p05_p95_m=0.000
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
version=1 voxel_um=250000 tiles=2370 camera_m=(-22.0, -19.0, -22.799999237060547) yaw=0.0 pitch=-4.0
|
|
||||||
lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max
|
|
||||||
0 512 15 30819 54860 9176 12102 -29.7482/-28.0275/-25.2555 0.5335/1.1856/1.9265
|
|
||||||
1 448 64 94272 163840 32768 131072 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
|
||||||
2 320 40 28476 45376 15296 20480 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
|
||||||
3 208 48 58599 93736 30968 24576 -60.0000/-60.0000/-60.0000 0.0000/0.0000/0.0000
|
|
||||||
4 240 48 105724 164240 27638 22945 -60.0000/-60.0000/-60.0000 0.0000/0.9167/1.8008
|
|
||||||
5 144 96 96543 156615 22706 109798 -506.3981/-502.5195/-60.0000 1.7900/110.1491/138.3581
|
|
||||||
6 96 48 114212 157583 28194 91871 -506.2913/-503.3460/-411.9775 78.1123/124.2575/136.4059
|
|
||||||
7 96 48 142940 234358 31690 95029 -506.1172/-503.3633/-412.8203 90.7064/105.1855/106.9902
|
|
||||||
8 306 153 290960 421667 73118 147296 -505.8558/-503.1657/-422.5029 82.1507/93.3314/110.2594
|
|
||||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
|
||||||
key=(-2, -2, -4, 1) xy_distance_m=5.39 vertices=1473 triangles=2560 skirt_triangles=512 upward_triangles=2048 z_m=-60.000/-60.000/-60.000 tile_span_p05_p95_m=0.000
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
version=1 voxel_um=250000 tiles=2370 camera_m=(-22.0, -19.0, -22.799999237060547) yaw=0.0 pitch=-4.0
|
|
||||||
lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max
|
|
||||||
0 512 143 588792 892284 293148 300091 3.5000/7.6738/8.2568 0.4215/4.4898/4.4913
|
|
||||||
1 448 119 509571 954666 323268 265175 -60.4932/-60.4491/8.1722 0.3356/0.9926/4.2986
|
|
||||||
2 320 130 93432 150346 51220 55675 -76.2021/-60.8983/8.0124 0.3035/2.9398/8.5642
|
|
||||||
3 208 135 91227 146168 33040 51843 -70.7997/-62.4979/8.0125 1.0249/11.2411/19.6108
|
|
||||||
4 240 103 173336 278284 61076 58421 -505.3757/-411.1270/-62.3657 8.2807/88.3431/118.1829
|
|
||||||
5 144 96 137096 173158 40930 117557 -506.3330/-502.1182/-62.4087 10.4471/110.1491/138.5524
|
|
||||||
6 96 48 151997 168987 43320 92193 -506.3014/-503.3398/-410.9469 78.1123/124.2575/136.4059
|
|
||||||
7 96 96 236120 321403 61348 120078 -505.8770/-502.4482/8.0020 0.0000/103.3187/110.4079
|
|
||||||
8 306 306 553491 741221 166096 225866 -505.4486/-472.2318/8.0020 0.0000/92.1234/110.0400
|
|
||||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
|
||||||
key=(-3, -3, 0, 0) xy_distance_m=2.24 vertices=8058 triangles=11660 skirt_triangles=3796 upward_triangles=3768 z_m=3.500/3.500/7.999 tile_span_p05_p95_m=4.445
|
|
||||||
key=(-3, -3, 1, 0) xy_distance_m=2.24 vertices=668 triangles=1052 skirt_triangles=376 upward_triangles=676 z_m=8.000/8.087/8.252 tile_span_p05_p95_m=0.203
|
|
||||||
key=(-2, -2, -4, 1) xy_distance_m=5.39 vertices=2562 triangles=8192 skirt_triangles=2048 upward_triangles=2048 z_m=-60.480/-60.471/-60.457 tile_span_p05_p95_m=0.018
|
|
||||||
key=(-4, -3, 0, 0) xy_distance_m=6.08 vertices=7502 triangles=10753 skirt_triangles=3764 upward_triangles=2893 z_m=3.500/3.500/8.000 tile_span_p05_p95_m=4.486
|
|
||||||
key=(-4, -3, 1, 0) xy_distance_m=6.08 vertices=1617 triangles=2507 skirt_triangles=928 upward_triangles=1579 z_m=8.000/8.099/8.372 tile_span_p05_p95_m=0.248
|
|
||||||
key=(-3, -2, 0, 0) xy_distance_m=7.28 vertices=7772 triangles=10994 skirt_triangles=4024 upward_triangles=2874 z_m=3.500/3.500/8.000 tile_span_p05_p95_m=4.491
|
|
||||||
key=(-3, -2, 1, 0) xy_distance_m=7.28 vertices=1883 triangles=2904 skirt_triangles=1116 upward_triangles=1788 z_m=8.000/8.115/8.363 tile_span_p05_p95_m=0.280
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
version=1 voxel_um=250000 tiles=2370 camera_m=(-11.612443923950195, -118.22123718261719, -19.84520721435547) yaw=0.0 pitch=0.0
|
|
||||||
lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max
|
|
||||||
0 512 128 579561 840946 290676 288126 3.5000/7.7495/8.2586 0.3781/4.4867/4.4932
|
|
||||||
1 448 124 503677 964366 331082 258850 -63.8505/-60.4908/8.1722 0.2965/2.9282/5.7538
|
|
||||||
2 320 127 83460 135059 46994 53146 -71.5347/-60.5904/8.0124 0.5224/2.7093/6.0349
|
|
||||||
3 208 126 93575 151669 35042 52200 -75.6995/-61.9814/8.0125 0.5630/13.5938/55.4526
|
|
||||||
4 240 103 172283 277754 60622 58086 -505.4072/-410.6667/-62.3188 6.7752/88.3431/118.1829
|
|
||||||
5 144 96 137096 173158 40930 117557 -506.3330/-502.1182/-62.4087 10.4471/110.1491/138.5524
|
|
||||||
6 96 48 151997 168987 43320 92193 -506.3014/-503.3398/-410.9469 78.1123/124.2575/136.4059
|
|
||||||
7 96 96 236120 321403 61348 120078 -505.8770/-502.4482/8.0020 0.0000/103.3187/110.4079
|
|
||||||
8 306 306 553491 741221 166096 225866 -505.4486/-472.2318/8.0020 0.0000/92.1234/110.0400
|
|
||||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
|
||||||
key=(-2, -15, 0, 0) xy_distance_m=2.25 vertices=7941 triangles=11569 skirt_triangles=3860 upward_triangles=3613 z_m=3.500/3.500/8.000 tile_span_p05_p95_m=4.472
|
|
||||||
key=(-2, -15, 1, 0) xy_distance_m=2.25 vertices=1159 triangles=1557 skirt_triangles=684 upward_triangles=873 z_m=8.000/8.082/8.275 tile_span_p05_p95_m=0.250
|
|
||||||
key=(-1, -8, -4, 1) xy_distance_m=4.03 vertices=2562 triangles=8192 skirt_triangles=2048 upward_triangles=2048 z_m=-60.494/-60.494/-60.493 tile_span_p05_p95_m=0.001
|
|
||||||
key=(-2, -16, 0, 0) xy_distance_m=5.79 vertices=8315 triangles=12274 skirt_triangles=3928 upward_triangles=4250 z_m=3.500/7.740/8.000 tile_span_p05_p95_m=4.459
|
|
||||||
key=(-2, -16, 1, 0) xy_distance_m=5.79 vertices=677 triangles=700 skirt_triangles=520 upward_triangles=180 z_m=8.000/8.008/8.057 tile_span_p05_p95_m=0.036
|
|
||||||
key=(-1, -15, 0, 0) xy_distance_m=7.93 vertices=6918 triangles=9878 skirt_triangles=3384 upward_triangles=2398 z_m=3.500/3.500/8.000 tile_span_p05_p95_m=4.453
|
|
||||||
key=(-1, -15, 1, 0) xy_distance_m=7.93 vertices=1611 triangles=2700 skirt_triangles=652 upward_triangles=2048 z_m=8.001/8.202/8.323 tile_span_p05_p95_m=0.242
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
version=1 voxel_um=250000 tiles=2553 camera_m=(0.0, -35.0, -487.94000244140625) yaw=0.0 pitch=-22.0
|
|
||||||
lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max
|
|
||||||
0 512 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
|
||||||
1 448 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
|
||||||
2 448 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
|
||||||
3 384 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
|
||||||
4 320 52 139437 209316 34996 8129 -56.2002/-42.7865/-32.7998 6.2138/32.5482/53.6770
|
|
||||||
5 144 102 100254 158707 25586 96013 -506.9092/-503.1813/-378.0397 12.8110/109.5153/138.3581
|
|
||||||
6 96 61 110399 149031 17088 92803 -506.4873/-503.3776/-412.3926 53.2054/124.2575/136.4059
|
|
||||||
7 48 48 132222 219052 16356 95003 -506.1172/-503.3639/-412.8861 90.7064/105.1855/106.9902
|
|
||||||
8 153 153 229080 384768 82518 147226 -505.8568/-503.1676/-422.8789 82.0687/93.3180/110.0400
|
|
||||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
version=1 voxel_um=250000 tiles=960 camera_m=(-11.612443923950195, -118.22123718261719, -506.7861328125) yaw=0.0 pitch=0.0
|
|
||||||
lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max
|
|
||||||
0 512 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
|
||||||
1 448 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
|
||||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
version=1 voxel_um=250000 tiles=960 camera_m=(0.0, -35.0, -75.0) yaw=0.0 pitch=0.0
|
|
||||||
lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max
|
|
||||||
0 512 0 0 0 0 0 nan/nan/nan nan/nan/nan
|
|
||||||
1 448 76 153596 265964 51168 82938 -43.9418/-14.9892/-6.3079 0.8803/5.3022/10.8345
|
|
||||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
version=1 voxel_um=250000 tiles=960 camera_m=(0.0, -35.0, -42.0) yaw=0.0 pitch=-8.0
|
|
||||||
lod tiles mesh_tiles vertices triangles skirt_triangles upward_surface_triangles upward_centroid_z_m_p05/median/p95 tile_up_z_span_m_p50/p95/max
|
|
||||||
0 512 25 80260 139176 28088 34692 -43.9640/-33.5509/-32.8013 0.7072/4.9851/7.0117
|
|
||||||
1 448 131 295583 474122 103438 230953 -33.6783/3.9507/4.1428 0.3514/4.4247/10.8345
|
|
||||||
near_camera_up_tiles (sorted by XY tile-centre distance):
|
|
||||||
@@ -25,8 +25,6 @@ namespace
|
|||||||
float Position[3];
|
float Position[3];
|
||||||
float Normal[3];
|
float Normal[3];
|
||||||
float Height;
|
float Height;
|
||||||
float WorldPosition[3];
|
|
||||||
float TileData[4];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FScene
|
struct FScene
|
||||||
@@ -34,8 +32,6 @@ namespace
|
|||||||
std::uint32_t VoxelSizeMicrons=0;
|
std::uint32_t VoxelSizeMicrons=0;
|
||||||
std::array<double,3> CameraMeters{};
|
std::array<double,3> CameraMeters{};
|
||||||
double YawDegrees=0.0,PitchDegrees=0.0;
|
double YawDegrees=0.0,PitchDegrees=0.0;
|
||||||
float MinimumHeight=std::numeric_limits<float>::infinity();
|
|
||||||
float MaximumHeight=-std::numeric_limits<float>::infinity();
|
|
||||||
std::vector<FVertex> Vertices;
|
std::vector<FVertex> Vertices;
|
||||||
std::vector<std::uint32_t> Indices;
|
std::vector<std::uint32_t> Indices;
|
||||||
std::uint64_t Triangles=0,SkirtTriangles=0;
|
std::uint64_t Triangles=0,SkirtTriangles=0;
|
||||||
@@ -74,8 +70,7 @@ namespace
|
|||||||
if(!Read(File,SurfaceVertices)||!Read(File,SkirtCount)||!Read(File,VertexCount)
|
if(!Read(File,SurfaceVertices)||!Read(File,SkirtCount)||!Read(File,VertexCount)
|
||||||
||!Read(File,IndexCount)||!Read(File,MaskCount)
|
||!Read(File,IndexCount)||!Read(File,MaskCount)
|
||||||
||VertexCount>10000000||IndexCount>60000000||MaskCount>20000000
|
||VertexCount>10000000||IndexCount>60000000||MaskCount>20000000
|
||||||
||IndexCount%3!=0||(MaskCount!=0&&MaskCount!=IndexCount/3)
|
||IndexCount%3!=0||(MaskCount!=0&&MaskCount!=IndexCount/3))
|
||||||
||Key[3]<0||Key[3]>8)
|
|
||||||
{Error="tile mesh header is malformed";return false;}
|
{Error="tile mesh header is malformed";return false;}
|
||||||
if(TotalVertices+VertexCount>std::numeric_limits<std::uint32_t>::max()
|
if(TotalVertices+VertexCount>std::numeric_limits<std::uint32_t>::max()
|
||||||
||TotalIndices+IndexCount>std::numeric_limits<std::size_t>::max())
|
||TotalIndices+IndexCount>std::numeric_limits<std::size_t>::max())
|
||||||
@@ -91,25 +86,6 @@ namespace
|
|||||||
for(float& Value:Scene.Vertices[I].Normal)if(!Read(File,Value))
|
for(float& Value:Scene.Vertices[I].Normal)if(!Read(File,Value))
|
||||||
{Error="tile normals are truncated";return false;}
|
{Error="tile normals are truncated";return false;}
|
||||||
Scene.Vertices[I].Height=Scene.Vertices[I].Position[2];
|
Scene.Vertices[I].Height=Scene.Vertices[I].Position[2];
|
||||||
Scene.MinimumHeight=std::min(Scene.MinimumHeight,Scene.Vertices[I].Height);
|
|
||||||
Scene.MaximumHeight=std::max(Scene.MaximumHeight,Scene.Vertices[I].Height);
|
|
||||||
}
|
|
||||||
const double TileVoxels=static_cast<double>(32u<<Key[3]);
|
|
||||||
const float TileExtentMeters=static_cast<float>(TileVoxels
|
|
||||||
*static_cast<double>(Scene.VoxelSizeMicrons)/1000000.0);
|
|
||||||
const float TileOriginX=static_cast<float>(static_cast<double>(Key[0])
|
|
||||||
*TileExtentMeters);
|
|
||||||
const float TileOriginY=static_cast<float>(static_cast<double>(Key[1])
|
|
||||||
*TileExtentMeters);
|
|
||||||
for(std::size_t I=Base;I<Base+VertexCount;++I)
|
|
||||||
{
|
|
||||||
FVertex& Vertex=Scene.Vertices[I];
|
|
||||||
std::copy(std::begin(Vertex.Position),std::end(Vertex.Position),
|
|
||||||
std::begin(Vertex.WorldPosition));
|
|
||||||
Vertex.TileData[0]=TileOriginX;
|
|
||||||
Vertex.TileData[1]=TileOriginY;
|
|
||||||
Vertex.TileData[2]=TileExtentMeters;
|
|
||||||
Vertex.TileData[3]=static_cast<float>(Key[3]);
|
|
||||||
}
|
}
|
||||||
const std::size_t IndexOffset=Scene.Indices.size();
|
const std::size_t IndexOffset=Scene.Indices.size();
|
||||||
Scene.Indices.resize(IndexOffset+IndexCount);
|
Scene.Indices.resize(IndexOffset+IndexCount);
|
||||||
@@ -175,12 +151,8 @@ precision highp float;
|
|||||||
layout(location=0) in vec3 inCameraPosition;
|
layout(location=0) in vec3 inCameraPosition;
|
||||||
layout(location=1) in vec3 inNormal;
|
layout(location=1) in vec3 inNormal;
|
||||||
layout(location=2) in float inHeight;
|
layout(location=2) in float inHeight;
|
||||||
layout(location=3) in vec3 inWorldPosition;
|
|
||||||
layout(location=4) in vec4 inTileData;
|
|
||||||
out vec3 vNormal;
|
out vec3 vNormal;
|
||||||
out float vHeight;
|
out float vHeight;
|
||||||
out vec3 vWorldPosition;
|
|
||||||
out vec4 vTileData;
|
|
||||||
uniform float uTanHalfHorizontal;
|
uniform float uTanHalfHorizontal;
|
||||||
uniform float uTanHalfVertical;
|
uniform float uTanHalfVertical;
|
||||||
uniform float uNear;
|
uniform float uNear;
|
||||||
@@ -193,54 +165,19 @@ void main()
|
|||||||
inCameraPosition.y/uTanHalfVertical,clipZ,z);
|
inCameraPosition.y/uTanHalfVertical,clipZ,z);
|
||||||
vNormal=normalize(inNormal);
|
vNormal=normalize(inNormal);
|
||||||
vHeight=inHeight;
|
vHeight=inHeight;
|
||||||
vWorldPosition=inWorldPosition;
|
|
||||||
vTileData=inTileData;
|
|
||||||
})GLSL";
|
})GLSL";
|
||||||
static constexpr char FragmentSource[]=R"GLSL(#version 300 es
|
static constexpr char FragmentSource[]=R"GLSL(#version 300 es
|
||||||
precision highp float;
|
precision highp float;
|
||||||
in vec3 vNormal;
|
in vec3 vNormal;
|
||||||
in float vHeight;
|
in float vHeight;
|
||||||
in vec3 vWorldPosition;
|
|
||||||
in vec4 vTileData;
|
|
||||||
out vec4 outColor;
|
out vec4 outColor;
|
||||||
uniform int uReadableMode;
|
|
||||||
uniform int uTileEdges;
|
|
||||||
uniform float uHeightCenter;
|
|
||||||
uniform float uHeightRange;
|
|
||||||
uniform float uTileEdgeWidth;
|
|
||||||
uniform vec3 uLightDirection;
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
vec3 n=normalize(vNormal);
|
vec3 n=normalize(vNormal);
|
||||||
vec3 color;
|
|
||||||
if(uReadableMode==0)
|
|
||||||
{
|
|
||||||
vec3 normalColor=abs(n)*0.78+vec3(0.12);
|
vec3 normalColor=abs(n)*0.78+vec3(0.12);
|
||||||
float band=mod(floor((vHeight+10000.0)/80.0),2.0);
|
float band=mod(floor((vHeight+10000.0)/80.0),2.0);
|
||||||
vec3 bandColor=mix(vec3(0.24,0.31,0.37),vec3(0.39,0.42,0.35),band);
|
vec3 bandColor=mix(vec3(0.24,0.31,0.37),vec3(0.39,0.42,0.35),band);
|
||||||
color=mix(bandColor,normalColor,0.68);
|
outColor=vec4(mix(bandColor,normalColor,0.68),1.0);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
float heightT=clamp(0.5+(vHeight-uHeightCenter)/max(uHeightRange,0.001),0.0,1.0);
|
|
||||||
vec3 heightTint=mix(vec3(0.80,0.91,1.0),vec3(1.0,0.91,0.77),heightT);
|
|
||||||
float diffuse=max(dot(n,normalize(uLightDirection)),0.0);
|
|
||||||
float illumination=0.62+0.38*diffuse;
|
|
||||||
color=vec3(0.64,0.62,0.56)*heightTint*illumination;
|
|
||||||
}
|
|
||||||
if(uTileEdges!=0&&vTileData.z>0.0)
|
|
||||||
{
|
|
||||||
vec2 local=mod(vWorldPosition.xy-vTileData.xy,vTileData.z);
|
|
||||||
vec2 edge=min(local,vec2(vTileData.z)-local);
|
|
||||||
float distanceToTileEdge=min(edge.x,edge.y);
|
|
||||||
float antialias=max(fwidth(distanceToTileEdge),0.005);
|
|
||||||
float line=1.0-smoothstep(uTileEdgeWidth-antialias,
|
|
||||||
uTileEdgeWidth+antialias,distanceToTileEdge);
|
|
||||||
vec3 edgeColor=mix(vec3(0.16,0.22,0.25),vec3(0.30,0.23,0.14),
|
|
||||||
clamp(vTileData.w/8.0,0.0,1.0));
|
|
||||||
color=mix(color,edgeColor,0.32*line);
|
|
||||||
}
|
|
||||||
outColor=vec4(color,1.0);
|
|
||||||
})GLSL";
|
})GLSL";
|
||||||
const GLuint VS=CompileShader(GL_VERTEX_SHADER,VertexSource);
|
const GLuint VS=CompileShader(GL_VERTEX_SHADER,VertexSource);
|
||||||
const GLuint FS=CompileShader(GL_FRAGMENT_SHADER,FragmentSource);
|
const GLuint FS=CompileShader(GL_FRAGMENT_SHADER,FragmentSource);
|
||||||
@@ -287,8 +224,7 @@ void main()
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height,
|
bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height,
|
||||||
double HorizontalFovDegrees,bool ReadableMode,bool TileEdges,
|
double HorizontalFovDegrees)
|
||||||
double TileEdgeWidthMeters)
|
|
||||||
{
|
{
|
||||||
EGLDisplay Display=MakeDisplay();
|
EGLDisplay Display=MakeDisplay();
|
||||||
EGLint Major=0,Minor=0;
|
EGLint Major=0,Minor=0;
|
||||||
@@ -368,10 +304,6 @@ void main()
|
|||||||
reinterpret_cast<void*>(offsetof(FVertex,Normal)));
|
reinterpret_cast<void*>(offsetof(FVertex,Normal)));
|
||||||
glEnableVertexAttribArray(2);glVertexAttribPointer(2,1,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
glEnableVertexAttribArray(2);glVertexAttribPointer(2,1,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||||
reinterpret_cast<void*>(offsetof(FVertex,Height)));
|
reinterpret_cast<void*>(offsetof(FVertex,Height)));
|
||||||
glEnableVertexAttribArray(3);glVertexAttribPointer(3,3,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
|
||||||
reinterpret_cast<void*>(offsetof(FVertex,WorldPosition)));
|
|
||||||
glEnableVertexAttribArray(4);glVertexAttribPointer(4,4,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
|
||||||
reinterpret_cast<void*>(offsetof(FVertex,TileData)));
|
|
||||||
glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST);
|
glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST);
|
||||||
glDepthFunc(GL_LEQUAL);glClearColor(0.035f,0.055f,0.075f,1.0f);
|
glDepthFunc(GL_LEQUAL);glClearColor(0.035f,0.055f,0.075f,1.0f);
|
||||||
glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
|
glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
|
||||||
@@ -380,14 +312,6 @@ void main()
|
|||||||
glUniform1f(glGetUniformLocation(Program,"uTanHalfVertical"),static_cast<float>(TanHalfVertical));
|
glUniform1f(glGetUniformLocation(Program,"uTanHalfVertical"),static_cast<float>(TanHalfVertical));
|
||||||
glUniform1f(glGetUniformLocation(Program,"uNear"),static_cast<float>(Near));
|
glUniform1f(glGetUniformLocation(Program,"uNear"),static_cast<float>(Near));
|
||||||
glUniform1f(glGetUniformLocation(Program,"uFar"),static_cast<float>(Far));
|
glUniform1f(glGetUniformLocation(Program,"uFar"),static_cast<float>(Far));
|
||||||
glUniform1i(glGetUniformLocation(Program,"uReadableMode"),ReadableMode?1:0);
|
|
||||||
glUniform1i(glGetUniformLocation(Program,"uTileEdges"),TileEdges?1:0);
|
|
||||||
glUniform1f(glGetUniformLocation(Program,"uHeightCenter"),
|
|
||||||
static_cast<float>(Scene.CameraMeters[2]));
|
|
||||||
glUniform1f(glGetUniformLocation(Program,"uHeightRange"),64.0f);
|
|
||||||
glUniform1f(glGetUniformLocation(Program,"uTileEdgeWidth"),
|
|
||||||
static_cast<float>(TileEdgeWidthMeters));
|
|
||||||
glUniform3f(glGetUniformLocation(Program,"uLightDirection"),-0.42f,-0.58f,0.70f);
|
|
||||||
glDrawElements(GL_TRIANGLES,static_cast<GLsizei>(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr);
|
glDrawElements(GL_TRIANGLES,static_cast<GLsizei>(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr);
|
||||||
glFinish();
|
glFinish();
|
||||||
std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4);
|
std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4);
|
||||||
@@ -399,10 +323,9 @@ void main()
|
|||||||
{
|
{
|
||||||
std::error_code SizeError;
|
std::error_code SizeError;
|
||||||
const auto Bytes=std::filesystem::file_size(Output,SizeError);
|
const auto Bytes=std::filesystem::file_size(Output,SizeError);
|
||||||
std::printf("[OffscreenCapture] triangles=%llu tiles=%u skirts=%llu shading=%s tile_edges=%s edge_width_m=%.3f png_bytes=%llu status=PASS path=%s\n",
|
std::printf("[OffscreenCapture] triangles=%llu tiles=%u skirts=%llu png_bytes=%llu status=PASS path=%s\n",
|
||||||
static_cast<unsigned long long>(Scene.Triangles),Scene.TileCount,
|
static_cast<unsigned long long>(Scene.Triangles),Scene.TileCount,
|
||||||
static_cast<unsigned long long>(Scene.SkirtTriangles),
|
static_cast<unsigned long long>(Scene.SkirtTriangles),
|
||||||
ReadableMode?"readable":"normal",TileEdges?"on":"off",TileEdgeWidthMeters,
|
|
||||||
static_cast<unsigned long long>(SizeError?0:Bytes),Output.string().c_str());
|
static_cast<unsigned long long>(SizeError?0:Bytes),Output.string().c_str());
|
||||||
}
|
}
|
||||||
glDeleteProgram(Program);glDeleteBuffers(1,&Ebo);glDeleteBuffers(1,&Vbo);
|
glDeleteProgram(Program);glDeleteBuffers(1,&Ebo);glDeleteBuffers(1,&Vbo);
|
||||||
@@ -418,13 +341,12 @@ int main(int argc,char** argv)
|
|||||||
{
|
{
|
||||||
if(argc<3)
|
if(argc<3)
|
||||||
{
|
{
|
||||||
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--tile-edges=on|off] [--tile-edge-width-m=N]\n",argv[0]);
|
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N]\n",argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
int Width=2560,Height=1440;double Fov=90.0;
|
int Width=2560,Height=1440;double Fov=90.0;
|
||||||
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
||||||
bool HasCamera=false;std::array<double,3> Camera{};
|
bool HasCamera=false;std::array<double,3> Camera{};
|
||||||
bool ReadableMode=true,TileEdges=false;double TileEdgeWidthMeters=0.15;
|
|
||||||
for(int I=3;I<argc;++I)
|
for(int I=3;I<argc;++I)
|
||||||
{
|
{
|
||||||
const std::string_view Arg(argv[I]);
|
const std::string_view Arg(argv[I]);
|
||||||
@@ -458,28 +380,6 @@ int main(int argc,char** argv)
|
|||||||
{std::fprintf(stderr,"Invalid camera angle\n");return 2;}
|
{std::fprintf(stderr,"Invalid camera angle\n");return 2;}
|
||||||
if(IsYaw){Yaw=Parsed;HasYaw=true;}else{Pitch=Parsed;HasPitch=true;}
|
if(IsYaw){Yaw=Parsed;HasYaw=true;}else{Pitch=Parsed;HasPitch=true;}
|
||||||
}
|
}
|
||||||
else if(Arg.starts_with("--shade="))
|
|
||||||
{
|
|
||||||
const std::string_view Mode=Arg.substr(8);
|
|
||||||
if(Mode=="readable")ReadableMode=true;
|
|
||||||
else if(Mode=="normal")ReadableMode=false;
|
|
||||||
else {std::fprintf(stderr,"Invalid shading mode\n");return 2;}
|
|
||||||
}
|
|
||||||
else if(Arg.starts_with("--tile-edges="))
|
|
||||||
{
|
|
||||||
const std::string_view Mode=Arg.substr(13);
|
|
||||||
if(Mode=="on")TileEdges=true;
|
|
||||||
else if(Mode=="off")TileEdges=false;
|
|
||||||
else {std::fprintf(stderr,"Invalid tile-edge mode\n");return 2;}
|
|
||||||
}
|
|
||||||
else if(Arg.starts_with("--tile-edge-width-m="))
|
|
||||||
{
|
|
||||||
char* End=nullptr;const std::string Value(Arg.substr(20));
|
|
||||||
TileEdgeWidthMeters=std::strtod(Value.c_str(),&End);
|
|
||||||
if(End==Value.c_str()||*End!='\0'||!std::isfinite(TileEdgeWidthMeters)
|
|
||||||
||TileEdgeWidthMeters<=0.0||TileEdgeWidthMeters>5.0)
|
|
||||||
{std::fprintf(stderr,"Invalid tile-edge width\n");return 2;}
|
|
||||||
}
|
|
||||||
else {std::fprintf(stderr,"Unknown option: %.*s\n",static_cast<int>(Arg.size()),Arg.data());return 2;}
|
else {std::fprintf(stderr,"Unknown option: %.*s\n",static_cast<int>(Arg.size()),Arg.data());return 2;}
|
||||||
}
|
}
|
||||||
FScene Scene;std::string Error;
|
FScene Scene;std::string Error;
|
||||||
@@ -488,12 +388,10 @@ int main(int argc,char** argv)
|
|||||||
if(HasCamera)Scene.CameraMeters=Camera;
|
if(HasCamera)Scene.CameraMeters=Camera;
|
||||||
if(HasYaw)Scene.YawDegrees=Yaw;
|
if(HasYaw)Scene.YawDegrees=Yaw;
|
||||||
if(HasPitch)Scene.PitchDegrees=Pitch;
|
if(HasPitch)Scene.PitchDegrees=Pitch;
|
||||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f shade=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f vertices=%zu triangles=%llu\n",
|
||||||
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
||||||
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
|
Scene.YawDegrees,Scene.PitchDegrees,Scene.Vertices.size(),
|
||||||
ReadableMode?"readable":"normal",TileEdges?"on":"off",Scene.Vertices.size(),
|
|
||||||
static_cast<unsigned long long>(Scene.Triangles));
|
static_cast<unsigned long long>(Scene.Triangles));
|
||||||
return Render(Scene,argv[2],Width,Height,Fov,ReadableMode,TileEdges,
|
return Render(Scene,argv[2],Width,Height,Fov)?0:3;
|
||||||
TileEdgeWidthMeters)?0:3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +42,7 @@ namespace StandaloneScreenSpaceGate
|
|||||||
double MaximumSlopeDegrees=0.0;
|
double MaximumSlopeDegrees=0.0;
|
||||||
std::array<std::uint64_t,4> SlopeSamples{};
|
std::array<std::uint64_t,4> SlopeSamples{};
|
||||||
std::array<double,4> SlopeP99{},SlopeMaximum{};
|
std::array<double,4> SlopeP99{},SlopeMaximum{};
|
||||||
|
std::vector<double> SamplePixels;
|
||||||
bool Valid=false;
|
bool Valid=false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,6 +53,14 @@ namespace StandaloneScreenSpaceGate
|
|||||||
double SlopeY=0.0;
|
double SlopeY=0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct FMeshSurfaceHit
|
||||||
|
{
|
||||||
|
double Z=0.0;
|
||||||
|
std::array<double,3> Normal{};
|
||||||
|
std::array<std::uint8_t,4> Color{};
|
||||||
|
bool Ceiling=false;
|
||||||
|
};
|
||||||
|
|
||||||
struct FViewBasis
|
struct FViewBasis
|
||||||
{
|
{
|
||||||
std::array<double,3> Right{};
|
std::array<double,3> Right{};
|
||||||
@@ -162,7 +171,7 @@ namespace StandaloneScreenSpaceGate
|
|||||||
{
|
{
|
||||||
int Side=65;
|
int Side=65;
|
||||||
double MinimumX=0.0,MaximumX=0.0,MinimumY=0.0,MaximumY=0.0;
|
double MinimumX=0.0,MaximumX=0.0,MinimumY=0.0,MaximumY=0.0;
|
||||||
std::vector<std::vector<double>> Hits;
|
std::vector<std::vector<FMeshSurfaceHit>> Hits;
|
||||||
|
|
||||||
FHeightGrid()=default;
|
FHeightGrid()=default;
|
||||||
FHeightGrid(int InSide,double MinX,double MaxX,double MinY,double MaxY)
|
FHeightGrid(int InSide,double MinX,double MaxX,double MinY,double MaxY)
|
||||||
@@ -175,13 +184,13 @@ namespace StandaloneScreenSpaceGate
|
|||||||
double Y(int Iy) const
|
double Y(int Iy) const
|
||||||
{return MinimumY+(MaximumY-MinimumY)*Iy/(Side-1);}
|
{return MinimumY+(MaximumY-MinimumY)*Iy/(Side-1);}
|
||||||
|
|
||||||
void Add(int Ix,int Iy,double Z)
|
void Add(int Ix,int Iy,const FMeshSurfaceHit& Hit)
|
||||||
{
|
{
|
||||||
auto& Values=Hits[static_cast<std::size_t>(Iy)*Side+Ix];
|
auto& Values=Hits[static_cast<std::size_t>(Iy)*Side+Ix];
|
||||||
Values.push_back(Z);
|
Values.push_back(Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<double>& At(int Ix,int Iy) const
|
const std::vector<FMeshSurfaceHit>& At(int Ix,int Iy) const
|
||||||
{return Hits[static_cast<std::size_t>(Iy)*Side+Ix];}
|
{return Hits[static_cast<std::size_t>(Iy)*Side+Ix];}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,6 +255,8 @@ namespace StandaloneScreenSpaceGate
|
|||||||
{
|
{
|
||||||
if(!Tile||Tile->Indices.size()<3)continue;
|
if(!Tile||Tile->Indices.size()<3)continue;
|
||||||
const std::size_t TriangleCount=Tile->Indices.size()/3;
|
const std::size_t TriangleCount=Tile->Indices.size()/3;
|
||||||
|
const std::size_t CeilingStart=TriangleCount
|
||||||
|
-std::min<std::size_t>(TriangleCount,Tile->CeilingTriangleCount);
|
||||||
for(std::size_t Triangle=0;Triangle<TriangleCount;++Triangle)
|
for(std::size_t Triangle=0;Triangle<TriangleCount;++Triangle)
|
||||||
{
|
{
|
||||||
if(Triangle<Tile->TriangleSkirtFaceMasks.size()
|
if(Triangle<Tile->TriangleSkirtFaceMasks.size()
|
||||||
@@ -274,15 +285,33 @@ namespace StandaloneScreenSpaceGate
|
|||||||
const double W1=((C[1]-A[1])*(X-C[0])+(A[0]-C[0])*(Y-C[1]))/Denominator;
|
const double W1=((C[1]-A[1])*(X-C[0])+(A[0]-C[0])*(Y-C[1]))/Denominator;
|
||||||
const double W2=1.0-W0-W1;
|
const double W2=1.0-W0-W1;
|
||||||
if(W0<-1e-6||W1<-1e-6||W2<-1e-6)continue;
|
if(W0<-1e-6||W1<-1e-6||W2<-1e-6)continue;
|
||||||
Grid.Add(Ix,Iy,W0*A[2]+W1*B[2]+W2*C[2]);
|
const auto& VA=Tile->Vertices[I0];
|
||||||
|
const auto& VB=Tile->Vertices[I1];
|
||||||
|
const auto& VC=Tile->Vertices[I2];
|
||||||
|
std::array<double,3> Normal{};
|
||||||
|
for(int Axis=0;Axis<3;++Axis)
|
||||||
|
Normal[Axis]=W0*VA.Normal[Axis]+W1*VB.Normal[Axis]+W2*VC.Normal[Axis];
|
||||||
|
const double NormalLength=std::sqrt(Normal[0]*Normal[0]
|
||||||
|
+Normal[1]*Normal[1]+Normal[2]*Normal[2]);
|
||||||
|
if(NormalLength>1.0e-12)
|
||||||
|
for(double& Component:Normal)Component/=NormalLength;
|
||||||
|
std::array<std::uint8_t,4> Color{};
|
||||||
|
if(Tile->VertexColors.size()==Tile->Vertices.size())
|
||||||
|
for(int Channel=0;Channel<4;++Channel)
|
||||||
|
Color[Channel]=static_cast<std::uint8_t>(std::clamp(std::lround(
|
||||||
|
W0*Tile->VertexColors[I0][Channel]
|
||||||
|
+W1*Tile->VertexColors[I1][Channel]
|
||||||
|
+W2*Tile->VertexColors[I2][Channel]),0l,255l));
|
||||||
|
Grid.Add(Ix,Iy,{W0*A[2]+W1*B[2]+W2*C[2],Normal,Color,Triangle>=CeilingStart});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(auto& Values:Grid.Hits)
|
for(auto& Values:Grid.Hits)
|
||||||
{
|
{
|
||||||
std::sort(Values.begin(),Values.end());
|
std::stable_sort(Values.begin(),Values.end(),[](const FMeshSurfaceHit& A,
|
||||||
Values.erase(std::unique(Values.begin(),Values.end(),[](double A,double B)
|
const FMeshSurfaceHit& B){return A.Z<B.Z;});
|
||||||
{return std::abs(A-B)<0.02;}),Values.end());
|
Values.erase(std::unique(Values.begin(),Values.end(),[](const FMeshSurfaceHit& A,
|
||||||
|
const FMeshSurfaceHit& B){return std::abs(A.Z-B.Z)<0.02;}),Values.end());
|
||||||
}
|
}
|
||||||
return Grid;
|
return Grid;
|
||||||
}
|
}
|
||||||
@@ -335,14 +364,14 @@ namespace StandaloneScreenSpaceGate
|
|||||||
return Roots;
|
return Roots;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool MatchNearest(const std::vector<double>& Hits,const std::vector<bool>& Used,
|
inline bool MatchNearest(const std::vector<FMeshSurfaceHit>& Hits,const std::vector<bool>& Used,
|
||||||
double Root,std::size_t& OutIndex,double& OutError)
|
double Root,std::size_t& OutIndex,double& OutError)
|
||||||
{
|
{
|
||||||
double Best=std::numeric_limits<double>::infinity();std::size_t BestIndex=Hits.size();
|
double Best=std::numeric_limits<double>::infinity();std::size_t BestIndex=Hits.size();
|
||||||
for(std::size_t I=0;I<Hits.size();++I)
|
for(std::size_t I=0;I<Hits.size();++I)
|
||||||
if(I>=Used.size()||!Used[I])
|
if(I>=Used.size()||!Used[I])
|
||||||
{
|
{
|
||||||
const double Error=std::abs(Hits[I]-Root);
|
const double Error=std::abs(Hits[I].Z-Root);
|
||||||
if(Error<Best){Best=Error;BestIndex=I;}
|
if(Error<Best){Best=Error;BestIndex=I;}
|
||||||
}
|
}
|
||||||
if(BestIndex==Hits.size())return false;
|
if(BestIndex==Hits.size())return false;
|
||||||
@@ -371,7 +400,7 @@ namespace StandaloneScreenSpaceGate
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Used[HitIndex]=true;
|
Used[HitIndex]=true;
|
||||||
AddError(Accumulator,MeshGrid.At(Ix,Iy)[HitIndex]-Root.ZMeters,
|
AddError(Accumulator,MeshGrid.At(Ix,Iy)[HitIndex].Z-Root.ZMeters,
|
||||||
Root,PixelBudgetMeters,View,MeshGrid.X(Ix),MeshGrid.Y(Iy));
|
Root,PixelBudgetMeters,View,MeshGrid.X(Ix),MeshGrid.Y(Iy));
|
||||||
++Row.Samples;
|
++Row.Samples;
|
||||||
}
|
}
|
||||||
@@ -399,7 +428,7 @@ namespace StandaloneScreenSpaceGate
|
|||||||
std::vector<bool> UsedReference(Reference.size(),false),UsedMesh(Mesh.size(),false);
|
std::vector<bool> UsedReference(Reference.size(),false),UsedMesh(Mesh.size(),false);
|
||||||
for(std::size_t ReferenceIndex=0;ReferenceIndex<Reference.size();++ReferenceIndex)
|
for(std::size_t ReferenceIndex=0;ReferenceIndex<Reference.size();++ReferenceIndex)
|
||||||
{
|
{
|
||||||
const double ReferenceZ=Reference[ReferenceIndex];
|
const double ReferenceZ=Reference[ReferenceIndex].Z;
|
||||||
UsedReference[ReferenceIndex]=true;
|
UsedReference[ReferenceIndex]=true;
|
||||||
std::size_t MeshIndex=0;double MeshError=0.0;
|
std::size_t MeshIndex=0;double MeshError=0.0;
|
||||||
if(!MatchNearest(Mesh,UsedMesh,ReferenceZ,MeshIndex,MeshError))
|
if(!MatchNearest(Mesh,UsedMesh,ReferenceZ,MeshIndex,MeshError))
|
||||||
@@ -416,7 +445,7 @@ namespace StandaloneScreenSpaceGate
|
|||||||
if(BestRoot!=Roots.end()&&std::abs(BestRoot->ZMeters-ReferenceZ)<1.0)
|
if(BestRoot!=Roots.end()&&std::abs(BestRoot->ZMeters-ReferenceZ)<1.0)
|
||||||
Root=*BestRoot;
|
Root=*BestRoot;
|
||||||
}
|
}
|
||||||
AddError(Accumulator,Mesh[MeshIndex]-Reference[ReferenceIndex],
|
AddError(Accumulator,Mesh[MeshIndex].Z-Reference[ReferenceIndex].Z,
|
||||||
Root,PixelBudgetMeters,View,ReferenceGrid.X(Ix),ReferenceGrid.Y(Iy));
|
Root,PixelBudgetMeters,View,ReferenceGrid.X(Ix),ReferenceGrid.Y(Iy));
|
||||||
}
|
}
|
||||||
for(std::size_t I=0;I<Mesh.size();++I)
|
for(std::size_t I=0;I<Mesh.size();++I)
|
||||||
@@ -426,6 +455,110 @@ namespace StandaloneScreenSpaceGate
|
|||||||
return Row;
|
return Row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FMeshComparisonRow
|
||||||
|
{
|
||||||
|
FErrorRow Geometry;
|
||||||
|
std::uint64_t Holes=0,Cracks=0,Floating=0,MaterialMismatches=0;
|
||||||
|
double ShadeP50=0.0,ShadeP99=0.0,ShadeMaximum=0.0;
|
||||||
|
double NormalDegreesP50=0.0,NormalDegreesP99=0.0,NormalDegreesMaximum=0.0;
|
||||||
|
std::vector<double> GeometryPixels,ShadeValues,NormalAngles;
|
||||||
|
bool Valid=false;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::array<double,3> UnitNormal(const FMeshSurfaceHit& Hit)
|
||||||
|
{
|
||||||
|
std::array<double,3> N=Hit.Normal;
|
||||||
|
const double Length=std::sqrt(N[0]*N[0]+N[1]*N[1]+N[2]*N[2]);
|
||||||
|
if(Length<=1.0e-12)return {0.0,0.0,Hit.Ceiling?-1.0:1.0};
|
||||||
|
for(double& Component:N)Component/=Length;
|
||||||
|
return N;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FMeshComparisonRow CompareMeshes(const FHeightGrid& Reference,
|
||||||
|
const FHeightGrid& Candidate,const FVoxelTileKey& Patch,double VoxelMeters,
|
||||||
|
double PixelBudgetMeters,const FViewBasis& View)
|
||||||
|
{
|
||||||
|
FMeshComparisonRow Row;Row.Valid=Reference.Side==Candidate.Side;
|
||||||
|
if(!Row.Valid)return Row;
|
||||||
|
FErrorAccumulator Geometry;
|
||||||
|
std::vector<double> ShadeValues,NormalAngles;
|
||||||
|
const std::size_t CellCount=static_cast<std::size_t>(Reference.Side)*Reference.Side;
|
||||||
|
ShadeValues.reserve(CellCount);NormalAngles.reserve(CellCount);
|
||||||
|
const double LightLength=std::sqrt(0.3*0.3+(-0.4)*(-0.4)+0.85*0.85);
|
||||||
|
const std::array<double,3> Light={0.3/LightLength,-0.4/LightLength,0.85/LightLength};
|
||||||
|
for(int Iy=0;Iy<Reference.Side;++Iy)
|
||||||
|
for(int Ix=0;Ix<Reference.Side;++Ix)
|
||||||
|
{
|
||||||
|
const auto& Before=Reference.At(Ix,Iy);
|
||||||
|
const auto& After=Candidate.At(Ix,Iy);
|
||||||
|
std::vector<bool> Used(After.size(),false);
|
||||||
|
for(std::size_t I=0;I<Before.size();++I)
|
||||||
|
{
|
||||||
|
std::size_t Matched=0;double Difference=0.0;
|
||||||
|
if(!MatchNearest(After,Used,Before[I].Z,Matched,Difference))
|
||||||
|
{
|
||||||
|
++Row.Holes;AddMissing(Geometry);ShadeValues.push_back(1.0);
|
||||||
|
NormalAngles.push_back(180.0);continue;
|
||||||
|
}
|
||||||
|
Used[Matched]=true;
|
||||||
|
const FMeshSurfaceHit& A=Before[I];
|
||||||
|
const FMeshSurfaceHit& B=After[Matched];
|
||||||
|
const auto NA=UnitNormal(A),NB=UnitNormal(B);
|
||||||
|
const double Dot=std::clamp(NA[0]*NB[0]+NA[1]*NB[1]+NA[2]*NB[2],-1.0,1.0);
|
||||||
|
constexpr double Pi=3.14159265358979323846;
|
||||||
|
const double Angle=std::acos(Dot)*180.0/Pi;
|
||||||
|
NormalAngles.push_back(Angle);
|
||||||
|
const double DiffuseA=std::max(0.0,NA[0]*Light[0]+NA[1]*Light[1]+NA[2]*Light[2]);
|
||||||
|
const double DiffuseB=std::max(0.0,NB[0]*Light[0]+NB[1]*Light[1]+NB[2]*Light[2]);
|
||||||
|
const bool bMaterialChanged=A.Color[0]!=B.Color[0]||A.Color[3]!=B.Color[3]
|
||||||
|
||A.Ceiling!=B.Ceiling;
|
||||||
|
if(bMaterialChanged)++Row.MaterialMismatches;
|
||||||
|
const double MaterialDelta=bMaterialChanged?1.0:std::max(
|
||||||
|
std::abs(static_cast<double>(A.Color[1])-B.Color[1]),
|
||||||
|
std::abs(static_cast<double>(A.Color[2])-B.Color[2]))/255.0;
|
||||||
|
ShadeValues.push_back(std::max(std::abs(DiffuseA-DiffuseB),MaterialDelta));
|
||||||
|
const double NX=NA[2]<0.0?NA[0]/std::max(1.0e-12,-NA[2])
|
||||||
|
:-NA[0]/std::max(1.0e-12,NA[2]);
|
||||||
|
const double NY=NA[2]<0.0?NA[1]/std::max(1.0e-12,-NA[2])
|
||||||
|
:-NA[1]/std::max(1.0e-12,NA[2]);
|
||||||
|
AddError(Geometry,B.Z-A.Z,{A.Z,NX,NY},PixelBudgetMeters,View,
|
||||||
|
Reference.X(Ix),Reference.Y(Iy));
|
||||||
|
}
|
||||||
|
for(std::size_t I=0;I<After.size();++I)
|
||||||
|
if(!Used[I])
|
||||||
|
{++Row.Floating;AddMissing(Geometry);ShadeValues.push_back(1.0);NormalAngles.push_back(180.0);}
|
||||||
|
}
|
||||||
|
for(int Iy=0;Iy<Reference.Side;++Iy)
|
||||||
|
for(int Ix=0;Ix<Reference.Side;++Ix)
|
||||||
|
{
|
||||||
|
const std::size_t Index=static_cast<std::size_t>(Iy)*Reference.Side+Ix;
|
||||||
|
const auto CheckEdge=[&](int NX,int NY)
|
||||||
|
{
|
||||||
|
if(NX>=Reference.Side||NY>=Reference.Side)return;
|
||||||
|
const auto& A=Reference.Hits[Index];
|
||||||
|
const auto& B=Reference.At(NX,NY);
|
||||||
|
const auto& CA=Candidate.Hits[Index];
|
||||||
|
const auto& CB=Candidate.At(NX,NY);
|
||||||
|
if(!A.empty()&&!B.empty()&&(CA.empty()!=CB.empty()))++Row.Cracks;
|
||||||
|
};
|
||||||
|
CheckEdge(Ix+1,Iy);CheckEdge(Ix,Iy+1);
|
||||||
|
}
|
||||||
|
Row.Geometry.Lod=Patch.Level;Row.Geometry.Valid=Row.Valid;
|
||||||
|
Row.Geometry.Missing=Row.Holes;Row.Geometry.Floating=Row.Floating;
|
||||||
|
FinalizeError(Row.Geometry,Geometry);
|
||||||
|
Row.ShadeP50=Percentile(ShadeValues,0.50);
|
||||||
|
Row.ShadeP99=Percentile(ShadeValues,0.99);
|
||||||
|
Row.ShadeMaximum=ShadeValues.empty()?0.0:*std::max_element(ShadeValues.begin(),ShadeValues.end());
|
||||||
|
Row.NormalDegreesP50=Percentile(NormalAngles,0.50);
|
||||||
|
Row.NormalDegreesP99=Percentile(NormalAngles,0.99);
|
||||||
|
Row.NormalDegreesMaximum=NormalAngles.empty()?0.0
|
||||||
|
:*std::max_element(NormalAngles.begin(),NormalAngles.end());
|
||||||
|
Row.GeometryPixels=std::move(Geometry.Pixels);
|
||||||
|
Row.ShadeValues=std::move(ShadeValues);
|
||||||
|
Row.NormalAngles=std::move(NormalAngles);
|
||||||
|
return Row;
|
||||||
|
}
|
||||||
|
|
||||||
inline FPopRow ComparePop(const FHeightGrid& Coarse,const FHeightGrid& Fine,
|
inline FPopRow ComparePop(const FHeightGrid& Coarse,const FHeightGrid& Fine,
|
||||||
const FVoxelTileKey& Patch,double VoxelMeters,double PixelBudgetMeters,
|
const FVoxelTileKey& Patch,double VoxelMeters,double PixelBudgetMeters,
|
||||||
const VoxelForgeCore::WorldAssemblyManifest& Assembly,const FViewBasis& View)
|
const VoxelForgeCore::WorldAssemblyManifest& Assembly,const FViewBasis& View)
|
||||||
@@ -452,11 +585,12 @@ namespace StandaloneScreenSpaceGate
|
|||||||
}
|
}
|
||||||
CoarseUsed[CoarseIndex]=true;
|
CoarseUsed[CoarseIndex]=true;
|
||||||
FineUsed[FineIndex]=true;
|
FineUsed[FineIndex]=true;
|
||||||
AddError(Accumulator,CoarseHits[CoarseIndex]-FineHits[FineIndex],
|
AddError(Accumulator,CoarseHits[CoarseIndex].Z-FineHits[FineIndex].Z,
|
||||||
Root,PixelBudgetMeters,View,Coarse.X(Ix),Coarse.Y(Iy));
|
Root,PixelBudgetMeters,View,Coarse.X(Ix),Coarse.Y(Iy));
|
||||||
++Row.Samples;
|
++Row.Samples;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Row.SamplePixels=Accumulator.Pixels;
|
||||||
FErrorRow Stats;Stats.Lod=Patch.Level;Stats.Valid=true;
|
FErrorRow Stats;Stats.Lod=Patch.Level;Stats.Valid=true;
|
||||||
FinalizeError(Stats,Accumulator);
|
FinalizeError(Stats,Accumulator);
|
||||||
Row.Samples=Stats.Samples;Row.P50=Stats.P50;Row.P99=Stats.P99;Row.Maximum=Stats.Maximum;
|
Row.Samples=Stats.Samples;Row.P50=Stats.P50;Row.P99=Stats.P99;Row.Maximum=Stats.Maximum;
|
||||||
@@ -472,6 +606,16 @@ namespace StandaloneScreenSpaceGate
|
|||||||
return Row;
|
return Row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool PopNotWorseAtAnySample(const FPopRow& Before,const FPopRow& After)
|
||||||
|
{
|
||||||
|
if(!Before.Valid||!After.Valid||Before.Samples<16
|
||||||
|
||Before.SamplePixels.size()!=After.SamplePixels.size()
|
||||||
|
||After.Cracks>Before.Cracks||After.Floating>Before.Floating)return false;
|
||||||
|
for(std::size_t I=0;I<Before.SamplePixels.size();++I)
|
||||||
|
if(After.SamplePixels[I]>Before.SamplePixels[I]+1.0e-4)return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
inline std::array<double,9> ClosestDisplayedDistancesMeters(
|
inline std::array<double,9> ClosestDisplayedDistancesMeters(
|
||||||
const FStandaloneOwnerWorld& World,const FVector& CameraVoxels,
|
const FStandaloneOwnerWorld& World,const FVector& CameraVoxels,
|
||||||
std::uint32_t VoxelSizeMicrons)
|
std::uint32_t VoxelSizeMicrons)
|
||||||
@@ -544,6 +688,7 @@ namespace StandaloneScreenSpaceGate
|
|||||||
|
|
||||||
inline bool Run(const std::array<std::optional<FStandaloneCpuTile>,9>& SampleTiles,
|
inline bool Run(const std::array<std::optional<FStandaloneCpuTile>,9>& SampleTiles,
|
||||||
FStandaloneStreamingGenerator& Streamer,
|
FStandaloneStreamingGenerator& Streamer,
|
||||||
|
const std::vector<FVoxelTileKey>& DesiredKeys,
|
||||||
const VoxelForgeCore::WorldAssemblyManifest& Assembly,
|
const VoxelForgeCore::WorldAssemblyManifest& Assembly,
|
||||||
const std::array<double,9>& Distances,std::uint32_t VoxelSizeMicrons,int MaxLod,
|
const std::array<double,9>& Distances,std::uint32_t VoxelSizeMicrons,int MaxLod,
|
||||||
const std::array<std::int64_t,9>& PixelBudgetsSubvoxels,
|
const std::array<std::int64_t,9>& PixelBudgetsSubvoxels,
|
||||||
@@ -552,17 +697,26 @@ namespace StandaloneScreenSpaceGate
|
|||||||
const double VoxelMeters=static_cast<double>(VoxelSizeMicrons)/1000000.0;
|
const double VoxelMeters=static_cast<double>(VoxelSizeMicrons)/1000000.0;
|
||||||
const FViewBasis View=MakeViewBasis(CameraYawDegrees,CameraPitchDegrees);
|
const FViewBasis View=MakeViewBasis(CameraYawDegrees,CameraPitchDegrees);
|
||||||
std::array<double,9> Budgets{};
|
std::array<double,9> Budgets{};
|
||||||
std::array<FErrorRow,9> ErrorRows{};
|
VoxelForgeCore::TileGenerationOptions BaselineOptions;
|
||||||
std::array<FPopRow,9> PopRows{};
|
BaselineOptions.overrideFarDisplayMesh=true;
|
||||||
|
VoxelForgeCore::TileGenerationOptions CandidateOptions;
|
||||||
|
CandidateOptions.overrideFarDisplayMesh=true;
|
||||||
|
CandidateOptions.farDisplayGeometryBudgetMillipixels=Assembly.recipe.farDisplayGeometryBudgetMillipixels;
|
||||||
|
CandidateOptions.farRefinementGridScale=Assembly.recipe.farRefinementGridScale;
|
||||||
|
CandidateOptions.farRefinementLodMask=Assembly.recipe.farRefinementLodMask;
|
||||||
|
CandidateOptions.farConnectorStationScale=Assembly.recipe.farConnectorStationScale;
|
||||||
|
CandidateOptions.farConnectorRayMask=Assembly.recipe.farConnectorRayMask;
|
||||||
|
CandidateOptions.farConnectorReuseStationRadii=Assembly.recipe.farConnectorReuseStationRadii;
|
||||||
bool GatePass=true;
|
bool GatePass=true;
|
||||||
std::printf("[ScreenSpaceGate] angular_pixel=%.9f rad (horizontal_fov=90deg,width=2560) lod>=3_limit=1.0px sampling=65x65 skirts=excluded metric=local_surface_normal_projected field_reference=direct-Heightfield2D-roots normal_probe_vox=1 view_yaw_deg=%.3f view_pitch_deg=%.3f\n",
|
constexpr double ShadeLimit=1.0/255.0;
|
||||||
|
std::printf("[ScreenSpaceGate] angular_pixel=%.9f rad (horizontal_fov=90deg,width=2560) lod>=3_geometry_limit=1.0px same_lod_reference=today mesh sampling=65x65 skirts=excluded shading=max(lambert-delta,material-selector-or-blend-delta) shade_limit=1/255 normal_probe_vox=1 view_yaw_deg=%.3f view_pitch_deg=%.3f\n",
|
||||||
OnePixelRadians,CameraYawDegrees,CameraPitchDegrees);
|
OnePixelRadians,CameraYawDegrees,CameraPitchDegrees);
|
||||||
for(int Lod=0;Lod<=MaxLod&&Lod<9;++Lod)
|
for(int Lod=0;Lod<=MaxLod&&Lod<9;++Lod)
|
||||||
{
|
{
|
||||||
Budgets[Lod]=Distances[Lod]*OnePixelRadians;
|
Budgets[Lod]=Distances[Lod]*OnePixelRadians;
|
||||||
std::printf("[ScreenSpaceDistance] lod=%d closest_m=%.3f pixel_budget_m=%.6f pixel_budget_cm=%.3f pixel_budget_subvoxels=%lld rule=%s\n",
|
std::printf("[ScreenSpaceDistance] lod=%d closest_m=%.3f pixel_budget_m=%.6f pixel_budget_cm=%.3f pixel_budget_subvoxels=%lld rule=%s\n",
|
||||||
Lod,Distances[Lod],Budgets[Lod],Budgets[Lod]*100.0,
|
Lod,Distances[Lod],Budgets[Lod],Budgets[Lod]*100.0,
|
||||||
static_cast<long long>(PixelBudgetsSubvoxels[Lod]),Lod<3?"exact-2cm":"1px");
|
static_cast<long long>(PixelBudgetsSubvoxels[Lod]),Lod<3?"exact-2cm":"same_lod_1px");
|
||||||
}
|
}
|
||||||
const double LOD3CellMeters=static_cast<double>(CHUNK_SIZE*(1<<3))*VoxelMeters/64.0;
|
const double LOD3CellMeters=static_cast<double>(CHUNK_SIZE*(1<<3))*VoxelMeters/64.0;
|
||||||
PrintKnownFailureControl(LOD3CellMeters,Distances[3],View);
|
PrintKnownFailureControl(LOD3CellMeters,Distances[3],View);
|
||||||
@@ -578,115 +732,117 @@ namespace StandaloneScreenSpaceGate
|
|||||||
const FStandaloneCpuTile* CoarsePtr=&CoarseTile;
|
const FStandaloneCpuTile* CoarsePtr=&CoarseTile;
|
||||||
const std::span<const FStandaloneCpuTile* const> CoarseSpan(&CoarsePtr,1);
|
const std::span<const FStandaloneCpuTile* const> CoarseSpan(&CoarsePtr,1);
|
||||||
const FHeightGrid CoarseGrid=Rasterize(CoarseSpan,CoarseTile.Key,VoxelMeters);
|
const FHeightGrid CoarseGrid=Rasterize(CoarseSpan,CoarseTile.Key,VoxelMeters);
|
||||||
const FStandaloneCpuTile ExactTile=Streamer.GenerateDiagnostic(
|
FStandaloneCpuTile BaselineTile=Streamer.GenerateDiagnostic(
|
||||||
CoarseTile.Key,0,1,true);
|
CoarseTile.Key,0,1,true,&BaselineOptions,true);
|
||||||
const FStandaloneCpuTile* ExactPointer=&ExactTile;
|
const FStandaloneCpuTile* BaselinePointer=&BaselineTile;
|
||||||
const std::span<const FStandaloneCpuTile* const> ExactSpan(&ExactPointer,1);
|
const std::span<const FStandaloneCpuTile* const> BaselineSpan(&BaselinePointer,1);
|
||||||
const FHeightGrid ExactGrid=Rasterize(ExactSpan,ExactTile.Key,VoxelMeters);
|
const FHeightGrid BaselineGrid=Rasterize(BaselineSpan,BaselineTile.Key,VoxelMeters);
|
||||||
ErrorRows[Lod]=CompareToReference(CoarseGrid,ExactGrid,CoarseTile.Key,VoxelMeters,
|
const FMeshComparisonRow MeshDelta=CompareMeshes(BaselineGrid,CoarseGrid,
|
||||||
Budgets[Lod],Assembly,View);
|
CoarseTile.Key,VoxelMeters,Budgets[Lod],View);
|
||||||
const FErrorRow& Error=ErrorRows[Lod];
|
const FErrorRow& Error=MeshDelta.Geometry;
|
||||||
const bool LodPass=Error.Valid&&Error.Missing==0&&Error.Floating==0
|
const bool LodPass=Error.Valid&&Error.Missing==0&&Error.Floating==0
|
||||||
&&Error.Samples>=16&&Error.Maximum<=1.0;
|
&&MeshDelta.Cracks==0&&Error.Samples>=16&&Error.Maximum<=1.0
|
||||||
|
&&MeshDelta.ShadeMaximum<=ShadeLimit;
|
||||||
GatePass=GatePass&&LodPass;
|
GatePass=GatePass&&LodPass;
|
||||||
std::printf("[ScreenSpaceGateLOD] lod=%d key=(%d,%d,%d) samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xy_m=(%.3f,%.3f) max_slope_deg=%.2f missing=%llu floating=%llu status=%s\n",
|
std::printf("[ScreenSpaceGateLOD] lod=%d key=(%d,%d,%d) samples=%llu geometry_px_p50=%.4f p99=%.4f max=%.4f shading_delta_p50=%.6f p99=%.6f max=%.6f normal_deg_p50=%.4f p99=%.4f max=%.4f material_mismatch=%llu holes=%llu cracks=%llu floating=%llu status=%s\n",
|
||||||
Lod,CoarseTile.Key.Coord.X,CoarseTile.Key.Coord.Y,CoarseTile.Key.Coord.Z,
|
Lod,CoarseTile.Key.Coord.X,CoarseTile.Key.Coord.Y,CoarseTile.Key.Coord.Z,
|
||||||
static_cast<unsigned long long>(Error.Samples),Error.P50,Error.P99,Error.Maximum,
|
static_cast<unsigned long long>(Error.Samples),Error.P50,Error.P99,Error.Maximum,
|
||||||
Error.VerticalP50,Error.VerticalP99,Error.VerticalMaximum,
|
MeshDelta.ShadeP50,MeshDelta.ShadeP99,MeshDelta.ShadeMaximum,
|
||||||
Error.MaximumNormalDistanceMeters,Error.MaximumX,Error.MaximumY,
|
MeshDelta.NormalDegreesP50,MeshDelta.NormalDegreesP99,MeshDelta.NormalDegreesMaximum,
|
||||||
Error.MaximumSlopeDegrees,
|
static_cast<unsigned long long>(MeshDelta.MaterialMismatches),
|
||||||
static_cast<unsigned long long>(Error.Missing),static_cast<unsigned long long>(Error.Floating),
|
static_cast<unsigned long long>(MeshDelta.Holes),
|
||||||
|
static_cast<unsigned long long>(MeshDelta.Cracks),
|
||||||
|
static_cast<unsigned long long>(MeshDelta.Floating),
|
||||||
LodPass?"PASS":"REJECT");
|
LodPass?"PASS":"REJECT");
|
||||||
PrintSlopeTail("same_lod",Lod,Error.SlopeSamples,Error.SlopeP99,Error.SlopeMaximum);
|
|
||||||
|
|
||||||
const FErrorRow FieldError=CompareToExact(CoarseGrid,CoarseTile.Key,VoxelMeters,
|
const FErrorRow FieldError=CompareToExact(CoarseGrid,CoarseTile.Key,VoxelMeters,
|
||||||
Budgets[Lod],Assembly,View);
|
Budgets[Lod],Assembly,View);
|
||||||
const bool FieldPass=FieldError.Valid&&FieldError.Missing==0&&FieldError.Floating==0
|
const bool FieldPass=FieldError.Valid&&FieldError.Missing==0&&FieldError.Floating==0
|
||||||
&&FieldError.Samples>=16&&FieldError.Maximum<=1.0;
|
&&FieldError.Samples>=16&&FieldError.Maximum<=1.0;
|
||||||
GatePass=GatePass&&FieldPass;
|
std::printf("[ScreenSpaceGateFieldLOD] lod=%d samples=%llu field_geometry_px_p50=%.4f p99=%.4f max=%.4f missing=%llu floating=%llu status=INFO_ONLY\n",
|
||||||
std::printf("[ScreenSpaceGateFieldLOD] lod=%d samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xyz_m=(%.3f,%.3f,%.3f) max_slope_deg=%.2f missing=%llu floating=%llu status=%s\n",
|
|
||||||
Lod,static_cast<unsigned long long>(FieldError.Samples),FieldError.P50,
|
Lod,static_cast<unsigned long long>(FieldError.Samples),FieldError.P50,
|
||||||
FieldError.P99,FieldError.Maximum,FieldError.VerticalP50,FieldError.VerticalP99,
|
FieldError.P99,FieldError.Maximum,
|
||||||
FieldError.VerticalMaximum,FieldError.MaximumNormalDistanceMeters,
|
|
||||||
FieldError.MaximumX,FieldError.MaximumY,FieldError.MaximumDisplayedZ,
|
|
||||||
FieldError.MaximumSlopeDegrees,
|
|
||||||
static_cast<unsigned long long>(FieldError.Missing),
|
static_cast<unsigned long long>(FieldError.Missing),
|
||||||
static_cast<unsigned long long>(FieldError.Floating),FieldPass?"PASS":"REJECT");
|
static_cast<unsigned long long>(FieldError.Floating));
|
||||||
PrintSlopeTail("field",Lod,FieldError.SlopeSamples,FieldError.SlopeP99,
|
|
||||||
FieldError.SlopeMaximum);
|
|
||||||
|
|
||||||
bool FaultControlsPass=true;
|
bool FaultControlsPass=true;
|
||||||
const FHeightGrid MissingGrid(ExactGrid.Side,ExactGrid.MinimumX,ExactGrid.MaximumX,
|
const FHeightGrid MissingGrid(BaselineGrid.Side,BaselineGrid.MinimumX,BaselineGrid.MaximumX,
|
||||||
ExactGrid.MinimumY,ExactGrid.MaximumY);
|
BaselineGrid.MinimumY,BaselineGrid.MaximumY);
|
||||||
const FErrorRow MissingError=CompareToReference(MissingGrid,ExactGrid,
|
const FMeshComparisonRow MissingError=CompareMeshes(BaselineGrid,MissingGrid,
|
||||||
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
CoarseTile.Key,VoxelMeters,Budgets[Lod],View);
|
||||||
const bool MissingRejected=MissingError.Missing>0&&MissingError.Maximum>1.0;
|
const bool MissingRejected=MissingError.Holes>0&&MissingError.Geometry.Maximum>1.0;
|
||||||
FaultControlsPass=FaultControlsPass&&MissingRejected;
|
FaultControlsPass=FaultControlsPass&&MissingRejected;
|
||||||
std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-displayed-surface missing=%llu status=%s\n",
|
std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-displayed-surface holes=%llu status=%s\n",
|
||||||
Lod,static_cast<unsigned long long>(MissingError.Missing),
|
Lod,static_cast<unsigned long long>(MissingError.Holes),
|
||||||
MissingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
MissingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
||||||
|
|
||||||
FStandaloneCpuTile FloatingTile=CoarseTile;
|
FStandaloneCpuTile FloatingTile=BaselineTile;
|
||||||
const std::uint32_t FloatingVertexBase=static_cast<std::uint32_t>(FloatingTile.Vertices.size());
|
const std::uint32_t FloatingVertexBase=static_cast<std::uint32_t>(FloatingTile.Vertices.size());
|
||||||
for(FStandaloneCpuVertex Vertex:CoarseTile.Vertices)
|
for(FStandaloneCpuVertex Vertex:BaselineTile.Vertices)
|
||||||
{
|
{
|
||||||
Vertex.Position[2]+=2.0f;
|
Vertex.Position[2]+=2.0f;
|
||||||
FloatingTile.Vertices.push_back(Vertex);
|
FloatingTile.Vertices.push_back(Vertex);
|
||||||
}
|
}
|
||||||
for(const std::uint32_t Index:CoarseTile.Indices)
|
FloatingTile.VertexColors.insert(FloatingTile.VertexColors.end(),
|
||||||
|
BaselineTile.VertexColors.begin(),BaselineTile.VertexColors.end());
|
||||||
|
for(const std::uint32_t Index:BaselineTile.Indices)
|
||||||
FloatingTile.Indices.push_back(FloatingVertexBase+Index);
|
FloatingTile.Indices.push_back(FloatingVertexBase+Index);
|
||||||
FloatingTile.TriangleSkirtFaceMasks.insert(FloatingTile.TriangleSkirtFaceMasks.end(),
|
FloatingTile.TriangleSkirtFaceMasks.insert(FloatingTile.TriangleSkirtFaceMasks.end(),
|
||||||
CoarseTile.TriangleSkirtFaceMasks.begin(),CoarseTile.TriangleSkirtFaceMasks.end());
|
BaselineTile.TriangleSkirtFaceMasks.begin(),BaselineTile.TriangleSkirtFaceMasks.end());
|
||||||
const FStandaloneCpuTile* FloatingPointer=&FloatingTile;
|
const FStandaloneCpuTile* FloatingPointer=&FloatingTile;
|
||||||
const std::span<const FStandaloneCpuTile* const> FloatingSpan(&FloatingPointer,1);
|
const std::span<const FStandaloneCpuTile* const> FloatingSpan(&FloatingPointer,1);
|
||||||
const FHeightGrid FloatingGrid=Rasterize(FloatingSpan,CoarseTile.Key,VoxelMeters);
|
const FHeightGrid FloatingGrid=Rasterize(FloatingSpan,BaselineTile.Key,VoxelMeters);
|
||||||
const FErrorRow FloatingError=CompareToReference(FloatingGrid,ExactGrid,
|
const FMeshComparisonRow FloatingError=CompareMeshes(BaselineGrid,FloatingGrid,
|
||||||
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
CoarseTile.Key,VoxelMeters,Budgets[Lod],View);
|
||||||
const bool FloatingRejected=FloatingError.Floating>0||FloatingError.Maximum>1.0;
|
const bool FloatingRejected=FloatingError.Floating>0||FloatingError.Geometry.Maximum>1.0;
|
||||||
FaultControlsPass=FaultControlsPass&&FloatingRejected;
|
FaultControlsPass=FaultControlsPass&&FloatingRejected;
|
||||||
std::printf("[ScreenSpaceFaultInjection] lod=%d case=2m-floating-surface floating=%llu max_px=%.4f status=%s\n",
|
std::printf("[ScreenSpaceFaultInjection] lod=%d case=2m-floating-surface floating=%llu max_px=%.4f status=%s\n",
|
||||||
Lod,static_cast<unsigned long long>(FloatingError.Floating),FloatingError.Maximum,
|
Lod,static_cast<unsigned long long>(FloatingError.Floating),FloatingError.Geometry.Maximum,
|
||||||
FloatingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
FloatingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
||||||
|
|
||||||
std::vector<FStandaloneCpuTile> FineTiles;
|
std::vector<FStandaloneCpuTile> FineBeforeTiles,FineAfterTiles;
|
||||||
FineTiles.reserve(8);
|
FineBeforeTiles.reserve(8);FineAfterTiles.reserve(8);
|
||||||
for(int Dz=0;Dz<=1;++Dz)
|
for(int Dz=0;Dz<=1;++Dz)
|
||||||
for(int Dy=0;Dy<=1;++Dy)
|
for(int Dy=0;Dy<=1;++Dy)
|
||||||
for(int Dx=0;Dx<=1;++Dx)
|
for(int Dx=0;Dx<=1;++Dx)
|
||||||
{
|
{
|
||||||
const FVoxelTileKey FineKey(FIntVector(2*CoarseTile.Key.Coord.X+Dx,
|
const FVoxelTileKey FineKey(FIntVector(2*CoarseTile.Key.Coord.X+Dx,
|
||||||
2*CoarseTile.Key.Coord.Y+Dy,2*CoarseTile.Key.Coord.Z+Dz),Lod-1);
|
2*CoarseTile.Key.Coord.Y+Dy,2*CoarseTile.Key.Coord.Z+Dz),Lod-1);
|
||||||
FStandaloneCpuTile FineTile=Streamer.GenerateDiagnostic(FineKey);
|
FStandaloneCpuTile FineBefore=Streamer.GenerateDiagnostic(
|
||||||
if(!FineTile.Vertices.empty())FineTiles.push_back(std::move(FineTile));
|
FineKey,0,1,true,&BaselineOptions,true);
|
||||||
|
FStandaloneCpuTile FineAfter=Streamer.GenerateDiagnostic(
|
||||||
|
FineKey,0,1,true,nullptr,true);
|
||||||
|
if(!FineBefore.Vertices.empty())FineBeforeTiles.push_back(std::move(FineBefore));
|
||||||
|
if(!FineAfter.Vertices.empty())FineAfterTiles.push_back(std::move(FineAfter));
|
||||||
}
|
}
|
||||||
std::vector<const FStandaloneCpuTile*> FinePointers;
|
std::vector<const FStandaloneCpuTile*> FineBeforePointers,FineAfterPointers;
|
||||||
FinePointers.reserve(FineTiles.size());
|
FineBeforePointers.reserve(FineBeforeTiles.size());FineAfterPointers.reserve(FineAfterTiles.size());
|
||||||
for(const auto& Tile:FineTiles)FinePointers.push_back(&Tile);
|
for(const auto& Tile:FineBeforeTiles)FineBeforePointers.push_back(&Tile);
|
||||||
const auto FineSpan=std::span<const FStandaloneCpuTile* const>(
|
for(const auto& Tile:FineAfterTiles)FineAfterPointers.push_back(&Tile);
|
||||||
FinePointers.data(),FinePointers.size());
|
const auto FineBeforeSpan=std::span<const FStandaloneCpuTile* const>(
|
||||||
const FHeightGrid FineGrid=Rasterize(FineSpan,CoarseTile.Key,VoxelMeters);
|
FineBeforePointers.data(),FineBeforePointers.size());
|
||||||
PopRows[Lod]=ComparePop(CoarseGrid,FineGrid,CoarseTile.Key,VoxelMeters,
|
const auto FineAfterSpan=std::span<const FStandaloneCpuTile* const>(
|
||||||
|
FineAfterPointers.data(),FineAfterPointers.size());
|
||||||
|
const FHeightGrid FineBeforeGrid=Rasterize(FineBeforeSpan,CoarseTile.Key,VoxelMeters);
|
||||||
|
const FHeightGrid FineAfterGrid=Rasterize(FineAfterSpan,CoarseTile.Key,VoxelMeters);
|
||||||
|
const FPopRow PopBefore=ComparePop(BaselineGrid,FineBeforeGrid,CoarseTile.Key,VoxelMeters,
|
||||||
Budgets[Lod],Assembly,View);
|
Budgets[Lod],Assembly,View);
|
||||||
const FPopRow& Pop=PopRows[Lod];
|
const FPopRow PopAfter=ComparePop(CoarseGrid,FineAfterGrid,CoarseTile.Key,VoxelMeters,
|
||||||
const bool PopPass=Pop.Valid&&Pop.Cracks==0&&Pop.Floating==0&&Pop.Maximum<=1.0;
|
Budgets[Lod],Assembly,View);
|
||||||
|
const bool PopPass=PopNotWorseAtAnySample(PopBefore,PopAfter);
|
||||||
GatePass=GatePass&&PopPass;
|
GatePass=GatePass&&PopPass;
|
||||||
std::printf("[ScreenSpaceSwap] lod=%d_to_%d samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xyz_m=(%.3f,%.3f,%.3f) max_slope_deg=%.2f cracks=%llu floating=%llu status=%s\n",
|
std::printf("[ScreenSpaceSwap] swap=%d_to_%d distance_m=%.3f samples_before=%llu pop_before_px_p50=%.4f p99=%.4f max=%.4f pop_after_px_p50=%.4f p99=%.4f max=%.4f missing_root_samples_before=%llu after=%llu status=%s\n",
|
||||||
Lod,Lod-1,static_cast<unsigned long long>(Pop.Samples),
|
Lod,Lod-1,Distances[Lod],static_cast<unsigned long long>(PopBefore.Samples),
|
||||||
Pop.P50,Pop.P99,Pop.Maximum,Pop.VerticalP50,Pop.VerticalP99,Pop.VerticalMaximum,
|
PopBefore.P50,PopBefore.P99,PopBefore.Maximum,PopAfter.P50,PopAfter.P99,PopAfter.Maximum,
|
||||||
Pop.MaximumNormalDistanceMeters,Pop.MaximumX,Pop.MaximumY,
|
static_cast<unsigned long long>(PopBefore.Cracks),static_cast<unsigned long long>(PopAfter.Cracks),
|
||||||
Pop.MaximumDisplayedZ,
|
|
||||||
Pop.MaximumSlopeDegrees,static_cast<unsigned long long>(Pop.Cracks),
|
|
||||||
static_cast<unsigned long long>(Pop.Floating),
|
|
||||||
PopPass?"PASS":"REJECT");
|
PopPass?"PASS":"REJECT");
|
||||||
PrintSlopeTail("swap",Lod,Pop.SlopeSamples,Pop.SlopeP99,Pop.SlopeMaximum);
|
|
||||||
|
|
||||||
const FHeightGrid MissingFineGrid(CoarseGrid.Side,CoarseGrid.MinimumX,
|
const FHeightGrid MissingFineGrid(BaselineGrid.Side,BaselineGrid.MinimumX,
|
||||||
CoarseGrid.MaximumX,CoarseGrid.MinimumY,CoarseGrid.MaximumY);
|
BaselineGrid.MaximumX,BaselineGrid.MinimumY,BaselineGrid.MaximumY);
|
||||||
const FPopRow MissingFineError=ComparePop(CoarseGrid,MissingFineGrid,
|
const FPopRow MissingFineError=ComparePop(BaselineGrid,MissingFineGrid,
|
||||||
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
||||||
const bool CrackRejected=MissingFineError.Cracks>0&&MissingFineError.Maximum>1.0;
|
const bool CrackRejected=MissingFineError.Cracks>0&&MissingFineError.Maximum>1.0;
|
||||||
FaultControlsPass=FaultControlsPass&&CrackRejected;
|
FaultControlsPass=FaultControlsPass&&CrackRejected;
|
||||||
GatePass=GatePass&&FaultControlsPass;
|
|
||||||
std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-fine-ring-surface cracks=%llu max_px=%.4f status=%s\n",
|
std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-fine-ring-surface cracks=%llu max_px=%.4f status=%s\n",
|
||||||
Lod,static_cast<unsigned long long>(MissingFineError.Cracks),
|
Lod,static_cast<unsigned long long>(MissingFineError.Cracks),
|
||||||
MissingFineError.Maximum,CrackRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
MissingFineError.Maximum,CrackRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
||||||
@@ -695,25 +851,170 @@ namespace StandaloneScreenSpaceGate
|
|||||||
const auto CheckNegative=[&](const char* Name,std::uint32_t Drop,std::uint32_t Stride)
|
const auto CheckNegative=[&](const char* Name,std::uint32_t Drop,std::uint32_t Stride)
|
||||||
{
|
{
|
||||||
FStandaloneCpuTile Bad=Streamer.GenerateDiagnostic(
|
FStandaloneCpuTile Bad=Streamer.GenerateDiagnostic(
|
||||||
CoarseTile.Key,Drop,Stride,true);
|
CoarseTile.Key,Drop,Stride,true,nullptr,true);
|
||||||
const FStandaloneCpuTile* BadPointer=&Bad;
|
const FStandaloneCpuTile* BadPointer=&Bad;
|
||||||
const std::span<const FStandaloneCpuTile* const> BadSpan(&BadPointer,1);
|
const std::span<const FStandaloneCpuTile* const> BadSpan(&BadPointer,1);
|
||||||
const FHeightGrid BadGrid=Rasterize(BadSpan,Bad.Key,VoxelMeters);
|
const FHeightGrid BadGrid=Rasterize(BadSpan,Bad.Key,VoxelMeters);
|
||||||
const FErrorRow BadError=CompareToReference(BadGrid,ExactGrid,
|
const FMeshComparisonRow BadError=CompareMeshes(BaselineGrid,BadGrid,
|
||||||
CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View);
|
CoarseTile.Key,VoxelMeters,Budgets[Lod],View);
|
||||||
const bool Rejected=BadError.Missing>0||BadError.Floating>0
|
const bool Rejected=BadError.Holes>0||BadError.Floating>0||BadError.Cracks>0
|
||||||
||BadError.Samples<16||BadError.Maximum>1.0;
|
||BadError.Geometry.Samples<16||BadError.Geometry.Maximum>1.0
|
||||||
|
||BadError.ShadeMaximum>ShadeLimit;
|
||||||
NegativeControlsPass=NegativeControlsPass&&Rejected;
|
NegativeControlsPass=NegativeControlsPass&&Rejected;
|
||||||
std::printf("[ScreenSpaceNegative] lod=%d case=%s p50_px=%.4f p99_px=%.4f max_px=%.4f missing=%llu floating=%llu status=%s\n",
|
std::printf("[ScreenSpaceNegative] lod=%d case=%s p50_px=%.4f p99_px=%.4f max_px=%.4f shade_max=%.6f holes=%llu cracks=%llu floating=%llu status=%s\n",
|
||||||
Lod,Name,BadError.P50,BadError.P99,BadError.Maximum,
|
Lod,Name,BadError.Geometry.P50,BadError.Geometry.P99,BadError.Geometry.Maximum,
|
||||||
static_cast<unsigned long long>(BadError.Missing),
|
BadError.ShadeMaximum,static_cast<unsigned long long>(BadError.Holes),
|
||||||
|
static_cast<unsigned long long>(BadError.Cracks),
|
||||||
static_cast<unsigned long long>(BadError.Floating),
|
static_cast<unsigned long long>(BadError.Floating),
|
||||||
Rejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
Rejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE");
|
||||||
};
|
};
|
||||||
CheckNegative("s13-global-octave-drop",1,1);
|
CheckNegative("s13-global-octave-drop",1,1);
|
||||||
CheckNegative("stride-2-column-interpolation",0,2);
|
CheckNegative("stride-2-column-interpolation",0,2);
|
||||||
GatePass=GatePass&&NegativeControlsPass;
|
GatePass=GatePass&&FaultControlsPass&&NegativeControlsPass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FCoverageRow
|
||||||
|
{
|
||||||
|
std::uint64_t Tiles=0,Holes=0,Cracks=0,Floating=0,MaterialMismatches=0;
|
||||||
|
std::vector<double> GeometryPixels,ShadeValues,NormalAngles;
|
||||||
|
};
|
||||||
|
std::array<FCoverageRow,9> Coverage;
|
||||||
|
for(const FVoxelTileKey& Key:DesiredKeys)
|
||||||
|
{
|
||||||
|
const int Lod=Key.Level;
|
||||||
|
if(Lod<3||Lod>MaxLod||Lod>=9)continue;
|
||||||
|
FStandaloneCpuTile BaselineTile=Streamer.GenerateDiagnostic(
|
||||||
|
Key,0,1,true,&BaselineOptions,true);
|
||||||
|
FStandaloneCpuTile CandidateTile=Streamer.GenerateDiagnostic(
|
||||||
|
Key,0,1,true,&CandidateOptions,true);
|
||||||
|
const FStandaloneCpuTile* BaselinePointer=&BaselineTile;
|
||||||
|
const FStandaloneCpuTile* CandidatePointer=&CandidateTile;
|
||||||
|
const std::span<const FStandaloneCpuTile* const> BaselineSpan(&BaselinePointer,1);
|
||||||
|
const std::span<const FStandaloneCpuTile* const> CandidateSpan(&CandidatePointer,1);
|
||||||
|
const FHeightGrid BaselineGrid=Rasterize(BaselineSpan,Key,VoxelMeters);
|
||||||
|
const FHeightGrid CandidateGrid=Rasterize(CandidateSpan,Key,VoxelMeters);
|
||||||
|
const FMeshComparisonRow Delta=CompareMeshes(BaselineGrid,CandidateGrid,
|
||||||
|
Key,VoxelMeters,Budgets[Lod],View);
|
||||||
|
auto& Row=Coverage[Lod];
|
||||||
|
++Row.Tiles;
|
||||||
|
Row.Holes+=Delta.Holes;Row.Cracks+=Delta.Cracks;Row.Floating+=Delta.Floating;
|
||||||
|
Row.MaterialMismatches+=Delta.MaterialMismatches;
|
||||||
|
Row.GeometryPixels.insert(Row.GeometryPixels.end(),
|
||||||
|
Delta.GeometryPixels.begin(),Delta.GeometryPixels.end());
|
||||||
|
Row.ShadeValues.insert(Row.ShadeValues.end(),Delta.ShadeValues.begin(),Delta.ShadeValues.end());
|
||||||
|
Row.NormalAngles.insert(Row.NormalAngles.end(),Delta.NormalAngles.begin(),Delta.NormalAngles.end());
|
||||||
|
}
|
||||||
|
for(int Lod=3;Lod<=MaxLod&&Lod<9;++Lod)
|
||||||
|
{
|
||||||
|
auto& Row=Coverage[Lod];
|
||||||
|
const double GeometryP50=Percentile(Row.GeometryPixels,0.50);
|
||||||
|
const double GeometryP99=Percentile(Row.GeometryPixels,0.99);
|
||||||
|
const double GeometryMaximum=Row.GeometryPixels.empty()?0.0
|
||||||
|
:*std::max_element(Row.GeometryPixels.begin(),Row.GeometryPixels.end());
|
||||||
|
const double ShadeP50=Percentile(Row.ShadeValues,0.50);
|
||||||
|
const double ShadeP99=Percentile(Row.ShadeValues,0.99);
|
||||||
|
const double ShadeMaximum=Row.ShadeValues.empty()?0.0
|
||||||
|
:*std::max_element(Row.ShadeValues.begin(),Row.ShadeValues.end());
|
||||||
|
const double NormalP50=Percentile(Row.NormalAngles,0.50);
|
||||||
|
const double NormalP99=Percentile(Row.NormalAngles,0.99);
|
||||||
|
const double NormalMaximum=Row.NormalAngles.empty()?0.0
|
||||||
|
:*std::max_element(Row.NormalAngles.begin(),Row.NormalAngles.end());
|
||||||
|
const bool LodPass=Row.Tiles>0&&Row.GeometryPixels.size()>=16
|
||||||
|
&&GeometryMaximum<=1.0&&ShadeMaximum<=ShadeLimit
|
||||||
|
&&Row.Holes==0&&Row.Cracks==0&&Row.Floating==0;
|
||||||
|
GatePass=GatePass&&LodPass;
|
||||||
|
std::printf("[ScreenSpaceGateCoverageLOD] lod=%d tiles=%llu geometry_samples=%llu geometry_px_p50=%.4f p99=%.4f max=%.4f shading_delta_p50=%.6f p99=%.6f max=%.6f normal_deg_p50=%.4f p99=%.4f max=%.4f material_mismatch=%llu holes=%llu cracks=%llu floating=%llu status=%s\n",
|
||||||
|
Lod,static_cast<unsigned long long>(Row.Tiles),
|
||||||
|
static_cast<unsigned long long>(Row.GeometryPixels.size()),
|
||||||
|
GeometryP50,GeometryP99,GeometryMaximum,ShadeP50,ShadeP99,ShadeMaximum,
|
||||||
|
NormalP50,NormalP99,NormalMaximum,
|
||||||
|
static_cast<unsigned long long>(Row.MaterialMismatches),
|
||||||
|
static_cast<unsigned long long>(Row.Holes),
|
||||||
|
static_cast<unsigned long long>(Row.Cracks),
|
||||||
|
static_cast<unsigned long long>(Row.Floating),LodPass?"PASS":"REJECT");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint64_t RepairOpportunities=0,BaselineWallHoles=0,BaselineWallFloating=0;
|
||||||
|
std::uint64_t CandidateWallRays=0,CandidateWallHoles=0,CandidateWallFloating=0;
|
||||||
|
std::vector<std::uint64_t> BaselineHoleIds,CandidateHoleIds;
|
||||||
|
std::vector<std::uint64_t> BaselineFloatingIds,CandidateFloatingIds;
|
||||||
|
std::vector<std::uint64_t> CandidatePanelRemovalHoleIds;
|
||||||
|
bool bRanPanelRemovalControl=false;
|
||||||
|
for(const FVoxelTileKey& Key:DesiredKeys)
|
||||||
|
{
|
||||||
|
if(Key.Level<6||Key.Level>8)continue;
|
||||||
|
VoxelForgeCore::TileGenerationOptions NoPanelBaseline=BaselineOptions;
|
||||||
|
NoPanelBaseline.diagnosticRemoveFarConnectorPanels=true;
|
||||||
|
NoPanelBaseline.diagnosticAuditFarConnectorCoverage=true;
|
||||||
|
const FStandaloneCpuTile BaseNoPanels=Streamer.GenerateDiagnostic(Key,0,1,true,&NoPanelBaseline);
|
||||||
|
RepairOpportunities+=BaseNoPanels.CoreDiagnostics.farConnectorAuditHoles;
|
||||||
|
|
||||||
|
VoxelForgeCore::TileGenerationOptions AuditedBaseline=BaselineOptions;
|
||||||
|
AuditedBaseline.diagnosticAuditFarConnectorCoverage=true;
|
||||||
|
const FStandaloneCpuTile BaseRepaired=Streamer.GenerateDiagnostic(Key,0,1,true,&AuditedBaseline);
|
||||||
|
BaselineWallHoles+=BaseRepaired.CoreDiagnostics.farConnectorAuditHoles;
|
||||||
|
BaselineWallFloating+=BaseRepaired.CoreDiagnostics.farConnectorAuditFloating;
|
||||||
|
BaselineHoleIds.insert(BaselineHoleIds.end(),
|
||||||
|
BaseRepaired.CoreDiagnostics.farConnectorAuditHoleIds.begin(),
|
||||||
|
BaseRepaired.CoreDiagnostics.farConnectorAuditHoleIds.end());
|
||||||
|
BaselineFloatingIds.insert(BaselineFloatingIds.end(),
|
||||||
|
BaseRepaired.CoreDiagnostics.farConnectorAuditFloatingIds.begin(),
|
||||||
|
BaseRepaired.CoreDiagnostics.farConnectorAuditFloatingIds.end());
|
||||||
|
|
||||||
|
VoxelForgeCore::TileGenerationOptions AuditedCandidate=CandidateOptions;
|
||||||
|
AuditedCandidate.diagnosticAuditFarConnectorCoverage=true;
|
||||||
|
const FStandaloneCpuTile CandidateRepaired=Streamer.GenerateDiagnostic(Key,0,1,true,&AuditedCandidate);
|
||||||
|
CandidateWallRays+=CandidateRepaired.CoreDiagnostics.farConnectorAuditRays;
|
||||||
|
CandidateWallHoles+=CandidateRepaired.CoreDiagnostics.farConnectorAuditHoles;
|
||||||
|
CandidateWallFloating+=CandidateRepaired.CoreDiagnostics.farConnectorAuditFloating;
|
||||||
|
CandidateHoleIds.insert(CandidateHoleIds.end(),
|
||||||
|
CandidateRepaired.CoreDiagnostics.farConnectorAuditHoleIds.begin(),
|
||||||
|
CandidateRepaired.CoreDiagnostics.farConnectorAuditHoleIds.end());
|
||||||
|
CandidateFloatingIds.insert(CandidateFloatingIds.end(),
|
||||||
|
CandidateRepaired.CoreDiagnostics.farConnectorAuditFloatingIds.begin(),
|
||||||
|
CandidateRepaired.CoreDiagnostics.farConnectorAuditFloatingIds.end());
|
||||||
|
|
||||||
|
if(!bRanPanelRemovalControl&&BaseNoPanels.CoreDiagnostics.farConnectorAuditHoles>0)
|
||||||
|
{
|
||||||
|
AuditedCandidate.diagnosticRemoveFarConnectorPanels=true;
|
||||||
|
const FStandaloneCpuTile CandidateNoPanels=Streamer.GenerateDiagnostic(
|
||||||
|
Key,0,1,true,&AuditedCandidate);
|
||||||
|
CandidatePanelRemovalHoleIds=CandidateNoPanels.CoreDiagnostics.farConnectorAuditHoleIds;
|
||||||
|
bRanPanelRemovalControl=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(BaselineHoleIds.begin(),BaselineHoleIds.end());
|
||||||
|
std::sort(CandidateHoleIds.begin(),CandidateHoleIds.end());
|
||||||
|
std::sort(BaselineFloatingIds.begin(),BaselineFloatingIds.end());
|
||||||
|
std::sort(CandidateFloatingIds.begin(),CandidateFloatingIds.end());
|
||||||
|
const auto IsSubset=[](const std::vector<std::uint64_t>& A,const std::vector<std::uint64_t>& B)
|
||||||
|
{return std::includes(B.begin(),B.end(),A.begin(),A.end());};
|
||||||
|
std::sort(CandidatePanelRemovalHoleIds.begin(),CandidatePanelRemovalHoleIds.end());
|
||||||
|
const bool bNoNewHoles=IsSubset(CandidateHoleIds,BaselineHoleIds);
|
||||||
|
const bool bNoNewFloating=IsSubset(CandidateFloatingIds,BaselineFloatingIds);
|
||||||
|
const bool bPanelRemovalDetected=bRanPanelRemovalControl
|
||||||
|
&&!IsSubset(CandidatePanelRemovalHoleIds,CandidateHoleIds);
|
||||||
|
std::vector<std::uint64_t> NewHoleIds,NewFloatingIds,PanelRemovalNewHoleIds;
|
||||||
|
std::set_difference(CandidateHoleIds.begin(),CandidateHoleIds.end(),
|
||||||
|
BaselineHoleIds.begin(),BaselineHoleIds.end(),std::back_inserter(NewHoleIds));
|
||||||
|
std::set_difference(CandidateFloatingIds.begin(),CandidateFloatingIds.end(),
|
||||||
|
BaselineFloatingIds.begin(),BaselineFloatingIds.end(),std::back_inserter(NewFloatingIds));
|
||||||
|
std::set_difference(CandidatePanelRemovalHoleIds.begin(),CandidatePanelRemovalHoleIds.end(),
|
||||||
|
CandidateHoleIds.begin(),CandidateHoleIds.end(),std::back_inserter(PanelRemovalNewHoleIds));
|
||||||
|
const bool bRepairRowsPass=RepairOpportunities>0&&bNoNewHoles&&bNoNewFloating
|
||||||
|
&&bPanelRemovalDetected;
|
||||||
|
GatePass=GatePass&&bRepairRowsPass;
|
||||||
|
std::printf("[ScreenSpaceConnectorGate] lods=6..8 baseline_missing_ray_stations=%llu candidate_audited_rays=%llu baseline_holes=%llu candidate_holes=%llu new_holes=%llu baseline_floating=%llu candidate_floating=%llu new_floating=%llu panel_removal_new_holes=%llu status=%s\n",
|
||||||
|
static_cast<unsigned long long>(RepairOpportunities),
|
||||||
|
static_cast<unsigned long long>(CandidateWallRays),
|
||||||
|
static_cast<unsigned long long>(BaselineWallHoles),
|
||||||
|
static_cast<unsigned long long>(CandidateWallHoles),
|
||||||
|
static_cast<unsigned long long>(NewHoleIds.size()),
|
||||||
|
static_cast<unsigned long long>(BaselineWallFloating),
|
||||||
|
static_cast<unsigned long long>(CandidateWallFloating),
|
||||||
|
static_cast<unsigned long long>(NewFloatingIds.size()),
|
||||||
|
static_cast<unsigned long long>(PanelRemovalNewHoleIds.size()),
|
||||||
|
bRepairRowsPass?"PASS":"REJECT");
|
||||||
std::printf("[ScreenSpaceGateResult] status=%s\n",GatePass?"PASS":"REJECT");
|
std::printf("[ScreenSpaceGateResult] status=%s\n",GatePass?"PASS":"REJECT");
|
||||||
return GatePass;
|
return GatePass;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ namespace
|
|||||||
float CameraYawDegrees=0.0f;
|
float CameraYawDegrees=0.0f;
|
||||||
float CameraPitchDegrees=0.0f;
|
float CameraPitchDegrees=0.0f;
|
||||||
std::filesystem::path MeshExportPath;
|
std::filesystem::path MeshExportPath;
|
||||||
std::filesystem::path WorldAssemblyManifestOutputPath;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
bool ParseInteger(std::string_view text,int& output)
|
bool ParseInteger(std::string_view text,int& output)
|
||||||
@@ -116,7 +115,6 @@ namespace
|
|||||||
" --region-crossing (compile entering Core regions at the camera step)\n"
|
" --region-crossing (compile entering Core regions at the camera step)\n"
|
||||||
" --column-cache=shared|thread-local (default: shared)\n"
|
" --column-cache=shared|thread-local (default: shared)\n"
|
||||||
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
|
" --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n"
|
||||||
" --world-assembler-manifest-output=FILE (write the resolved plan for diagnostics)\n"
|
|
||||||
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
||||||
" --seed=N --recipe=surface-world|floating-islands\n"
|
" --seed=N --recipe=surface-world|floating-islands\n"
|
||||||
" --auto-surface-camera (place the 27-tile cube on this seed's surface)\n"
|
" --auto-surface-camera (place the 27-tile cube on this seed's surface)\n"
|
||||||
@@ -206,13 +204,6 @@ namespace
|
|||||||
if(options.MeshExportPath.empty())
|
if(options.MeshExportPath.empty())
|
||||||
{std::fprintf(stderr,"--mesh-export needs a file path\n");return false;}
|
{std::fprintf(stderr,"--mesh-export needs a file path\n");return false;}
|
||||||
}
|
}
|
||||||
else if(arg.starts_with("--world-assembler-manifest-output="))
|
|
||||||
{
|
|
||||||
options.WorldAssemblyManifestOutputPath=std::filesystem::path(
|
|
||||||
arg.substr(std::string_view("--world-assembler-manifest-output=").size()));
|
|
||||||
if(options.WorldAssemblyManifestOutputPath.empty())
|
|
||||||
{std::fprintf(stderr,"--world-assembler-manifest-output needs a file path\n");return false;}
|
|
||||||
}
|
|
||||||
else if(arg.starts_with("--workers="))
|
else if(arg.starts_with("--workers="))
|
||||||
{if(!readInt("--workers=",options.WorkerCount))return false;}
|
{if(!readInt("--workers=",options.WorkerCount))return false;}
|
||||||
else if(arg.starts_with("--clip-radius="))
|
else if(arg.starts_with("--clip-radius="))
|
||||||
@@ -323,22 +314,6 @@ int main(int argc,char** argv)
|
|||||||
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
&&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
||||||
*ownerWorld,ownerJsonRoot,worldAssembly,error))
|
*ownerWorld,ownerJsonRoot,worldAssembly,error))
|
||||||
{std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
{std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
||||||
if(!options.WorldAssemblyManifestOutputPath.empty())
|
|
||||||
{
|
|
||||||
std::error_code directoryError;
|
|
||||||
const auto parent=options.WorldAssemblyManifestOutputPath.parent_path();
|
|
||||||
if(!parent.empty())std::filesystem::create_directories(parent,directoryError);
|
|
||||||
if(directoryError)
|
|
||||||
{std::fprintf(stderr,"Manifest output directory failed: %s\n",directoryError.message().c_str());return 2;}
|
|
||||||
std::ofstream manifestFile(options.WorldAssemblyManifestOutputPath,std::ios::binary|std::ios::trunc);
|
|
||||||
if(!manifestFile)
|
|
||||||
{std::fprintf(stderr,"Cannot write manifest output: %s\n",options.WorldAssemblyManifestOutputPath.string().c_str());return 2;}
|
|
||||||
manifestFile<<worldAssembly.ToJson()<<'\n';
|
|
||||||
if(!manifestFile)
|
|
||||||
{std::fprintf(stderr,"Manifest output write failed: %s\n",options.WorldAssemblyManifestOutputPath.string().c_str());return 2;}
|
|
||||||
std::printf("[WorldAssemblyManifest] path=%s status=PASS\n",
|
|
||||||
options.WorldAssemblyManifestOutputPath.string().c_str());
|
|
||||||
}
|
|
||||||
if(options.AutoSurfaceCamera)
|
if(options.AutoSurfaceCamera)
|
||||||
{
|
{
|
||||||
constexpr std::int64_t UnitsPerVoxel=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
constexpr std::int64_t UnitsPerVoxel=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||||
|
|||||||
@@ -412,6 +412,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
|
FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
|
||||||
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap,
|
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap,
|
||||||
FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache);
|
FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache);
|
||||||
|
Streamer.SetDisplayAttributeCaptureEnabled(bScreenSpaceGate);
|
||||||
Streamer.SetUniformBlockProofAuditEnabled(bAuditUniformBlockProofs);
|
Streamer.SetUniformBlockProofAuditEnabled(bAuditUniformBlockProofs);
|
||||||
if(bScreenSpaceGate||bReportLodWorkProfile)Streamer.SetMesherStageProfilingEnabled(true);
|
if(bScreenSpaceGate||bReportLodWorkProfile)Streamer.SetMesherStageProfilingEnabled(true);
|
||||||
const auto StreamStart=Clock::now();
|
const auto StreamStart=Clock::now();
|
||||||
@@ -1480,7 +1481,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
{
|
{
|
||||||
if(Assembly)
|
if(Assembly)
|
||||||
bScreenSpaceGatePassing=StandaloneScreenSpaceGate::Run(GateSampleTiles,
|
bScreenSpaceGatePassing=StandaloneScreenSpaceGate::Run(GateSampleTiles,
|
||||||
Streamer,*Assembly,ClosestDisplayDistances,World.CoreVoxelSizeMicrons,
|
Streamer,Desired.Tiles,*Assembly,ClosestDisplayDistances,World.CoreVoxelSizeMicrons,
|
||||||
World.MaxClipLevel,PixelBudgetsSubvoxels,CameraYawDegrees,
|
World.MaxClipLevel,PixelBudgetsSubvoxels,CameraYawDegrees,
|
||||||
CameraPitchDegrees);
|
CameraPitchDegrees);
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -43,9 +43,11 @@ struct FStandaloneCpuTile
|
|||||||
FVoxelTileKey Key;
|
FVoxelTileKey Key;
|
||||||
uint64 RequestedCrossing=0;
|
uint64 RequestedCrossing=0;
|
||||||
std::vector<FStandaloneCpuVertex> Vertices;
|
std::vector<FStandaloneCpuVertex> Vertices;
|
||||||
|
std::vector<std::array<uint8,4>> VertexColors;
|
||||||
std::vector<uint32> Indices;
|
std::vector<uint32> Indices;
|
||||||
std::vector<uint8> TriangleSkirtFaceMasks;
|
std::vector<uint8> TriangleSkirtFaceMasks;
|
||||||
uint32 SurfaceVertexCount=0;
|
uint32 SurfaceVertexCount=0;
|
||||||
|
uint32 CeilingTriangleCount=0;
|
||||||
uint32 SkirtTriangleCount=0;
|
uint32 SkirtTriangleCount=0;
|
||||||
uint64 DensitySamples=0;
|
uint64 DensitySamples=0;
|
||||||
double WorkerSeconds=0.0;
|
double WorkerSeconds=0.0;
|
||||||
@@ -653,12 +655,17 @@ public:
|
|||||||
{bProfileMesherStages=Enabled;}
|
{bProfileMesherStages=Enabled;}
|
||||||
void SetUniformBlockProofAuditEnabled(bool Enabled)
|
void SetUniformBlockProofAuditEnabled(bool Enabled)
|
||||||
{bAuditUniformBlockProofs=Enabled;}
|
{bAuditUniformBlockProofs=Enabled;}
|
||||||
|
void SetDisplayAttributeCaptureEnabled(bool Enabled)
|
||||||
|
{bCaptureDisplayAttributes=Enabled;}
|
||||||
FStandaloneCpuTile GenerateDiagnostic(const FVoxelTileKey& Key,
|
FStandaloneCpuTile GenerateDiagnostic(const FVoxelTileKey& Key,
|
||||||
std::uint32_t GlobalOctaveDrop=0,std::uint32_t CellStride=1,
|
std::uint32_t GlobalOctaveDrop=0,std::uint32_t CellStride=1,
|
||||||
bool ForceGenerate=false)
|
bool ForceGenerate=false,
|
||||||
|
const VoxelForgeCore::TileGenerationOptions* FarDisplayOverride=nullptr,
|
||||||
|
bool CaptureDisplayAttributes=false)
|
||||||
{
|
{
|
||||||
FStandaloneTileJob Job;Job.Key=Key;Job.StartedAt=FClock::now();Job.QueuedAt=Job.StartedAt;
|
FStandaloneTileJob Job;Job.Key=Key;Job.StartedAt=FClock::now();Job.QueuedAt=Job.StartedAt;
|
||||||
return Generate(Job,GlobalOctaveDrop,CellStride,ForceGenerate);
|
return Generate(Job,GlobalOctaveDrop,CellStride,ForceGenerate,FarDisplayOverride,
|
||||||
|
CaptureDisplayAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -778,7 +785,9 @@ private:
|
|||||||
}
|
}
|
||||||
FStandaloneCpuTile Generate(const FStandaloneTileJob& Job,
|
FStandaloneCpuTile Generate(const FStandaloneTileJob& Job,
|
||||||
std::uint32_t DiagnosticGlobalOctaveDrop=0,std::uint32_t DiagnosticCellStride=1,
|
std::uint32_t DiagnosticGlobalOctaveDrop=0,std::uint32_t DiagnosticCellStride=1,
|
||||||
bool ForceGenerate=false)
|
bool ForceGenerate=false,
|
||||||
|
const VoxelForgeCore::TileGenerationOptions* FarDisplayOverride=nullptr,
|
||||||
|
bool CaptureDisplayAttributes=false)
|
||||||
{
|
{
|
||||||
const auto StartTime=Job.StartedAt;
|
const auto StartTime=Job.StartedAt;
|
||||||
FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing;
|
FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing;
|
||||||
@@ -829,6 +838,18 @@ private:
|
|||||||
Options.diagnosticCellStride=std::max<std::uint32_t>(1,DiagnosticCellStride);
|
Options.diagnosticCellStride=std::max<std::uint32_t>(1,DiagnosticCellStride);
|
||||||
Options.diagnosticColumnInterpolationStride=
|
Options.diagnosticColumnInterpolationStride=
|
||||||
std::max<std::uint32_t>(1,DiagnosticCellStride);
|
std::max<std::uint32_t>(1,DiagnosticCellStride);
|
||||||
|
if(FarDisplayOverride)
|
||||||
|
{
|
||||||
|
Options.overrideFarDisplayMesh=FarDisplayOverride->overrideFarDisplayMesh;
|
||||||
|
Options.farDisplayGeometryBudgetMillipixels=FarDisplayOverride->farDisplayGeometryBudgetMillipixels;
|
||||||
|
Options.farRefinementGridScale=FarDisplayOverride->farRefinementGridScale;
|
||||||
|
Options.farRefinementLodMask=FarDisplayOverride->farRefinementLodMask;
|
||||||
|
Options.farConnectorStationScale=FarDisplayOverride->farConnectorStationScale;
|
||||||
|
Options.farConnectorRayMask=FarDisplayOverride->farConnectorRayMask;
|
||||||
|
Options.farConnectorReuseStationRadii=FarDisplayOverride->farConnectorReuseStationRadii;
|
||||||
|
Options.diagnosticRemoveFarConnectorPanels=FarDisplayOverride->diagnosticRemoveFarConnectorPanels;
|
||||||
|
Options.diagnosticAuditFarConnectorCoverage=FarDisplayOverride->diagnosticAuditFarConnectorCoverage;
|
||||||
|
}
|
||||||
std::string Error;
|
std::string Error;
|
||||||
auto Result=CoreWorld->generateTile(
|
auto Result=CoreWorld->generateTile(
|
||||||
{{Job.Key.Coord.X,Job.Key.Coord.Y,Job.Key.Coord.Z},Job.Key.Level},Options,Error);
|
{{Job.Key.Coord.X,Job.Key.Coord.Y,Job.Key.Coord.Z},Job.Key.Level},Options,Error);
|
||||||
@@ -842,6 +863,7 @@ private:
|
|||||||
Out.DensitySamples=Result->densitySampleCount;
|
Out.DensitySamples=Result->densitySampleCount;
|
||||||
Out.CoreDiagnostics=Result->diagnostics;
|
Out.CoreDiagnostics=Result->diagnostics;
|
||||||
Out.SurfaceVertexCount=Result->groups.surfaceVertexCount;
|
Out.SurfaceVertexCount=Result->groups.surfaceVertexCount;
|
||||||
|
Out.CeilingTriangleCount=Result->groups.ceilingTriangleCount;
|
||||||
Out.SkirtTriangleCount=Result->groups.skirtTriangleCount;
|
Out.SkirtTriangleCount=Result->groups.skirtTriangleCount;
|
||||||
Out.MeshPayloadAllocatedBytes=Result->meshPayloadAllocatedBytes;
|
Out.MeshPayloadAllocatedBytes=Result->meshPayloadAllocatedBytes;
|
||||||
Out.DensityHash=Result->densityHash;Out.MeshHash=Result->meshHash;
|
Out.DensityHash=Result->densityHash;Out.MeshHash=Result->meshHash;
|
||||||
@@ -849,12 +871,18 @@ private:
|
|||||||
Out.bHasDensityHash=Result->hasDensityHash;Out.bHasMeshHash=Result->hasMeshHash;
|
Out.bHasDensityHash=Result->hasDensityHash;Out.bHasMeshHash=Result->hasMeshHash;
|
||||||
Out.bHasPayloadHash=Result->hasPayloadHash;Out.bHasTopologyHash=Result->hasTopologyHash;
|
Out.bHasPayloadHash=Result->hasPayloadHash;Out.bHasTopologyHash=Result->hasTopologyHash;
|
||||||
Out.Vertices.reserve(Result->vertices.size());
|
Out.Vertices.reserve(Result->vertices.size());
|
||||||
|
const bool bCaptureAttributes=CaptureDisplayAttributes||bCaptureDisplayAttributes;
|
||||||
|
if(bCaptureAttributes)Out.VertexColors.reserve(Result->vertices.size());
|
||||||
for(const auto& Vertex:Result->vertices)
|
for(const auto& Vertex:Result->vertices)
|
||||||
|
{
|
||||||
Out.Vertices.push_back({
|
Out.Vertices.push_back({
|
||||||
{static_cast<float>(Vertex.position.x*0.01),static_cast<float>(Vertex.position.y*0.01),
|
{static_cast<float>(Vertex.position.x*0.01),static_cast<float>(Vertex.position.y*0.01),
|
||||||
static_cast<float>(Vertex.position.z*0.01)},
|
static_cast<float>(Vertex.position.z*0.01)},
|
||||||
{static_cast<float>(Vertex.normal.x),static_cast<float>(Vertex.normal.y),
|
{static_cast<float>(Vertex.normal.x),static_cast<float>(Vertex.normal.y),
|
||||||
static_cast<float>(Vertex.normal.z)}});
|
static_cast<float>(Vertex.normal.z)}});
|
||||||
|
if(bCaptureAttributes)
|
||||||
|
Out.VertexColors.push_back({Vertex.color.r,Vertex.color.g,Vertex.color.b,Vertex.color.a});
|
||||||
|
}
|
||||||
Out.Indices=std::move(Result->indices);
|
Out.Indices=std::move(Result->indices);
|
||||||
Out.TriangleSkirtFaceMasks=std::move(Result->skirtFaceMasks);
|
Out.TriangleSkirtFaceMasks=std::move(Result->skirtFaceMasks);
|
||||||
if(bProfile)
|
if(bProfile)
|
||||||
@@ -1001,6 +1029,7 @@ private:
|
|||||||
bool bSkipUV=false;
|
bool bSkipUV=false;
|
||||||
bool bSkipColors=false;
|
bool bSkipColors=false;
|
||||||
bool bCaptureColumnCoordinates=false;
|
bool bCaptureColumnCoordinates=false;
|
||||||
|
bool bCaptureDisplayAttributes=false;
|
||||||
bool bSharedColumnCache=true;
|
bool bSharedColumnCache=true;
|
||||||
int32 Lod8HorizonInteriorStepVoxels=0;
|
int32 Lod8HorizonInteriorStepVoxels=0;
|
||||||
bool bProfileMesherStages=false;
|
bool bProfileMesherStages=false;
|
||||||
|
|||||||
@@ -30,6 +30,34 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
return std::max<std::int64_t>(1,WorldAssemblyFixed(wavelength));
|
return std::max<std::int64_t>(1,WorldAssemblyFixed(wavelength));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool ApplyFarDisplayMeshDeclaration(const VFJson::Value& root,
|
||||||
|
VoxelForgeCore::WorldAssemblyRecipeV1& recipe,std::string& error)
|
||||||
|
{
|
||||||
|
const VFJson::Value& declaration=root.Get("far_display_mesh");
|
||||||
|
if(std::holds_alternative<std::nullptr_t>(declaration.Data))return true;
|
||||||
|
if(!declaration.IsObject())
|
||||||
|
{error="far_display_mesh must be an object when declared";return false;}
|
||||||
|
const auto ReadUnsigned=[&](const char* key,std::uint32_t& output)
|
||||||
|
{
|
||||||
|
const VFJson::Value& value=declaration.Get(key);
|
||||||
|
if(!value.IsNumber()||!std::isfinite(value.Number())||value.Number()<0.0
|
||||||
|
||value.Number()>static_cast<double>(UINT32_MAX)
|
||||||
|
||std::floor(value.Number())!=value.Number())return false;
|
||||||
|
output=static_cast<std::uint32_t>(value.Number());return true;
|
||||||
|
};
|
||||||
|
if(!ReadUnsigned("geometry_budget_millipixels",recipe.farDisplayGeometryBudgetMillipixels)
|
||||||
|
||!ReadUnsigned("refinement_grid_scale",recipe.farRefinementGridScale)
|
||||||
|
||!ReadUnsigned("refinement_lod_mask",recipe.farRefinementLodMask)
|
||||||
|
||!ReadUnsigned("connector_station_scale",recipe.farConnectorStationScale)
|
||||||
|
||!ReadUnsigned("connector_ray_mask",recipe.farConnectorRayMask))
|
||||||
|
{error="far_display_mesh declaration has a missing or non-integer parameter";return false;}
|
||||||
|
const VFJson::Value& reuse=declaration.Get("connector_reuse_station_radii");
|
||||||
|
if(!std::holds_alternative<bool>(reuse.Data))
|
||||||
|
{error="far_display_mesh connector_reuse_station_radii must be boolean";return false;}
|
||||||
|
recipe.farConnectorReuseStationRadii=std::get<bool>(reuse.Data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
inline uint64 WorldAssemblyFeatureId(uint32 seed,int64 x,int64 y,uint32 kind)
|
inline uint64 WorldAssemblyFeatureId(uint32 seed,int64 x,int64 y,uint32 kind)
|
||||||
{
|
{
|
||||||
uint64 hash=14695981039346656037ull;
|
uint64 hash=14695981039346656037ull;
|
||||||
@@ -223,6 +251,7 @@ namespace StandaloneWorldAssemblyPrototype
|
|||||||
recipe.biomeClimateReliefWavelengthSubvoxels=WorldAssemblyWavelength(map.ReliefFrequency,3333.0);
|
recipe.biomeClimateReliefWavelengthSubvoxels=WorldAssemblyWavelength(map.ReliefFrequency,3333.0);
|
||||||
recipe.biomeClimateMoistureWavelengthSubvoxels=WorldAssemblyWavelength(map.MoistureFrequency,2500.0);
|
recipe.biomeClimateMoistureWavelengthSubvoxels=WorldAssemblyWavelength(map.MoistureFrequency,2500.0);
|
||||||
recipe.biomeClimateReliefContrastQ16=WorldAssemblyQ16(map.ReliefContrast);
|
recipe.biomeClimateReliefContrastQ16=WorldAssemblyQ16(map.ReliefContrast);
|
||||||
|
if(!ApplyFarDisplayMeshDeclaration(root,recipe,error))return false;
|
||||||
const auto makeProfile=[&](uint32 stableId,const FSurfaceGenerationParams& params,
|
const auto makeProfile=[&](uint32 stableId,const FSurfaceGenerationParams& params,
|
||||||
const FBiomeResolved* climate)
|
const FBiomeResolved* climate)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user