standalone: far-survival audit + LOD8 default with explicit far shells; neighbourhood-first viewer

- FarSurvivalAudit: checks every stratum top/bottom boundary (4x4 columns per far tile) and every
  in-view far connector (sections <=128 voxels, 4 radial rays) against the emitted mesh. Falsified:
  removing one tile's shell triangles -> 16 missing, 16 holes, exit 14. Passes at LOD6/7/8.
- Far connectors follow source radial crossings; strata patches emitted at LOD7/8. Default LOD8.
- Viewer: separate near ShapeWorld for the 27 player tiles, full world built on its own thread;
  27/27 tiles hash-identical to the direct build. W1/W8 60 hash fields equal; routes 5642/5642.
Cost, measured by me head to head (8 workers, cold, BelowNormal 0xFF00): LOD8 without the shells
1.17-1.25 s, LOD8 with the shells 1.80 s; payload 92.6 -> 276.7 MiB. The shells are far too
expensive, and the pipelined benchmark whole view is 2.92 s (shape compiled twice): both to fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-04 22:51:44 +02:00
1 parent 5602a281b9
commit a40b0454b3
7 files changed
+1350 -17

No files matched your search

@@ -1,5 +1,49 @@
# Shape world speed round — final half-load verification # Shape world speed round — final half-load verification
## Round 6 — far survival proof and neighborhood-first startup (2026-10-04)
This result supersedes Round 5's LOD6 default. The far-survival audit now passes at LOD6, LOD7, and LOD8, and LOD8 is the least expensive passing level. The standalone viewer also starts the player's neighborhood before compiling the full shape world.
### Far-field survival proof and default
[`FarSurvivalAudit.h`](../../../Source/FarSurvivalAudit.h) compares the source density field with intersections against the triangles actually emitted in each far tile. For each unique top and bottom boundary of the loaded strata, it samples 4×4 interior columns per far tile and checks every source density crossing in the seal band against the emitted mesh. Missing crossings fail the check; an odd number of missing crossings records a hole. For each far connector in view, it samples longitudinal sections at no more than 128-voxel spacing, casts radial rays in four directions, and compares each source-visible connector wall with mesh hits. Every visible section must pass, and each in-view connector must have a passing section.
The bad-setting control removes the emitted stratum-shell triangles around z=32 voxels in one LOD6 tile inside the LOD8 clipmap. The audit then reports 16 missing surfaces and 16 holes and exits with status 14. The final control is [here](round6_final_bad_shell_lod8_w8.stdout.log). The injected failure demonstrates that the check detects an actual mesh loss.
| Max LOD | Far tiles | Boundary surfaces checked | Missing / holes | In-view connectors / sections | Connector missing / holes / unsampled | Full-payload allocation |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 6 | 6,498 | 38,160 | 0 / 0 | 9,752 / 158,240 | 0 / 0 / 0 | 415.34 MiB |
| 7 | 3,042 | 20,256 | 0 / 0 | 10,152 / 164,806 | 0 / 0 / 0 | 413.90 MiB |
| 8 | 2,226 | 7,200 | 0 / 0 | 10,976 / 178,351 | 0 / 0 / 0 | **276.74 MiB** |
The LOD6, LOD7, and LOD8 audit outputs are [LOD6](round6_connector_aligned_lod6_w8.stdout.log), [LOD7](round6_connector_aligned_lod7_w8.stdout.log), and [LOD8](round6_connector_aligned_lod8_w8.stdout.log). The connector counts include sections with no source-visible radial wall at a room join; those sections are recorded separately, while each source-visible wall at every sampled section is checked. The final candidate passes with no missing surfaces, holes, or unsampled connectors.
The strict connector audit exposed why a fixed-radius far tube lost walls: source-visible radii varied well outside its nominal radius. [`StreamingWorld.h`](../../../Source/StreamingWorld.h) now follows source radial crossings at connector sections aligned with the audit samples and emits the sampled strata patches at LOD7 and LOD8. [`StandaloneShapeWorld.h`](../../../Source/StandaloneShapeWorld.h) retains connector paths and forced air samples in an independent far-shape build. LOD8 therefore passes the feature check and allocates 276.74 MiB versus 413.90 MiB at LOD7 and 415.34 MiB at LOD6. The standalone and JSON defaults are now LOD8 in [`OwnerWorldLoader.h`](../../../Source/OwnerWorldLoader.h) and [`owner_world.json`](../../../data/owner_world.json).
The six fixed LOD6→LOD8 capture pairs are pixel-identical: [chamber](round6_far_survival/lod6_fixed_chamber.png) → [LOD8](round6_far_survival/lod8_fixed_chamber.png), [tunnel](round6_far_survival/lod6_fixed_tunnel.png) → [LOD8](round6_far_survival/lod8_fixed_tunnel.png), [passage](round6_far_survival/lod6_fixed_passage.png) → [LOD8](round6_far_survival/lod8_fixed_passage.png), [far](round6_far_survival/lod6_fixed_far.png) → [LOD8](round6_far_survival/lod8_fixed_far.png), [floor close-up](round6_far_survival/lod6_fixed_passage_floor_closeup.png) → [LOD8](round6_far_survival/lod8_fixed_passage_floor_closeup.png), and [tunnel bend](round6_far_survival/lod6_fixed_tunnel_bend.png) → [LOD8](round6_far_survival/lod8_fixed_tunnel_bend.png). A remote camera at X=500 m was captured at four headings: [yaw 0 LOD6/LOD8](round6_far_survival/lod6_x500_yaw0.png) / [LOD8](round6_far_survival/lod8_x500_yaw0.png), [yaw 90](round6_far_survival/lod6_x500_yaw90.png) / [LOD8](round6_far_survival/lod8_x500_yaw90.png), [yaw 180](round6_far_survival/lod6_x500_yaw180.png) / [LOD8](round6_far_survival/lod8_x500_yaw180.png), and [yaw 270](round6_far_survival/lod6_x500_yaw270.png) / [LOD8](round6_far_survival/lod8_x500_yaw270.png). At yaw 0, 180, and 270, 0, 29, and 4 pixels changed respectively; yaw 90 changed 710/1,024,000 pixels (mean absolute channel delta 0.010650, max 60). The strict audit passes at all four coarse levels; the yaw-90 difference is a small far-silhouette change. The [capture comparison](round6_far_survival/capture_pair_compare_live_pipeline.log) also confirms the live-pipeline chamber capture is pixel-identical to the saved LOD8 chamber image.
### Neighborhood-first shape pipeline
[`StandaloneViewer.cpp`](../../../Source/StandaloneViewer.cpp) now compiles a separate near `ShapeWorld` for the 27 LOD0 tiles around the player, starts their mesh and upload work, and launches the full/far `ShapeWorld` build on a separate thread with its own bins. Once all 27 meshes are uploaded and the full shape is ready, the viewer switches to the full shape, retains the already resident neighborhood tiles, and schedules the remaining view. The full-shape build no longer changes data used by the near build.
The final live W8 cold-start capture reports **2.036 s** to the uploaded 3×3×3 neighborhood, **3.059 s** to the full-detail area, and **3.959 s** to the full view (2,226 tiles). These milestones use process start as their common zero point, including near-region compilation and startup. The capture log is [here](round6_live_pipeline_capture_cold_clock_w8.stdout.log); the captured live view is [here](round6_far_survival/live_pipeline_fixed_chamber.png). The runner recorded 0.59 GiB peak working set and 1.06 GiB peak private memory.
The isolated full-payload stream pipeline reports W8 near shape 0.442 s, near mesh 0.047 s, full shape 0.737 s, neighborhood 0.504 s, full detail 1.539 s, and whole view 2.920 s. At W1 the corresponding readiness times are 0.862 s, 4.962 s, and 12.338 s. These interval measurements are in [W8](round6_pipeline_near_parity_final_w8.stdout.log) and [W1](round6_pipeline_near_parity_final_w1.stdout.log). The first 27 tiles are re-meshed from the completed full shape for parity: W1 and W8 each report 27/27 tiles, 108/108 hash fields, zero mismatches. Both report `full_payload=1`, cache hits 0, and the same 960 full-detail / 2,226 whole-view tile counts.
On the same clock-corrected executable, the direct non-pipelined shape-world runs at W1 and W8 match the pipeline hashes. The [60-field comparison](round6_final_stream_hash_compare.stdout.log) passes across direct W1/W8 full detail and LOD0–8, plus direct-vs-pipeline full-detail and whole-view aggregates. Full-detail hashes are density `0x67609e3f9aa3a0b0`, mesh `0xd98eb46b92a59917`, payload `0x8f148240ca4a4a8f`, topology `0x954e28f6fd44967f`. Whole-view hashes are density `0xfb5dd9640642e9d0`, mesh `0xaff324f3fa7374b0`, payload `0x55f9995a91861f83`, topology `0x20e1c051dfcf2b23`. The direct [W1](round6_current_baseline_shape_w1.stdout.log) and [W8](round6_current_baseline_shape_w8.stdout.log) runs both report 5,642/5,642 route samples and zero cache hits. The W8 baseline retains full UV, color, and material-group payloads (`payload_alloc=276.74 MiB`, `viewer_output=150.35 MiB`).
The current W8 look audit is green for walkability, room floors, floor slope/profile, tunnel radius, flat surfaces, grid stairs, thin sheets, strata/cave surface bands, flat patches, and detail scale: [audit log](round6_final_shape_audit_cold_clock_w8.stdout.log). Both route and coherent-detail checks pass.
### Failed builds and runs in this round
Five intermediate builds failed and each was corrected before the final build: [`far_connector_shell_build.log`](far_connector_shell_build.log) (C2039 `ElementType`; [retry](far_connector_shell_build_retry2.log)), [`far_survival_audit_match_build.log`](far_survival_audit_match_build.log) (C2065 `MeshEnd`; [retry](far_survival_audit_match_build_retry.log)), [`far_survival_connector_section_build.log`](far_survival_connector_section_build.log) (C2248 private `Finalize`; [retry](far_survival_connector_section_build_retry.log)), [`far_survival_debug_build.log`](far_survival_debug_build.log) (C3490 mutation of `DebugColumns` in a const method; [retry](far_survival_debug_build_retry.log)), and [`pipeline_build.log`](pipeline_build.log) (C2676 `FIntVector -=`; [retry](pipeline_build_retry.log)). The final `/MP8` builds completed successfully: [live pipeline](round6_live_pipeline_build.log) and [cold-clock update](round6_cold_clock_build.log).
Exploratory far-shell/connector runs that failed before the final source-following rings and aligned stations were: [far_audit_match_nearest_lod7_w8](far_audit_match_nearest_lod7_w8.stdout.log), [far_audit_section_diag_lod7_w8](far_audit_section_diag_lod7_w8.stdout.log), [far_connector_clipped_shell_lod7_w8](far_connector_clipped_shell_lod7_w8.stdout.log), [far_explicit_survival_lod7_w8](far_explicit_survival_lod7_w8.stdout.log), [far_opposing_pair_lod7_w8](far_opposing_pair_lod7_w8.stdout.log), [far_shell_half_resolution_lod7_w8](far_shell_half_resolution_lod7_w8.stdout.log), [far_survival_cell_force_lod7_w8](far_survival_cell_force_lod7_w8.stdout.log), [far_survival_explicit_connectors_lod7_w8](far_survival_explicit_connectors_lod7_w8.stdout.log), [far_survival_height_force_lod7_w8](far_survival_height_force_lod7_w8.stdout.log), [far_survival_lod6_debug_w8](far_survival_lod6_debug_w8.stdout.log), [far_survival_lod6_final_w8](far_survival_lod6_final_w8.stdout.log), [far_survival_lod6_forced_w8](far_survival_lod6_forced_w8.stdout.log), [far_survival_lod6_match_w8](far_survival_lod6_match_w8.stdout.log), [far_survival_lod6_tolerance_w8](far_survival_lod6_tolerance_w8.stdout.log), [far_survival_lod6_w8](far_survival_lod6_w8.stdout.log), [far_survival_section_lod7_w8](far_survival_section_lod7_w8.stdout.log), and [far_survival_section_lod8_w8](far_survival_section_lod8_w8.stdout.log). The stricter connector-section candidates also failed at [strict LOD6](round6_survival_strict_lod6_w8.stdout.log), [diagnostic LOD6](round6_survival_connector_diag_lod6_w8.stdout.log), [local-band LOD6](round6_survival_localband_lod6_w8.stdout.log), [source-ring LOD6](round6_connector_source_rings_lod6_w8.stdout.log), and [dense LOD6](round6_connector_dense_lod6_w8.stdout.log). These failures exposed missing source-radius walls or shell intersections and were used to correct the emitted far geometry.
The deliberately bad-shell controls [`far_survival_section_bad_shell_w8`](far_survival_section_bad_shell_w8.stdout.log), [`round6_survival_lod8_w8_bad_shell`](round6_survival_lod8_w8_bad_shell.stdout.log), and [`round6_final_bad_shell_lod8_w8`](round6_final_bad_shell_lod8_w8.stdout.log) are expected audit failures; the last one is the retained falsifiability result above. An early remote capture used a path rooted with an extra `Standalone/` and repeatedly failed to save; [the failed invocation](far_final8_capture_remote_yaw90.stderr.log) was rerun successfully with the working-directory-relative path in [the retry](far_final8_capture_remote_yaw90_retry.stdout.log). One W1 launch selected stale `VoxelForgeRound5Default6.exe`, entered the interactive viewer instead of the benchmark, and was stopped after about 100.5 s. A separate baseline run omitted `--shape-world --force-generate`, completed against the legacy workload, and was excluded from all results: [log](round6_pipeline_final_baseline_w8.stdout.log) (`StreamFullClipmap` 65.994 s, cache hits 0).
All successful workloads ran BelowNormal with affinity `0xFF00`, and builds used `/MP8`. No process failed with `0xC0000142` or an out-of-memory condition. No viewer not started by this work was closed. No commit, push, or stash was made.
## Round 5 — far-field LOD8 trial (2026-10-04) ## Round 5 — far-field LOD8 trial (2026-10-04)
### Result and decision ### Result and decision
+523
View File
@@ -0,0 +1,523 @@
#pragma once
// Standalone-only proof that far meshes retain the owner's strata-boundary shells and
// generated far-parent connectors. This deliberately compares the dense source field with
// vertical/radial intersections against the triangles that were actually emitted.
struct FFarSurvivalAudit
{
struct FBoundary
{
double Z=0.0;
double HalfWidth=16.0;
};
struct FConnectorSample
{
size_t ConnectorIndex=0;
V3 Center{},U{},V{};
float Radius=0.0f;
uint8 DirectionMask=0;
};
struct FCounts
{
uint64 BoundaryColumns=0,BoundaryReferenceSurfaces=0;
uint64 BoundaryMissing=0,BoundaryExtra=0,BoundaryHoles=0;
uint64 ConnectorsInView=0,ConnectorSamples=0,ConnectorRays=0;
uint64 ConnectorReferenceSurfaces=0,ConnectorRayMissing=0,ConnectorGoodSamples=0,ConnectorMissing=0;
uint64 ConnectorVisibleSamples=0,ConnectorNoSurfaceSamples=0,ConnectorSampleMissing=0;
uint64 ConnectorExtra=0,ConnectorHoles=0,ConnectorUnsampled=0;
uint64 CorruptedTriangles=0;
};
FFarSurvivalAudit(FStandaloneOwnerWorld& InWorld,const FStandaloneShapeWorld& InShape,
const std::vector<FVoxelTileKey>& DesiredTiles,bool bInDropOneShell)
:World(InWorld),Shape(InShape),bDropOneShell(bInDropOneShell)
{
const auto& Layout=World.Manager->GetLayout();
for(const FStrateSlot& Slot:Layout)
{
const FIntVector Mid(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2);
const FStrateGenerationParams Params=World.Manager->GetGenerationParams(Mid);
const double HalfWidth=std::max(8.0,static_cast<double>(Params.BoundarySealThickness)+8.0);
AddBoundary(static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth);
AddBoundary(static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth);
}
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
{return A.Z<B.Z;});
BuildConnectorSamples(DesiredTiles);
}
bool AuditTile(FStandaloneCpuTile& Tile)
{
if(Tile.Key.Level<AuditMinLevel)return true;
const FIntVector Origin=Tile.Key.OriginVoxels();
const int32 Extent=Tile.Key.ExtentVoxels();
const int32 Cells=std::max(1,World.CoarseTileCells);
const double Step=static_cast<double>(std::max(1,Extent/Cells));
if(bDropOneShell&&!bCorruptionApplied)
TryDropShellInTile(Tile,Origin,Extent,Step);
FMeshColumnIndex MeshIndex(Tile,Origin,Extent,Cells,Step);
static constexpr int32 ColumnOffsets[4]={1,5,9,13};
for(const FBoundary& Boundary:Boundaries)
{
if(Boundary.Z+Boundary.HalfWidth<Origin.Z
||Boundary.Z-Boundary.HalfWidth>Origin.Z+Extent)continue;
for(const int32 CY:ColumnOffsets)for(const int32 CX:ColumnOffsets)
{
const double X=Origin.X+(CX+0.371)*Step;
const double Y=Origin.Y+(CY+0.619)*Step;
const std::vector<double> Source=FindVerticalCrossings(X,Y,
Boundary.Z-Boundary.HalfWidth,Boundary.Z+Boundary.HalfWidth,1.0);
if(Source.empty())continue;
++Counts.BoundaryColumns;
Counts.BoundaryReferenceSurfaces+=Source.size();
CompareVerticalColumn(MeshIndex,X,Y,Boundary.Z-Boundary.HalfWidth,
Boundary.Z+Boundary.HalfWidth,Source,Step,
Counts.BoundaryMissing,Counts.BoundaryExtra,Counts.BoundaryHoles);
}
}
const auto Samples=ConnectorSamples.find(Tile.Key);
if(Samples!=ConnectorSamples.end())
{
for(const FConnectorSample& Sample:Samples->second)
AuditConnectorSample(Tile,Sample,Step,MeshIndex);
}
return true;
}
bool Passing() const
{
return bCorruptionApplied==bDropOneShell
&&Counts.BoundaryReferenceSurfaces>0
&&Counts.ConnectorsInView>0&&Counts.ConnectorSamples>0
&&Counts.BoundaryMissing==0&&Counts.BoundaryHoles==0
&&Counts.ConnectorMissing==0&&Counts.ConnectorSampleMissing==0
&&Counts.ConnectorHoles==0
&&Counts.ConnectorUnsampled==0;
}
void Print() const
{
std::printf("[FarSurvivalAudit] boundaries=%zu boundary_columns=%llu boundary_surfaces=%llu boundary_missing=%llu boundary_extra=%llu boundary_holes=%llu far_connectors=%llu connector_samples=%llu connector_visible_samples=%llu connector_no_surface_samples=%llu connector_sample_missing=%llu connector_good_connectors=%llu connector_rays=%llu connector_surfaces=%llu connector_ray_missing=%llu connector_missing=%llu connector_extra=%llu connector_holes=%llu connector_unsampled=%llu bad_shell_removed_triangles=%llu status=%s\n",
Boundaries.size(),static_cast<unsigned long long>(Counts.BoundaryColumns),
static_cast<unsigned long long>(Counts.BoundaryReferenceSurfaces),
static_cast<unsigned long long>(Counts.BoundaryMissing),
static_cast<unsigned long long>(Counts.BoundaryExtra),
static_cast<unsigned long long>(Counts.BoundaryHoles),
static_cast<unsigned long long>(Counts.ConnectorsInView),
static_cast<unsigned long long>(Counts.ConnectorSamples),
static_cast<unsigned long long>(Counts.ConnectorVisibleSamples),
static_cast<unsigned long long>(Counts.ConnectorNoSurfaceSamples),
static_cast<unsigned long long>(Counts.ConnectorSampleMissing),
static_cast<unsigned long long>(Counts.ConnectorGoodSamples),
static_cast<unsigned long long>(Counts.ConnectorRays),
static_cast<unsigned long long>(Counts.ConnectorReferenceSurfaces),
static_cast<unsigned long long>(Counts.ConnectorRayMissing),
static_cast<unsigned long long>(Counts.ConnectorMissing),
static_cast<unsigned long long>(Counts.ConnectorExtra),
static_cast<unsigned long long>(Counts.ConnectorHoles),
static_cast<unsigned long long>(Counts.ConnectorUnsampled),
static_cast<unsigned long long>(Counts.CorruptedTriangles),Passing()?"PASS":"FAIL");
std::fflush(stdout);
}
private:
struct FMeshColumnIndex
{
FMeshColumnIndex(const FStandaloneCpuTile& InTile,const FIntVector& InOrigin,
int32 InExtent,int32 InCells,double InStep)
:Tile(InTile),Origin(InOrigin),Extent(InExtent),Cells(InCells),Step(InStep),
ByColumn(static_cast<size_t>(InCells)*InCells)
{
const size_t TriangleCount=Tile.Indices.size()/3;
for(size_t Triangle=0;Triangle<TriangleCount;++Triangle)
{
uint32 A=Tile.Indices[Triangle*3],B=Tile.Indices[Triangle*3+1],C=Tile.Indices[Triangle*3+2];
if(A>=Tile.Vertices.size()||B>=Tile.Vertices.size()||C>=Tile.Vertices.size())continue;
const double MinX=std::min({PositionAxis(A,0),PositionAxis(B,0),PositionAxis(C,0)});
const double MaxX=std::max({PositionAxis(A,0),PositionAxis(B,0),PositionAxis(C,0)});
const double MinY=std::min({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)});
const double MaxY=std::max({PositionAxis(A,1),PositionAxis(B,1),PositionAxis(C,1)});
const int32 X0=std::clamp(static_cast<int32>(std::floor((MinX-Origin.X)/Step)),0,Cells-1);
const int32 X1=std::clamp(static_cast<int32>(std::floor((MaxX-Origin.X)/Step)),0,Cells-1);
const int32 Y0=std::clamp(static_cast<int32>(std::floor((MinY-Origin.Y)/Step)),0,Cells-1);
const int32 Y1=std::clamp(static_cast<int32>(std::floor((MaxY-Origin.Y)/Step)),0,Cells-1);
for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X)
ByColumn[static_cast<size_t>(Y)*Cells+X].push_back(static_cast<uint32>(Triangle));
}
}
std::vector<double> VerticalHits(double X,double Y,double ZMin,double ZMax) const
{
const int32 CX=std::clamp(static_cast<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
const auto& Candidates=ByColumn[static_cast<size_t>(CY)*Cells+CX];
std::vector<double> Hits;Hits.reserve(8);
for(const uint32 Triangle:Candidates)
{
const uint32 IA=Tile.Indices[static_cast<size_t>(Triangle)*3];
const uint32 IB=Tile.Indices[static_cast<size_t>(Triangle)*3+1];
const uint32 IC=Tile.Indices[static_cast<size_t>(Triangle)*3+2];
const double AX=PositionAxis(IA,0),AY=PositionAxis(IA,1),AZ=PositionAxis(IA,2);
const double BX=PositionAxis(IB,0),BY=PositionAxis(IB,1),BZ=PositionAxis(IB,2);
const double CXv=PositionAxis(IC,0),CYv=PositionAxis(IC,1),CZ=PositionAxis(IC,2);
const double Den=(BX-AX)*(CYv-AY)-(BY-AY)*(CXv-AX);
if(std::abs(Den)<1.0e-10)continue;
const double U=((X-AX)*(CYv-AY)-(Y-AY)*(CXv-AX))/Den;
const double V=((BX-AX)*(Y-AY)-(BY-AY)*(X-AX))/Den;
const double W=1.0-U-V;
if(U<-1.0e-7||V<-1.0e-7||W<-1.0e-7)continue;
const double Z=AZ+U*(BZ-AZ)+V*(CZ-AZ);
if(Z>=ZMin-1.0e-5&&Z<=ZMax+1.0e-5)Hits.push_back(Z);
}
SortUnique(Hits,0.25);
return Hits;
}
std::vector<double> SegmentHits(V3 Start,V3 Direction,double Length) const
{
std::vector<double> Hits;Hits.reserve(8);
const size_t TriangleCount=Tile.Indices.size()/3;
for(size_t Triangle=0;Triangle<TriangleCount;++Triangle)
{
const uint32 IA=Tile.Indices[Triangle*3],IB=Tile.Indices[Triangle*3+1],IC=Tile.Indices[Triangle*3+2];
if(IA>=Tile.Vertices.size()||IB>=Tile.Vertices.size()||IC>=Tile.Vertices.size())continue;
const V3 A=PositionVector(IA),B=PositionVector(IB),C=PositionVector(IC);
const V3 E1=Sub(B,A),E2=Sub(C,A),P=Cross(Direction,E2);
const float Determinant=Dot(E1,P);
if(std::abs(Determinant)<1.0e-8f)continue;
const float Inv=1.0f/Determinant;
const V3 T=Sub(Start,A);
const float U=Dot(T,P)*Inv;
if(U<-1.0e-5f||U>1.00001f)continue;
const V3 Q=Cross(T,E1);
const float V=Dot(Direction,Q)*Inv;
if(V<-1.0e-5f||U+V>1.00001f)continue;
const float Distance=Dot(E2,Q)*Inv;
if(Distance>=-1.0e-4f&&Distance<=Length+1.0e-4f)
Hits.push_back(std::clamp(static_cast<double>(Distance),0.0,Length));
}
SortUnique(Hits,0.25);
return Hits;
}
private:
V3 PositionVector(uint32 Index) const
{return {Tile.Vertices[Index].Position[0]*4.0f,Tile.Vertices[Index].Position[1]*4.0f,
Tile.Vertices[Index].Position[2]*4.0f};}
double PositionAxis(uint32 Index,int32 Axis) const
{return static_cast<double>(Tile.Vertices[Index].Position[Axis])*4.0;}
static void SortUnique(std::vector<double>& Values,double Epsilon)
{
std::sort(Values.begin(),Values.end());
Values.erase(std::unique(Values.begin(),Values.end(),[&](double A,double B)
{return std::abs(A-B)<=Epsilon;}),Values.end());
}
const FStandaloneCpuTile& Tile;
FIntVector Origin;
int32 Extent=0,Cells=0;
double Step=1.0;
std::vector<std::vector<uint32>> ByColumn;
};
static V3 AddV(V3 A,V3 B){return {A.X+B.X,A.Y+B.Y,A.Z+B.Z};}
static V3 MulV(V3 A,float S){return {A.X*S,A.Y*S,A.Z*S};}
static float LengthV(V3 A){return std::sqrt(std::max(0.0f,Dot(A,A)));}
static void SortUnique(std::vector<double>& Values,double Epsilon)
{
std::sort(Values.begin(),Values.end());
Values.erase(std::unique(Values.begin(),Values.end(),[&](double A,double B)
{return std::abs(A-B)<=Epsilon;}),Values.end());
}
void AddBoundary(double Z,double HalfWidth)
{
for(FBoundary& Existing:Boundaries)
if(std::abs(Existing.Z-Z)<0.01)
{Existing.HalfWidth=std::max(Existing.HalfWidth,HalfWidth);return;}
Boundaries.push_back({Z,HalfWidth});
}
static V3 ToV3(const FVector& P)
{return {static_cast<float>(P.X),static_cast<float>(P.Y),static_cast<float>(P.Z)};}
static bool Contains(const FVoxelTileKey& Key,V3 P,double Margin=0.0)
{
const FIntVector Origin=Key.OriginVoxels();
const int32 Extent=Key.ExtentVoxels();
return P.X>=Origin.X+Margin&&P.X<=Origin.X+Extent-Margin
&&P.Y>=Origin.Y+Margin&&P.Y<=Origin.Y+Extent-Margin
&&P.Z>=Origin.Z+Margin&&P.Z<=Origin.Z+Extent-Margin;
}
void BuildConnectorSamples(const std::vector<FVoxelTileKey>& DesiredTiles)
{
const auto& Connectors=Shape.GetFarConnectorSegments();
ConnectorInView.assign(Connectors.size(),0);
ConnectorHasSamples.assign(Connectors.size(),0);
ConnectorHasGoodSample.assign(Connectors.size(),0);
std::vector<FVoxelTileKey> FarTiles;
for(const FVoxelTileKey& Key:DesiredTiles)
if(Key.Level>=AuditMinLevel)FarTiles.push_back(Key);
for(size_t I=0;I<Connectors.size();++I)
{
const auto& Connector=Connectors[I];
const V3 A=ToV3(Connector.Start),B=ToV3(Connector.End);
const V3 Delta=Sub(B,A);const float Length=LengthV(Delta);
if(Length<=2.0f*Connector.Radius)continue;
const V3 Axis=MulV(Delta,1.0f/Length);
V3 U=Cross(Axis,{0,0,1});
if(LengthV(U)<0.1f)U=Cross(Axis,{0,1,0});
U=Normalize(U);const V3 V=Normalize(Cross(Axis,U));
bool bInsideView=false;
const int32 SampleCount=std::max(1,static_cast<int32>(std::ceil(Length/128.0f)));
for(int32 S=0;S<SampleCount;++S)
{
const float T=(static_cast<float>(S)+0.5f)/SampleCount;
const V3 Center=AddV(A,MulV(Delta,T));
for(const FVoxelTileKey& Key:FarTiles)
{
if(!Contains(Key,Center))continue;
bInsideView=true;
uint8 Mask=0;
const V3 Directions[4]={U,MulV(U,-1.0f),V,MulV(V,-1.0f)};
const double RayLength=Connector.Radius*2.0+32.0;
for(int32 D=0;D<4;++D)
if(Contains(Key,AddV(Center,MulV(Directions[D],static_cast<float>(RayLength)))))
Mask|=static_cast<uint8>(1u<<D);
if(!Mask)continue;
ConnectorSamples[Key].push_back({I,Center,U,V,Connector.Radius,Mask});
ConnectorHasSamples[I]=1;break;
}
}
ConnectorInView[I]=bInsideView?1:0;
}
}
public:
void Finalize()
{
Counts.ConnectorsInView=0;
Counts.ConnectorUnsampled=0;
Counts.ConnectorMissing=0;
Counts.ConnectorGoodSamples=0;
for(size_t I=0;I<ConnectorInView.size();++I)
{
if(!ConnectorInView[I])continue;
++Counts.ConnectorsInView;
if(!ConnectorHasSamples[I])++Counts.ConnectorUnsampled;
if(!ConnectorHasGoodSample[I])++Counts.ConnectorMissing;
else ++Counts.ConnectorGoodSamples;
}
}
private:
std::vector<double> FindVerticalCrossings(double X,double Y,double ZMin,double ZMax,
double Stride) const
{
std::vector<double> Crossings;
if(ZMax<=ZMin)return Crossings;
double PreviousZ=ZMin;
bool PreviousSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(PreviousZ))<0.0f;
for(double Z=std::min(ZMin+Stride,ZMax);Z<=ZMax+1.0e-6;Z=std::min(Z+Stride,ZMax))
{
const bool Solid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z))<0.0f;
if(Solid!=PreviousSolid)
{
double Lo=PreviousZ,Hi=Z;const bool LoSolid=PreviousSolid;
for(int32 I=0;I<6;++I)
{
const double Mid=(Lo+Hi)*0.5;
const bool MidSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Mid))<0.0f;
if(MidSolid==LoSolid)Lo=Mid;else Hi=Mid;
}
Crossings.push_back((Lo+Hi)*0.5);
}
if(Z>=ZMax-1.0e-6)break;
PreviousZ=Z;PreviousSolid=Solid;
}
return Crossings;
}
void CompareVerticalColumn(const FMeshColumnIndex& MeshIndex,double X,double Y,
double ZMin,double ZMax,const std::vector<double>& Source,double Step,
uint64& Missing,uint64& Extra,uint64& Holes)
{
std::vector<double> Mesh=MeshIndex.VerticalHits(X,Y,ZMin,ZMax);
const double Tolerance=std::max(4.0,Step*1.5);
std::vector<uint8> Used(Mesh.size(),0);
uint64 ColumnMissing=0,ColumnExtra=0;
for(const double Crossing:Source)
{
size_t Best=Mesh.size();double BestDistance=std::numeric_limits<double>::infinity();
for(size_t I=0;I<Mesh.size();++I)
if(!Used[I]&&std::abs(Mesh[I]-Crossing)<BestDistance)
{Best=I;BestDistance=std::abs(Mesh[I]-Crossing);}
if(Best==Mesh.size()||BestDistance>Tolerance)++ColumnMissing;
else Used[Best]=1;
}
for(const uint8 WasUsed:Used)if(!WasUsed)++ColumnExtra;
Missing+=ColumnMissing;Extra+=ColumnExtra;
if(ColumnMissing)
{
if((ColumnMissing&1u)!=0)++Holes;
if(DebugColumns<6)
{
std::printf("[FarAuditColumnMismatch] xy=(%.1f,%.1f) z=(%.1f..%.1f) source=%zu mesh=%zu first=(%.1f,%.1f) counts=(%llu,%llu)\n",
X,Y,ZMin,ZMax,Source.size(),Mesh.size(),Source.empty()?0.0:Source.front(),
Mesh.empty()?0.0:Mesh.front(),static_cast<unsigned long long>(ColumnMissing),
static_cast<unsigned long long>(ColumnExtra));
++DebugColumns;
}
}
}
void AuditConnectorSample(const FStandaloneCpuTile& Tile,const FConnectorSample& Sample,
double Step,const FMeshColumnIndex& MeshIndex)
{
++Counts.ConnectorSamples;
const V3 Directions[4]={Sample.U,MulV(Sample.U,-1.0f),Sample.V,MulV(Sample.V,-1.0f)};
const float RayLength=Sample.Radius*2.0f+32.0f;
const double SourceRadiusBand=std::max(16.0,static_cast<double>(Sample.Radius)*0.75);
uint32 MatchedDirections=0,ReferenceDirections=0;
uint8 ReferenceMask=0,MatchedMask=0;
for(int32 D=0;D<4;++D)
{
if((Sample.DirectionMask&(1u<<D))==0)continue;
++Counts.ConnectorRays;
const V3 Direction=Directions[D];
const std::vector<double> Source=FindRadialCrossings(Sample.Center,Direction,RayLength);
if(Source.empty())continue;
const auto SourceHit=std::min_element(Source.begin(),Source.end(),[&](double A,double B)
{return std::abs(A-Sample.Radius)<std::abs(B-Sample.Radius);});
if(SourceHit==Source.end()||std::abs(*SourceHit-Sample.Radius)>SourceRadiusBand)continue;
++ReferenceDirections;
ReferenceMask|=static_cast<uint8>(1u<<D);
++Counts.ConnectorReferenceSurfaces;
const std::vector<double> Mesh=MeshIndex.SegmentHits(Sample.Center,Direction,RayLength);
const double Tolerance=std::max(8.0,static_cast<double>(Sample.Radius)*0.35);
double BestDistance=std::numeric_limits<double>::infinity();
for(const double Hit:Mesh)
{
const double Distance=std::abs(Hit-*SourceHit);
if(Distance<BestDistance)BestDistance=Distance;
}
if(BestDistance<=Tolerance)
{++MatchedDirections;MatchedMask|=static_cast<uint8>(1u<<D);}
else
{
++Counts.ConnectorRayMissing;
if(DebugConnectorMismatches<24)
{
std::printf("[FarConnectorRayMismatch] connector=%zu tile_lod=%d center=(%.1f,%.1f,%.1f) radius=%.1f direction=%d source=%.2f nearest_mesh=%.2f hits=%zu step=%.1f tolerance=%.1f\n",
Sample.ConnectorIndex,Tile.Key.Level,Sample.Center.X,Sample.Center.Y,Sample.Center.Z,
Sample.Radius,D,*SourceHit,BestDistance,Mesh.size(),Step,Tolerance);
++DebugConnectorMismatches;
}
}
if(Mesh.size()>1)Counts.ConnectorExtra+=Mesh.size()-1;
}
// The source field can expose only one or two radial walls where a link
// joins a parent room. Preserve every source wall at every sampled section;
// require both sides whenever the source itself exposes an opposing pair.
const bool bSourcePair=(ReferenceMask&0x03u)==0x03u||(ReferenceMask&0x0Cu)==0x0Cu;
const bool bMatchedPair=(MatchedMask&0x03u)==0x03u||(MatchedMask&0x0Cu)==0x0Cu;
if(ReferenceDirections==0)
{++Counts.ConnectorNoSurfaceSamples;return;}
++Counts.ConnectorVisibleSamples;
const bool bSectionPassing=ReferenceDirections>0
&&MatchedDirections==ReferenceDirections
&&(!bSourcePair||bMatchedPair);
if(bSectionPassing&&!ConnectorHasGoodSample[Sample.ConnectorIndex])
ConnectorHasGoodSample[Sample.ConnectorIndex]=1;
if(!bSectionPassing)
{
++Counts.ConnectorSampleMissing;
if(MatchedDirections<ReferenceDirections||(bSourcePair&&!bMatchedPair))
++Counts.ConnectorHoles;
}
}
std::vector<double> FindRadialCrossings(V3 Start,V3 Direction,double Length) const
{
std::vector<double> Crossings;
double PreviousDistance=0.0;
bool PreviousSolid=Shape.DensityAt(Start.X,Start.Y,Start.Z)<0.0f;
constexpr double Stride=2.0;
for(double Distance=Stride;Distance<=Length+1.0e-6;Distance=std::min(Distance+Stride,Length))
{
const V3 P=AddV(Start,MulV(Direction,static_cast<float>(Distance)));
const bool Solid=Shape.DensityAt(P.X,P.Y,P.Z)<0.0f;
if(Solid!=PreviousSolid)
{
double Lo=PreviousDistance,Hi=Distance;
for(int32 I=0;I<6;++I)
{
const double Mid=(Lo+Hi)*0.5;
const V3 M=AddV(Start,MulV(Direction,static_cast<float>(Mid)));
const bool MidSolid=Shape.DensityAt(M.X,M.Y,M.Z)<0.0f;
if(MidSolid==PreviousSolid)Lo=Mid;else Hi=Mid;
}
Crossings.push_back((Lo+Hi)*0.5);
}
if(Distance>=Length-1.0e-6)break;
PreviousDistance=Distance;PreviousSolid=Solid;
}
return Crossings;
}
void TryDropShellInTile(FStandaloneCpuTile& Tile,const FIntVector& Origin,int32 Extent,double Step)
{
static constexpr int32 Offsets[4]={1,5,9,13};
for(const FBoundary& Boundary:Boundaries)
{
if(Boundary.Z+Boundary.HalfWidth<Origin.Z
||Boundary.Z-Boundary.HalfWidth>Origin.Z+Extent)continue;
for(const int32 CY:Offsets)for(const int32 CX:Offsets)
{
const double X=Origin.X+(CX+0.371)*Step;
const double Y=Origin.Y+(CY+0.619)*Step;
if(FindVerticalCrossings(X,Y,Boundary.Z-Boundary.HalfWidth,
Boundary.Z+Boundary.HalfWidth,1.0).empty())continue;
const double DropHalfWidth=std::max(Boundary.HalfWidth+4.0,Step*0.75);
std::vector<uint32> Kept;Kept.reserve(Tile.Indices.size());
for(size_t T=0;T+2<Tile.Indices.size();T+=3)
{
const uint32 A=Tile.Indices[T],B=Tile.Indices[T+1],C=Tile.Indices[T+2];
if(A>=Tile.Vertices.size()||B>=Tile.Vertices.size()||C>=Tile.Vertices.size())continue;
const double CenterZ=(Tile.Vertices[A].Position[2]+Tile.Vertices[B].Position[2]
+Tile.Vertices[C].Position[2])*4.0/3.0;
if(std::abs(CenterZ-Boundary.Z)<=DropHalfWidth)
{++Counts.CorruptedTriangles;continue;}
Kept.push_back(A);Kept.push_back(B);Kept.push_back(C);
}
Tile.Indices.swap(Kept);
bCorruptionApplied=Counts.CorruptedTriangles>0;
std::printf("[FarAuditBadSetting] removed stratum shell triangles at z=%.1f vox tile_lod=%d removed=%llu\n",
Boundary.Z,Tile.Key.Level,static_cast<unsigned long long>(Counts.CorruptedTriangles));
std::fflush(stdout);
return;
}
}
}
FStandaloneOwnerWorld& World;
const FStandaloneShapeWorld& Shape;
static constexpr int32 AuditMinLevel=6;
bool bDropOneShell=false,bCorruptionApplied=false;
std::vector<FBoundary> Boundaries;
std::unordered_map<FVoxelTileKey,std::vector<FConnectorSample>,FStandaloneTileKeyHash> ConnectorSamples;
FCounts Counts;
std::vector<uint8> ConnectorInView,ConnectorHasSamples,ConnectorHasGoodSample;
uint32 DebugColumns=0;
uint32 DebugConnectorMismatches=0;
};
+2 -2
View File
@@ -30,7 +30,7 @@ struct FStandaloneOwnerWorld
bool ViewerStartFound = false; bool ViewerStartFound = false;
int32 Seed = 0; int32 Seed = 0;
int32 ClipRadius = 6; int32 ClipRadius = 6;
int32 MaxClipLevel = 6; int32 MaxClipLevel = 8;
int32 FullResClipLevels = 2; int32 FullResClipLevels = 2;
int32 CoarseTileCells = 16; int32 CoarseTileCells = 16;
int32 RenderDistanceChunks = 1536; int32 RenderDistanceChunks = 1536;
@@ -253,7 +253,7 @@ namespace StandaloneOwnerLoader
} }
Out.Seed = static_cast<int32>(Root.Get("seed").Integer()); Out.Seed = static_cast<int32>(Root.Get("seed").Integer());
Out.ClipRadius = static_cast<int32>(Root.Get("clip_radius").Integer(6)); Out.ClipRadius = static_cast<int32>(Root.Get("clip_radius").Integer(6));
Out.MaxClipLevel = static_cast<int32>(Root.Get("max_clip_level").Integer(6)); Out.MaxClipLevel = static_cast<int32>(Root.Get("max_clip_level").Integer(8));
Out.FullResClipLevels = static_cast<int32>(Root.Get("full_res_clip_levels").Integer(2)); Out.FullResClipLevels = static_cast<int32>(Root.Get("full_res_clip_levels").Integer(2));
Out.CoarseTileCells = static_cast<int32>(Root.Get("coarse_tile_cells").Integer(16)); Out.CoarseTileCells = static_cast<int32>(Root.Get("coarse_tile_cells").Integer(16));
Out.RenderDistanceChunks = static_cast<int32>(Root.Get("render_distance_chunks").Integer(1536)); Out.RenderDistanceChunks = static_cast<int32>(Root.Get("render_distance_chunks").Integer(1536));
+83
View File
@@ -21,6 +21,7 @@
#include <thread> #include <thread>
#include <type_traits> #include <type_traits>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <vector> #include <vector>
// Standalone-only world compiler. It compiles the runtime room/tunnel/passages into // Standalone-only world compiler. It compiles the runtime room/tunnel/passages into
@@ -30,6 +31,12 @@ class FStandaloneShapeWorld
{ {
public: public:
enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel }; enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel };
struct FFarConnectorSegment
{
FVector Start=FVector::ZeroVector;
FVector End=FVector::ZeroVector;
float Radius=0.0f;
};
struct FEditBrush struct FEditBrush
{ {
enum class EShape : uint8 { Sphere, Capsule, RoundedBox }; enum class EShape : uint8 { Sphere, Capsule, RoundedBox };
@@ -393,6 +400,64 @@ public:
const FBuildStats& GetStats() const { return Stats; } const FBuildStats& GetStats() const { return Stats; }
bool IsBuilt() const { return Built; } bool IsBuilt() const { return Built; }
bool HasFarWorld() const { return FarBuilt; } bool HasFarWorld() const { return FarBuilt; }
const std::vector<FFarConnectorSegment>& GetFarConnectorSegments() const
{ return FarConnectorSegments; }
size_t BuildFarConnectorAirSamples(const FIntVector& TileOrigin,int32 Step,int32 Cells,
std::unordered_set<uint64>& OutSamples) const
{
OutSamples.clear();
if(FarConnectorSegments.empty()||Step<=0||Cells<=0)return 0;
const double Pad=Step;
const double TileMin[3]={TileOrigin.X-Pad,TileOrigin.Y-Pad,TileOrigin.Z-Pad};
const double TileMax[3]={TileOrigin.X+(Cells+1)*Step,TileOrigin.Y+(Cells+1)*Step,
TileOrigin.Z+(Cells+1)*Step};
for(const FFarConnectorSegment& Connector:FarConnectorSegments)
{
const double Start[3]={Connector.Start.X,Connector.Start.Y,Connector.Start.Z};
const double End[3]={Connector.End.X,Connector.End.Y,Connector.End.Z};
bool bOverlaps=true;
for(int32 Axis=0;Axis<3;++Axis)
bOverlaps &= std::min(Start[Axis],End[Axis])-Connector.Radius<=TileMax[Axis]
&&std::max(Start[Axis],End[Axis])+Connector.Radius>=TileMin[Axis];
if(!bOverlaps)continue;
const double Delta[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]};
const double Length=std::sqrt(Delta[0]*Delta[0]+Delta[1]*Delta[1]+Delta[2]*Delta[2]);
if(Length<=1.0e-6)continue;
const int32 Samples=std::max(1,static_cast<int32>(std::ceil(Length/(Step*0.25))));
for(int32 I=0;I<=Samples;++I)
{
const double T=static_cast<double>(I)/Samples;
const double P[3]={Start[0]+Delta[0]*T,Start[1]+Delta[1]*T,Start[2]+Delta[2]*T};
if(P[0]<TileMin[0]||P[0]>TileMax[0]||P[1]<TileMin[1]||P[1]>TileMax[1]
||P[2]<TileMin[2]||P[2]>TileMax[2])continue;
int32 Base[3];
for(int32 Axis=0;Axis<3;++Axis)
Base[Axis]=static_cast<int32>(std::floor((P[Axis]-(&TileOrigin.X)[Axis])/Step));
for(int32 DZ=0;DZ<=1;++DZ)for(int32 DY=0;DY<=1;++DY)for(int32 DX=0;DX<=1;++DX)
{
const int32 IX=Base[0]+DX,IY=Base[1]+DY,IZ=Base[2]+DZ;
if(IX<0||IY<0||IZ<0||IX>Cells||IY>Cells||IZ>Cells)continue;
OutSamples.insert(PackLatticeKey(TileOrigin.X+IX*Step,
TileOrigin.Y+IY*Step,TileOrigin.Z+IZ*Step));
}
}
}
return OutSamples.size();
}
class FScopedForcedFarAirSamples
{
public:
explicit FScopedForcedFarAirSamples(const std::unordered_set<uint64>* AirSamples)
:PreviousAir(ForcedFarAirSamples)
{ ForcedFarAirSamples=AirSamples; }
~FScopedForcedFarAirSamples(){ForcedFarAirSamples=PreviousAir;}
FScopedForcedFarAirSamples(const FScopedForcedFarAirSamples&)=delete;
FScopedForcedFarAirSamples& operator=(const FScopedForcedFarAirSamples&)=delete;
private:
const std::unordered_set<uint64>* PreviousAir=nullptr;
};
bool DebugCriticalAirAt(double X,double Y,double Z) const { return IsInsideCriticalAir(X,Y,Z); } bool DebugCriticalAirAt(double X,double Y,double Z) const { return IsInsideCriticalAir(X,Y,Z); }
bool DebugSolidOverlayAt(double X,double Y,double Z) const { return IsInsideSolidOverlay(X,Y,Z); } bool DebugSolidOverlayAt(double X,double Y,double Z) const { return IsInsideSolidOverlay(X,Y,Z); }
@@ -451,6 +516,7 @@ public:
{ {
int32 IX=0,IY=0,IZ=0; int32 IX=0,IY=0,IZ=0;
const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ); const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ);
if(bInteger&&IsForcedFarAirSample(IX,IY,IZ))return 0.05f;
const bool bPacked=bInteger&&Contains(IX,IY,IZ); const bool bPacked=bInteger&&Contains(IX,IY,IZ);
if(bPacked) if(bPacked)
return PackedDensityAt(IX,IY,IZ,IsRock(IX,IY,IZ),X,Y,Z); return PackedDensityAt(IX,IY,IZ,IsRock(IX,IY,IZ),X,Y,Z);
@@ -459,6 +525,7 @@ public:
float DensityAtLattice(int32 X,int32 Y,int32 Z) const float DensityAtLattice(int32 X,int32 Y,int32 Z) const
{ {
if(IsForcedFarAirSample(X,Y,Z))return 0.05f;
const bool bPacked=Contains(X,Y,Z); const bool bPacked=Contains(X,Y,Z);
if(bPacked) if(bPacked)
return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),static_cast<float>(X), return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),static_cast<float>(X),
@@ -468,6 +535,18 @@ public:
} }
private: private:
static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z)
{
constexpr uint64 Mask=0x1FFFFFull;
return (static_cast<uint64>(static_cast<uint32>(X)&Mask)<<42)
|(static_cast<uint64>(static_cast<uint32>(Y)&Mask)<<21)
|(static_cast<uint64>(static_cast<uint32>(Z)&Mask));
}
static bool IsForcedFarAirSample(int32 X,int32 Y,int32 Z)
{
const auto* Samples=ForcedFarAirSamples;
return Samples&&Samples->contains(PackLatticeKey(X,Y,Z));
}
float PackedDensityAt(int32 X,int32 Y,int32 Z,bool bRock,float FX,float FY,float FZ) const float PackedDensityAt(int32 X,int32 Y,int32 Z,bool bRock,float FX,float FY,float FZ) const
{ {
const size_t Column=static_cast<size_t>(Y-Min[1])*Size[0]+static_cast<size_t>(X-Min[0]); const size_t Column=static_cast<size_t>(Y-Min[1])*Size[0]+static_cast<size_t>(X-Min[0]);
@@ -1970,6 +2049,7 @@ private:
void BuildFarParentShapes() void BuildFarParentShapes()
{ {
constexpr int32 ParentPitch=2048; constexpr int32 ParentPitch=2048;
FarConnectorSegments.clear();
struct FParent { int32 GX=0,GY=0,Strate=0; FVector Center; double Rx=0,Ry=0,Rz=0; }; struct FParent { int32 GX=0,GY=0,Strate=0; FVector Center; double Rx=0,Ry=0,Rz=0; };
const int32 GX0=FloorDiv(ClipMin[0],ParentPitch)-1,GY0=FloorDiv(ClipMin[1],ParentPitch)-1; const int32 GX0=FloorDiv(ClipMin[0],ParentPitch)-1,GY0=FloorDiv(ClipMin[1],ParentPitch)-1;
const int32 GX1=FloorDiv(ClipMax[0],ParentPitch)+1,GY1=FloorDiv(ClipMax[1],ParentPitch)+1; const int32 GX1=FloorDiv(ClipMax[0],ParentPitch)+1,GY1=FloorDiv(ClipMax[1],ParentPitch)+1;
@@ -2024,6 +2104,7 @@ private:
if(MaxX>=Min[0]&&MinX<=Max[0]&&MaxY>=Min[1]&&MinY<=Max[1])continue; if(MaxX>=Min[0]&&MinX<=Max[0]&&MaxY>=Min[1]&&MinY<=Max[1])continue;
AddTaperedCapsule(A,B,64.0f,64.0f,true); AddTaperedCapsule(A,B,64.0f,64.0f,true);
Primitives.back().BlendRadius=0.0; Primitives.back().BlendRadius=0.0;
FarConnectorSegments.push_back({A,B,64.0f});
} }
} }
if(!Parents.empty()) if(!Parents.empty())
@@ -3154,6 +3235,7 @@ private:
std::vector<FInterval> LayerRanges; std::vector<FInterval> LayerRanges;
std::vector<FLayerField> LayerFields; std::vector<FLayerField> LayerFields;
std::vector<FPrimitive> Primitives; std::vector<FPrimitive> Primitives;
std::vector<FFarConnectorSegment> FarConnectorSegments;
std::vector<FEditBrush> EditBrushes; std::vector<FEditBrush> EditBrushes;
std::array<FAxisBins,3> AxisBins; std::array<FAxisBins,3> AxisBins;
std::unordered_map<FBinKey,FBinItems,FBinHash> SpatialBins; std::unordered_map<FBinKey,FBinItems,FBinHash> SpatialBins;
@@ -3164,6 +3246,7 @@ private:
std::array<std::vector<uint64>,3> CandidateAxisWords; std::array<std::vector<uint64>,3> CandidateAxisWords;
std::vector<uint64> CandidateWords; std::vector<uint64> CandidateWords;
FBuildStats Stats; FBuildStats Stats;
inline static thread_local const std::unordered_set<uint64>* ForcedFarAirSamples=nullptr;
}; };
namespace FStandaloneShapeWorldAccess namespace FStandaloneShapeWorldAccess
+414 -14
View File
@@ -49,6 +49,8 @@ namespace
V3 Cross(V3 A,V3 B){return {A.Y*B.Z-A.Z*B.Y,A.Z*B.X-A.X*B.Z,A.X*B.Y-A.Y*B.X};} V3 Cross(V3 A,V3 B){return {A.Y*B.Z-A.Z*B.Y,A.Z*B.X-A.X*B.Z,A.X*B.Y-A.Y*B.X};}
V3 Normalize(V3 A){return Mul(A,1.0f/std::sqrt(std::max(1.0e-20f,Dot(A,A))));} V3 Normalize(V3 A){return Mul(A,1.0f/std::sqrt(std::max(1.0e-20f,Dot(A,A))));}
#include "FarSurvivalAudit.h"
struct FMeshSheetAudit struct FMeshSheetAudit
{ {
uint64 Components=0; uint64 Components=0;
@@ -337,7 +339,13 @@ namespace
ID3D11RasterizerState* WireRasterizer=nullptr; ID3D11RasterizerState* WireRasterizer=nullptr;
std::unique_ptr<FStandaloneOwnerWorld> World; std::unique_ptr<FStandaloneOwnerWorld> World;
std::unique_ptr<FStandaloneShapeWorld> ShapeWorld; std::unique_ptr<FStandaloneShapeWorld> ShapeWorld;
std::unique_ptr<FStandaloneShapeWorld> PendingFullShapeWorld;
std::unique_ptr<FStandaloneStreamingGenerator> Streamer; std::unique_ptr<FStandaloneStreamingGenerator> Streamer;
std::thread FullShapeBuildThread;
std::atomic<bool> bFullShapeBuildFinished{false};
bool bFullShapeBuildSucceeded=false;
std::string FullShapeBuildError;
std::chrono::steady_clock::time_point FullShapeBuildStart{};
FStandaloneMeshCache MeshCache; FStandaloneMeshCache MeshCache;
std::string DataPath; std::string DataPath;
bool bHasCameraOverride=false; bool bHasCameraOverride=false;
@@ -349,6 +357,7 @@ namespace
std::unordered_set<FStandaloneBatchKey,FStandaloneBatchKeyHash> DirtyBatchSet; std::unordered_set<FStandaloneBatchKey,FStandaloneBatchKeyHash> DirtyBatchSet;
std::unordered_map<uint64,FCrossingWork> CrossingWork; std::unordered_map<uint64,FCrossingWork> CrossingWork;
std::vector<FVoxelTileKey> DesiredTiles; std::vector<FVoxelTileKey> DesiredTiles;
std::vector<FVoxelTileKey> InitialNeighborhoodTiles;
int32 DesiredByLevel[MaxLevels+1]{}; int32 DesiredByLevel[MaxLevels+1]{};
uint64 GeneratedByLevel[MaxLevels+1]{}; uint64 GeneratedByLevel[MaxLevels+1]{};
uint64 SamplesByLevel[MaxLevels+1]{}; uint64 SamplesByLevel[MaxLevels+1]{};
@@ -362,6 +371,8 @@ namespace
bool bRoundTunnels=false; bool bRoundTunnels=false;
bool bRoundTunnelEnhanced=true; bool bRoundTunnelEnhanced=true;
bool bShapeWorld=false; bool bShapeWorld=false;
bool bShapePipelineActive=false;
bool bNeighborhoodReadyReported=false;
bool bShapeAuditOnly=false; bool bShapeAuditOnly=false;
bool bShapeCompare=false; bool bShapeCompare=false;
bool bFarShapeStagePending=false; bool bFarShapeStagePending=false;
@@ -386,6 +397,7 @@ namespace
bool bForceGenerate=false; bool bForceGenerate=false;
std::string CapturePath; std::string CapturePath;
std::string MeshCachePath; std::string MeshCachePath;
std::chrono::steady_clock::time_point ColdProcessStart{};
std::chrono::steady_clock::time_point GenerationStart{}; std::chrono::steady_clock::time_point GenerationStart{};
bool bShowLevel[MaxLevels+1]={true,true,true,true,true,true,true,true,true}; bool bShowLevel[MaxLevels+1]={true,true,true,true,true,true,true,true,true};
bool Keys[256]{}; bool Keys[256]{};
@@ -435,6 +447,15 @@ namespace
(void)bCancel; (void)bCancel;
if(Streamer)Streamer->Stop(); if(Streamer)Streamer->Stop();
Streamer.reset(); Streamer.reset();
if(FullShapeBuildThread.joinable())FullShapeBuildThread.join();
PendingFullShapeWorld.reset();
bFullShapeBuildFinished.store(false,std::memory_order_relaxed);
bFullShapeBuildSucceeded=false;
FullShapeBuildError.clear();
InitialNeighborhoodTiles.clear();
bShapePipelineActive=false;
bNeighborhoodReadyReported=false;
bFarShapeStagePending=false;
bGenerationRunning=false; bGenerationRunning=false;
} }
@@ -653,42 +674,95 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::printf("Standalone max clip level override applied after world reload: max_level=%d.\n", std::printf("Standalone max clip level override applied after world reload: max_level=%d.\n",
MaxClipLevelOverride); MaxClipLevelOverride);
} }
if(bShapeWorld&&!BuildShapeWorldForCamera(Error))return false; if(bShapeWorld&&!BuildShapeWorldForCamera(Error,false,true))return false;
LoadMeshCache(); LoadMeshCache();
return true; return true;
} }
bool BuildShapeWorldForCamera(std::string& Error,bool bIncludeFarWorld=false) bool BuildShapeWorldForCamera(std::string& Error,bool bIncludeFarWorld=false,
bool bNeighborhoodOnly=false)
{ {
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0); const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels); const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels);
FIntVector Min(INT32_MAX,INT32_MAX,INT32_MAX),Max(INT32_MIN,INT32_MIN,INT32_MIN); FIntVector Min(INT32_MAX,INT32_MAX,INT32_MAX),Max(INT32_MIN,INT32_MIN,INT32_MIN);
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN); FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
uint64 FineTiles=0; uint64 FineTiles=0;
InitialNeighborhoodTiles.clear();
const FIntVector CenterChunk=Desired.CenterChunk;
for(const FVoxelTileKey& Tile:Desired.Tiles) for(const FVoxelTileKey& Tile:Desired.Tiles)
{ {
const FIntVector Origin=Tile.OriginVoxels(); const FIntVector Origin=Tile.OriginVoxels();
const int32 Extent=Tile.ExtentVoxels(); const int32 Extent=Tile.ExtentVoxels();
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z); ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent); ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
if(Tile.Level>=World->FullResClipLevels)continue; if(bNeighborhoodOnly)
{
if(Tile.Level!=0||std::abs(Tile.Coord.X-CenterChunk.X)>1
||std::abs(Tile.Coord.Y-CenterChunk.Y)>1
||std::abs(Tile.Coord.Z-CenterChunk.Z)>1)continue;
InitialNeighborhoodTiles.push_back(Tile);
}
else if(Tile.Level>=World->FullResClipLevels)continue;
Min.X=std::min(Min.X,Origin.X);Min.Y=std::min(Min.Y,Origin.Y);Min.Z=std::min(Min.Z,Origin.Z); Min.X=std::min(Min.X,Origin.X);Min.Y=std::min(Min.Y,Origin.Y);Min.Z=std::min(Min.Z,Origin.Z);
Max.X=std::max(Max.X,Origin.X+Extent);Max.Y=std::max(Max.Y,Origin.Y+Extent);Max.Z=std::max(Max.Z,Origin.Z+Extent); Max.X=std::max(Max.X,Origin.X+Extent);Max.Y=std::max(Max.Y,Origin.Y+Extent);Max.Z=std::max(Max.Z,Origin.Z+Extent);
++FineTiles; ++FineTiles;
} }
if(!FineTiles){Error="no full-detail tiles are available for shape compilation";return false;} if(!FineTiles){Error="no full-detail tiles are available for shape compilation";return false;}
if(bNeighborhoodOnly)
{
if(InitialNeighborhoodTiles.size()!=27)
{Error="the 3x3x3 player neighborhood does not contain 27 LOD0 tiles";return false;}
constexpr int32 NearBuildPad=64;
Min.X-=NearBuildPad;Min.Y-=NearBuildPad;Min.Z-=NearBuildPad;
Max.X+=NearBuildPad;Max.Y+=NearBuildPad;Max.Z+=NearBuildPad;
}
ShapeWorld=std::make_unique<FStandaloneShapeWorld>(); ShapeWorld=std::make_unique<FStandaloneShapeWorld>();
if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,WorkerCount, const int32 ShapeWorkers=bNeighborhoodOnly?(WorkerCount>=8?2:1):WorkerCount;
if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,ShapeWorkers,
ShapeDetailVariant,bIncludeFarWorld,Error)) ShapeDetailVariant,bIncludeFarWorld,Error))
{ShapeWorld.reset();return false;} {ShapeWorld.reset();return false;}
FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get()); FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get());
bFarShapeStagePending=bShapeWorld&&!ShapeWorld->HasFarWorld(); bShapePipelineActive=bNeighborhoodOnly;
bNeighborhoodReadyReported=false;
bFarShapeStagePending=bShapeWorld&&!bNeighborhoodOnly&&!ShapeWorld->HasFarWorld();
std::printf("[ShapeRegion] fine_tiles=%llu bounds=(%d,%d,%d)..(%d,%d,%d) clip=(%d,%d,%d)..(%d,%d,%d)\n", std::printf("[ShapeRegion] fine_tiles=%llu bounds=(%d,%d,%d)..(%d,%d,%d) clip=(%d,%d,%d)..(%d,%d,%d)\n",
static_cast<unsigned long long>(FineTiles),Min.X,Min.Y,Min.Z,Max.X,Max.Y,Max.Z, static_cast<unsigned long long>(FineTiles),Min.X,Min.Y,Min.Z,Max.X,Max.Y,Max.Z,
ClipMin.X,ClipMin.Y,ClipMin.Z,ClipMax.X,ClipMax.Y,ClipMax.Z); ClipMin.X,ClipMin.Y,ClipMin.Z,ClipMax.X,ClipMax.Y,ClipMax.Z);
return true; return true;
} }
bool StartFullShapeBuild(const std::vector<FVoxelTileKey>& FullDesired,std::string& Error)
{
FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN);
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
for(const FVoxelTileKey& Tile:FullDesired)
{
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
if(Tile.Level>=World->FullResClipLevels)continue;
FineMin.X=std::min(FineMin.X,Origin.X);FineMin.Y=std::min(FineMin.Y,Origin.Y);FineMin.Z=std::min(FineMin.Z,Origin.Z);
FineMax.X=std::max(FineMax.X,Origin.X+Extent);FineMax.Y=std::max(FineMax.Y,Origin.Y+Extent);FineMax.Z=std::max(FineMax.Z,Origin.Z+Extent);
}
if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX)
{Error="the full shape build has no desired detail or clipmap tiles";return false;}
PendingFullShapeWorld=std::make_unique<FStandaloneShapeWorld>();
FullShapeBuildError.clear();bFullShapeBuildSucceeded=false;
bFullShapeBuildFinished.store(false,std::memory_order_relaxed);
const int32 FullWorkers=WorkerCount>=8?6:WorkerCount;
FullShapeBuildStart=std::chrono::steady_clock::now();
FullShapeBuildThread=std::thread([this,FineMin,FineMax,ClipMin,ClipMax,FullWorkers]()
{
bFullShapeBuildSucceeded=PendingFullShapeWorld->Build(*World,FineMin,FineMax,
ClipMin,ClipMax,FullWorkers,ShapeDetailVariant,true,FullShapeBuildError);
bFullShapeBuildFinished.store(true,std::memory_order_release);
});
std::printf("[ShapePipelineBuildStart] neighborhood_tiles=%zu near_mesh_workers=%d full_shape_workers=%d\n",
InitialNeighborhoodTiles.size(),WorkerCount>=8?2:1,FullWorkers);
std::fflush(stdout);
return true;
}
FStandaloneMeshCacheMetadata MakeMeshCacheMetadata()const FStandaloneMeshCacheMetadata MakeMeshCacheMetadata()const
{ {
FStandaloneMeshCacheMetadata Metadata; FStandaloneMeshCacheMetadata Metadata;
@@ -752,10 +826,19 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
DesiredTiles=Desired.Tiles; DesiredTiles=Desired.Tiles;
for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0; for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0;
for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level]; for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level];
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount,false, const int32 StreamWorkers=bShapePipelineActive?(WorkerCount>=8?2:1):WorkerCount;
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,StreamWorkers,false,
(!bShapeWorld&&MeshCache.IsLoaded())?&MeshCache:nullptr, (!bShapeWorld&&MeshCache.IsLoaded())?&MeshCache:nullptr,
bFarShapeStagePending?World->FullResClipLevels-1:-1); bFarShapeStagePending?World->FullResClipLevels-1:-1);
if(bShapePipelineActive)Streamer->SetDesiredTileFilter(InitialNeighborhoodTiles);
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex); const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex);
if(bShapePipelineActive&&Update.DesiredTiles!=static_cast<int32>(InitialNeighborhoodTiles.size()))
{
StopGeneration();
Error="the initial neighborhood streamer did not accept all 27 tiles";
std::fprintf(stderr,"World load failed: %s\n",Error.c_str());
return;
}
FCrossingWork& InitialWork=CrossingWork[CrossingIndex]; FCrossingWork& InitialWork=CrossingWork[CrossingIndex];
InitialWork.SubmittedJobs=Update.NewJobs;InitialWork.PendingJobs=Update.NewJobs; InitialWork.SubmittedJobs=Update.NewJobs;InitialWork.PendingJobs=Update.NewJobs;
InitialWork.StartTime=GenerationStart; InitialWork.StartTime=GenerationStart;
@@ -765,6 +848,16 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::printf("Streaming seed %d%s%s: %d desired tiles, %d full-detail tiles, %d workers; upload budget %d batches / %.1f ms per frame.\n", std::printf("Streaming seed %d%s%s: %d desired tiles, %d full-detail tiles, %d workers; upload budget %d batches / %.1f ms per frame.\n",
Seed,TunnelMode,bShapeWorld?" [shape intervals]":"",Update.DesiredTiles, Seed,TunnelMode,bShapeWorld?" [shape intervals]":"",Update.DesiredTiles,
Update.FullResTiles,Streamer->GetWorkerCount(),MaxBatchUploadsPerFrame,BatchUploadBudgetMilliseconds); Update.FullResTiles,Streamer->GetWorkerCount(),MaxBatchUploadsPerFrame,BatchUploadBudgetMilliseconds);
if(bShapePipelineActive)
{
std::string ShapeError;
if(!StartFullShapeBuild(DesiredTiles,ShapeError))
{
StopGeneration();
std::fprintf(stderr,"World load failed: %s\n",ShapeError.c_str());
return;
}
}
std::fflush(stdout); std::fflush(stdout);
} }
@@ -797,6 +890,67 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::fflush(stdout); std::fflush(stdout);
} }
void TryCompleteShapePipeline()
{
if(!bShapePipelineActive||InitialNeighborhoodTiles.empty())return;
bool bNeighborhoodUploaded=true;
for(const FVoxelTileKey& Key:InitialNeighborhoodTiles)
if(!ResidentTiles.contains(Key)){bNeighborhoodUploaded=false;break;}
bNeighborhoodUploaded &= DirtyBatchSet.empty();
if(bNeighborhoodUploaded&&!bNeighborhoodReadyReported)
{
bNeighborhoodReadyReported=true;
const double Elapsed=std::chrono::duration<double>(
std::chrono::steady_clock::now()-ColdProcessStart).count();
std::printf("[NeighborhoodReady] %.3fs after cold process start; tiles=%zu/%zu vertices=%llu indices=%llu\n",
Elapsed,InitialNeighborhoodTiles.size(),InitialNeighborhoodTiles.size(),
static_cast<unsigned long long>(TotalVertices),
static_cast<unsigned long long>(TotalIndices));
std::fflush(stdout);
}
if(!bNeighborhoodReadyReported
||!bFullShapeBuildFinished.load(std::memory_order_acquire))return;
if(FullShapeBuildThread.joinable())FullShapeBuildThread.join();
if(!bFullShapeBuildSucceeded||!PendingFullShapeWorld)
{
bShapePipelineActive=false;bGenerationRunning=false;
std::fprintf(stderr,"[ShapePipeline] full shape build failed: %s\n",
FullShapeBuildError.c_str());
PostMessageA(Window,WM_CLOSE,0,0);
return;
}
const double FullShapeSeconds=std::chrono::duration<double>(
std::chrono::steady_clock::now()-FullShapeBuildStart).count();
std::vector<FVoxelTileKey> ResidentKeys;ResidentKeys.reserve(ResidentTiles.size());
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
if(Streamer)Streamer->Stop();
Streamer.reset();
ShapeWorld=std::move(PendingFullShapeWorld);
FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get());
bFarShapeStagePending=false;bShapePipelineActive=false;
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount,false,nullptr,-1);
++CrossingIndex;
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,ResidentKeys,CrossingIndex);
if(Update.DesiredTiles!=static_cast<int32>(DesiredTiles.size()))
{
Streamer->Stop();Streamer.reset();bGenerationRunning=false;
std::fprintf(stderr,"[ShapePipeline] full streamer accepted %d of %zu desired tiles.\n",
Update.DesiredTiles,DesiredTiles.size());
PostMessageA(Window,WM_CLOSE,0,0);
return;
}
FCrossingWork& Work=CrossingWork[CrossingIndex];
Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs;
Work.StartTime=std::chrono::steady_clock::now();
bGenerationRunning=true;
std::printf("[ShapePipelineFullReady] full_shape=%.3fs retained=%d new_jobs=%d workers=%d; filling outward\n",
FullShapeSeconds,Update.RetainedResidentTiles,Update.NewJobs,WorkerCount);
std::fflush(stdout);
}
void MarkBatchDirty(const FStandaloneBatchKey& Key) void MarkBatchDirty(const FStandaloneBatchKey& Key)
{DirtyBatchSet.insert(Key);} {DirtyBatchSet.insert(Key);}
@@ -1050,6 +1204,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
} }
ReportCrossingWork(); ReportCrossingWork();
UploadDirtyBatches(); UploadDirtyBatches();
if(bShapePipelineActive)TryCompleteShapePipeline();
if(bTestOneChunkCrossing&&bCrossingMoveFinished&&!bCrossingClosePosted if(bTestOneChunkCrossing&&bCrossingMoveFinished&&!bCrossingClosePosted
&&DirtyBatchSet.empty()) &&DirtyBatchSet.empty())
{ {
@@ -1062,11 +1217,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
PostMessageA(Window,WM_CLOSE,0,0); PostMessageA(Window,WM_CLOSE,0,0);
} }
} }
if(!bFullDetailReported&&IsFullDetailUploaded()) if(!bShapePipelineActive&&!bFullDetailReported&&IsFullDetailUploaded())
{ {
bFullDetailReported=true; bFullDetailReported=true;
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-GenerationStart).count(); const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-ColdProcessStart).count();
std::printf("[FullDetailReady] %.3f s after spawn; resident=%zu/%d tiles through LOD%d.\n", std::printf("[FullDetailReady] %.3f s after cold process start; resident=%zu/%d tiles through LOD%d.\n",
Elapsed,ResidentTiles.size(),Streamer->FullResDesiredCount(),World->FullResClipLevels-1); Elapsed,ResidentTiles.size(),Streamer->FullResDesiredCount(),World->FullResClipLevels-1);
ReportTileTimings();std::fflush(stdout); ReportTileTimings();std::fflush(stdout);
StartFarShapeGeneration(); StartFarShapeGeneration();
@@ -1074,8 +1229,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
if(!bInitialClipmapReported&&ResidentTiles.size()==DesiredTiles.size()&&DirtyBatchSet.empty()) if(!bInitialClipmapReported&&ResidentTiles.size()==DesiredTiles.size()&&DirtyBatchSet.empty())
{ {
bInitialClipmapReported=true; bInitialClipmapReported=true;
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-GenerationStart).count(); const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-ColdProcessStart).count();
std::printf("[InitialClipmapReady] %.3f s after spawn; %zu tiles, %llu vertices, %llu indices, %zu GPU batches.\n", std::printf("[InitialClipmapReady] %.3f s after cold process start; %zu tiles, %llu vertices, %llu indices, %zu GPU batches.\n",
Elapsed,ResidentTiles.size(),static_cast<unsigned long long>(TotalVertices), Elapsed,ResidentTiles.size(),static_cast<unsigned long long>(TotalVertices),
static_cast<unsigned long long>(TotalIndices),StreamingBatches.size()); static_cast<unsigned long long>(TotalIndices),StreamingBatches.size());
ReportTileTimings();std::fflush(stdout); ReportTileTimings();std::fflush(stdout);
@@ -1709,12 +1864,225 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
return Hash; return Hash;
} }
int RunPipelinedStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels,
int32 WorkerCount,int32 MaxLevel,FStandaloneShapeWorld::EDetailVariant DetailVariant,
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors,
std::chrono::steady_clock::time_point ColdStart)
{
using Clock=std::chrono::steady_clock;
FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels);
if(MaxLevel>=0)
Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(),
[&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),Desired.Tiles.end());
FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN);
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
for(const FVoxelTileKey& Tile:Desired.Tiles)
{
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
if(Tile.Level<World.FullResClipLevels)
{
FineMin.X=std::min(FineMin.X,Origin.X);FineMin.Y=std::min(FineMin.Y,Origin.Y);FineMin.Z=std::min(FineMin.Z,Origin.Z);
FineMax.X=std::max(FineMax.X,Origin.X+Extent);FineMax.Y=std::max(FineMax.Y,Origin.Y+Extent);FineMax.Z=std::max(FineMax.Z,Origin.Z+Extent);
}
}
if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX)
{std::fprintf(stderr,"[StreamPipeline] no desired detail or clipmap tiles.\n");return 7;}
const FIntVector CenterChunk=Desired.CenterChunk;
std::vector<FVoxelTileKey> Neighborhood;
FIntVector NearMin(INT32_MAX,INT32_MAX,INT32_MAX),NearMax(INT32_MIN,INT32_MIN,INT32_MIN);
for(const FVoxelTileKey& Tile:Desired.Tiles)
{
if(Tile.Level!=0||std::abs(Tile.Coord.X-CenterChunk.X)>1
||std::abs(Tile.Coord.Y-CenterChunk.Y)>1||std::abs(Tile.Coord.Z-CenterChunk.Z)>1)continue;
Neighborhood.push_back(Tile);const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
NearMin.X=std::min(NearMin.X,Origin.X);NearMin.Y=std::min(NearMin.Y,Origin.Y);NearMin.Z=std::min(NearMin.Z,Origin.Z);
NearMax.X=std::max(NearMax.X,Origin.X+Extent);NearMax.Y=std::max(NearMax.Y,Origin.Y+Extent);NearMax.Z=std::max(NearMax.Z,Origin.Z+Extent);
}
if(Neighborhood.empty())
{std::fprintf(stderr,"[StreamPipeline] the player neighborhood is absent from the desired set.\n");return 7;}
constexpr int32 NearBuildPad=64;
NearMin.X-=NearBuildPad;NearMin.Y-=NearBuildPad;NearMin.Z-=NearBuildPad;
NearMax.X+=NearBuildPad;NearMax.Y+=NearBuildPad;NearMax.Z+=NearBuildPad;
const int32 NearWorkers=WorkerCount>=8?2:1;
const int32 FullBuildWorkers=WorkerCount>=8?6:1;
auto NearShape=std::make_unique<FStandaloneShapeWorld>();
std::string BuildError;
const auto NearBuildStart=Clock::now();
if(!NearShape->Build(World,NearMin,NearMax,ClipMin,ClipMax,NearWorkers,
DetailVariant,false,BuildError))
{std::fprintf(stderr,"[StreamPipeline] near shape build failed: %s\n",BuildError.c_str());return 2;}
const double NearShapeSeconds=std::chrono::duration<double>(Clock::now()-NearBuildStart).count();
FStandaloneShapeWorldAccess::SetActive(NearShape.get());
FStandaloneStreamingGenerator NearStreamer(World,NearWorkers,bHashDensity,nullptr,MaxLevel,
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors);
NearStreamer.SetDesiredTileFilter(Neighborhood);
const auto NearStreamStart=Clock::now();
const FStandaloneStreamingUpdate NearUpdate=NearStreamer.Start(CameraPositionVoxels);
if(NearUpdate.DesiredTiles!=static_cast<int32>(Neighborhood.size()))
{NearStreamer.Stop();FStandaloneShapeWorldAccess::SetActive(nullptr);
std::fprintf(stderr,"[StreamPipeline] expected %zu player tiles but queued %d.\n",Neighborhood.size(),NearUpdate.DesiredTiles);return 7;}
FStandaloneShapeWorld FullShape;
bool bFullBuildOk=false;std::string FullBuildError;
const auto FullBuildStart=Clock::now();
std::thread FullBuildThread([&]
{bFullBuildOk=FullShape.Build(World,FineMin,FineMax,ClipMin,ClipMax,FullBuildWorkers,
DetailVariant,true,FullBuildError);});
struct FPipelineTileHashes {uint64 Density=0,Mesh=0,Payload=0,Topology=0;};
std::unordered_map<FVoxelTileKey,FPipelineTileHashes,FStandaloneTileKeyHash> NearHashByKey;
NearHashByKey.reserve(Neighborhood.size()*2+1);
std::vector<FHashedTile> DensityRows,MeshRows,PayloadRows,TopologyRows;
std::vector<FStandaloneCpuTile> NearTiles;NearTiles.reserve(Neighborhood.size());
std::chrono::steady_clock::time_point NeighborhoodReady{};
int32 NearFinished=0;
while(NearFinished<NearUpdate.DesiredTiles)
{
std::vector<FStandaloneCpuTile> Ready=NearStreamer.TakeReady();
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
for(FStandaloneCpuTile& Tile:Ready)
{
++NearFinished;
if(bHashDensity)
{
if(Tile.bHasDensityHash)DensityRows.push_back({Tile.Key,Tile.DensityHash});
if(Tile.bHasMeshHash)MeshRows.push_back({Tile.Key,Tile.MeshHash});
if(Tile.bHasPayloadHash)PayloadRows.push_back({Tile.Key,Tile.PayloadHash});
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
NearHashByKey[Tile.Key]={Tile.DensityHash,Tile.MeshHash,Tile.PayloadHash,Tile.TopologyHash};
}
NeighborhoodReady=std::max(NeighborhoodReady,Tile.CompletedAt);
NearTiles.push_back(std::move(Tile));
}
}
NearStreamer.Stop();
const double NearStreamSeconds=std::chrono::duration<double>(NeighborhoodReady-NearStreamStart).count();
FStandaloneShapeWorldAccess::SetActive(nullptr);
// Release the small shape before the large build finishes allocating its packed field.
NearTiles.clear();NearTiles.shrink_to_fit();NearShape.reset();
FullBuildThread.join();
const double FullShapeSeconds=std::chrono::duration<double>(Clock::now()-FullBuildStart).count();
if(!bFullBuildOk)
{std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",FullBuildError.c_str());return 2;}
FStandaloneShapeWorldAccess::SetActive(&FullShape);
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> NearSet;
NearSet.reserve(Neighborhood.size()*2+1);for(const FVoxelTileKey& Key:Neighborhood)NearSet.insert(Key);
std::vector<FVoxelTileKey> Remaining;Remaining.reserve(Desired.Tiles.size()-Neighborhood.size());
int32 FullDetailExpected=0;
for(const FVoxelTileKey& Tile:Desired.Tiles)
{
if(Tile.Level<World.FullResClipLevels)++FullDetailExpected;
if(!NearSet.contains(Tile))Remaining.push_back(Tile);
}
FStandaloneStreamingGenerator FullStreamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors);
FullStreamer.SetDesiredTileFilter(Remaining);
const auto FillStart=Clock::now();
const FStandaloneStreamingUpdate FillUpdate=FullStreamer.Start(CameraPositionVoxels);
int32 Finished=0,FullDetailFinished=static_cast<int32>(Neighborhood.size());
const int32 FullDetailRemaining=std::max(0,FullDetailExpected-FullDetailFinished);
std::chrono::steady_clock::time_point FullDetailReady=NeighborhoodReady,WholeViewReady{};
bool bFullDetailReported=false;
while(Finished<FillUpdate.DesiredTiles)
{
std::vector<FStandaloneCpuTile> Ready=FullStreamer.TakeReady();
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
for(const FStandaloneCpuTile& Tile:Ready)
{
++Finished;WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt);
if(Tile.Key.Level<World.FullResClipLevels)
{++FullDetailFinished;FullDetailReady=std::max(FullDetailReady,Tile.CompletedAt);}
if(bHashDensity)
{
if(Tile.bHasDensityHash)DensityRows.push_back({Tile.Key,Tile.DensityHash});
if(Tile.bHasMeshHash)MeshRows.push_back({Tile.Key,Tile.MeshHash});
if(Tile.bHasPayloadHash)PayloadRows.push_back({Tile.Key,Tile.PayloadHash});
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
}
}
if(!bFullDetailReported&&FullDetailRemaining>0&&FullDetailFinished>=FullDetailExpected)
{
std::printf("[PipelineMilestone] full_detail_ready=%.3fs generated=%d/%d\n",
std::chrono::duration<double>(FullDetailReady-ColdStart).count(),
FullDetailFinished,FullDetailExpected);
bFullDetailReported=true;
}
}
FullStreamer.Stop();
const double WholeViewSeconds=std::chrono::duration<double>(WholeViewReady-ColdStart).count();
const double NeighborhoodSeconds=std::chrono::duration<double>(NeighborhoodReady-ColdStart).count();
const double FullDetailSeconds=std::chrono::duration<double>(FullDetailReady-ColdStart).count();
bool bNearParityPassing=true;
if(bHashDensity)
{
FStandaloneStreamingGenerator NearVerifier(World,NearWorkers,true,nullptr,MaxLevel,
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors);
NearVerifier.SetDesiredTileFilter(Neighborhood);
const FStandaloneStreamingUpdate VerifyUpdate=NearVerifier.Start(CameraPositionVoxels);
int32 VerifiedTiles=0,NearHashMismatches=0;
while(VerifiedTiles<VerifyUpdate.DesiredTiles)
{
std::vector<FStandaloneCpuTile> Ready=NearVerifier.TakeReady();
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
for(const FStandaloneCpuTile& Tile:Ready)
{
++VerifiedTiles;
const auto It=NearHashByKey.find(Tile.Key);
if(It==NearHashByKey.end()||!Tile.bHasDensityHash||!Tile.bHasMeshHash
||!Tile.bHasPayloadHash||!Tile.bHasTopologyHash
||It->second.Density!=Tile.DensityHash||It->second.Mesh!=Tile.MeshHash
||It->second.Payload!=Tile.PayloadHash||It->second.Topology!=Tile.TopologyHash)
{
++NearHashMismatches;
if(NearHashMismatches<=4)
std::fprintf(stderr,"[PipelineNeighborhoodHashMismatch] key=(%d,%d,%d,lod%d)\n",
Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,Tile.Key.Level);
}
}
}
NearVerifier.Stop();
bNearParityPassing=VerifiedTiles==static_cast<int32>(Neighborhood.size())
&&NearHashMismatches==0;
std::printf("[PipelineNeighborhoodParity] tiles=%d/%zu hash_fields=%d mismatched_tiles=%d status=%s\n",
VerifiedTiles,Neighborhood.size(),VerifiedTiles*4,NearHashMismatches,
bNearParityPassing?"PASS":"FAIL");
}
std::printf("[StreamPipeline] workers=%d near_mesh_workers=%d full_shape_workers=%d near_shape=%.3fs near_mesh=%.3fs full_shape=%.3fs neighborhood_ready=%.3fs full_detail_ready=%.3fs whole_view_ready=%.3fs neighborhood_tiles=%zu/%zu full_detail_tiles=%d/%d whole_tiles=%d/%zu cache_hits=0 full_payload=1\n",
WorkerCount,NearWorkers,FullBuildWorkers,NearShapeSeconds,NearStreamSeconds,FullShapeSeconds,
NeighborhoodSeconds,FullDetailSeconds,WholeViewSeconds,Neighborhood.size(),Neighborhood.size(),
FullDetailFinished,FullDetailExpected,static_cast<int32>(Desired.Tiles.size()),Desired.Tiles.size());
if(bHashDensity)
{
std::vector<FHashedTile> FullDensity,FullMesh,FullPayload,FullTopology;
for(const FHashedTile& Row:DensityRows)if(Row.Key.Level<World.FullResClipLevels)FullDensity.push_back(Row);
for(const FHashedTile& Row:MeshRows)if(Row.Key.Level<World.FullResClipLevels)FullMesh.push_back(Row);
for(const FHashedTile& Row:PayloadRows)if(Row.Key.Level<World.FullResClipLevels)FullPayload.push_back(Row);
for(const FHashedTile& Row:TopologyRows)if(Row.Key.Level<World.FullResClipLevels)FullTopology.push_back(Row);
std::printf("[PipelineFullDetailHash] tiles=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
FullDetailExpected,static_cast<unsigned long long>(HashTileRows(FullDensity)),
static_cast<unsigned long long>(HashTileRows(FullMesh)),
static_cast<unsigned long long>(HashTileRows(FullPayload)),
static_cast<unsigned long long>(HashTileRows(FullTopology)));
std::printf("[PipelineWholeViewHash] tiles=%zu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
Desired.Tiles.size(),static_cast<unsigned long long>(HashTileRows(DensityRows)),
static_cast<unsigned long long>(HashTileRows(MeshRows)),
static_cast<unsigned long long>(HashTileRows(PayloadRows)),
static_cast<unsigned long long>(HashTileRows(TopologyRows)));
}
FStandaloneShapeWorldAccess::SetActive(nullptr);
return FullDetailFinished==FullDetailExpected&&Finished==FillUpdate.DesiredTiles
&&bNearParityPassing?0:6;
}
int RunStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels, int RunStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels,
int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels, int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels,
bool bEnhancedWalls,const FStandaloneMeshCache* Cache, bool bEnhancedWalls,const FStandaloneMeshCache* Cache,
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups, bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,
bool bSkipUV,bool bSkipColors, bool bSkipUV,bool bSkipColors,
bool bShardAudit,float CameraYaw,float CameraPitch, bool bShardAudit,bool bFarSurvivalAudit,bool bFarAuditBadShell,
float CameraYaw,float CameraPitch,
const std::string& BakeCachePath, const std::string& BakeCachePath,
const FStandaloneMeshCacheMetadata& CacheMetadata) const FStandaloneMeshCacheMetadata& CacheMetadata)
{ {
@@ -1731,6 +2099,14 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
const int32 FullDetailExpected=static_cast<int32>(std::count_if( const int32 FullDetailExpected=static_cast<int32>(std::count_if(
Desired.Tiles.begin(),Desired.Tiles.end(),[&](const FVoxelTileKey& Tile) Desired.Tiles.begin(),Desired.Tiles.end(),[&](const FVoxelTileKey& Tile)
{return Tile.Level<World.FullResClipLevels;})); {return Tile.Level<World.FullResClipLevels;}));
std::unique_ptr<FFarSurvivalAudit> FarAudit;
if(bFarSurvivalAudit)
{
const FStandaloneShapeWorld* Shape=FStandaloneShapeWorldAccess::GetActive();
if(!Shape||!Shape->HasFarWorld())
{std::fprintf(stderr,"Far survival audit requires an active shape world with far-parent data.\n");return 15;}
FarAudit=std::make_unique<FFarSurvivalAudit>(World,*Shape,Desired.Tiles,bFarAuditBadShell);
}
std::array<std::vector<FHashedTile>,MaxLevels+1> HashRows; std::array<std::vector<FHashedTile>,MaxLevels+1> HashRows;
std::array<std::vector<FHashedTile>,MaxLevels+1> MeshHashRows; std::array<std::vector<FHashedTile>,MaxLevels+1> MeshHashRows;
std::array<std::vector<FHashedTile>,MaxLevels+1> PayloadHashRows; std::array<std::vector<FHashedTile>,MaxLevels+1> PayloadHashRows;
@@ -1787,6 +2163,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
} }
for(FStandaloneCpuTile& Tile:Ready) for(FStandaloneCpuTile& Tile:Ready)
{ {
if(FarAudit)FarAudit->AuditTile(Tile);
++Finished; ++Finished;
const int32 Level=Tile.Key.Level; const int32 Level=Tile.Key.Level;
if(PlayerNeighborhoodTiles.contains(Tile.Key)) if(PlayerNeighborhoodTiles.contains(Tile.Key))
@@ -1960,6 +2337,12 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
} }
if(Finished!=DesiredTotal||FullDetailFinished!=FullDetailExpected)return 6; if(Finished!=DesiredTotal||FullDetailFinished!=FullDetailExpected)return 6;
if(bShardAudit&&(MeshSheetStats.ThinSheets!=0||MeshSheetStats.SmallComponentsInView!=0))return 13; if(bShardAudit&&(MeshSheetStats.ThinSheets!=0||MeshSheetStats.SmallComponentsInView!=0))return 13;
if(FarAudit)
{
FarAudit->Finalize();
FarAudit->Print();
if(!FarAudit->Passing())return 14;
}
return 0; return 0;
} }
@@ -2038,7 +2421,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
int main(int Argc,char** Argv) int main(int Argc,char** Argv)
{ {
FApp App;App.DataPath=DefaultDataPath().string(); const auto ColdProcessStart=std::chrono::steady_clock::now();
FApp App;App.DataPath=DefaultDataPath().string();App.ColdProcessStart=ColdProcessStart;
std::string JoinedCommandLine; std::string JoinedCommandLine;
for(int32 I=1;I<Argc;++I) for(int32 I=1;I<Argc;++I)
{if(!JoinedCommandLine.empty())JoinedCommandLine.push_back(' ');JoinedCommandLine+=Argv[I];} {if(!JoinedCommandLine.empty())JoinedCommandLine.push_back(' ');JoinedCommandLine+=Argv[I];}
@@ -2047,8 +2431,11 @@ int main(int Argc,char** Argv)
int32 MaxClipLevelOverride=-1; int32 MaxClipLevelOverride=-1;
int32 ShapeCompareLevel=-1,ShapeCompareTileLimit=0; int32 ShapeCompareLevel=-1,ShapeCompareTileLimit=0;
bool bStreamBenchmark=false; bool bStreamBenchmark=false;
bool bStreamPipeline=false;
bool bProfileDetailBand=false; bool bProfileDetailBand=false;
bool bShardAudit=false; bool bShardAudit=false;
bool bFarSurvivalAudit=false;
bool bFarAuditBadShell=false;
bool bStreamHash=false; bool bStreamHash=false;
bool bSkipUvAndColors=false; bool bSkipUvAndColors=false;
bool bSkipUV=false; bool bSkipUV=false;
@@ -2085,6 +2472,8 @@ int main(int Argc,char** Argv)
else if(Arg=="--round-tunnels-legacy") else if(Arg=="--round-tunnels-legacy")
{App.bRoundTunnels=true;App.bRoundTunnelEnhanced=false;} {App.bRoundTunnels=true;App.bRoundTunnelEnhanced=false;}
else if(Arg=="--stream-benchmark")bStreamBenchmark=true; else if(Arg=="--stream-benchmark")bStreamBenchmark=true;
else if(Arg=="--stream-pipeline")
{bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;}
else if(Arg=="--edit-benchmark") else if(Arg=="--edit-benchmark")
{bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;} {bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;}
else if(Arg=="--stream-hash")bStreamHash=true; else if(Arg=="--stream-hash")bStreamHash=true;
@@ -2093,6 +2482,9 @@ int main(int Argc,char** Argv)
else if(Arg=="--skip-colors")bSkipColors=true; else if(Arg=="--skip-colors")bSkipColors=true;
else if(Arg=="--skip-material-groups")bSkipMaterialGroups=true; else if(Arg=="--skip-material-groups")bSkipMaterialGroups=true;
else if(Arg=="--shard-audit")bShardAudit=true; else if(Arg=="--shard-audit")bShardAudit=true;
else if(Arg=="--far-survival-audit")bFarSurvivalAudit=true,App.bShapeWorld=true;
else if(Arg=="--far-audit-bad-shell")
{bFarAuditBadShell=true;bFarSurvivalAudit=true;App.bShapeWorld=true;}
else if(Arg=="--compare-round-walls") else if(Arg=="--compare-round-walls")
{bRoundWallCompare=true;App.bRoundTunnels=true;} {bRoundWallCompare=true;App.bRoundTunnels=true;}
else if(Arg.starts_with("--round-compare-tiles=")) else if(Arg.starts_with("--round-compare-tiles="))
@@ -2166,6 +2558,7 @@ int main(int Argc,char** Argv)
:(App.bShapeCompare?ShapeCompareLevel>=App.World->FullResClipLevels :(App.bShapeCompare?ShapeCompareLevel>=App.World->FullResClipLevels
:(BenchmarkLevel>=App.World->FullResClipLevels)); :(BenchmarkLevel>=App.World->FullResClipLevels));
if(App.bShapeWorld&&(App.bShapeAuditOnly||App.bShapeCompare||bStreamBenchmark||BenchmarkLevel>=0) if(App.bShapeWorld&&(App.bShapeAuditOnly||App.bShapeCompare||bStreamBenchmark||BenchmarkLevel>=0)
&&!bStreamPipeline
&&!App.BuildShapeWorldForCamera(Error,bNeedFarShapeWorld)) &&!App.BuildShapeWorldForCamera(Error,bNeedFarShapeWorld))
{std::fprintf(stderr,"Shape interval build failed: %s\n",Error.c_str());return 2;} {std::fprintf(stderr,"Shape interval build failed: %s\n",Error.c_str());return 2;}
if(App.bShapeAuditOnly) if(App.bShapeAuditOnly)
@@ -2181,6 +2574,12 @@ int main(int Argc,char** Argv)
return RunRoundWallComparison(*App.World,App.Position,RoundCompareTileCount); return RunRoundWallComparison(*App.World,App.Position,RoundCompareTileCount);
if(bStreamBenchmark) if(bStreamBenchmark)
{ {
if(bStreamPipeline)
return RunPipelinedStreamingBenchmark(*App.World,
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),
App.WorkerCount,StreamBenchmarkMaxLevel,App.ShapeDetailVariant,
bStreamHash||!BakeMeshCachePath.empty(),bSkipUvAndColors,bSkipMaterialGroups,
bSkipUV,bSkipColors,ColdProcessStart);
if(bEditBenchmark) if(bEditBenchmark)
return RunEditTransactionBenchmark(*App.World,*App.ShapeWorld, return RunEditTransactionBenchmark(*App.World,*App.ShapeWorld,
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),App.WorkerCount); FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),App.WorkerCount);
@@ -2194,7 +2593,8 @@ int main(int Argc,char** Argv)
App.WorkerCount,StreamBenchmarkMaxLevel,App.bRoundTunnels, App.WorkerCount,StreamBenchmarkMaxLevel,App.bRoundTunnels,
App.bRoundTunnelEnhanced,(!App.bShapeWorld&&App.MeshCache.IsLoaded())?&App.MeshCache:nullptr, App.bRoundTunnelEnhanced,(!App.bShapeWorld&&App.MeshCache.IsLoaded())?&App.MeshCache:nullptr,
bStreamHash,bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,bShardAudit, bStreamHash,bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,bShardAudit,
App.Yaw,App.Pitch,BakeMeshCachePath,App.MakeMeshCacheMetadata()); bFarSurvivalAudit,bFarAuditBadShell,App.Yaw,App.Pitch,
BakeMeshCachePath,App.MakeMeshCacheMetadata());
} }
if(BenchmarkLevel>=0) if(BenchmarkLevel>=0)
return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount, return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount,
+283
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "WorldMesher.h" #include "WorldMesher.h"
#include "StandaloneShapeWorld.h"
#include <condition_variable> #include <condition_variable>
#include <algorithm> #include <algorithm>
@@ -152,6 +153,277 @@ inline uint64 HashStandaloneCpuTileMesh(const FStandaloneCpuTile& Tile)
return Hash; return Hash;
} }
inline double FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center,
const FVector& Direction,double PreferredRadius)
{
const double SearchLength=PreferredRadius*2.0+32.0;
const double AcceptBand=std::max(16.0,PreferredRadius*0.75);
constexpr double Stride=8.0;
double PreviousDistance=0.0;
bool PreviousSolid=Shape.DensityAt(static_cast<float>(Center.X),static_cast<float>(Center.Y),
static_cast<float>(Center.Z))<0.0f;
double BestRadius=PreferredRadius,BestDifference=std::numeric_limits<double>::infinity();
for(double Distance=Stride;Distance<=SearchLength+1.0e-6;Distance=std::min(Distance+Stride,SearchLength))
{
const FVector P=Center+Direction*Distance;
const bool Solid=Shape.DensityAt(static_cast<float>(P.X),static_cast<float>(P.Y),
static_cast<float>(P.Z))<0.0f;
if(Solid!=PreviousSolid)
{
double Lo=PreviousDistance,Hi=Distance;
for(int32 I=0;I<6;++I)
{
const double Mid=(Lo+Hi)*0.5;
const FVector M=Center+Direction*Mid;
const bool MidSolid=Shape.DensityAt(static_cast<float>(M.X),static_cast<float>(M.Y),
static_cast<float>(M.Z))<0.0f;
if(MidSolid==PreviousSolid)Lo=Mid;else Hi=Mid;
}
const double Crossing=(Lo+Hi)*0.5;
const double Difference=std::abs(Crossing-PreferredRadius);
if(Difference<=AcceptBand&&Difference<BestDifference)
{BestRadius=Crossing;BestDifference=Difference;}
}
if(Distance>=SearchLength-1.0e-6)break;
PreviousDistance=Distance;PreviousSolid=Solid;
}
return BestRadius;
}
inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key,
int32 Step,FVoxelMeshData& Mesh)
{
if(Key.Level<6||Step<=0)return;
constexpr int32 SideCount=8;
const FIntVector TileOrigin=Key.OriginVoxels();
const int32 TileExtent=Key.ExtentVoxels();
std::vector<FVector> NewVertices;
std::vector<FVector> NewNormals;
std::vector<FVector2f> NewUVs;
std::vector<FColor> NewColors;
std::vector<int32> NewTriangles;
const int32 OldVertexCount=Mesh.Vertices.Num();
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
for(const auto& Connector:Shape.GetFarConnectorSegments())
{
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 double Starts[3]={Connector.Start.X,Connector.Start.Y,Connector.Start.Z};
const double Deltas[3]={Delta.X,Delta.Y,Delta.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 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 int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/128.0)));
std::vector<double> RingParameters{TMin};
for(int32 Sample=0;Sample<AxialSampleCount;++Sample)
{
const double T=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
if(T>TMin+1.0e-6&&T<TMax-1.0e-6)RingParameters.push_back(T);
}
RingParameters.push_back(TMax);
const int32 RingCount=static_cast<int32>(RingParameters.size())-1;
const int32 VertexBase=static_cast<int32>(NewVertices.size());
for(int32 Ring=0;Ring<=RingCount;++Ring)
{
const double T=RingParameters[static_cast<size_t>(Ring)];
const FVector Center=Connector.Start+Delta*T;
for(int32 Side=0;Side<SideCount;++Side)
{
const double Angle=2.0*PI*Side/SideCount;
FVector Radial=U*std::cos(Angle)+V*std::sin(Angle);Radial.Normalize();
const double SourceRadius=FindFarConnectorSurfaceRadius(Shape,Center,Radial,
Connector.Radius);
const FVector P=Center+Radial*SourceRadius;
NewVertices.emplace_back(P.X*25.0,P.Y*25.0,P.Z*25.0);
NewNormals.emplace_back(-Radial.X,-Radial.Y,-Radial.Z);
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(T*Length/256.0),
static_cast<float>(Side)/SideCount);
if(bHasColors)NewColors.push_back(Color);
}
}
for(int32 Ring=0;Ring<RingCount;++Ring)
for(int32 Side=0;Side<SideCount;++Side)
{
const int32 Next=(Side+1)%SideCount;
const int32 A=VertexBase+Ring*SideCount+Side;
const int32 B=VertexBase+Ring*SideCount+Next;
const int32 C=VertexBase+(Ring+1)*SideCount+Side;
const int32 D=VertexBase+(Ring+1)*SideCount+Next;
// The far links are carved passages, so their visible normals face inward.
NewTriangles.insert(NewTriangles.end(),{A,C,B,B,C,D});
}
}
if(NewVertices.empty()||NewTriangles.empty())return;
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
auto InsertVertices=[&](auto& Existing,const auto& Added)
{
using TArrayType=std::remove_reference_t<decltype(Existing)>;
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
for(const auto& Value:Added)Combined.Add(Value);
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
Existing=std::move(Combined);
};
InsertVertices(Mesh.Vertices,NewVertices);
InsertVertices(Mesh.Normals,NewNormals);
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
Mesh.Triangles=std::move(CombinedTriangles);
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
}
inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FStandaloneShapeWorld& Shape,
const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
{
if(Key.Level<7||Step<4||!World.Manager)return;
FStandaloneShapeWorld::FScopedForcedFarAirSamples SourceFieldScope(nullptr);
struct FBoundary { double Z=0.0,HalfWidth=8.0; };
const auto& Layout=World.Manager->GetLayout();
std::vector<FBoundary> Boundaries;
for(const FStrateSlot& Slot:Layout)
{
const FStrateGenerationParams Params=World.Manager->GetGenerationParams(
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
const double HalfWidth=std::max(8.0,static_cast<double>(Params.BoundarySealThickness)+8.0);
Boundaries.push_back({static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth});
Boundaries.push_back({static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth});
}
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z<B.Z;});
Boundaries.erase(std::unique(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
{return std::abs(A.Z-B.Z)<0.01;}),Boundaries.end());
const FIntVector Origin=Key.OriginVoxels();const int32 Extent=Key.ExtentVoxels();
const int32 FineStep=std::max(1,Step/2);const int32 GridCells=Extent/FineStep;
if(GridCells<=0||GridCells>256)return;
struct FNode { double Z=0.0;FVector Normal{};int32 Index=INDEX_NONE;bool bValid=false,bBelowSolid=false; };
std::vector<FVector> NewVertices,NewNormals;
std::vector<FVector2f> NewUVs;std::vector<FColor> NewColors;
std::vector<int32> NewTriangles;
const int32 OldVertexCount=Mesh.Vertices.Num();
const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount;
const bool bHasColors=Mesh.Colors.Num()==OldVertexCount;
const FColor Color=bHasColors&&OldVertexCount>0?Mesh.Colors[0]:FColor(160,160,160,255);
const auto Density=[&](double X,double Y,double Z)
{return Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z));};
for(const FBoundary& Boundary:Boundaries)
{
if(Boundary.Z+Boundary.HalfWidth<Origin.Z||Boundary.Z-Boundary.HalfWidth>Origin.Z+Extent)continue;
const int32 ZMin=std::max(Origin.Z,static_cast<int32>(std::floor(Boundary.Z-Boundary.HalfWidth)));
const int32 ZMax=std::min(Origin.Z+Extent,static_cast<int32>(std::ceil(Boundary.Z+Boundary.HalfWidth)));
std::vector<FNode> Nodes(static_cast<size_t>(GridCells+1)*(GridCells+1));
for(int32 YI=0;YI<=GridCells;++YI)for(int32 XI=0;XI<=GridCells;++XI)
{
const double X=Origin.X+XI*FineStep,Y=Origin.Y+YI*FineStep;
int32 PreviousZ=ZMin;bool PreviousSolid=Density(X,Y,PreviousZ)<0.0f;
FNode& Node=Nodes[static_cast<size_t>(YI)*(GridCells+1)+XI];
for(int32 Z=ZMin+1;Z<=ZMax;++Z)
{
const bool Solid=Density(X,Y,Z)<0.0f;
if(Solid==PreviousSolid){PreviousZ=Z;PreviousSolid=Solid;continue;}
double Lo=PreviousZ,Hi=Z;
for(int32 I=0;I<8;++I)
{
const double Mid=(Lo+Hi)*0.5;
if((Density(X,Y,Mid)<0.0f)==PreviousSolid)Lo=Mid;else Hi=Mid;
}
Node.Z=(Lo+Hi)*0.5;Node.bBelowSolid=PreviousSolid;Node.bValid=true;
const float GX=Density(X+2,Y,Node.Z)-Density(X-2,Y,Node.Z);
const float GY=Density(X,Y+2,Node.Z)-Density(X,Y-2,Node.Z);
const float GZ=Density(X,Y,Node.Z+2)-Density(X,Y,Node.Z-2);
Node.Normal=FVector(GX,GY,GZ);
if(Node.Normal.SizeSquared()<1.0e-8)Node.Normal=FVector(0,0,PreviousSolid?1:-1);
Node.Normal.Normalize();break;
}
}
const auto EnsureVertex=[&](FNode& Node,int32 XI,int32 YI)
{
if(Node.Index!=INDEX_NONE)return Node.Index;
Node.Index=static_cast<int32>(NewVertices.size());
NewVertices.emplace_back((Origin.X+XI*FineStep)*25.0,(Origin.Y+YI*FineStep)*25.0,Node.Z*25.0);
NewNormals.push_back(Node.Normal);
if(bHasUVs)NewUVs.emplace_back(static_cast<float>(XI)*FineStep/256.0f,
static_cast<float>(YI)*FineStep/256.0f);
if(bHasColors)NewColors.push_back(Color);
return Node.Index;
};
for(int32 YI=0;YI<GridCells;++YI)for(int32 XI=0;XI<GridCells;++XI)
{
FNode* Corners[4]={&Nodes[static_cast<size_t>(YI)*(GridCells+1)+XI],
&Nodes[static_cast<size_t>(YI)*(GridCells+1)+XI+1],
&Nodes[static_cast<size_t>(YI+1)*(GridCells+1)+XI+1],
&Nodes[static_cast<size_t>(YI+1)*(GridCells+1)+XI]};
int32 Valid[4],Count=0;
for(int32 I=0;I<4;++I)if(Corners[I]->bValid)Valid[Count++]=I;
if(Count<3)continue;
int32 Indices[4];
const int32 CornerXI[4]={XI,XI+1,XI+1,XI};
const int32 CornerYI[4]={YI,YI,YI+1,YI+1};
for(int32 I=0;I<Count;++I)
{
const int32 C=Valid[I];Indices[I]=EnsureVertex(*Corners[C],CornerXI[C],CornerYI[C]);
}
const bool bUp=Corners[Valid[0]]->bBelowSolid;
if(Count==4)
{
if(bUp)NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[1],Indices[3],Indices[1],Indices[2],Indices[3]});
else NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[3],Indices[1],Indices[1],Indices[3],Indices[2]});
}
else if(bUp)NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[1],Indices[2]});
else NewTriangles.insert(NewTriangles.end(),{Indices[0],Indices[2],Indices[1]});
}
}
if(NewVertices.empty()||NewTriangles.empty())return;
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount);
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
auto InsertVertices=[&](auto& Existing,const auto& Added)
{
using TArrayType=std::remove_reference_t<decltype(Existing)>;
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
for(const auto& Value:Added)Combined.Add(Value);
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
Existing=std::move(Combined);
};
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
Mesh.Triangles=std::move(CombinedTriangles);Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
}
struct FStandaloneMeshCacheMetadata struct FStandaloneMeshCacheMetadata
{ {
uint64 SourceDataHash=0; uint64 SourceDataHash=0;
@@ -556,6 +828,12 @@ private:
const int32 Extent=Job.Key.ExtentVoxels(); const int32 Extent=Job.Key.ExtentVoxels();
const int32 Cells=Level<World->FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World->CoarseTileCells); const int32 Cells=Level<World->FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World->CoarseTileCells);
const int32 Step=FMath::Max(1,Extent/Cells); const int32 Step=FMath::Max(1,Extent/Cells);
std::unordered_set<uint64> ForcedFarAirSamples;
const FStandaloneShapeWorld* Shape=FStandaloneShapeWorldAccess::GetActive();
if(Level>=6&&Shape&&Shape->HasFarWorld())
Shape->BuildFarConnectorAirSamples(Job.Key.OriginVoxels(),Step,Cells,ForcedFarAirSamples);
FStandaloneShapeWorld::FScopedForcedFarAirSamples ForcedSamplesScope(
ForcedFarAirSamples.empty()?nullptr:&ForcedFarAirSamples);
int64 SampleCount=0; int64 SampleCount=0;
const bool bProfile=VoxelDensityProfile::GetMode()!=VoxelDensityProfile::EMode::Disabled; const bool bProfile=VoxelDensityProfile::GetMode()!=VoxelDensityProfile::EMode::Disabled;
const VoxelDensityProfile::FSnapshot Before=bProfile const VoxelDensityProfile::FSnapshot Before=bProfile
@@ -564,6 +842,11 @@ private:
FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr, FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr,
Job.BandZMin,Job.BandZMax,&SampleCount,nullptr,nullptr, Job.BandZMin,Job.BandZMax,&SampleCount,nullptr,nullptr,
bHashDensity?&DensityValues:nullptr); bHashDensity?&DensityValues:nullptr);
if(Shape&&Shape->HasFarWorld())
{
AppendFarConnectorShells(*Shape,Job.Key,Step,Mesh);
AppendFarStratumShells(*World,*Shape,Job.Key,Step,Mesh);
}
if(bHashDensity) if(bHashDensity)
{ {
Out.bHasDensityHash=true; Out.bHasDensityHash=true;
+1 -1
View File
@@ -1 +1 @@
{"schema_version":1,"source":"VoxelForgeExplore standalone base-world export","voxel_size_cm":25,"seed":0,"open_surface_entry":true,"origin_spine_radius":28,"world_radius_voxels":0,"edge_seal_thickness":128,"total_strates":2,"inter_strate_gap_chunks":0,"clip_radius":6,"max_clip_level":6,"full_res_clip_levels":2,"coarse_tile_cells":16,"render_distance_chunks":1536,"generate_skirts":true,"skirt_cells":4,"lod_octave_drop":1,"strate_content_cut_min_level":9,"settings_asset":{"ViewDistanceXY":32,"ViewDistanceUp":10,"ViewDistanceDown":10,"MaxConcurrentTasks":16,"MaxGenerationWorkerCores":0,"NearTaskReserve":4,"MaxMeshAppliesPerFrame":4,"MaxMeshApplyMilliseconds":2,"MaxUnloadsPerFrame":16,"PawnCollisionRadiusChunks":2,"bClampViewToStrate":true,"StrateViewMarginChunks":6,"StrateContentCutMinLevel":9,"ClipRadius":6,"MaxClipLevel":6,"FullResClipLevels":2,"CoarseTileCells":16,"RenderDistanceChunks":1536,"bGenerateSkirts":true,"SkirtCells":4,"LODOctaveDrop":1,"WorldRadiusVoxels":0,"EdgeSealThickness":128,"DecorationRadiusChunks":12,"DecorationNearRadiusChunks":6,"DecorationRegionSizeCells":8,"DecorationSpacingVoxels":8,"DecorationFarSpacingVoxels":8,"DecorationMarchStepVoxels":4,"DecorationMaxCrossingsPerColumn":4,"DecorationColumnDepthVoxels":320,"MaxDecorationCellsPerFrame":2,"MaxConcurrentDecorationTasks":4,"bEnableDensityVolume":false,"DensityVolumeResolution":256,"DensityVolumeLevels":3,"DensityVolumeFillSlabCells":8,"DensityVolumeMarchSteps":64,"bDensityVolumeGPUUpload":false,"bDebugDrawDensityVolume":false,"DensityVolumeDebugRadiusCells":12,"VoxelMaterial":"/Game/VoxelForge/M_Test1_Inst.M_Test1_Inst","Seed":0,"Season":"","CurrentSeason":1,"StratePool":["/Game/VoxelForge/DA_Strate1.DA_Strate1","/Game/VoxelForge/DA_Strate3.DA_Strate3"],"FixedStrates":[],"TotalStrates":2,"InterStrateGapChunks":0,"OriginSpineRadius":28,"bOpenSurfaceEntry":true,"MaxModifications":0,"MaxBrushRadius":30,"MaxTotalVolume":0,"AssetBundleData":{"Bundles":[]},"NativeClass":"/Script/VoxelForge.VoxelSettings"},"strates":[{"index":0,"top_chunk_z":0,"bottom_chunk_z":-15,"height_chunks":16,"asset":{"StrateName":"","StrateDescription":"","StrateHeightInChunks":16,"TransitionType":0,"TransitionBlendChunks":2,"GeneratorType":0,"bEnableWorms":false,"GenerationParams":{"BaseDensity":8,"VerticalScale":0.80000001192092896,"WormFrequency":0.014999999664723873,"WormHorizontalBias":3,"WormThreshold":0.059999998658895493,"WormStrength":10,"WormNetworkRange":64,"RoomSpacing":128,"RoomDensity":0.34999999403953552,"MinRoomRadius":65,"MaxRoomRadius":120,"RoomHeightRatio":0.5,"RoomFloorCutMin":0,"RoomFloorCutMax":0,"FloorReliefStrength":0,"FloorReliefFrequency":1,"RoomMouthRiseStrength":0,"RoomMouthRiseBlendVoxels":12,"RoomShapeVariety":0.5,"OriginRoomRadius":65,"OriginRoomMaxConnections":4,"TunnelMinRadius":8,"TunnelMaxRadius":12,"TunnelDensity":0.40000000596046448,"MaxTunnelLength":360,"TunnelWarpStrength":24,"TunnelHorizontalBias":0.5,"bTunnelsFlowTowardOrigin":true,"TunnelEndpointZOffset":0.25,"bTunnelFloorEnabled":true,"bTunnelFloorTerracingEnabled":true,"TunnelFloorTerraceStepHeight":5,"TunnelFloorMaxLedgeHeight":24,"TunnelFloorGentleSlopeThreshold":88.090850830078125,"TunnelFloorLedgeCountPreference":0,"TunnelFloorMaxLedges":4096,"SDFBlendRadius":25,"CaveWarpStrength":16,"CaveWarpFrequency":0.014999999664723873,"SurfaceRoughness":1,"RoughnessFrequency":0.05000000074505806,"RoughnessNoiseType":2,"DomainWarpStrength":0,"DomainWarpFrequency":0.029999999329447746,"FloorBias":4,"BoundarySealThickness":8,"WaterLevelRelative":0},"SlabParams":{"FloorRelativeHeight":0.25,"CeilingRelativeHeight":0.60000002384185791,"FloorRoughness":4,"FloorRoughnessFrequency":0.039999999105930328,"CeilingRoughness":6,"CeilingRoughnessFrequency":0.039999999105930328,"ColumnDensity":0.079999998211860657,"ColumnMinRadius":8,"ColumnMaxRadius":16,"ColumnSpacing":96,"BoundarySealThickness":4,"BaseDensity":8},"MazeParams":{"CellSize":160,"CorridorRadius":24,"BranchProbability":1,"Verticality":0.20000000298023224,"SurfaceRoughness":2,"BoundarySealThickness":4,"BaseDensity":8},"SurfaceParams":{"BaseGroundRelative":0.25,"ElevationRange":80,"ContinentFrequency":0.0060000000521540642,"MountainStrength":0.5,"MountainFrequency":0.012000000104308128,"DetailFrequency":0.039999999105930328,"SurfaceRoughness":2,"HeightWarpStrength":48,"HeightWarpFrequency":0.0080000003799796104,"ReliefFrequency":0.001500000013038516,"ReliefStrength":0.69999998807907104,"ReliefContrast":1.6000000238418579,"TerraceStrength":0,"TerraceHeight":8,"TerraceHardness":0.5,"LayerLineDepth":0,"LayerLineSpacing":8,"CliffStrength":0,"CliffSlopeThreshold":0.5,"CliffSharpness":2,"CliffSampleDist":2,"OverhangStrength":0,"OverhangReach":16,"OverhangHeight":24,"OverhangFrequency":0.019999999552965164,"OverhangZScale":0.5,"OverhangSlopeThreshold":0.30000001192092896,"WaterLevelRelative":0.27000001072883606,"BeachWidth":12,"CeilingRelative":0.94999998807907104,"CeilingRoughness":6,"CeilingRoughnessFrequency":0.039999999105930328,"CeiLine truncated {"schema_version":1,"source":"VoxelForgeExplore standalone base-world export","voxel_size_cm":25,"seed":0,"open_surface_entry":true,"origin_spine_radius":28,"world_radius_voxels":0,"edge_seal_thickness":128,"total_strates":2,"inter_strate_gap_chunks":0,"clip_radius":6,"max_clip_level":8,"full_res_clip_levels":2,"coarse_tile_cells":16,"render_distance_chunks":1536,"generate_skirts":true,"skirt_cells":4,"lod_octave_drop":1,"strate_content_cut_min_level":9,"settings_asset":{"ViewDistanceXY":32,"ViewDistanceUp":10,"ViewDistanceDown":10,"MaxConcurrentTasks":16,"MaxGenerationWorkerCores":0,"NearTaskReserve":4,"MaxMeshAppliesPerFrame":4,"MaxMeshApplyMilliseconds":2,"MaxUnloadsPerFrame":16,"PawnCollisionRadiusChunks":2,"bClampViewToStrate":true,"StrateViewMarginChunks":6,"StrateContentCutMinLevel":9,"ClipRadius":6,"MaxClipLevel":8,"FullResClipLevels":2,"CoarseTileCells":16,"RenderDistanceChunks":1536,"bGenerateSkirts":true,"SkirtCells":4,"LODOctaveDrop":1,"WorldRadiusVoxels":0,"EdgeSealThickness":128,"DecorationRadiusChunks":12,"DecorationNearRadiusChunks":6,"DecorationRegionSizeCells":8,"DecorationSpacingVoxels":8,"DecorationFarSpacingVoxels":8,"DecorationMarchStepVoxels":4,"DecorationMaxCrossingsPerColumn":4,"DecorationColumnDepthVoxels":320,"MaxDecorationCellsPerFrame":2,"MaxConcurrentDecorationTasks":4,"bEnableDensityVolume":false,"DensityVolumeResolution":256,"DensityVolumeLevels":3,"DensityVolumeFillSlabCells":8,"DensityVolumeMarchSteps":64,"bDensityVolumeGPUUpload":false,"bDebugDrawDensityVolume":false,"DensityVolumeDebugRadiusCells":12,"VoxelMaterial":"/Game/VoxelForge/M_Test1_Inst.M_Test1_Inst","Seed":0,"Season":"","CurrentSeason":1,"StratePool":["/Game/VoxelForge/DA_Strate1.DA_Strate1","/Game/VoxelForge/DA_Strate3.DA_Strate3"],"FixedStrates":[],"TotalStrates":2,"InterStrateGapChunks":0,"OriginSpineRadius":28,"bOpenSurfaceEntry":true,"MaxModifications":0,"MaxBrushRadius":30,"MaxTotalVolume":0,"AssetBundleData":{"Bundles":[]},"NativeClass":"/Script/VoxelForge.VoxelSettings"},"strates":[{"index":0,"top_chunk_z":0,"bottom_chunk_z":-15,"height_chunks":16,"asset":{"StrateName":"","StrateDescription":"","StrateHeightInChunks":16,"TransitionType":0,"TransitionBlendChunks":2,"GeneratorType":0,"bEnableWorms":false,"GenerationParams":{"BaseDensity":8,"VerticalScale":0.80000001192092896,"WormFrequency":0.014999999664723873,"WormHorizontalBias":3,"WormThreshold":0.059999998658895493,"WormStrength":10,"WormNetworkRange":64,"RoomSpacing":128,"RoomDensity":0.34999999403953552,"MinRoomRadius":65,"MaxRoomRadius":120,"RoomHeightRatio":0.5,"RoomFloorCutMin":0,"RoomFloorCutMax":0,"FloorReliefStrength":0,"FloorReliefFrequency":1,"RoomMouthRiseStrength":0,"RoomMouthRiseBlendVoxels":12,"RoomShapeVariety":0.5,"OriginRoomRadius":65,"OriginRoomMaxConnections":4,"TunnelMinRadius":8,"TunnelMaxRadius":12,"TunnelDensity":0.40000000596046448,"MaxTunnelLength":360,"TunnelWarpStrength":24,"TunnelHorizontalBias":0.5,"bTunnelsFlowTowardOrigin":true,"TunnelEndpointZOffset":0.25,"bTunnelFloorEnabled":true,"bTunnelFloorTerracingEnabled":true,"TunnelFloorTerraceStepHeight":5,"TunnelFloorMaxLedgeHeight":24,"TunnelFloorGentleSlopeThreshold":88.090850830078125,"TunnelFloorLedgeCountPreference":0,"TunnelFloorMaxLedges":4096,"SDFBlendRadius":25,"CaveWarpStrength":16,"CaveWarpFrequency":0.014999999664723873,"SurfaceRoughness":1,"RoughnessFrequency":0.05000000074505806,"RoughnessNoiseType":2,"DomainWarpStrength":0,"DomainWarpFrequency":0.029999999329447746,"FloorBias":4,"BoundarySealThickness":8,"WaterLevelRelative":0},"SlabParams":{"FloorRelativeHeight":0.25,"CeilingRelativeHeight":0.60000002384185791,"FloorRoughness":4,"FloorRoughnessFrequency":0.039999999105930328,"CeilingRoughness":6,"CeilingRoughnessFrequency":0.039999999105930328,"ColumnDensity":0.079999998211860657,"ColumnMinRadius":8,"ColumnMaxRadius":16,"ColumnSpacing":96,"BoundarySealThickness":4,"BaseDensity":8},"MazeParams":{"CellSize":160,"CorridorRadius":24,"BranchProbability":1,"Verticality":0.20000000298023224,"SurfaceRoughness":2,"BoundarySealThickness":4,"BaseDensity":8},"SurfaceParams":{"BaseGroundRelative":0.25,"ElevationRange":80,"ContinentFrequency":0.0060000000521540642,"MountainStrength":0.5,"MountainFrequency":0.012000000104308128,"DetailFrequency":0.039999999105930328,"SurfaceRoughness":2,"HeightWarpStrength":48,"HeightWarpFrequency":0.0080000003799796104,"ReliefFrequency":0.001500000013038516,"ReliefStrength":0.69999998807907104,"ReliefContrast":1.6000000238418579,"TerraceStrength":0,"TerraceHeight":8,"TerraceHardness":0.5,"LayerLineDepth":0,"LayerLineSpacing":8,"CliffStrength":0,"CliffSlopeThreshold":0.5,"CliffSharpness":2,"CliffSampleDist":2,"OverhangStrength":0,"OverhangReach":16,"OverhangHeight":24,"OverhangFrequency":0.019999999552965164,"OverhangZScale":0.5,"OverhangSlopeThreshold":0.30000001192092896,"WaterLevelRelative":0.27000001072883606,"BeachWidth":12,"CeilingRelative":0.94999998807907104,"CeilingRoughness":6,"CeilingRoughnessFrequency":0.039999999105930328,"CeiLine truncated