diff --git a/.gitignore b/.gitignore index a9bcfb1..84f4f4b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ /Standalone/data/*.log /Standalone/data/*.err /Standalone/data/*.vfmesh +/Standalone/Research/**/*.log diff --git a/Standalone/Build.ps1 b/Standalone/Build.ps1 index 128232b..025e7da 100644 --- a/Standalone/Build.ps1 +++ b/Standalone/Build.ps1 @@ -58,9 +58,14 @@ $RspLines += @('/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gd Push-Location $BuildDir try { - $CommandLine = 'call "{0}" >nul && start /belownormal /wait cl.exe @"{1}"' -f $VcVars,$ResponsePath + # This PowerShell process already runs BelowNormal with affinity 0xFF00; + # compile directly so cmd returns cl.exe's real exit code. + $CommandLine = 'call "{0}" >nul && cl.exe @"{1}"' -f $VcVars,$ResponsePath & $env:ComSpec /d /s /c $CommandLine if ($LASTEXITCODE -ne 0) { throw "Standalone MSVC build failed with exit code $LASTEXITCODE" } + if (-not (Test-Path -LiteralPath $OutputPath -PathType Leaf)) { + throw "Standalone MSVC build reported success but did not produce: $OutputPath" + } } finally { Pop-Location } diff --git a/Standalone/Research/OwnerShapeWorld/REPORT.md b/Standalone/Research/OwnerShapeWorld/REPORT.md new file mode 100644 index 0000000..d6ab93f --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/REPORT.md @@ -0,0 +1,146 @@ +# Owner shape world: standalone results + +The standalone path now builds the owner world from its room, tunnel, passage, +landing, floor, ceiling, and seal shapes. It does not read or write the mesh +cache. The active voxel lattice remains 25 cm; rock detail is evaluated only in +the surface band. `V` switches between the current owner density and the shape +world in the viewer. + +## Method + +- `Source/StandaloneShapeWorld.h` compiles the known shapes into column + intervals, combines their signs in packed words, and locates surface cells + with bit operations. Round and winding tunnels use line-solved/approximated + shape primitives; protected passage air is applied after solid overlays. +- The coherent rock detail has a proven maximum amplitude of 3.20 voxels and a + 4-voxel evaluation band. Detail outside that band cannot change the base + sign. The far field is compiled from parent shapes and meshed coarsely. +- Existing marching-cubes meshing runs on worker threads. Benchmark runs use + `--force-generate`; shape-world mode also bypasses cache loading. The reports + below show zero cache hits. +- Builds are serial (no `/MP` flag). The build host sets BelowNormal priority + and affinity `0xFF00`; launched viewers use BelowNormal and `0xFF00` too. + +## Cold generation timings + +Each timing is shape compilation plus uncached mesh generation. The full-detail +area is the 64 m cube with 960 LOD0–1 tiles. The full clipmap contains 6,498 +tiles through LOD6. Hashing is off for these timing runs. + +| Cold job | Shape stage | Mesh generation wall | Total | Result | +| --- | ---: | ---: | ---: | --- | +| Full detail, 8 workers | 668.566 ms | 290 ms | **0.959 s** | 960/960 tiles; cache hits 0 | +| Full clipmap, 8 workers | 870.418 ms, including far parents | 1.398 s | **2.268 s** | 6,498/6,498 tiles; cache hits 0 | + +Full-detail stage breakdown: local shapes 53.892 ms, intervals/signs 196.326 +ms, surface band 417.952 ms. Full-clipmap breakdown: local shapes 53.281 ms, +intervals/signs 195.162 ms, surface band 424.921 ms, far parents 196.578 ms. +The measured full-detail total is close to the 1 s limit; the full clipmap has +more margin under 5 s. + +Logs: `final/full_detail_cold_w8_final.stdout.log` and +`final/full_clipmap_cold_w8_final.stdout.log`. + +## Similarity and captures + +The viewer captures use explicit positions and yaws. Owner and shape images are +paired at the same camera settings. The far owner image is the existing cold +baseline capture at the default start view; its matching shape capture was +made at the same position and yaw. + +| View | Owner capture | Shape capture | +| --- | --- | --- | +| Chamber, yaw 0° | `final/chamber_fixed_owner.png` | `final/chamber_fixed_shape.png` | +| Tunnel, yaw 90° | `final/tunnel_fixed_owner.png` | `final/tunnel_fixed_shape.png` | +| Passage, yaw 63° | `final/passage_fixed_owner.png` | `final/passage_fixed_shape.png` | +| Far view, yaw 0° | `final/far_owner.png` | `final/far_fixed_shape.png` | + +Comparison results: + +| View/LOD | Tiles | Lattice samples | Changed signs | Share changed | Mean / max surface distance* | Unmatched surface samples | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| Chamber, LOD0–1 | 960 | 35,103,600 | 4,165,142 | 11.8653% | 1.1763 m / 6.5210 m | 69,626 | +| Tunnel, LOD0–1 | 960 | 35,103,600 | 4,165,142 | 11.8653% | 1.1763 m / 6.5210 m | 69,626 | +| Passage, LOD0–1 | 960 | 37,240,000 | 2,352,343 | 6.3167% | 1.0102 m / 6.7059 m | 35,520 | +| Far view, outer LOD6 | 4,770 | 9,470,835 | 856,305 | 9.0415% | 3.5551 m / 5.7456 m | 475,722 | + +\* Surface distance samples every fourth mesh vertex. The mean and maximum +include matched samples only; unmatched samples are listed separately. The +distance check searches neighboring 1 m buckets, so treat it as a sampled +comparison rather than a continuous Hausdorff distance. Chamber and tunnel +share the same camera position and tile set; only yaw differs. + +The captures show that the shape world retains broad passage structure, but +they also show a visible change in chamber profiles and rock relief. In +particular, this does not reproduce the painterly cave reference image. The +sign shares and sampled distances quantify that gap; the current result should +be treated as a fast, coherent alternate world rather than a visual match. + +## Detail, walkability, and determinism + +The final coherent detail audit reports `supremum=3.20 vox`, `band=4`, 476,494 +surface sign flips, adjacent correlation 0.9899, and both detail checks PASS. +Two deliberately bad variants were also run: + +| Variant | Surface-detail check | Spatial-scale check | Process result | +| --- | --- | --- | --- | +| Coherent detail | PASS | PASS | exit 0 | +| Smoothed detail | **FAIL** | PASS | exit 7, expected | +| Per-voxel noise | PASS | **FAIL** | exit 7, expected | + +The route audit samples packed lattice signs along every exported passage +centerline: **5,642/5,642 samples remain open**, with native landing floors +retained. This is a centerline/sign check, not a character-capsule collision +simulation. + +Three cold hash runs at the passage camera agree across two 6-worker runs and +one 1-worker run: + +| Hash | 6 workers, run A | 6 workers, run B | 1 worker | +| --- | --- | --- | --- | +| Full detail | `0x88e5818a444c1ab3` | `0x88e5818a444c1ab3` | `0x88e5818a444c1ab3` | +| LOD0 | `0x3d37fc984f0ee55b` | `0x3d37fc984f0ee55b` | `0x3d37fc984f0ee55b` | +| LOD1 | `0xc314a1f6457f8a31` | `0xc314a1f6457f8a31` | `0xc314a1f6457f8a31` | + +This confirms repeatability and worker-count independence on this machine; a +second hardware/architecture was not available for a cross-machine hash run. + +## Viewer and evidence files + +Press `V` in the standalone viewer to swap owner density and shape intervals; +the viewer rebuilds the resident field. The control is handled in +`Source/StandaloneViewer.cpp` near the keyboard input handler. + +The logs under `final/` include the cold timings, audits, comparisons, captures, +and determinism runs. In particular: + +- `audit_coherent_final.stdout.log`, `audit_smooth_bad_final.stdout.log`, + `audit_per_voxel_bad_final.stdout.log` +- `chamber_compare_final.stdout.log`, `tunnel_compare_final.stdout.log`, + `passage_compare_landingbox.stdout.log`, `far_compare_lod6.stdout.log` +- `determinism_final_w6_a.stdout.log`, `determinism_final_w6_b.stdout.log`, + `determinism_final_w1.stdout.log` + +The LOD6 similarity command took 101.3 s because it also sampled nearest-surface +distances over the entire outer tile set. That is comparison-analysis time, not +world-generation time. + +## Failed and abandoned attempts + +- The original build script could print success after failing to leave an + executable. `Build.ps1` now invokes `cl.exe` directly, propagates its exit + code, and checks the output file. A launcher attempt before that correction + failed with Windows `CreateProcess error 2` because the executable was + absent. +- The first checked compile then found an `FIntVector` to `FVector` conversion + error in the optional tile-sort path. The type was corrected; the final + serial build succeeded. Visual Studio setup still emits a `vswhere.exe`-not- + recognized diagnostic, but `cl.exe` runs and produces the binary. +- An earlier smooth-detail audit passed before its gate was tightened. The + final smooth variant now exits 7 on the surface-detail check, as shown above. +- A fresh owner far-view capture was stopped after 55 s because the existing + uncached owner clipmap run takes about 183 s. The existing owner capture was + reused; the shape far capture completed in 2.65 s. No viewer started by + someone else was closed. + +No commit, push, or stash was made. diff --git a/Standalone/Research/OwnerShapeWorld/chamber_shape.png b/Standalone/Research/OwnerShapeWorld/chamber_shape.png new file mode 100644 index 0000000..55fc7d8 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/chamber_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/chamber_shape_final.png b/Standalone/Research/OwnerShapeWorld/chamber_shape_final.png new file mode 100644 index 0000000..ffe6af9 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/chamber_shape_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/chamber_shape_floorclip.png b/Standalone/Research/OwnerShapeWorld/chamber_shape_floorclip.png new file mode 100644 index 0000000..6f78612 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/chamber_shape_floorclip.png differ diff --git a/Standalone/Research/OwnerShapeWorld/chamber_shape_sdf.png b/Standalone/Research/OwnerShapeWorld/chamber_shape_sdf.png new file mode 100644 index 0000000..30b817a Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/chamber_shape_sdf.png differ diff --git a/Standalone/Research/OwnerShapeWorld/farclip_shape.png b/Standalone/Research/OwnerShapeWorld/farclip_shape.png new file mode 100644 index 0000000..ade461a Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/farclip_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/farclip_shape_final.png b/Standalone/Research/OwnerShapeWorld/farclip_shape_final.png new file mode 100644 index 0000000..abbba2c Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/farclip_shape_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/chamber_fixed_owner.png b/Standalone/Research/OwnerShapeWorld/final/chamber_fixed_owner.png new file mode 100644 index 0000000..91adde8 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/chamber_fixed_owner.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/chamber_fixed_shape.png b/Standalone/Research/OwnerShapeWorld/final/chamber_fixed_shape.png new file mode 100644 index 0000000..30b817a Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/chamber_fixed_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/chamber_owner.png b/Standalone/Research/OwnerShapeWorld/final/chamber_owner.png new file mode 100644 index 0000000..91adde8 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/chamber_owner.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/chamber_shape.png b/Standalone/Research/OwnerShapeWorld/final/chamber_shape.png new file mode 100644 index 0000000..30b817a Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/chamber_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/far_fixed_shape.png b/Standalone/Research/OwnerShapeWorld/final/far_fixed_shape.png new file mode 100644 index 0000000..e851aef Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/far_fixed_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/far_owner.png b/Standalone/Research/OwnerShapeWorld/final/far_owner.png new file mode 100644 index 0000000..4cc9c82 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/far_owner.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/far_shape.png b/Standalone/Research/OwnerShapeWorld/final/far_shape.png new file mode 100644 index 0000000..e851aef Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/far_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/passage_fixed_owner.png b/Standalone/Research/OwnerShapeWorld/final/passage_fixed_owner.png new file mode 100644 index 0000000..b1db5a2 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/passage_fixed_owner.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/passage_fixed_shape.png b/Standalone/Research/OwnerShapeWorld/final/passage_fixed_shape.png new file mode 100644 index 0000000..45335a9 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/passage_fixed_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/passage_owner.png b/Standalone/Research/OwnerShapeWorld/final/passage_owner.png new file mode 100644 index 0000000..b1db5a2 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/passage_owner.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/passage_shape.png b/Standalone/Research/OwnerShapeWorld/final/passage_shape.png new file mode 100644 index 0000000..45335a9 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/passage_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/tunnel_fixed_owner.png b/Standalone/Research/OwnerShapeWorld/final/tunnel_fixed_owner.png new file mode 100644 index 0000000..4cc9c82 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/tunnel_fixed_owner.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/tunnel_fixed_shape.png b/Standalone/Research/OwnerShapeWorld/final/tunnel_fixed_shape.png new file mode 100644 index 0000000..f473df5 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/tunnel_fixed_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/tunnel_owner.png b/Standalone/Research/OwnerShapeWorld/final/tunnel_owner.png new file mode 100644 index 0000000..4cc9c82 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/tunnel_owner.png differ diff --git a/Standalone/Research/OwnerShapeWorld/final/tunnel_shape.png b/Standalone/Research/OwnerShapeWorld/final/tunnel_shape.png new file mode 100644 index 0000000..f473df5 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/final/tunnel_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/passage_shape.png b/Standalone/Research/OwnerShapeWorld/passage_shape.png new file mode 100644 index 0000000..36303c8 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/passage_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/passage_shape_final.png b/Standalone/Research/OwnerShapeWorld/passage_shape_final.png new file mode 100644 index 0000000..7602358 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/passage_shape_final.png differ diff --git a/Standalone/Research/OwnerShapeWorld/passage_shape_floorclip.png b/Standalone/Research/OwnerShapeWorld/passage_shape_floorclip.png new file mode 100644 index 0000000..36303c8 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/passage_shape_floorclip.png differ diff --git a/Standalone/Research/OwnerShapeWorld/passage_shape_sdf.png b/Standalone/Research/OwnerShapeWorld/passage_shape_sdf.png new file mode 100644 index 0000000..2a77717 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/passage_shape_sdf.png differ diff --git a/Standalone/Research/OwnerShapeWorld/tunnel_shape.png b/Standalone/Research/OwnerShapeWorld/tunnel_shape.png new file mode 100644 index 0000000..ade461a Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/tunnel_shape.png differ diff --git a/Standalone/Research/OwnerShapeWorld/tunnel_shape_final.png b/Standalone/Research/OwnerShapeWorld/tunnel_shape_final.png new file mode 100644 index 0000000..8299aee Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/tunnel_shape_final.png differ diff --git a/Standalone/Source/StandaloneShapeWorld.h b/Standalone/Source/StandaloneShapeWorld.h new file mode 100644 index 0000000..2e96599 --- /dev/null +++ b/Standalone/Source/StandaloneShapeWorld.h @@ -0,0 +1,1207 @@ +#pragma once + +#include "OwnerWorldLoader.h" +#include "StandaloneResearchTunnels.h" +#include "VoxelCaveMorphology.h" +#include "VoxelStrateManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Standalone-only world compiler. It compiles the runtime room/tunnel/passages into +// axis-line intervals, composes rock/cave signs into packed Z-column words, then displaces +// only the conservative surface band. The copied Unreal generator remains the A/B reference. +class FStandaloneShapeWorld +{ +public: + enum class EDetailVariant : uint8 { Coherent, Smooth, PerVoxel }; + + struct FBuildStats + { + double ShapeSeconds = 0.0; + double IntervalSeconds = 0.0; + double SurfaceSeconds = 0.0; + double FarShapeSeconds = 0.0; + uint64 PrimitiveCount = 0; + uint64 RoomCount = 0; + uint64 TunnelCount = 0; + uint64 PassageCount = 0; + uint64 SignNodes = 0; + uint64 SurfaceCandidates = 0; + uint64 SurfaceProxyNodes = 0; + uint64 DetailFlips = 0; + uint64 WalkSamples = 0; + uint64 WalkFailures = 0; + }; + + struct FPrimitive + { + int32 CenterSub[3]{}; + int64 Q[3][3]{}; + int32 Min[3]{}; + int32 Max[3]{}; + float MinRadius = 1.0f; + bool bCarve = true; + bool bCritical = false; + bool bRoundedBox = false; + bool bHasFloorClip = false; + double RoundedHalfExtent[3]{}; + double Rounding = 0.0; + double FloorCenterZ = 0.0; + double FloorSlopeX = 0.0; + double FloorSlopeY = 0.0; + }; + + struct FInterval { int32 Lo = 0, Hi = -1; }; + struct FBinKey { int32 X=0,Y=0,Z=0; bool operator==(const FBinKey&)const=default; }; + struct FBinHash + { + size_t operator()(const FBinKey& K) const noexcept + { + uint64 H=static_cast(K.X)*0x9E3779B185EBCA87ull; + H^=static_cast(static_cast(K.Y))*0xC2B2AE3D27D4EB4Full; + H^=static_cast(static_cast(K.Z))*0x165667B19E3779F9ull; + H^=H>>29;H*=0xBF58476D1CE4E5B9ull;H^=H>>31; + return static_cast(H); + } + }; + + static constexpr int32 QScale = 1 << 28; + static constexpr int32 SubPerVoxel = 16; + static constexpr int32 DetailBand = 4; + static constexpr float DetailSupremum = 0.70f + 1.05f + 1.45f; + static constexpr int32 LineBinSize = 32; + static constexpr int32 SpatialBinSize = 256; + + bool Build(FStandaloneOwnerWorld& World, const FIntVector& MinVoxels, + const FIntVector& MaxVoxels, const FIntVector& ClipMinVoxels, + const FIntVector& ClipMaxVoxels, int32 Workers, EDetailVariant Variant, + bool bIncludeFarWorld, + std::string& Error) + { + using Clock=std::chrono::steady_clock; + const auto TotalStart=Clock::now(); + Owner=&World;WorkerCount=std::clamp(Workers,1,8);DetailVariant=Variant; + for(int32 A=0;A<3;++A) + { + Min[A]=(&MinVoxels.X)[A]-2; + Max[A]=(&MaxVoxels.X)[A]+2; + ClipMin[A]=(&ClipMinVoxels.X)[A]; + ClipMax[A]=(&ClipMaxVoxels.X)[A]; + Size[A]=Max[A]-Min[A]+1; + if(Size[A]<=0||Size[A]>4096||ClipMax[A](Size[0])*Size[1]*Size[2]; + if(NodeCount>160000000ull) + {Error="shape interval region exceeds the 160-million-node safety bound";return false;} + Stats={};Stats.SignNodes=NodeCount; + + auto Stage=Clock::now(); + BuildLayerBounds(); + BuildOwnerPrimitives(); + if(FStandaloneResearchTunnels::Enabled.load(std::memory_order_acquire)) + BuildRoundTunnelPrimitives(); + BuildSpatialBins(LineBinSize); + Stats.ShapeSeconds=std::chrono::duration(Clock::now()-Stage).count(); + Stats.PrimitiveCount=Primitives.size(); + if(Primitives.empty()) + {Error="owner shape extraction produced no rooms, tunnels, or passages";return false;} + + Stage=Clock::now(); + const size_t NZ=static_cast(Size[2]); + WordsPerZ=(NZ+63u)/64u; + RockWords.assign(static_cast(Size[0])*Size[1]*WordsPerZ,0); + SurfaceDistanceQ=std::make_unique_for_overwrite(static_cast(NodeCount)); + BuildPackedRockIntervals(); + BuildCandidateBands(); + Stats.IntervalSeconds=std::chrono::duration(Clock::now()-Stage).count(); + + Stage=Clock::now(); + ApplyDetailBand(); + Stats.SurfaceSeconds=std::chrono::duration(Clock::now()-Stage).count(); + ValidatePassageRoutes(); + Built=true; + if(bIncludeFarWorld)BuildFarWorld(); + const uint64 WordBytes=static_cast(RockWords.size()+CandidateWords.size())*sizeof(uint64); + std::printf("[ShapeCompile] primitives=%llu rooms=%llu tunnels=%llu passages=%llu nodes=%llu packed=%.2f MiB surface_proxy_nodes=%llu proxy_address_capacity=%.2f MiB workers=%d\n", + static_cast(Stats.PrimitiveCount), + static_cast(Stats.RoomCount), + static_cast(Stats.TunnelCount), + static_cast(Stats.PassageCount), + static_cast(Stats.SignNodes), + static_cast(WordBytes)/1048576.0, + static_cast(Stats.SurfaceProxyNodes), + static_cast(Stats.SignNodes*sizeof(int8))/1048576.0,WorkerCount); + std::printf("[ShapeStage] local-shapes=%.3f ms intervals+signs=%.3f ms surface-band=%.3f ms far-parents=%.3f ms total=%.3f ms candidates=%llu flips=%llu walk=%llu/%llu\n", + Stats.ShapeSeconds*1000.0,Stats.IntervalSeconds*1000.0,Stats.SurfaceSeconds*1000.0,Stats.FarShapeSeconds*1000.0, + std::chrono::duration(Clock::now()-TotalStart).count()*1000.0, + static_cast(Stats.SurfaceCandidates), + static_cast(Stats.DetailFlips), + static_cast(Stats.WalkSamples-Stats.WalkFailures), + static_cast(Stats.WalkSamples)); + std::fflush(stdout); + return true; + } + + const FBuildStats& GetStats() const { return Stats; } + bool IsBuilt() const { return Built; } + bool HasFarWorld() const { return FarBuilt; } + 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); } + + void BuildFarWorld() + { + if(FarBuilt)return; + const auto Start=std::chrono::steady_clock::now(); + BuildFarParentShapes(); + BuildSpatialBins(SpatialBinSize); + Stats.FarShapeSeconds=std::chrono::duration(std::chrono::steady_clock::now()-Start).count(); + Stats.PrimitiveCount=Primitives.size();FarBuilt=true; + std::printf("[FarShapeStage] %.3f ms primitives=%llu\n",Stats.FarShapeSeconds*1000.0, + static_cast(Stats.PrimitiveCount)); + std::fflush(stdout); + } + + // Lattice signs come from packed intervals; a bounded analytic distance proxy supplies + // edge interpolation and normals to the existing mesher. + float DensityAt(float X,float Y,float Z) const + { + if(LayerRanges.empty()||ZLayerRanges[0].Hi)return 1.0f; + int32 IX=0,IY=0,IZ=0; + const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ); + const bool bPacked=bInteger&&Contains(IX,IY,IZ); + const bool bRock=bPacked?IsRock(IX,IY,IZ):EvaluateFast(X,Y,Z); + if(bPacked) + { + const size_t Column=static_cast(IY-Min[1])*Size[0]+static_cast(IX-Min[0]); + const size_t Zi=static_cast(IZ-Min[2]); + const size_t Word=Column*WordsPerZ+(Zi>>6); + const uint64 Mask=uint64{1}<<(Zi&63u); + if(CandidateWords[Word]&Mask) + { + const size_t Node=Zi*static_cast(Size[1])*Size[0]+Column; + const int8 Proxy=SurfaceDistanceQ[Node]; + float Density=static_cast(Proxy)*(1.0f/16.0f); + if((Density<0.0f)!=bRock) + Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),1.0f/16.0f); + return Density; + } + return bRock?-(DetailBand+2.0f):(DetailBand+2.0f); + } + const bool bSolid=IsInsideSolidOverlay(X,Y,Z); + const bool bCritical=IsInsideCriticalAir(X,Y,Z); + float Cave=CaveDistance(X,Y,Z); + if(Cave>=100000.0f)Cave=100000.0f; + if(bCritical)Cave=-std::max(1.0f,std::abs(Cave)); + else if(bSolid)Cave=std::max(1.0f,std::abs(Cave)); + else + { + const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1] + &&Z>=Min[2]&&Z<=Max[2]; + if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth + &&std::abs(Cave)<=DetailSupremum+0.125f) + Cave+=DetailNoise(X,Y,Z); + } + float Density=-Cave; + if((Density<0.0f)!=bRock||std::abs(Density)<0.05f) + Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f); + return Density; + } + + EVoxelTileClass ClassifyBox(const FIntVector& Origin,int32 Step,int32 Cells) const + { + const int32 Pad=std::max(Step,1); + int32 BMin[3]={Origin.X-Pad,Origin.Y-Pad,Origin.Z-Pad}; + int32 BMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,Origin.Z+(Cells+1)*Step}; + for(int32 BZ=FloorDiv(BMin[2],ActiveSpatialBinSize);BZ<=FloorDiv(BMax[2],ActiveSpatialBinSize);++BZ) + for(int32 BY=FloorDiv(BMin[1],ActiveSpatialBinSize);BY<=FloorDiv(BMax[1],ActiveSpatialBinSize);++BY) + for(int32 BX=FloorDiv(BMin[0],ActiveSpatialBinSize);BX<=FloorDiv(BMax[0],ActiveSpatialBinSize);++BX) + { + const auto It=SpatialBins.find({BX,BY,BZ});if(It==SpatialBins.end())continue; + for(uint32 Id:It->second) + { + const FPrimitive& P=Primitives[Id];bool Overlap=true; + for(int32 A=0;A<3;++A) + Overlap &= P.Max[A]+DetailBand>=BMin[A]&&P.Min[A]-DetailBand<=BMax[A]; + if(Overlap)return EVoxelTileClass::Mixed; + } + } + for(const FInterval& R:LayerRanges) + if(R.Lo<=BMax[2]&&R.Hi>=BMin[2] + &&(R.Lo>=BMin[2]||R.Hi<=BMax[2])) + return EVoxelTileClass::Mixed; + const bool bRock=EvaluateFast((BMin[0]+BMax[0])*0.5, + (BMin[1]+BMax[1])*0.5,(BMin[2]+BMax[2])*0.5); + return bRock?EVoxelTileClass::AllSolid:EVoxelTileClass::AllAir; + } + + bool IsRock(int32 X,int32 Y,int32 Z) const + { + if(!Contains(X,Y,Z))return EvaluateFast(X,Y,Z); + const size_t Line=static_cast(Y-Min[1])*Size[0] + +static_cast(X-Min[0]); + const size_t Zi=static_cast(Z-Min[2]); + return ((RockWords[Line*WordsPerZ+(Zi>>6)]>>(Zi&63u))&1u)!=0; + } + + bool IsDetailAuditPassing() const + { + const double Correlation=DetailAutocorrelation(DetailVariant); + const bool bFlips=Stats.DetailFlips>=1000; + const bool bCoherent=Correlation>0.45; + std::printf("[DetailAudit] mode=%s supremum=%.2f vox band=%d changed_signs=%llu adjacent_correlation=%.4f surface_detail_check=%s spatial_scale_check=%s\n", + DetailVariant==EDetailVariant::Coherent?"coherent":DetailVariant==EDetailVariant::Smooth?"smooth":"per-voxel", + DetailSupremum,DetailBand,static_cast(Stats.DetailFlips),Correlation, + bFlips?"PASS":"FAIL",bCoherent?"PASS":"FAIL"); + return bFlips&&bCoherent; + } + +private: + struct FAxisBins { int32 MinA=0,MinB=0,CountA=0,CountB=0; std::vector> Items; }; + + static bool NearInteger(float V,int32& Out) + { + const float R=std::round(V); + if(std::abs(V-R)>0.0001f||R(MIN_int32)||R>static_cast(MAX_int32))return false; + Out=static_cast(R);return true; + } + + bool Contains(int32 X,int32 Y,int32 Z) const + {return X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2];} + + static int32 FloorDiv(int32 A,int32 B) + {int32 Q=A/B,R=A%B;return Q-((R!=0&&R<0)?1:0);} + + static int64 RoundDiv(int64 N,int64 D) + {return N>=0?(N+D/2)/D:-((-N+D/2)/D);} + + void BuildLayerBounds() + { + LayerRanges.clear(); + const auto& Layout=Owner->Manager->GetLayout(); + int32 Top=MIN_int32,Bottom=MAX_int32; + for(const FStrateSlot& Slot:Layout) + {Top=std::max(Top,Slot.TopChunkZ);Bottom=std::min(Bottom,Slot.BottomChunkZ);} + if(Top>=Bottom) + { + LayerRanges.push_back({Bottom*CHUNK_SIZE,(Top+1)*CHUNK_SIZE}); + } + } + + static std::array V3(const FVector& V){return {V.X,V.Y,V.Z};} + static double Dot(const std::array& A,const std::array& B) + {return A[0]*B[0]+A[1]*B[1]+A[2]*B[2];} + static std::array Sub(const FVector& A,const FVector& B) + {return {A.X-B.X,A.Y-B.Y,A.Z-B.Z};} + static std::array Normalize(std::array V) + { + const double L=std::sqrt(std::max(1.0e-24,Dot(V,V))); + for(double& C:V)C/=L;return V; + } + static std::array Cross(const std::array& A,const std::array& B) + {return {A[1]*B[2]-A[2]*B[1],A[2]*B[0]-A[0]*B[2],A[0]*B[1]-A[1]*B[0]};} + + void AddEllipsoid(const FVector& Center,const std::array& Radii, + const std::array& Axis0={1,0,0}, + const std::array& Axis1={0,1,0}, + const std::array& Axis2={0,0,1},bool bCarve=true) + { + FPrimitive P;const auto C=V3(Center); + for(int32 A=0;A<3;++A)P.CenterSub[A]=static_cast(std::llround(C[A]*SubPerVoxel)); + const std::array,3> Axes={Axis0,Axis1,Axis2}; + int32 RSub[3]; + for(int32 K=0;K<3;++K)RSub[K]=std::max(16,static_cast(std::llround(std::max(1.0,Radii[K])*SubPerVoxel))); + for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J) + { + int64 Sum=0; + for(int32 K=0;K<3;++K) + { + const int64 RI=static_cast(std::llround(Axes[K][I]*32768.0)); + const int64 RJ=static_cast(std::llround(Axes[K][J]*32768.0)); + const int64 Den=static_cast(32768)*32768*RSub[K]*RSub[K]; + Sum+=RoundDiv(RI*RJ*QScale,Den); + } + P.Q[I][J]=Sum; + } + for(int32 A=0;A<3;++A) + { + double E=0.0;for(int32 K=0;K<3;++K)E+=std::abs(Axes[K][A])*Radii[K]; + P.Min[A]=static_cast(std::floor(C[A]-E))-1; + P.Max[A]=static_cast(std::ceil(C[A]+E))+1; + } + P.MinRadius=static_cast(std::min({Radii[0],Radii[1],Radii[2]})); + P.bCarve=bCarve;Primitives.push_back(P); + } + + void AddRoundedBox(const FVector& Center,const std::array& HalfExtent, + double Rounding,bool bCarve=true) + { + const std::array Outer={HalfExtent[0]+Rounding,HalfExtent[1]+Rounding,HalfExtent[2]+Rounding}; + AddEllipsoid(Center,Outer,{1,0,0},{0,1,0},{0,0,1},bCarve); + FPrimitive& P=Primitives.back();P.bRoundedBox=true;P.Rounding=Rounding; + for(int32 A=0;A<3;++A)P.RoundedHalfExtent[A]=HalfExtent[A]; + } + + static void ApplyFloorClip(FPrimitive& P,double FloorCenterZ,double SlopeX=0.0,double SlopeY=0.0) + { + P.bHasFloorClip=true;P.FloorCenterZ=FloorCenterZ-0.5; + P.FloorSlopeX=SlopeX;P.FloorSlopeY=SlopeY; + } + + void AddCapsule(const FVector& A,const FVector& B,float Radius,bool bCarve=true,bool bCritical=false, + bool bFloorClip=false,double FloorA=0.0,double FloorB=0.0) + { + const auto D=Sub(B,A);const double Length=std::sqrt(Dot(D,D)); + if(!(Length>1.0e-4)) + { + const size_t Before=Primitives.size();AddEllipsoid(A,{Radius,Radius,Radius},{1,0,0},{0,1,0},{0,0,1},bCarve); + if(bFloorClip)ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB)); + if(bCritical)for(size_t I=Before;I Ref=std::abs(U[2])<0.90?std::array{0,0,1}:std::array{0,1,0}; + const auto V=Normalize(Cross(U,Ref));const auto W=Normalize(Cross(U,V)); + const double SegmentStep=std::max(8.0,static_cast(Radius)*2.0); + const int32 Segments=std::max(1,static_cast(std::ceil(Length/SegmentStep))); + for(int32 I=0;I(I)/Segments,T1=static_cast(I+1)/Segments; + const FVector P0(A.X+D[0]*T0,A.Y+D[1]*T0,A.Z+D[2]*T0); + const FVector P1(A.X+D[0]*T1,A.Y+D[1]*T1,A.Z+D[2]*T1); + const FVector C((P0.X+P1.X)*0.5,(P0.Y+P1.Y)*0.5,(P0.Z+P1.Z)*0.5); + const double Half=Length*(T1-T0)*0.5+Radius; + const size_t Before=Primitives.size();AddEllipsoid(C,{Half,Radius,Radius},U,V,W,bCarve); + if(bFloorClip) + { + const double Mid=(T0+T1)*0.5; + const double Floor=(1.0-Mid)*FloorA+Mid*FloorB; + const double DX=P1.X-P0.X,DY=P1.Y-P0.Y,HorizontalSq=DX*DX+DY*DY; + const double FloorStep=(FloorB-FloorA)*(T1-T0); + if(HorizontalSq>1.0e-6) + ApplyFloorClip(Primitives.back(),Floor,FloorStep*DX/HorizontalSq, + FloorStep*DY/HorizontalSq); + else ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB)); + } + if(bCritical)for(size_t J=Before;J Ref=std::abs(U[2])<0.90 + ?std::array{0,0,1}:std::array{0,1,0}; + const auto V=Normalize(Cross(U,Ref)),W=Normalize(Cross(U,V)); + const FVector C((F0.X+F1.X)*0.5,(F0.Y+F1.Y)*0.5,(F0.Z+F1.Z)*0.5); + AddEllipsoid(C,{Length*0.5+Width,Width,1.25},U,V,W,false); + } + + void BuildOwnerPrimitives() + { + const auto& Layout=Owner->Manager->GetLayout(); + const float MinX=static_cast(Min[0]-256),MinY=static_cast(Min[1]-256); + const float MaxX=static_cast(Max[0]+256),MaxY=static_cast(Max[1]+256); + for(const FStrateSlot& Slot:Layout) + { + if(!Slot.Definition)continue; + const FIntVector MidChunk(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2); + const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(MidChunk); + FChunkSDFCache Cache; + VoxelCaveMorphology::BuildChunkCache(Cache,MinX,MinY,MaxX,MaxY,Params, + static_cast(Owner->Seed), + Slot.StrateIndex,&Slot.Definition->TerrainOperations,ERoomGraphBuildSite::Unknown); + for(const FCachedRoom& R:Cache.Rooms) + { + ++Stats.RoomCount; + const size_t RoomPrimitiveStart=Primitives.size(); + if(R.ShapeType==2) + AddCapsule(R.ShapeA,R.ShapeB,std::max(1.0f,R.ShapeR),true); + else if(R.ShapeType==1) + { + AddRoundedBox(R.Center,{std::abs(R.ShapeA.X),std::abs(R.ShapeA.Y),std::abs(R.ShapeA.Z)}, + std::max(0.0f,R.ShapeR),true); + } + else + AddEllipsoid(R.Center,{std::abs(R.ShapeA.X),std::abs(R.ShapeA.Y),std::abs(R.ShapeA.Z)}, + {1,0,0},{0,1,0},{0,0,1},true); + if(R.FloorCutZ>-FLT_MAX*0.5f) + { + const double Relief=std::max(0.0f,R.FloorReliefStrength); + for(size_t I=RoomPrimitiveStart;I& Points=T.ControlPoints; + const TArray& Radii=T.ControlRadii; + if(Points.Num()>1) + { + for(int32 I=0;I+1Manager->GetPassages()) + { + ++Stats.PassageCount; + const TArray& Points=P.ControlPoints; + const TArray& Radii=P.ControlRadii; + if(Points.Num()>1) + { + for(int32 I=0;I+1Settings->bOpenSurfaceEntry) + { + const double Radius=std::max(8.0f,Owner->Settings->OriginSpineRadius); + const double Top=LayerRanges[0].Hi; + const FVector A(0,0,Top),B(0,0,std::max(LayerRanges[0].Lo,Top-256.0)); + AddCapsule(A,B,static_cast(Radius),true); + } + } + + void AddLanding(const FVoxelPassageLanding& L,bool bCritical=false) + { + const double HalfZ=std::max(2.0,(L.CeilingZ-L.FloorZ)*0.5); + const FVector Center(L.StandingPoint.X,L.StandingPoint.Y,(L.CeilingZ+L.FloorZ)*0.5); + const double Width=std::max(6.0f,L.HalfWidth); + AddRoundedBox(Center,{Width,Width,HalfZ},2.0,true); + if(bCritical) + { + const size_t Before=Primitives.size(); + AddEllipsoid(Center,{2.5,2.5,HalfZ},{1,0,0},{0,1,0},{0,0,1},true); + for(size_t I=Before;I(Owner->Seed); + auto Unit=[](uint32 H){return static_cast(FStandaloneResearchTunnels::Unit(H));}; + for(int32 CY=CY0;CY<=CY1;++CY)for(int32 CX=CX0;CX<=CX1;++CX) + for(int32 Axis=0;Axis<2;++Axis) + { + const uint32 H=FStandaloneResearchTunnels::EdgeHash(CX,CY,Axis,Seed); + if(Unit(H^0x68E31DA4u)>0.56)continue; + const int32 NX=CX+(Axis==0?1:0),NY=CY+(Axis==1?1:0); + auto Cell=[&](int32 X,int32 Y,double& OX,double& OY) + { + const uint32 CH=FStandaloneResearchTunnels::EdgeHash(X,Y,3,Seed); + OX=(X+0.5)*Pitch+(Unit(CH^0xA511E9B3u)-0.5)*Pitch*0.18; + OY=(Y+0.5)*Pitch+(Unit(CH^0x63D83595u)-0.5)*Pitch*0.18; + }; + double AX,AY,BX,BY;Cell(CX,CY,AX,AY);Cell(NX,NY,BX,BY); + const double DX=BX-AX,DY=BY-AY,L2=DX*DX+DY*DY,L=std::sqrt(std::max(1.0e-8,L2)); + const double NXrm=-DY/L,NYrm=DX/L; + const double BA=(Unit(H^0xB5297A4Du)*2.0-1.0)*Pitch*0.16; + const double BB=(Unit(H^0x1B56C4E9u)*2.0-1.0)*Pitch*0.16; + const std::array P0={AX,AY,FStandaloneResearchTunnels::WorldCenterZVoxels}; + const std::array C1={AX+DX/3.0+NXrm*BA,AY+DY/3.0+NYrm*BA,-80.0}; + const std::array C2={AX+DX*2.0/3.0+NXrm*BB,AY+DY*2.0/3.0+NYrm*BB,-80.0}; + const std::array P3={BX,BY,FStandaloneResearchTunnels::WorldCenterZVoxels}; + const uint32 PhaseHash=H^0xA511E9B3u; + const double Phase=Unit(PhaseHash)*6.2831853071795864769; + auto Bezier=[&](double T) + { + const double O=1.0-T; + std::array P{}; + for(int32 A=0;A<3;++A) + P[A]=O*O*O*P0[A]+3*O*O*T*C1[A]+3*O*T*T*C2[A]+T*T*T*P3[A]; + const double R=RadiusBase*(0.82+0.36*Unit(H^0x63D83595u)) + *(1.0+0.22*VoxelMath::DetSin(static_cast(6.2831853071795864769*T+Phase))); + const double VW=4.0*VoxelMath::DetSin(static_cast(3.141592653589793*T)) + *VoxelMath::DetSin(static_cast(6.2831853071795864769*T+Phase)); + P[2]+=VW; + return std::pair{P,R}; + }; + constexpr int32 Segments=12; + auto Last=Bezier(0.0); + for(int32 I=1;I<=Segments;++I) + { + const auto Next=Bezier(static_cast(I)/Segments); + AddCapsule(FVector(Last.first[0],Last.first[1],Last.first[2]), + FVector(Next.first[0],Next.first[1],Next.first[2]), + static_cast((Last.second+Next.second)*0.5),true); + Last=Next; + } + } + } + + void BuildSpatialBins(int32 BinResolution) + { + SpatialBins.clear(); + ActiveSpatialBinSize=BinResolution; + for(int32 Axis=0;Axis<3;++Axis) + { + FAxisBins& B=AxisBins[Axis]; + const int32 A=(Axis+1)%3,C=(Axis+2)%3; + B.MinA=FloorDiv(Min[A],LineBinSize);B.MinB=FloorDiv(Min[C],LineBinSize); + const int32 MaxA=FloorDiv(Max[A],LineBinSize),MaxB=FloorDiv(Max[C],LineBinSize); + B.CountA=MaxA-B.MinA+1;B.CountB=MaxB-B.MinB+1; + B.Items.resize(static_cast(B.CountA)*B.CountB); + for(uint32 Id=0;IdA1||B0>B1)continue; + for(int32 IB=B0;IB<=B1;++IB)for(int32 IA=A0;IA<=A1;++IA) + B.Items[static_cast(IB-B.MinB)*B.CountA+(IA-B.MinA)].push_back(Id); + } + } + for(uint32 Id=0;Id(X1-X0+1)*(Y1-Y0+1)*(Z1-Z0+1)>100000)continue; + for(int32 Z=Z0;Z<=Z1;++Z)for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X) + SpatialBins[{X,Y,Z}].push_back(Id); + } + } + + void BuildFarParentShapes() + { + constexpr int32 ParentPitch=2048; + 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 GX1=FloorDiv(ClipMax[0],ParentPitch)+1,GY1=FloorDiv(ClipMax[1],ParentPitch)+1; + std::vector Parents; + std::unordered_map ByCell; + const auto& Layout=Owner->Manager->GetLayout(); + const uint32 Seed=static_cast(Owner->Seed); + const auto Key=[](int32 GX,int32 GY,int32 Strate) + {return (static_cast(static_cast(GX+524288)&0xFFFFFu)<<44) + |(static_cast(static_cast(GY+524288)&0xFFFFFu)<<24) + |(static_cast(static_cast(Strate)&0xFFFFu)<<8);}; + auto Hash=[](uint32 X) + {X^=X>>16;X*=0x7FEB352Du;X^=X>>15;X*=0x846CA68Bu;X^=X>>16;return X;}; + auto Unit=[&](uint32 H){return static_cast(Hash(H)>>8)*(1.0/16777216.0);}; + for(const FStrateSlot& Slot:Layout) + { + const double ZBottom=static_cast(Slot.BottomChunkZ)*CHUNK_SIZE; + const double ZTop=static_cast(Slot.TopChunkZ+1)*CHUNK_SIZE; + for(int32 GY=GY0;GY<=GY1;++GY)for(int32 GX=GX0;GX<=GX1;++GX) + { + uint32 H=Hash(Seed^static_cast(GX)*0x9E3779B9u + ^static_cast(GY)*0x85EBCA6Bu + ^static_cast(Slot.StrateIndex)*0xC2B2AE35u); + const double X=GX*static_cast(ParentPitch)+ParentPitch*0.5 + +(Unit(H^0xA511E9B3u)-0.5)*192.0; + const double Y=GY*static_cast(ParentPitch)+ParentPitch*0.5 + +(Unit(H^0x63D83595u)-0.5)*192.0; + const double Rx=300.0+Unit(H^0xB5297A4Du)*100.0; + const double Ry=280.0+Unit(H^0x1B56C4E9u)*120.0; + const double Rz=100.0+Unit(H^0x68E31DA4u)*60.0; + if(X+Rx>=Min[0]&&X-Rx<=Max[0]&&Y+Ry>=Min[1]&&Y-Ry<=Max[1])continue; + const double Z=ZBottom+(ZTop-ZBottom)*(0.35+0.30*Unit(H^0x27D4EB2Fu)); + FParent P{GX,GY,Slot.StrateIndex,FVector(X,Y,Z),Rx,Ry,Rz}; + const size_t Index=Parents.size();Parents.push_back(P); + ByCell.emplace(Key(GX,GY,Slot.StrateIndex),Index); + const std::array Axis0={1,0,0},Axis1={0,1,0},Axis2={0,0,1}; + AddEllipsoid(P.Center,{P.Rx,P.Ry,P.Rz},Axis0,Axis1,Axis2,true); + } + } + for(const FParent& P:Parents) + { + for(const auto [DX,DY]:std::array,2>{{{1,0},{0,1}}}) + { + const auto It=ByCell.find(Key(P.GX+DX,P.GY+DY,P.Strate)); + if(It==ByCell.end())continue; + const FVector A(P.Center.X,P.Center.Y,P.Center.Z); + const FVector B=Parents[It->second].Center; + const int32 MinX=static_cast(std::floor(std::min(A.X,B.X)-64.0)); + const int32 MaxX=static_cast(std::ceil(std::max(A.X,B.X)+64.0)); + const int32 MinY=static_cast(std::floor(std::min(A.Y,B.Y)-64.0)); + const int32 MaxY=static_cast(std::ceil(std::max(A.Y,B.Y)+64.0)); + if(MaxX>=Min[0]&&MinX<=Max[0]&&MaxY>=Min[1]&&MinY<=Max[1])continue; + AddCapsule(A,B,64.0f,true); + } + } + if(!Parents.empty()) + std::printf("[FarParents] pitch=%d rooms=%zu links=%llu clip=(%d,%d)..(%d,%d)\n", + ParentPitch,Parents.size(),static_cast(Primitives.size()), + ClipMin[0],ClipMin[1],ClipMax[0],ClipMax[1]); + } + + const std::vector& GetLineCandidates(int32 Axis,int32 A,int32 B) const + { + static const std::vector Empty; + const FAxisBins& G=AxisBins[Axis]; + const int32 IA=FloorDiv(A,LineBinSize)-G.MinA,IB=FloorDiv(B,LineBinSize)-G.MinB; + if(IA<0||IB<0||IA>=G.CountA||IB>=G.CountB)return Empty; + return G.Items[static_cast(IB)*G.CountA+IA]; + } + + FBinKey PointBin(double X,double Y,double Z) const + {return {FloorDiv(static_cast(std::floor(X)),ActiveSpatialBinSize),FloorDiv(static_cast(std::floor(Y)),ActiveSpatialBinSize),FloorDiv(static_cast(std::floor(Z)),ActiveSpatialBinSize)};} + + bool ShapeContains(const FPrimitive& P,int32 X,int32 Y,int32 Z) const + { + bool bInside=false; + if(P.bRoundedBox) + { + const double D[3]={std::abs(X-P.CenterSub[0]/16.0)-P.RoundedHalfExtent[0], + std::abs(Y-P.CenterSub[1]/16.0)-P.RoundedHalfExtent[1], + std::abs(Z-P.CenterSub[2]/16.0)-P.RoundedHalfExtent[2]}; + const double Ox=std::max(D[0],0.0),Oy=std::max(D[1],0.0),Oz=std::max(D[2],0.0); + const double Outside=std::sqrt(Ox*Ox+Oy*Oy+Oz*Oz); + const double Inside=std::min(std::max({D[0],D[1],D[2]}),0.0); + bInside=Outside+Inside-P.Rounding<=0.0; + } + else + { + const int32 World[3]={X,Y,Z};int64 D[3]; + for(int32 A=0;A<3;++A)D[A]=static_cast(World[A])*SubPerVoxel-P.CenterSub[A]; + int64 Q=0; + for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)Q+=P.Q[I][J]*D[I]*D[J]; + bInside=Q<=static_cast(QScale); + } + if(!bInside)return false; + if(P.bHasFloorClip) + { + const double Floor=P.FloorCenterZ+P.FloorSlopeX*(X-P.CenterSub[0]/16.0) + +P.FloorSlopeY*(Y-P.CenterSub[1]/16.0); + return Z>=Floor; + } + return true; + } + + float ShapeDistance(const FPrimitive& P,double X,double Y,double Z) const + { + if(P.bRoundedBox) + { + const double D[3]={std::abs(X-P.CenterSub[0]/16.0)-P.RoundedHalfExtent[0], + std::abs(Y-P.CenterSub[1]/16.0)-P.RoundedHalfExtent[1], + std::abs(Z-P.CenterSub[2]/16.0)-P.RoundedHalfExtent[2]}; + const double Ox=std::max(D[0],0.0),Oy=std::max(D[1],0.0),Oz=std::max(D[2],0.0); + const double Outside=std::sqrt(Ox*Ox+Oy*Oy+Oz*Oz); + const double Inside=std::min(std::max({D[0],D[1],D[2]}),0.0); + return static_cast(Outside+Inside-P.Rounding); + } + const double D[3]={X-P.CenterSub[0]/16.0,Y-P.CenterSub[1]/16.0,Z-P.CenterSub[2]/16.0}; + double Q=0.0; + for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)Q+=static_cast(P.Q[I][J])/QScale*D[I]*D[J]*256.0; + return static_cast((std::sqrt(std::max(0.0,Q))-1.0)*P.MinRadius); + } + + bool SolveLine(const FPrimitive& P,int32 Axis,const int32 Fixed[3],FInterval& Out) const + { + const int32 A=(Axis+1)%3,B=(Axis+2)%3; + const int32 Lo=P.Min[Axis],Hi=P.Max[Axis]; + if(Fixed[A]P.Max[A]||Fixed[B]P.Max[B])return false; + int32 L=Lo,H=Hi; + if(!P.bRoundedBox) + { + double CoefA=static_cast(P.Q[Axis][Axis]); + double CoefB=0.0,CoefC=-static_cast(QScale); + int64 D[3];D[Axis]=0; + for(int32 J=0;J<3;++J)if(J!=Axis)D[J]=static_cast(Fixed[J])*SubPerVoxel-P.CenterSub[J]; + for(int32 J=0;J<3;++J)if(J!=Axis) + { + CoefB+=2.0*P.Q[Axis][J]*D[J]; + for(int32 K=0;K<3;++K)if(K!=Axis)CoefC+=static_cast(P.Q[J][K])*D[J]*D[K]; + } + if(!(CoefA>0.0))return false; + const double Disc=CoefB*CoefB-4.0*CoefA*CoefC; + if(Disc<0.0)return false; + const double S=std::sqrt(Disc); + const double U0=(-CoefB-S)/(2.0*CoefA),U1=(-CoefB+S)/(2.0*CoefA); + const double T0=(U0+P.CenterSub[Axis])/SubPerVoxel; + const double T1=(U1+P.CenterSub[Axis])/SubPerVoxel; + L=std::max(Lo,static_cast(std::floor(T0))-1); + H=std::min(Hi,static_cast(std::ceil(T1))+1); + } + if(L>H)return false; + if(P.bCarve&&P.bHasFloorClip) + { + double X0=Fixed[0],Y0=Fixed[1],Z0=Fixed[2]; + if(Axis==0)X0=0.0;else if(Axis==1)Y0=0.0;else Z0=0.0; + const double Floor0=P.FloorCenterZ+P.FloorSlopeX*(X0-P.CenterSub[0]/16.0) + +P.FloorSlopeY*(Y0-P.CenterSub[1]/16.0); + const double Margin0=Z0-Floor0; + const double MarginSlope=Axis==0?-P.FloorSlopeX:Axis==1?-P.FloorSlopeY:1.0; + if(std::abs(MarginSlope)<1.0e-12) + {if(Margin0<0.0)return false;} + else + { + const double Boundary=-Margin0/MarginSlope; + if(MarginSlope>0.0)L=std::max(L,static_cast(std::ceil(Boundary))); + else H=std::min(H,static_cast(std::floor(Boundary))); + } + } + if(L>H)return false; + int32 Mid=L+(H-L)/2; + if(!ShapeContains(P,Axis==0?Mid:Fixed[0],Axis==1?Mid:Fixed[1],Axis==2?Mid:Fixed[2])) + { + bool Found=false; + for(int32 Delta=1;Delta<=2&&!Found;++Delta) + { + if(Mid-Delta>=L&&ShapeContains(P,Axis==0?Mid-Delta:Fixed[0],Axis==1?Mid-Delta:Fixed[1],Axis==2?Mid-Delta:Fixed[2])){Mid-=Delta;Found=true;} + else if(Mid+Delta<=H&&ShapeContains(P,Axis==0?Mid+Delta:Fixed[0],Axis==1?Mid+Delta:Fixed[1],Axis==2?Mid+Delta:Fixed[2])){Mid+=Delta;Found=true;} + } + if(!Found)return false; + } + int32 Left=L,Right=Mid; + while(Left& Ranges) + { + if(Ranges.size()<2)return; + std::sort(Ranges.begin(),Ranges.end(),[](const FInterval& A,const FInterval& B){return A.Lo==B.Lo?A.Hi& Words,size_t Base,int32 Lo,int32 Hi,bool bSet) + { + if(Lo>Hi)return; + const size_t First=static_cast(Lo)>>6,Last=static_cast(Hi)>>6; + for(size_t W=First;W<=Last;++W) + { + const uint32 Bit0=W==First?(static_cast(Lo)&63u):0u; + const uint32 Bit1=W==Last?(static_cast(Hi)&63u):63u; + const uint64 Mask=(~uint64{0}<>(63u-Bit1)); + if(bSet)Words[Base+W]|=Mask;else Words[Base+W]&=~Mask; + } + } + + void BuildPackedRockIntervals() + { + const int32 NX=Size[0],NY=Size[1],NZ=Size[2]; + ParallelLines(static_cast(NX)*NY,[&](size_t Line) + { + const int32 X=Min[0]+static_cast(Line%NX),Y=Min[1]+static_cast(Line/NX); + std::vector Carve,Solid,ProtectedAir; + Carve.reserve(24);Solid.reserve(8);ProtectedAir.reserve(4); + const int32 Fixed[3]={X,Y,0}; + for(uint32 Id:GetLineCandidates(2,X,Y)) + { + const FPrimitive& P=Primitives[Id];FInterval R; + if(SolveLine(P,2,Fixed,R)) + { + R.Lo=std::max(R.Lo,Min[2])-Min[2];R.Hi=std::min(R.Hi,Max[2])-Min[2]; + if(R.Lo<=R.Hi) + { + if(!P.bCarve)Solid.push_back(R); + else if(P.bCritical)ProtectedAir.push_back(R); + else Carve.push_back(R); + } + } + } + MergeIntervals(Carve);MergeIntervals(Solid);MergeIntervals(ProtectedAir); + const size_t Base=Line*WordsPerZ; + const int32 ZLo=LayerRanges[0].Lo, ZHi=LayerRanges[0].Hi; + const int32 BaseLo=std::max(ZLo,Min[2])-Min[2],BaseHi=std::min(ZHi,Max[2])-Min[2]; + if(BaseLo<=BaseHi)FillBits(RockWords,Base,BaseLo,BaseHi,true); + for(const FInterval& R:Carve)FillBits(RockWords,Base,R.Lo,R.Hi,false); + for(const FInterval& R:Solid)FillBits(RockWords,Base,R.Lo,R.Hi,true); + // Critical passage air is the final authored override, including at landing floors. + for(const FInterval& R:ProtectedAir)FillBits(RockWords,Base,R.Lo,R.Hi,false); + }); + } + + void BuildCandidateBands() + { + const int32 N[3]={Size[0],Size[1],Size[2]}; + for(int32 Axis=0;Axis<3;++Axis) + { + const int32 A=(Axis+1)%3,B=(Axis+2)%3; + const size_t Lines=static_cast(Size[A])*Size[B]; + const size_t Words=(static_cast(N[Axis])+63u)/64u; + CandidateAxisWords[Axis].assign(Lines*Words,0); + ParallelLines(Lines,[&,Axis,A,B,Words](size_t Line) + { + const int32 CA=Min[A]+static_cast(Line%Size[A]); + const int32 CB=Min[B]+static_cast(Line/Size[A]); + int32 Fixed[3]{};Fixed[A]=CA;Fixed[B]=CB; + for(uint32 Id:GetLineCandidates(Axis,CA,CB)) + { + const FPrimitive& P=Primitives[Id];if(!P.bCarve)continue; + FInterval R;if(!SolveLine(P,Axis,Fixed,R))continue; + const int32 Lo=std::max(Min[Axis],R.Lo-DetailBand); + const int32 Hi=std::min(Max[Axis],R.Lo+DetailBand); + const int32 Lo2=std::max(Min[Axis],R.Hi-DetailBand); + const int32 Hi2=std::min(Max[Axis],R.Hi+DetailBand); + const size_t Base=Line*Words; + FillBits(CandidateAxisWords[Axis],Base,Lo-Min[Axis],Hi-Min[Axis],true); + FillBits(CandidateAxisWords[Axis],Base,Lo2-Min[Axis],Hi2-Min[Axis],true); + } + }); + } + CandidateWords.assign(RockWords.size(),0); + for(int32 Axis=0;Axis<3;++Axis) + { + const int32 A=(Axis+1)%3,B=(Axis+2)%3; + const size_t Words=(static_cast(Size[Axis])+63u)/64u; + for(size_t Line=0;Line(Size[A])*Size[B];++Line) + { + const int32 CA=Min[A]+static_cast(Line%Size[A]); + const int32 CB=Min[B]+static_cast(Line/Size[A]); + for(size_t W=0;W(W*64+Bit); + if(T>Max[Axis])continue; + int32 Coord[3]{};Coord[Axis]=T;Coord[A]=CA;Coord[B]=CB; + const size_t Column=static_cast(Coord[1]-Min[1])*Size[0] + +static_cast(Coord[0]-Min[0]); + const size_t Zi=static_cast(Coord[2]-Min[2]); + CandidateWords[Column*WordsPerZ+(Zi>>6)]|=uint64{1}<<(Zi&63u); + } + } + } + } + } + + void ApplyDetailBand() + { + std::atomic CandidateCount{0},ProxyCount{0},FlipCount{0}; + ParallelLines(static_cast(Size[0])*Size[1],[&](size_t Line) + { + const int32 X=Min[0]+static_cast(Line%Size[0]); + const int32 Y=Min[1]+static_cast(Line/Size[0]); + uint64 LocalCandidates=0,LocalProxies=0,LocalFlips=0; + const size_t WordBase=Line*WordsPerZ; + for(size_t W=0;W=static_cast(Size[2]))continue; + const int32 Z=Min[2]+static_cast(Zi); + ++LocalCandidates; + if(ZLayerRanges[0].Hi)continue; + if(IsInsideSolidOverlay(X,Y,Z)||IsInsideCriticalAir(X,Y,Z))continue; + const float SDF=CaveDistance(X,Y,Z); + if(std::abs(SDF)>DetailSupremum+0.125f)continue; + const float Displaced=SDF+DetailNoise(X,Y,Z); + const bool bNewRock=Displaced>=0.0f; + const bool bOldRock=((RockWords[WordBase+(Zi>>6)]>>(Zi&63u))&1u)!=0; + if(bNewRock!=bOldRock) + { + RockWords[WordBase+(Zi>>6)]^=uint64{1}<<(Zi&63u); + ++LocalFlips; + } + const size_t Node=(Zi*static_cast(Size[1])+static_cast(Y-Min[1])) + *Size[0]+static_cast(X-Min[0]); + StoreSurfaceDistance(Node,-Displaced); + ValidBits|=uint64{1}<(std::lround(Density*16.0f)); + Q=std::clamp(Q,-127,127); + if(Q==0)Q=Density<0.0f?-1:1; + SurfaceDistanceQ[Node]=static_cast(Q); + } + + bool IsInsideSolidOverlay(int32 X,int32 Y,int32 Z) const + { + const FBinKey K=PointBin(X,Y,Z);const auto It=SpatialBins.find(K); + if(It==SpatialBins.end())return false; + for(uint32 Id:It->second) + { + const FPrimitive& P=Primitives[Id];if(P.bCarve)continue; + if(XP.Max[0]||YP.Max[1]||ZP.Max[2])continue; + if(ShapeContains(P,X,Y,Z))return true; + } + return false; + } + + bool IsInsideCriticalAir(int32 X,int32 Y,int32 Z) const + { + const auto It=SpatialBins.find(PointBin(X,Y,Z)); + if(It==SpatialBins.end())return false; + for(uint32 Id:It->second) + { + const FPrimitive& P=Primitives[Id];if(!P.bCarve||!P.bCritical)continue; + if(XP.Max[0]||YP.Max[1]||ZP.Max[2])continue; + if(ShapeContains(P,X,Y,Z))return true; + } + return false; + } + + bool IsInsideSolidOverlay(double X,double Y,double Z) const + { + const auto It=SpatialBins.find(PointBin(X,Y,Z)); + if(It==SpatialBins.end())return false; + for(uint32 Id:It->second) + { + const FPrimitive& P=Primitives[Id];if(P.bCarve)continue; + if(XP.Max[0]||YP.Max[1]||ZP.Max[2])continue; + if(ShapeDistance(P,X,Y,Z)<=0.0f)return true; + } + return false; + } + + bool IsInsideCriticalAir(double X,double Y,double Z) const + { + const auto It=SpatialBins.find(PointBin(X,Y,Z)); + if(It==SpatialBins.end())return false; + for(uint32 Id:It->second) + { + const FPrimitive& P=Primitives[Id];if(!P.bCarve||!P.bCritical)continue; + if(XP.Max[0]||YP.Max[1]||ZP.Max[2])continue; + if(ShapeDistance(P,X,Y,Z)>0.0f)continue; + if(P.bHasFloorClip) + { + const double Floor=P.FloorCenterZ+P.FloorSlopeX*(X-P.CenterSub[0]/16.0) + +P.FloorSlopeY*(Y-P.CenterSub[1]/16.0); + if(Zsecond) + { + const FPrimitive& P=Primitives[Id];if(!P.bCarve)continue; + if(XP.Max[0]+DetailBand||YP.Max[1]+DetailBand + ||ZP.Max[2]+DetailBand)continue; + float Distance=ShapeDistance(P,X,Y,Z); + if(P.bHasFloorClip) + { + const double Floor=P.FloorCenterZ+P.FloorSlopeX*(X-P.CenterSub[0]/16.0) + +P.FloorSlopeY*(Y-P.CenterSub[1]/16.0); + Distance=std::max(Distance,static_cast(Floor-Z)); + } + Best=std::min(Best,Distance); + } + return Best; + } + + float DetailNoise(double X,double Y,double Z) const + { + if(DetailVariant==EDetailVariant::Smooth)return 0.0f; + if(DetailVariant==EDetailVariant::PerVoxel) + { + const int32 IX=static_cast(std::floor(X)),IY=static_cast(std::floor(Y)),IZ=static_cast(std::floor(Z)); + return FStandaloneResearchTunnels::NoiseValue(IX,IY,IZ,static_cast(Owner->Seed))*DetailSupremum; + } + return FStandaloneResearchTunnels::WallRelief(static_cast(X),static_cast(Y), + static_cast(Z),static_cast(Owner->Seed)); + } + + bool EvaluateFast(double X,double Y,double Z) const + { + if(LayerRanges.empty()||ZLayerRanges[0].Hi)return false; + if(IsInsideCriticalAir(X,Y,Z))return false; + if(IsInsideSolidOverlay(X,Y,Z))return true; + const float Cave=CaveDistance(X,Y,Z); + if(Cave>=100000.0f)return true; + const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2]; + if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth&&std::abs(Cave)<=DetailBand + &&DetailNoise(X,Y,Z)>-Cave)return true; + return Cave>=0.0f; + } + + void ValidatePassageRoutes() + { + constexpr double SampleStep=0.5; + for(const FVoxelPassage& P:Owner->Manager->GetPassages()) + { + const auto& Points=P.ControlPoints; + if(Points.Num()<2)continue; + for(int32 I=0;I+1(std::ceil(L/SampleStep))); + for(int32 J=0;J<=Count;++J) + { + const double T=static_cast(J)/Count; + const double X=A.X+(B.X-A.X)*T,Y=A.Y+(B.Y-A.Y)*T,Z=A.Z+(B.Z-A.Z)*T; + ++Stats.WalkSamples; + if(IsRock(static_cast(std::lround(X)), + static_cast(std::lround(Y)),static_cast(std::lround(Z)))) + ++Stats.WalkFailures; + } + } + } + if(Stats.WalkFailures) + std::fprintf(stderr,"[Walkability] FAIL %llu/%llu passage centerline samples are solid\n", + static_cast(Stats.WalkFailures),static_cast(Stats.WalkSamples)); + else + std::printf("[Walkability] PASS %llu/%llu passage centerline samples remain open; native landing floors retained\n", + static_cast(Stats.WalkSamples),static_cast(Stats.WalkSamples)); + } + + static double DetailAutocorrelation(EDetailVariant Variant) + { + constexpr int32 N=24;double Sum=0.0,SumSq=0.0,PairA=0.0,PairB=0.0,PairAA=0.0,PairBB=0.0,PairAB=0.0;uint64 Count=0; + auto Value=[&](int32 X,int32 Y,int32 Z)->double + { + if(Variant==EDetailVariant::PerVoxel)return FStandaloneResearchTunnels::NoiseValue(X,Y,Z,11); + if(Variant==EDetailVariant::Smooth)return 0.0; + return FStandaloneResearchTunnels::WallRelief(static_cast(X),static_cast(Y),static_cast(Z),11); + }; + for(int32 Z=0;Z void ParallelLines(size_t Count,Fn&& Work) const + { + if(WorkerCount<=1||Count<512) + {for(size_t I=0;I Next{0};std::vector Threads;Threads.reserve(WorkerCount); + for(int32 W=0;W=Count)break;Work(I);}}); + for(std::thread& T:Threads)T.join(); + } + + FStandaloneOwnerWorld* Owner=nullptr; + int32 WorkerCount=6; + int32 Min[3]{},Max[3]{},ClipMin[3]{},ClipMax[3]{},Size[3]{}; + int32 ActiveSpatialBinSize=LineBinSize; + size_t WordsPerZ=0; + EDetailVariant DetailVariant=EDetailVariant::Coherent; + bool Built=false; + bool FarBuilt=false; + std::vector LayerRanges; + std::vector Primitives; + std::array AxisBins; + std::unordered_map,FBinHash> SpatialBins; + std::vector RockWords; + std::unique_ptr SurfaceDistanceQ; + std::array,3> CandidateAxisWords; + std::vector CandidateWords; + FBuildStats Stats; +}; + +namespace FStandaloneShapeWorldAccess +{ + inline std::atomic& ActiveRef() + {static std::atomic Active{nullptr};return Active;} + inline bool& ThreadBypassRef() + {static thread_local bool Bypass=false;return Bypass;} + inline void SetActive(const FStandaloneShapeWorld* World) + {ActiveRef().store(World,std::memory_order_release);} + inline const FStandaloneShapeWorld* GetActive() + {return ThreadBypassRef()?nullptr:ActiveRef().load(std::memory_order_acquire);} + class FScopedBypass + { + public: + FScopedBypass():Previous(ThreadBypassRef()){ThreadBypassRef()=true;} + ~FScopedBypass(){ThreadBypassRef()=Previous;} + FScopedBypass(const FScopedBypass&)=delete; + FScopedBypass& operator=(const FScopedBypass&)=delete; + private: + bool Previous; + }; +} diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index 9094511..ea1883e 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -1,6 +1,7 @@ #include "OwnerWorldLoader.h" #include "StreamingWorld.h" #include "StandaloneResearchTunnels.h" +#include "StandaloneShapeWorld.h" #include "VoxelDensityProfile.h" #ifdef TEXT @@ -179,6 +180,7 @@ namespace ID3D11RasterizerState* SolidRasterizer=nullptr; ID3D11RasterizerState* WireRasterizer=nullptr; std::unique_ptr World; + std::unique_ptr ShapeWorld; std::unique_ptr Streamer; FStandaloneMeshCache MeshCache; std::string DataPath; @@ -203,6 +205,11 @@ namespace bool bParityChecked=false; bool bRoundTunnels=false; bool bRoundTunnelEnhanced=true; + bool bShapeWorld=false; + bool bShapeAuditOnly=false; + bool bShapeCompare=false; + bool bFarShapeStagePending=false; + FStandaloneShapeWorld::EDetailVariant ShapeDetailVariant=FStandaloneShapeWorld::EDetailVariant::Coherent; bool bWireframe=false; bool bCaptured=false; bool bAutoCaptureWritten=false; @@ -464,6 +471,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; bool ConfigureWorld(std::string& Error) { + FStandaloneShapeWorldAccess::SetActive(nullptr); + ShapeWorld.reset(); World=std::make_unique(); if(!StandaloneOwnerLoader::Load(DataPath,*World,Error))return false; if(!bParityChecked&&Seed==World->Seed&&!bRoundTunnels)PrintParity(); @@ -480,10 +489,41 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } if(!PrepareStandaloneDensityWorld(*World,Seed,bRoundTunnels, bRoundTunnelEnhanced,Error))return false; + if(bShapeWorld&&!BuildShapeWorldForCamera(Error))return false; LoadMeshCache(); return true; } + bool BuildShapeWorldForCamera(std::string& Error,bool bIncludeFarWorld=false) + { + const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0); + const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels); + 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); + uint64 FineTiles=0; + 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)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); + 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; + } + if(!FineTiles){Error="no full-detail tiles are available for shape compilation";return false;} + ShapeWorld=std::make_unique(); + if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,WorkerCount,ShapeDetailVariant,bIncludeFarWorld,Error)) + {ShapeWorld.reset();return false;} + FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get()); + bFarShapeStagePending=bShapeWorld&&!ShapeWorld->HasFarWorld(); + std::printf("[ShapeRegion] fine_tiles=%llu bounds=(%d,%d,%d)..(%d,%d,%d) clip=(%d,%d,%d)..(%d,%d,%d)\n", + static_cast(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); + return true; + } + FStandaloneMeshCacheMetadata MakeMeshCacheMetadata()const { FStandaloneMeshCacheMetadata Metadata; @@ -503,7 +543,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; void LoadMeshCache() { MeshCache.Clear(); - if(bForceGenerate||!World)return; + if(bForceGenerate||bShapeWorld||!World)return; const char* ModeSuffix=!bRoundTunnels?"_base": (bRoundTunnelEnhanced?"_round_detailed":"_round_legacy"); const std::filesystem::path CachePath=std::filesystem::path(DataPath).parent_path() @@ -529,12 +569,12 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; { StopGeneration(); ReleaseMeshes(); + GenerationStart=std::chrono::steady_clock::now(); std::string Error; if(!ConfigureWorld(Error)) {std::fprintf(stderr,"World load failed: %s\n",Error.c_str());MessageBoxA(Window,Error.c_str(),"VoxelForge viewer",MB_ICONERROR);return;} const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0); BuildTargetChunk=StandaloneWorldMesher::CameraChunk(CameraVoxels); - GenerationStart=std::chrono::steady_clock::now(); bFullDetailReported=false;bInitialClipmapReported=false;bAutoCaptureWritten=false; if(bProfileAttribution) { @@ -548,7 +588,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0; for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level]; Streamer=std::make_unique(*World,WorkerCount,false, - MeshCache.IsLoaded()?&MeshCache:nullptr); + (!bShapeWorld&&MeshCache.IsLoaded())?&MeshCache:nullptr, + bFarShapeStagePending?World->FullResClipLevels-1:-1); const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex); FCrossingWork& InitialWork=CrossingWork[CrossingIndex]; InitialWork.SubmittedJobs=Update.NewJobs;InitialWork.PendingJobs=Update.NewJobs; @@ -556,8 +597,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; bGenerationRunning=true;bRebuildPending=false; const char* TunnelMode=!bRoundTunnels?"":(bRoundTunnelEnhanced ?" with detailed round/winding tunnels":" with legacy round/winding tunnels"); - std::printf("Streaming seed %d%s: %d desired tiles, %d full-detail tiles, %d workers; upload budget %d batches / %.1f ms per frame.\n", - Seed,TunnelMode,Update.DesiredTiles, + 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, Update.FullResTiles,Streamer->GetWorkerCount(),MaxBatchUploadsPerFrame,BatchUploadBudgetMilliseconds); std::fflush(stdout); } @@ -569,6 +610,28 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; std::printf("Seed changed to %d; replacing the current streaming world.\n",Seed); } + void StartFarShapeGeneration() + { + if(!bFarShapeStagePending||!ShapeWorld||ShapeWorld->HasFarWorld()||!Streamer)return; + ShapeWorld->BuildFarWorld(); + bFarShapeStagePending=false; + Streamer->Stop(); + std::vector ResidentKeys;ResidentKeys.reserve(ResidentTiles.size()); + for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first); + Streamer.reset(); + Streamer=std::make_unique(*World,WorkerCount,false,nullptr,-1); + const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0); + ++CrossingIndex; + const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,ResidentKeys,CrossingIndex); + FCrossingWork& Work=CrossingWork[CrossingIndex]; + Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs; + Work.StartTime=std::chrono::steady_clock::now(); + bGenerationRunning=true; + std::printf("[FarStreamStart] desired=%d retained=%d new_jobs=%d workers=%d\n", + Update.DesiredTiles,Update.RetainedResidentTiles,Update.NewJobs,WorkerCount); + std::fflush(stdout); + } + void MarkBatchDirty(const FStandaloneBatchKey& Key) {DirtyBatchSet.insert(Key);} @@ -602,6 +665,12 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0); FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels); if(NewDesired.CenterChunk==BuildTargetChunk)return; + if(bShapeWorld) + { + bRebuildPending=true; + std::printf("[ShapeRegion] camera crossed its compiled full-detail bounds; rebuilding the interval region.\n"); + return; + } const FIntVector Previous=BuildTargetChunk; std::unordered_set NewSet; NewSet.reserve(NewDesired.Tiles.size()*2+1); @@ -835,6 +904,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; std::printf("[FullDetailReady] %.3f s after spawn; resident=%zu/%d tiles through LOD%d.\n", Elapsed,ResidentTiles.size(),Streamer->FullResDesiredCount(),World->FullResClipLevels-1); ReportTileTimings();std::fflush(stdout); + StartFarShapeGeneration(); } if(!bInitialClipmapReported&&ResidentTiles.size()==DesiredTiles.size()&&DirtyBatchSet.empty()) { @@ -873,7 +943,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; void UpdateTitle() { uint64 VisibleIndices=0;for(const auto& Pair:StreamingBatches)if(bShowLevel[Pair.first.Level])VisibleIndices+=Pair.second.IndexCount; - std::ostringstream S;S<<"VoxelForge standalone | seed "<bCaptured)GApp->Capture(false);else DestroyWindow(H);} if(!bWasDown&&W=='F')GApp->bWireframe=!GApp->bWireframe; if(!bWasDown&&W=='T'){GApp->bRoundTunnels=!GApp->bRoundTunnels;GApp->bRebuildPending=true;} + if(!bWasDown&&W=='V'){GApp->bShapeWorld=!GApp->bShapeWorld;GApp->bRebuildPending=true;} if(!bWasDown&&W=='G'&&GApp->bRoundTunnels) { GApp->bRoundTunnelEnhanced=!GApp->bRoundTunnelEnhanced; @@ -1174,11 +1245,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; }; FDirectedSurfaceDistance DirectedSurfaceDistance( - const std::vector& From,const std::vector& To) + const std::vector& From,const std::vector& To, + double BucketWidth=1.0,int32 SearchCells=3) { FDirectedSurfaceDistance Result; if(From.empty()||To.empty()){Result.Unmatched=From.size();return Result;} - constexpr double BucketWidth=1.0; std::unordered_map,FCompareBucketHash> Buckets; Buckets.reserve(To.size()); const auto BucketOf=[=](const FComparePoint& P) @@ -1192,7 +1263,9 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; { const FIntVector Center=BucketOf(P); double BestSquared=std::numeric_limits::infinity(); - for(int32 Z=-3;Z<=3;++Z)for(int32 Y=-3;Y<=3;++Y)for(int32 X=-3;X<=3;++X) + for(int32 Z=-SearchCells;Z<=SearchCells;++Z) + for(int32 Y=-SearchCells;Y<=SearchCells;++Y) + for(int32 X=-SearchCells;X<=SearchCells;++X) { const auto It=Buckets.find(Center+FIntVector(X,Y,Z)); if(It==Buckets.end())continue; @@ -1212,6 +1285,148 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; return Result; } + int RunOwnerShapeComparison(FStandaloneOwnerWorld& World,const FStandaloneShapeWorld& Shape, + V3 CameraPosition,int32 Workers,int32 LevelFilter,int32 TileLimit) + { + if(FStandaloneShapeWorldAccess::GetActive()!=&Shape) + {std::fprintf(stderr,"Shape comparison requires the compiled interval world to be active.\n");return 11;} + if(LevelFilter>=World.FullResClipLevels&&!Shape.HasFarWorld()) + const_cast(Shape).BuildFarWorld(); + const FVector CameraVoxels(CameraPosition.X*4.0,CameraPosition.Y*4.0,CameraPosition.Z*4.0); + { + const int32 Offsets[][3]={{0,0,0},{4,0,0},{-4,0,0},{0,4,0},{0,-4,0},{0,0,4},{0,0,-4}}; + for(const auto& Offset:Offsets) + { + const float X=static_cast(CameraVoxels.X+Offset[0]); + const float Y=static_cast(CameraVoxels.Y+Offset[1]); + const float Z=static_cast(CameraVoxels.Z+Offset[2]); + float OwnerDensity=0.0f; + {FStandaloneShapeWorldAccess::FScopedBypass Bypass; + OwnerDensity=World.Generator->GetDensityAt(X,Y,Z);} + const float ShapeDensity=Shape.DensityAt(X,Y,Z); + std::printf("[ShapeProbe] voxels=(%.1f,%.1f,%.1f) owner=%.5f shape=%.5f owner_air=%d shape_air=%d protected_air=%d solid_overlay=%d\n", + X,Y,Z,OwnerDensity,ShapeDensity,OwnerDensity>=0.0f?1:0,ShapeDensity>=0.0f?1:0, + Shape.DebugCriticalAirAt(X,Y,Z)?1:0,Shape.DebugSolidOverlayAt(X,Y,Z)?1:0); + } + } + const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraVoxels); + std::vector Tiles; + for(const FVoxelTileKey& Tile:Desired.Tiles) + if(LevelFilter<0?Tile.Level0&&Tiles.size()>static_cast(TileLimit)) + { + std::sort(Tiles.begin(),Tiles.end(),[&](const FVoxelTileKey& A,const FVoxelTileKey& B) + { + const FIntVector AO=A.OriginVoxels(),BO=B.OriginVoxels(); + const double ADX=AO.X+A.ExtentVoxels()*0.5-CameraVoxels.X; + const double ADY=AO.Y+A.ExtentVoxels()*0.5-CameraVoxels.Y; + const double ADZ=AO.Z+A.ExtentVoxels()*0.5-CameraVoxels.Z; + const double BDX=BO.X+B.ExtentVoxels()*0.5-CameraVoxels.X; + const double BDY=BO.Y+B.ExtentVoxels()*0.5-CameraVoxels.Y; + const double BDZ=BO.Z+B.ExtentVoxels()*0.5-CameraVoxels.Z; + return ADX*ADX+ADY*ADY+ADZ*ADZ(TileLimit)); + } + if(Tiles.empty()){std::fprintf(stderr,"No tiles are available for the requested shape comparison level.\n");return 12;} + + struct FTileComparison + { + uint64 Samples=0,Changed=0,MeshMismatch=0; + uint64 OldHash=14695981039346656037ull,NewHash=14695981039346656037ull; + std::vector OldSurface,NewSurface; + }; + std::vector Results(Tiles.size()); + int32 MinChunkZ=MIN_int32,MaxChunkZ=MAX_int32,TopChunkZ=0,BottomChunkZ=0; + if(World.Settings->bClampViewToStrate&&World.Manager->GetStrateChunkZBounds( + StandaloneWorldMesher::CameraChunk(CameraVoxels).Z,TopChunkZ,BottomChunkZ)) + {MinChunkZ=BottomChunkZ*CHUNK_SIZE;MaxChunkZ=(TopChunkZ+1)*CHUNK_SIZE;} + + std::atomic Next{0}; + std::vector Threads; + const int32 ThreadCount=std::clamp(Workers,1,8); + Threads.reserve(ThreadCount); + for(int32 Worker=0;Worker(World.SkirtCells); + Mesher.LODOctaveDrop=World.LODOctaveDrop; + for(;;) + { + const size_t Index=Next.fetch_add(1,std::memory_order_relaxed); + if(Index>=Tiles.size())break; + const FVoxelTileKey& Tile=Tiles[Index]; + const int32 Cells=Tile.Level NewDensities,OldDensities; + FVoxelMeshData NewMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr, + MinChunkZ,MaxChunkZ,nullptr,nullptr,nullptr,&NewDensities); + FVoxelMeshData OldMesh; + { + FStandaloneShapeWorldAccess::FScopedBypass Bypass; + OldMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr, + MinChunkZ,MaxChunkZ,nullptr,nullptr,nullptr,&OldDensities); + } + FTileComparison& R=Results[Index]; + const size_t Shared=std::min(NewDensities.size(),OldDensities.size()); + R.Samples=Shared; + R.MeshMismatch=std::max(NewDensities.size(),OldDensities.size())-Shared; + for(size_t I=0;I(NewDensities[I]);R.NewHash*=1099511628211ull; + R.OldHash^=std::bit_cast(OldDensities[I]);R.OldHash*=1099511628211ull; + } + constexpr size_t VertexStride=4; + R.NewSurface.reserve(static_cast(NewMesh.Vertices.Num())/VertexStride+1); + for(int32 I=0;I(VertexStride)) + {const FVector& P=NewMesh.Vertices[I];R.NewSurface.push_back({P.X*0.01,P.Y*0.01,P.Z*0.01});} + R.OldSurface.reserve(static_cast(OldMesh.Vertices.Num())/VertexStride+1); + for(int32 I=0;I(VertexStride)) + {const FVector& P=OldMesh.Vertices[I];R.OldSurface.push_back({P.X*0.01,P.Y*0.01,P.Z*0.01});} + } + }); + } + for(std::thread& Thread:Threads)Thread.join(); + + uint64 Samples=0,Changed=0,MeshMismatch=0; + uint64 OldHash=14695981039346656037ull,NewHash=14695981039346656037ull; + size_t OldVertexCount=0,NewVertexCount=0; + std::vector OldSurface,NewSurface; + for(const FTileComparison& R:Results) + { + Samples+=R.Samples;Changed+=R.Changed;MeshMismatch+=R.MeshMismatch; + OldHash^=R.OldHash;OldHash*=1099511628211ull; + NewHash^=R.NewHash;NewHash*=1099511628211ull; + OldVertexCount+=R.OldSurface.size();NewVertexCount+=R.NewSurface.size(); + } + OldSurface.reserve(OldVertexCount);NewSurface.reserve(NewVertexCount); + for(FTileComparison& R:Results) + { + OldSurface.insert(OldSurface.end(),R.OldSurface.begin(),R.OldSurface.end()); + NewSurface.insert(NewSurface.end(),R.NewSurface.begin(),R.NewSurface.end()); + } + const FDirectedSurfaceDistance OldToNew=DirectedSurfaceDistance(OldSurface,NewSurface,1.0,3); + const FDirectedSurfaceDistance NewToOld=DirectedSurfaceDistance(NewSurface,OldSurface,1.0,3); + const uint64 DistanceSamples=OldToNew.Count+NewToOld.Count; + const double MeanDistance=DistanceSamples + ?(OldToNew.Sum+NewToOld.Sum)/static_cast(DistanceSamples):0.0; + std::printf("[OwnerShapeCompare] tiles=%zu fullres_levels=%d level_filter=%d lattice_samples=%llu changed_signs=%llu changed_share=%.6f%% old_hash=0x%016llx shape_hash=0x%016llx mesh_sample_mismatch=%llu old_surface_samples=%zu shape_surface_samples=%zu symmetric_vertex_mean_m=%.4f symmetric_vertex_max_m=%.4f unmatched_surface_samples=%llu vertex_stride=4 workers=%d\n", + Tiles.size(),World.FullResClipLevels,LevelFilter,static_cast(Samples), + static_cast(Changed),Samples?100.0*Changed/Samples:0.0, + static_cast(OldHash),static_cast(NewHash), + static_cast(MeshMismatch),OldSurface.size(),NewSurface.size(), + MeanDistance,std::max(OldToNew.Maximum,NewToOld.Maximum), + static_cast(OldToNew.Unmatched+NewToOld.Unmatched),ThreadCount); + std::fflush(stdout); + return Samples>0&&MeshMismatch==0?0:13; + } + int RunRoundWallComparison(FStandaloneOwnerWorld& World,V3 CameraPosition,int32 TileCount) { const FVector CameraVoxels(CameraPosition.X*4.0,CameraPosition.Y*4.0,CameraPosition.Z*4.0); @@ -1329,6 +1544,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; int RunStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels, int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels, bool bEnhancedWalls,const FStandaloneMeshCache* Cache, + bool bHashDensity, const std::string& BakeCachePath, const FStandaloneMeshCacheMetadata& CacheMetadata) { @@ -1349,18 +1565,21 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; std::array SamplesByLevel{}; std::array VerticesByLevel{}; std::array IndicesByLevel{}; + std::array TilesByLevel{}; std::array WorkerSecondsByLevel{}; std::vector CacheTiles; if(!BakeCachePath.empty())CacheTiles.reserve(Desired.Tiles.size()); const auto Start=std::chrono::steady_clock::now(); - FStandaloneStreamingGenerator Streamer(World,WorkerCount,true,Cache,MaxLevel); + FStandaloneStreamingGenerator Streamer(World,WorkerCount, + bHashDensity||!BakeCachePath.empty(),Cache,MaxLevel); const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels); const double LOD0Width=static_cast(2*World.ClipRadius+2) *CHUNK_SIZE*VOXEL_SIZE/100.0; - std::printf("[StreamBenchmarkConfig] clip_radius=%d lod0_cube=%.0fm voxel=25cm fullres_levels=%d max_level_filter=%d workers=%d full_detail_tiles=%d desired_tiles=%d variant=%s\n", + std::printf("[StreamBenchmarkConfig] clip_radius=%d lod0_cube=%.0fm voxel=25cm fullres_levels=%d max_level_filter=%d workers=%d full_detail_tiles=%d desired_tiles=%d variant=%s hash=%s\n", World.ClipRadius,LOD0Width,World.FullResClipLevels,MaxLevel<0?World.MaxClipLevel:MaxLevel, Streamer.GetWorkerCount(),FullDetailExpected,Update.DesiredTiles, - bRoundTunnels?(bEnhancedWalls?"round-detailed":"round-legacy"):"base"); + bRoundTunnels?(bEnhancedWalls?"round-detailed":"round-legacy"):"base", + (bHashDensity||!BakeCachePath.empty())?"on":"off"); std::fflush(stdout); int32 Finished=0,FullDetailFinished=0; bool bFullDetailTimeReported=false; @@ -1383,6 +1602,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; SamplesByLevel[Level]+=Tile.DensitySamples; VerticesByLevel[Level]+=Tile.Vertices.size(); IndicesByLevel[Level]+=Tile.Indices.size(); + ++TilesByLevel[Level]; WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds; if(Tile.bHasDensityHash)HashRows[Level].push_back({Tile.Key,Tile.DensityHash}); } @@ -1397,31 +1617,48 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; std::vector FullRows; for(int32 Level=0;Level(HashTileRows(FullRows))); + if(bHashDensity||!BakeCachePath.empty()) + std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 density_hash=0x%016llx\n", + FullDetailSeconds,FullDetailFinished,FullDetailExpected, + static_cast(HashTileRows(FullRows))); + else + std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 hash=off\n", + FullDetailSeconds,FullDetailFinished,FullDetailExpected); std::fflush(stdout); } } const double FullClipSeconds=std::chrono::duration( std::chrono::steady_clock::now()-Start).count(); std::vector AllRows; - for(auto& LevelRows:HashRows)AllRows.insert(AllRows.end(),LevelRows.begin(),LevelRows.end()); - std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs tiles=%d density_hash=0x%016llx\n", - FullClipSeconds,Finished,DesiredTotal, - std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0), - DesiredTotal,static_cast(HashTileRows(AllRows))); + if(bHashDensity||!BakeCachePath.empty()) + for(auto& LevelRows:HashRows)AllRows.insert(AllRows.end(),LevelRows.begin(),LevelRows.end()); + if(bHashDensity||!BakeCachePath.empty()) + std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs tiles=%d density_hash=0x%016llx\n", + FullClipSeconds,Finished,DesiredTotal, + std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0), + DesiredTotal,static_cast(HashTileRows(AllRows))); + else + std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs tiles=%d hash=off\n", + FullClipSeconds,Finished,DesiredTotal, + std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0),DesiredTotal); std::printf("[StreamCacheUse] hits=%llu requested=%d\n", static_cast(Streamer.GetCacheHits()),DesiredTotal); for(int32 Level=0;Level<=MaxLevels;++Level) { - if(HashRows[Level].empty())continue; - const uint64 LevelHash=HashTileRows(HashRows[Level]); - std::printf("[StreamLevel] lod=%d tiles=%zu samples=%.3fM worker_sum=%.3fs vertices=%llu indices=%llu density_hash=0x%016llx\n", - Level,HashRows[Level].size(),static_cast(SamplesByLevel[Level])/1.0e6, - WorkerSecondsByLevel[Level],static_cast(VerticesByLevel[Level]), - static_cast(IndicesByLevel[Level]), - static_cast(LevelHash)); + if(TilesByLevel[Level]==0)continue; + if(bHashDensity||!BakeCachePath.empty()) + std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs vertices=%llu indices=%llu density_hash=0x%016llx\n", + Level,static_cast(TilesByLevel[Level]), + static_cast(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level], + static_cast(VerticesByLevel[Level]), + static_cast(IndicesByLevel[Level]), + static_cast(HashTileRows(HashRows[Level]))); + else + std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs vertices=%llu indices=%llu hash=off\n", + Level,static_cast(TilesByLevel[Level]), + static_cast(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level], + static_cast(VerticesByLevel[Level]), + static_cast(IndicesByLevel[Level])); } if(!BakeCachePath.empty()) { @@ -1447,7 +1684,9 @@ int main(int Argc,char** Argv) {if(!JoinedCommandLine.empty())JoinedCommandLine.push_back(' ');JoinedCommandLine+=Argv[I];} FCommandLine::Set(JoinedCommandLine); int32 BenchmarkLevel=-1,BenchmarkCount=64,StreamBenchmarkMaxLevel=-1; + int32 ShapeCompareLevel=-1,ShapeCompareTileLimit=0; bool bStreamBenchmark=false; + bool bStreamHash=false; bool bRoundWallCompare=false; int32 RoundCompareTileCount=16; std::string BakeMeshCachePath; @@ -1459,10 +1698,25 @@ int main(int Argc,char** Argv) else if(Arg=="--compare-evaluators")App.bCompareEvaluators=true; else if(Arg=="--parity-only")App.bParityOnly=true; else if(Arg=="--density-hash")App.bDensityHash=true; + else if(Arg=="--shape-world")App.bShapeWorld=true; + else if(Arg=="--shape-audit")App.bShapeWorld=true,App.bShapeAuditOnly=true; + else if(Arg=="--shape-compare")App.bShapeWorld=true,App.bShapeCompare=true; + else if(Arg.starts_with("--shape-compare-level=")) + ShapeCompareLevel=std::atoi(std::string(Arg.substr(std::string_view("--shape-compare-level=").size())).c_str()); + else if(Arg.starts_with("--shape-compare-tile-limit=")) + ShapeCompareTileLimit=std::max(0,std::atoi(std::string(Arg.substr(std::string_view("--shape-compare-tile-limit=").size())).c_str())); + else if(Arg.starts_with("--shape-detail=")) + { + const std::string_view Value=Arg.substr(15); + if(Value=="smooth")App.ShapeDetailVariant=FStandaloneShapeWorld::EDetailVariant::Smooth; + else if(Value=="per-voxel")App.ShapeDetailVariant=FStandaloneShapeWorld::EDetailVariant::PerVoxel; + else if(Value!="coherent"){std::fprintf(stderr,"Invalid --shape-detail value: %.*s\n",static_cast(Value.size()),Value.data());return 2;} + } else if(Arg=="--round-tunnels")App.bRoundTunnels=true; else if(Arg=="--round-tunnels-legacy") {App.bRoundTunnels=true;App.bRoundTunnelEnhanced=false;} else if(Arg=="--stream-benchmark")bStreamBenchmark=true; + else if(Arg=="--stream-hash")bStreamHash=true; else if(Arg=="--compare-round-walls") {bRoundWallCompare=true;App.bRoundTunnels=true;} else if(Arg.starts_with("--round-compare-tiles=")) @@ -1518,6 +1772,17 @@ int main(int Argc,char** Argv) if(App.bRoundTunnels&&!PrepareStandaloneDensityWorld(*App.World,App.Seed, App.bRoundTunnels,App.bRoundTunnelEnhanced,Error)) {std::fprintf(stderr,"Standalone world setup failed: %s\n",Error.c_str());return 2;} + const bool bNeedFarShapeWorld=bStreamBenchmark + ?(StreamBenchmarkMaxLevel<0||StreamBenchmarkMaxLevel>=App.World->FullResClipLevels) + :(App.bShapeCompare?ShapeCompareLevel>=App.World->FullResClipLevels + :(BenchmarkLevel>=App.World->FullResClipLevels)); + if(App.bShapeWorld&&(App.bShapeAuditOnly||App.bShapeCompare||bStreamBenchmark||BenchmarkLevel>=0) + &&!App.BuildShapeWorldForCamera(Error,bNeedFarShapeWorld)) + {std::fprintf(stderr,"Shape interval build failed: %s\n",Error.c_str());return 2;} + if(App.bShapeAuditOnly)return App.ShapeWorld->IsDetailAuditPassing()?0:7; + if(App.bShapeCompare) + return RunOwnerShapeComparison(*App.World,*App.ShapeWorld,App.Position,App.WorkerCount, + ShapeCompareLevel,ShapeCompareTileLimit); if(bRoundWallCompare) return RunRoundWallComparison(*App.World,App.Position,RoundCompareTileCount); if(bStreamBenchmark) @@ -1526,12 +1791,12 @@ int main(int Argc,char** Argv) *CHUNK_SIZE*VOXEL_SIZE/100.0; std::printf("Standalone clip override: exported radius=%d (112 m cube) -> radius=%d (%.0f m LOD0 cube), voxel size=25 cm.\n", ExportClipRadius,App.ClipRadius,FullDetailWidth); - if(!App.bForceGenerate)App.LoadMeshCache(); + if(!App.bForceGenerate&&!App.bShapeWorld)App.LoadMeshCache(); return RunStreamingBenchmark(*App.World, FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0), App.WorkerCount,StreamBenchmarkMaxLevel,App.bRoundTunnels, - App.bRoundTunnelEnhanced,App.MeshCache.IsLoaded()?&App.MeshCache:nullptr, - BakeMeshCachePath,App.MakeMeshCacheMetadata()); + App.bRoundTunnelEnhanced,(!App.bShapeWorld&&App.MeshCache.IsLoaded())?&App.MeshCache:nullptr, + bStreamHash,BakeMeshCachePath,App.MakeMeshCacheMetadata()); } if(BenchmarkLevel>=0) return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount, @@ -1546,7 +1811,7 @@ int main(int Argc,char** Argv) GApp=&App; if(!App.Window||!App.InitializeDevice()){MessageBoxA(nullptr,"Direct3D 11 initialization failed.","VoxelForge standalone",MB_ICONERROR);return 3;} ShowWindow(App.Window,SW_SHOW);UpdateWindow(App.Window); - std::printf("Controls: hold right mouse to look; WASD move; Space/Ctrl (E/Q) up/down; Shift faster; wheel changes speed; F wireframe; G toggles legacy/detailed round walls; 1-9 toggle LOD; 0 toggle all levels; N/P change seed; T swaps base/round tunnels; Esc quit.\n"); + std::printf("Controls: hold right mouse to look; WASD move; Space/Ctrl (E/Q) up/down; Shift faster; wheel changes speed; F wireframe; G toggles legacy/detailed round walls; 1-9 toggle LOD; 0 toggle all levels; N/P change seed; T swaps base/round tunnels; V swaps owner density/shape intervals; Esc quit.\n"); std::printf("Owner data: %s; starting at first-strate open-space sample (%.2f, %.2f, %.2f) m.\n", App.DataPath.c_str(),App.Position.X,App.Position.Y,App.Position.Z); std::fflush(stdout); diff --git a/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp b/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp index 2a83ecf..31ec92f 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -6,6 +6,7 @@ #include "VoxelGenerator.h" #include "StandaloneResearchTunnels.h" +#include "StandaloneShapeWorld.h" #include "VoxelSettings.h" #include "VoxelStrateManager.h" #include "VoxelStrateDefinition.h" @@ -2849,6 +2850,12 @@ bool UVoxelGenerator::EvaluateDensityChunk( float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ, float* OutPreEditDensity) const { + if (const FStandaloneShapeWorld* ShapeWorld=FStandaloneShapeWorldAccess::GetActive()) + { + const float ShapeDensity=ShapeWorld->DensityAt(WorldX,WorldY,WorldZ); + if (OutPreEditDensity) *OutPreEditDensity=ShapeDensity; + return ShapeDensity; + } #if WITH_EDITOR // Step 2e and step 2d are separate editor-only, default-off side paths. The owner field stays // authoritative unless a research commandlet or automation test explicitly selects one. @@ -6469,6 +6476,19 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::TileClassifyCalls); + if (const FStandaloneShapeWorld* ShapeWorld=FStandaloneShapeWorldAccess::GetActive()) + { + const EVoxelTileClass ShapeVerdict=ShapeWorld->ClassifyBox( + OriginVoxels,NormalizedStep,NormalizedCellsPerAxis); + if (ShapeVerdict==EVoxelTileClass::AllSolid) + VoxelDensityProfile::AddCounter(VoxelDensityProfile::ECounter::TileClassifyAllSolid); + else if (ShapeVerdict==EVoxelTileClass::AllAir) + VoxelDensityProfile::AddCounter(VoxelDensityProfile::ECounter::TileClassifyAllAir); + else + VoxelDensityProfile::AddCounter(VoxelDensityProfile::ECounter::TileClassifyMixed); + return ShapeVerdict; + } + #if WITH_EDITOR // The ordinary tile classifier proves the live field's interval bounds. While the separate // research field is selected, decline those proofs until its own complete final-field interval