From 557f6aac3006c0c53386dc3a44be6be416830611 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 5 Oct 2026 11:39:02 +0200 Subject: [PATCH] standalone: VoxelForgeCore library -- the fast generator behind a clean C++20 API, output unchanged - Standalone/Core: public header VoxelForgeCore.h (no Unreal types), static library target, ResolvedWorldSnapshot (from the schema-v1 JSON), World (compile, generateTile, density/sign, point/segment solid, classifyBox), host-supplied JobScheduler (the core starts no threads), sequenced, idempotent EditTransaction returning affected tiles per LOD. - The viewer, benchmarks, edits and audits run on Core; StreamingWorld stays host code. - Same as round 11: 11 hash rows at W1 and W8, routes 5642/5642, look/passage/far audits pass, bad-shell control fails, six fixed captures byte-identical; cold W8 1.025 s vs 1.041 s reference. - SHARED_CPP_LIBRARY_PLAN.md rewritten for a UBT module compiling the same sources via shims. Open issue before Unreal: Core's implementation still compiles the Compat stand-ins named like Unreal types (TArray, FVector...); inside a UE binary that is an ODR clash. Must be namespaced. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- Standalone/Build.ps1 | 100 +- Standalone/Core/API.md | 111 + Standalone/Core/core_sources.txt | 20 + .../include/VoxelForgeCore/VoxelForgeCore.h | 235 + Standalone/Core/src/FarShellsInternal.h | 821 ++++ Standalone/Core/src/ResearchTunnelsInternal.h | 173 + Standalone/Core/src/ShapeWorldInternal.h | 3724 ++++++++++++++++ .../Core/src/StandaloneSnapshotAdapter.h | 12 + Standalone/Core/src/VoxelForgeCore.cpp | 484 +++ Standalone/README.md | 32 +- Standalone/SHARED_CPP_LIBRARY_PLAN.md | 180 +- Standalone/Source/FarSurvivalAudit.h | 35 +- Standalone/Source/OwnerWorldLoader.h | 18 +- Standalone/Source/StandaloneCoreScheduler.h | 52 + Standalone/Source/StandaloneResearchTunnels.h | 182 +- Standalone/Source/StandaloneShapeWorld.h | 3765 +---------------- Standalone/Source/StandaloneViewer.cpp | 329 +- Standalone/Source/StreamingWorld.h | 893 +--- .../VoxelForge/Private/VoxelGenerator.cpp | 29 +- .../Private/VoxelMarchingCubesMesher.cpp | 41 +- .../Source/VoxelForge/Public/VoxelGenerator.h | 5 + .../Public/VoxelMarchingCubesMesher.h | 7 + 22 files changed, 6185 insertions(+), 5063 deletions(-) create mode 100644 Standalone/Core/API.md create mode 100644 Standalone/Core/core_sources.txt create mode 100644 Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h create mode 100644 Standalone/Core/src/FarShellsInternal.h create mode 100644 Standalone/Core/src/ResearchTunnelsInternal.h create mode 100644 Standalone/Core/src/ShapeWorldInternal.h create mode 100644 Standalone/Core/src/StandaloneSnapshotAdapter.h create mode 100644 Standalone/Core/src/VoxelForgeCore.cpp create mode 100644 Standalone/Source/StandaloneCoreScheduler.h diff --git a/Standalone/Build.ps1 b/Standalone/Build.ps1 index 746143b..cb41a9b 100644 --- a/Standalone/Build.ps1 +++ b/Standalone/Build.ps1 @@ -1,72 +1,82 @@ [CmdletBinding()] -param([string]$OutputName = 'VoxelForgeStandalone.exe') +param( + [string]$OutputName = 'VoxelForgeCoreStandalone.exe', + [ValidateSet('All','Core')][string]$Target = 'All' +) $ErrorActionPreference = 'Stop' $BuildHostProcess = Get-Process -Id $PID $BuildHostProcess.PriorityClass = 'BelowNormal' $BuildHostProcess.ProcessorAffinity = [IntPtr]0xFF00 $Root = [IO.Path]::GetFullPath($PSScriptRoot) -$BuildDir = Join-Path $Root 'Build' +$BuildDir = Join-Path $Root 'Build\VoxelForgeCore' $BinDir = Join-Path $Root 'bin' -if ([IO.Path]::GetFileName($OutputName) -ne $OutputName -or [IO.Path]::GetExtension($OutputName) -ne '.exe') { +[IO.Directory]::CreateDirectory($BuildDir) | Out-Null +[IO.Directory]::CreateDirectory($BinDir) | Out-Null + +if ($Target -eq 'All' -and ([IO.Path]::GetFileName($OutputName) -ne $OutputName -or [IO.Path]::GetExtension($OutputName) -ne '.exe')) { throw "OutputName must be an executable filename, not a path: $OutputName" } $OutputPath = Join-Path $BinDir $OutputName -[IO.Directory]::CreateDirectory($BuildDir) | Out-Null -[IO.Directory]::CreateDirectory($BinDir) | Out-Null +$CoreLibrary = Join-Path $BuildDir 'VoxelForgeCore.lib' $VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat' if (-not (Test-Path -LiteralPath $VcVars -PathType Leaf)) { throw "MSVC environment script not found: $VcVars" } -$Sources = @( - (Join-Path $Root 'Source\StandaloneViewer.cpp'), - (Join-Path $Root 'Compat\UnrealPerlin.cpp'), - (Join-Path $Root 'Compat\StandaloneDataAssetGlue.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelBiomeDefinition.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelCaveMorphology.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelDensityAblation.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelDensityOpStack.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelDensityProfile.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelDiffLayer.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelGenerator.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelHeightOpStack.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelMarchingCubesMesher.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelSettings.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelStrateComposer.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelStrateManager.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelStrateMeasure.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelTerrainOpDefinition.cpp'), - (Join-Path $Root 'Source\VoxelForge\Private\VoxelWormField.cpp') -) -foreach ($Source in $Sources) { - if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { throw "Required source is missing: $Source" } +$SourceManifest = Join-Path $Root 'Core\core_sources.txt' +if (-not (Test-Path -LiteralPath $SourceManifest -PathType Leaf)) { throw "Core source manifest not found: $SourceManifest" } +$CoreSourceNames = @(Get-Content -LiteralPath $SourceManifest | ForEach-Object { $_.Trim() } | + Where-Object { $_ -and -not $_.StartsWith('#') }) +if ($CoreSourceNames.Count -eq 0) { throw "Core source manifest is empty: $SourceManifest" } +$CoreSources = @($CoreSourceNames | ForEach-Object { + $SourcePath = [IO.Path]::GetFullPath((Join-Path $Root $_)) + if (-not $SourcePath.StartsWith($Root,[StringComparison]::OrdinalIgnoreCase)) { + throw "Core source path escapes Standalone: $_" + } + $SourcePath +}) +foreach ($Source in $CoreSources) { + if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { throw "Required core source is missing: $Source" } } -$ResponsePath = Join-Path $BuildDir 'standalone.rsp' -$RspLines = @( - '/nologo', '/std:c++20', '/O2', '/arch:AVX2', '/fp:precise', '/EHsc', '/MP8', - '/D_CRT_SECURE_NO_WARNINGS', +$IncludeArgs = @( + ('/I"{0}"' -f (Join-Path $Root 'Core\include')), + ('/I"{0}"' -f (Join-Path $Root 'Core\src')), ('/I"{0}"' -f (Join-Path $Root 'Compat')), ('/I"{0}"' -f (Join-Path $Root 'Source')), ('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Public')), - ('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Private')), - ('/Fe"{0}"' -f $OutputPath) + ('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Private')) ) -# Run at most eight compiler processes on the half-load runner. -foreach ($Source in $Sources) { $RspLines += ('"{0}"' -f $Source) } -$RspLines += @('/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib') -[IO.File]::WriteAllLines($ResponsePath,$RspLines,[Text.UTF8Encoding]::new($false)) +$CommonFlags = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc','/MP8','/D_CRT_SECURE_NO_WARNINGS') + $IncludeArgs +$CoreRsp = Join-Path $BuildDir 'core-compile.rsp' +$CoreRspLines = $CommonFlags + @('/c') + @($CoreSources | ForEach-Object { '"{0}"' -f $_ }) +[IO.File]::WriteAllLines($CoreRsp,$CoreRspLines,[Text.UTF8Encoding]::new($false)) + +$Objects = @($CoreSources | ForEach-Object { '"{0}.obj"' -f [IO.Path]::GetFileNameWithoutExtension($_) }) +$LibRsp = Join-Path $BuildDir 'core-library.rsp' +[IO.File]::WriteAllLines($LibRsp,@('/nologo',('/OUT:"{0}"' -f $CoreLibrary)) + $Objects,[Text.UTF8Encoding]::new($false)) + +$ViewerRsp = Join-Path $BuildDir 'viewer-link.rsp' +$ViewerRspLines = $CommonFlags + @( + ('/Fe"{0}"' -f $OutputPath), + ('"{0}"' -f (Join-Path $Root 'Source\StandaloneViewer.cpp')), + ('"{0}"' -f $CoreLibrary), + '/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib' +) +[IO.File]::WriteAllLines($ViewerRsp,$ViewerRspLines,[Text.UTF8Encoding]::new($false)) Push-Location $BuildDir try { - # 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 + # One cl process with /MP8 bounds compiler work to eight processes; MSVC, lib.exe, and the + # viewer link inherit BelowNormal priority and the 0xFF00 process affinity from this host. + $CommandLine = 'call "{0}" >nul && cl.exe @"{1}" && lib.exe @"{2}"' -f $VcVars,$CoreRsp,$LibRsp + if ($Target -eq 'All') { $CommandLine += ' && cl.exe @"{0}"' -f $ViewerRsp } & $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" - } + if ($LASTEXITCODE -eq -1073741502) { throw 'A build process exited with 0xC0000142; stopping as required.' } + if ($LASTEXITCODE -ne 0) { throw "VoxelForgeCore build failed with exit code $LASTEXITCODE" } + if (-not (Test-Path -LiteralPath $CoreLibrary -PathType Leaf)) { throw "Core library was not produced: $CoreLibrary" } + if ($Target -eq 'All' -and -not (Test-Path -LiteralPath $OutputPath -PathType Leaf)) { throw "Viewer was not produced: $OutputPath" } } finally { Pop-Location } -Write-Host "Built $OutputPath" +Write-Host "Built $CoreLibrary" +if ($Target -eq 'All') { Write-Host "Built $OutputPath" } diff --git a/Standalone/Core/API.md b/Standalone/Core/API.md new file mode 100644 index 0000000..0e497a6 --- /dev/null +++ b/Standalone/Core/API.md @@ -0,0 +1,111 @@ +# VoxelForgeCore API reference + +Public header: `Core/include/VoxelForgeCore/VoxelForgeCore.h` (C++20, standard library only). +Build the static library and viewer with `Build.ps1 -Target All`, or only the library with +`Build.ps1 -Target Core`. + +## Units and ownership + +- `Vec3d` query positions and `Vec3i` voxel coordinates are in **voxels**. +- `TileKey.coordinate` is a tile coordinate; one coordinate step spans `32 << lod` voxels. +- Mesh vertex positions are **centimetres**. UVs, RGBA8 colors, normals and 32-bit indices are + returned per vertex / triangle in owned `std::vector`s. +- `MeshGroups::groundTriangleCount` and `ceilingTriangleCount` describe the contiguous index runs + emitted by the mesher. Ceiling is the final run. `skirtTriangleCount` is a subset of the ground + and ceiling runs. `surfaceVertexCount` ends before skirt vertices. +- A `World` owns its compiled field and edit log. The snapshot can be shared as immutable input. + Serialize compile/edit changes against query and tile-generation calls on that same `World`. + +## Load and compile + +`ResolvedWorldSnapshot::FromJson` accepts the normalized UTF-8 schema-v1 owner export used by the +standalone loader. It returns `std::nullopt` and fills `error` when parsing or asset resolution +fails. Read `configuration()` for the resolved scalar settings. + +Implement `JobScheduler::parallel_for` in the host. It must run each index from zero to `count - 1` +exactly once and return only after all submitted work is complete. The library creates no threads. +`ShapeBuildRequest` supplies the detail and clip bounds in voxels, worker limit, detail variant, +far-world inclusion, and optional tunnel/detail profiles. + +```cpp +std::string error; +auto snapshot = VoxelForgeCore::ResolvedWorldSnapshot::FromJson(json, error); +if (!snapshot) { /* report error */ } + +VoxelForgeCore::World world(std::move(*snapshot)); +VoxelForgeCore::ShapeBuildRequest build; +build.minVoxel = {-256, -768, -384}; +build.maxVoxel = { 256, -256, 128}; +build.clipMinVoxel = {-57344, -57344, -8192}; +build.clipMaxVoxel = { 49152, 49152, 8192}; +build.workerLimit = 8; +build.includeFarWorld = true; +if (!world.compile(build, hostScheduler, error)) { /* report error */ } +``` + +## Queries and tiles + +`densityAt(position)` returns signed density; negative is solid and nonnegative is air. +`isSolidAt` wraps that sign test. `isSolidAlongSegment(start, end, spacing)` samples evenly along +the segment with no interval wider than `spacing` voxels; it returns true if any sample is solid. +`classifyBox(origin, step, cells)` returns `allSolid`, `allAir` or `mixed` for mesher tile +classification. + +`desiredTiles(cameraVoxels)` returns the snapshot's clipmap set. Pass it to +`prioritizeTiles(tiles, centerChunk)` when submitting work. Generate independent keys concurrently +on host workers: + +```cpp +VoxelForgeCore::TileGenerationOptions options; +auto mesh = world.generateTile(key, options, error); +if (!mesh) { /* report error */ } +// mesh->vertices, mesh->indices and mesh->groups are owned by this result. +``` + +The optional `hashDensity` flag fills deterministic density, viewer-mesh, payload and topology +hashes. `captureDensitySamples` is a diagnostic option and retains the sampled lattice values. + +## Deterministic edits + +Build an `EditTransaction` with a monotonically increasing authority sequence and an ordered brush +array (`sphere`, `capsule`, `roundedBox`; `carve`, `addRock`, `densityOffset`). `carve` and +`addRock` apply hard signed-distance CSG. `densityOffset` applies `strength` inside a sphere or +inside/around a capsule or box with its `falloff` band; negative strength carves and positive +strength adds rock. This operation maps the plugin's soft `FVoxelModification` brushes into the +shared field. Apply the transaction against the currently desired keys: + +```cpp +VoxelForgeCore::EditTransaction edit; +edit.sequence = 12; +VoxelForgeCore::EditBrush brush; +brush.shape = VoxelForgeCore::EditBrush::Shape::capsule; +brush.operation = VoxelForgeCore::EditBrush::Operation::densityOffset; +brush.a = {0.0, 0.0, 0.0}; +brush.b = {12.0, 0.0, 0.0}; +brush.radius = 3.0; +brush.strength = -10.0f; +brush.falloff = 2.0f; +edit.brushes.push_back(brush); +const auto affected = world.applyEditTransaction(edit, currentTiles, error); +if (affected) { + for (const auto& lodTiles : affected->affectedByLod) + for (const auto& key : lodTiles) + submitHostJob([&, key] { + std::string tileError; + return world.generateTile(key, options, tileError); + }); +} +``` + +The transaction digest covers the sequence and each brush field in order. Replaying the same +sequence and values returns the original result; reusing a sequence with different values or +applying an older sequence is rejected. Each client must deserialize identical brush values and +apply transactions in the same sequence. `affectedByLod` is limited to the desired keys passed to +the call. + +## Other outputs + +`farConnectorSegments()` returns owned connector geometry, while `farConnectorCandidates(min,max)` +returns candidate indices overlapping a voxel-space box. The audit methods expose shape detail, +surface-family and camera-look checks for host diagnostics. All returned values are standard C++; +the host owns conversion to engine mesh, collision or renderer objects. diff --git a/Standalone/Core/core_sources.txt b/Standalone/Core/core_sources.txt new file mode 100644 index 0000000..c6bac3a --- /dev/null +++ b/Standalone/Core/core_sources.txt @@ -0,0 +1,20 @@ +# One relative source path per translation unit. Build.ps1 consumes this manifest; a future UBT +# module should use the same list to generate one thin source include shim per row. +Core/src/VoxelForgeCore.cpp +Compat/UnrealPerlin.cpp +Compat/StandaloneDataAssetGlue.cpp +Source/VoxelForge/Private/VoxelBiomeDefinition.cpp +Source/VoxelForge/Private/VoxelCaveMorphology.cpp +Source/VoxelForge/Private/VoxelDensityAblation.cpp +Source/VoxelForge/Private/VoxelDensityOpStack.cpp +Source/VoxelForge/Private/VoxelDensityProfile.cpp +Source/VoxelForge/Private/VoxelDiffLayer.cpp +Source/VoxelForge/Private/VoxelGenerator.cpp +Source/VoxelForge/Private/VoxelHeightOpStack.cpp +Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +Source/VoxelForge/Private/VoxelSettings.cpp +Source/VoxelForge/Private/VoxelStrateComposer.cpp +Source/VoxelForge/Private/VoxelStrateManager.cpp +Source/VoxelForge/Private/VoxelStrateMeasure.cpp +Source/VoxelForge/Private/VoxelTerrainOpDefinition.cpp +Source/VoxelForge/Private/VoxelWormField.cpp diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h new file mode 100644 index 0000000..2830875 --- /dev/null +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -0,0 +1,235 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace VoxelForgeCore +{ + struct Vec2f { float x = 0.0f, y = 0.0f; }; + struct Vec3d { double x = 0.0, y = 0.0, z = 0.0; }; + struct Vec3i { std::int32_t x = 0, y = 0, z = 0; }; + + struct TileKey + { + Vec3i coordinate{}; // Units of (32 << lod) voxels. + std::int32_t lod = 0; + friend bool operator==(const TileKey&, const TileKey&) = default; + }; + + struct TileKeyHash + { + std::size_t operator()(const TileKey& key) const noexcept; + }; + + struct ColorRgba8 { std::uint8_t r = 0, g = 0, b = 0, a = 0; }; + + struct MeshVertex + { + Vec3d position; // World centimetres, matching the density field coordinate system. + Vec3d normal; + Vec2f uv; + ColorRgba8 color; + }; + + struct MeshGroups + { + std::uint32_t surfaceVertexCount = 0; + std::uint32_t groundTriangleCount = 0; // Ground index run; includes skirts emitted from ground faces. + std::uint32_t ceilingTriangleCount = 0; // Final index run belongs to the ceiling group. + std::uint32_t skirtTriangleCount = 0; + }; + + struct TileMesh + { + TileKey key; + std::vector vertices; + std::vector indices; + std::vector debugDensitySamples; + MeshGroups groups; + std::uint64_t densitySampleCount = 0; + std::uint64_t meshPayloadAllocatedBytes = 0; + std::uint64_t densityHash = 0, meshHash = 0, payloadHash = 0, topologyHash = 0; + bool hasDensityHash = false, hasMeshHash = false, hasPayloadHash = false, hasTopologyHash = false; + }; + + struct WorldConfiguration + { + std::int32_t seed = 0; + std::int32_t clipRadius = 0; + std::int32_t maxLod = 0; + std::int32_t fullResolutionLodCount = 0; + std::int32_t coarseTileCells = 0; + std::int32_t renderDistanceChunks = 0; + std::int32_t contentCutMinLod = 0; + std::int32_t skirtCells = 0; + std::int32_t lodOctaveDrop = 0; + std::int32_t viewDistanceUp = 0; + std::int32_t viewDistanceDown = 0; + std::int32_t strateViewMarginChunks = 0; + bool clampViewToStrate = false; + bool generateSkirts = false; + bool viewerStartFound = false; + Vec3d viewerStartVoxels{}; + }; + + class ResolvedWorldSnapshot + { + public: + ResolvedWorldSnapshot() = default; + + // Schema-v1 is the normalized, resolved UTF-8 owner snapshot written by the standalone + // exporter. Hosts may build the same document from their own resolved assets. + static std::optional FromJson( + std::string_view json, std::string& error); + + explicit operator bool() const noexcept { return static_cast(data_); } + const WorldConfiguration& configuration() const; + + private: + struct Data; + explicit ResolvedWorldSnapshot(std::shared_ptr data) : data_(std::move(data)) {} + std::shared_ptr data_; + friend class World; + friend struct detail_StandaloneSnapshotAdapter; + }; + + // The library requests work; the host decides whether that means a pool, fibers, or serial + // execution. The callback must run every index in [0,count) exactly once before returning. + class JobScheduler + { + public: + using Task = void(*)(void* context, std::size_t index, std::size_t workerIndex); + virtual ~JobScheduler() = default; + virtual double parallel_for(std::size_t count, std::size_t batchSize, + std::size_t workerLimit, void* context, Task task, + double* workerActiveSeconds, + std::size_t workerActiveCapacity) const = 0; + }; + + enum class DetailVariant : std::uint8_t { coherent, smooth, perVoxel }; + enum class TileClass : std::uint8_t { allSolid, allAir, mixed }; + + struct ShapeBuildRequest + { + Vec3i minVoxel{}; + Vec3i maxVoxel{}; + Vec3i clipMinVoxel{}; + Vec3i clipMaxVoxel{}; + std::uint32_t workerLimit = 1; + DetailVariant detail = DetailVariant::coherent; + bool includeFarWorld = false; + bool roundTunnels = false; + bool enhancedTunnelWalls = true; + bool profileDetailBand = false; + }; + + struct ShapeBuildStats + { + double shapeSeconds = 0.0; + double intervalSeconds = 0.0; + double surfaceSeconds = 0.0; + double farShapeSeconds = 0.0; + std::uint64_t primitiveCount = 0; + std::uint64_t surfaceCandidates = 0; + std::uint64_t detailFlips = 0; + std::uint64_t walkSamples = 0; + std::uint64_t walkFailures = 0; + }; + + struct FarConnectorSegment { Vec3d start, end; float radius = 0.0f; }; + + struct EditBrush + { + enum class Shape : std::uint8_t { sphere, capsule, roundedBox }; + enum class Operation : std::uint8_t { carve, addRock, densityOffset }; + Shape shape = Shape::sphere; + Operation operation = Operation::carve; + Vec3d a{}, b{}, halfExtent{}; + double radius = 0.0; + double rounding = 0.0; + float strength = 0.0f; // densityOffset only: negative carves, positive adds rock. + float falloff = 0.0f; // densityOffset fade band in voxels. + }; + + struct EditTransaction + { + // Sequence is supplied by the authority and participates in the deterministic digest. + std::uint64_t sequence = 0; + std::vector brushes; + }; + + struct EditResult + { + std::uint64_t digest = 0; + std::vector> affectedByLod; + }; + + struct TileGenerationOptions + { + std::int32_t bandMinZVoxel = -2147483647 - 1; + std::int32_t bandMaxZVoxel = 2147483647; + bool hashDensity = false; + bool captureDensitySamples = false; + bool skipUvAndColors = false; + bool skipUv = false; + bool skipColors = false; + bool skipMaterialGroups = false; + }; + + class World + { + public: + explicit World(ResolvedWorldSnapshot snapshot); + ~World(); + World(World&&) noexcept; + World& operator=(World&&) noexcept; + World(const World&) = delete; + World& operator=(const World&) = delete; + + bool compile(const ShapeBuildRequest& request, const JobScheduler& scheduler, + std::string& error); + void buildFarWorld(); + bool isCompiled() const noexcept; + bool hasFarWorld() const noexcept; + ShapeBuildStats buildStats() const; + + float densityAt(Vec3d voxelPosition) const; + float densityAtLattice(Vec3i voxelPosition) const; + bool isSolidAt(Vec3d voxelPosition) const; + bool isSolidAlongSegment(Vec3d start, Vec3d end, double sampleSpacingVoxels = 0.5) const; + TileClass classifyBox(Vec3i origin, std::int32_t step, std::int32_t cells) const; + + std::vector desiredTiles(Vec3d cameraPositionVoxels) const; + static std::vector prioritizeTiles(std::span tiles, + Vec3i centerChunk); + std::optional generateTile(const TileKey& key, + const TileGenerationOptions& options, + std::string& error) const; + + std::optional applyEditTransaction(const EditTransaction& transaction, + std::span desiredTiles, + std::string& error); + std::vector farConnectorSegments() const; + std::vector farConnectorCandidates(Vec3i minVoxel, Vec3i maxVoxel) const; + + bool auditDetailPassing() const; + bool auditSurfaceFamilies(bool passageOnly = false) const; + bool auditViewSurface(Vec3d cameraVoxel, double yawDegrees, double pitchDegrees) const; + bool debugCriticalAirAt(Vec3d voxelPosition) const; + bool debugSolidOverlayAt(Vec3d voxelPosition) const; + + const WorldConfiguration& configuration() const; + + private: + struct Impl; + std::unique_ptr impl_; + }; +} diff --git a/Standalone/Core/src/FarShellsInternal.h b/Standalone/Core/src/FarShellsInternal.h new file mode 100644 index 0000000..4dd60e5 --- /dev/null +++ b/Standalone/Core/src/FarShellsInternal.h @@ -0,0 +1,821 @@ +#pragma once + +#include "OwnerWorldLoader.h" +#include "StandaloneShapeWorld.h" +#include "VoxelTypes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct FFarMeshSegmentIndex +{ + FFarMeshSegmentIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent, + int32 InCells,double InStep) + :Mesh(InMesh),Origin(InOrigin),Extent(InExtent),Cells(std::max(1,InCells)),Step(InStep), + ByCell(static_cast(Cells)*Cells*Cells), + TriangleMarks(static_cast(InMesh.Triangles.Num()/3),0) + { + for(int32 Triangle=0;Triangle+2=Mesh.Vertices.Num()||B>=Mesh.Vertices.Num()||C>=Mesh.Vertices.Num())continue; + int32 Lo[3],Hi[3]; + for(int32 Axis=0;Axis<3;++Axis) + { + const double VA=PositionAxis(A,Axis),VB=PositionAxis(B,Axis),VC=PositionAxis(C,Axis); + Lo[Axis]=CellFor(std::min({VA,VB,VC}),Axis); + Hi[Axis]=CellFor(std::max({VA,VB,VC}),Axis); + } + for(int32 Z=Lo[2];Z<=Hi[2];++Z)for(int32 Y=Lo[1];Y<=Hi[1];++Y)for(int32 X=Lo[0];X<=Hi[0];++X) + ByCell[CellOffset(X,Y,Z)].push_back(Triangle/3); + } + } + + double NearestHitDifference(const FVector& Start,const FVector& Direction,double Length, + double TargetDistance) + { + double BestDifference=std::numeric_limits::infinity(); + ++QueryStamp; + if(QueryStamp==0){std::fill(TriangleMarks.begin(),TriangleMarks.end(),0);QueryStamp=1;} + const auto TestCell=[&](int32 X,int32 Y,int32 Z) + { + if(X<0||X>=Cells||Y<0||Y>=Cells||Z<0||Z>=Cells)return; + for(const int32 Triangle:ByCell[CellOffset(X,Y,Z)]) + { + if(TriangleMarks[static_cast(Triangle)]==QueryStamp)continue; + TriangleMarks[static_cast(Triangle)]=QueryStamp; + const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],IC=Mesh.Triangles[Triangle*3+2]; + const FVector A=PositionVector(IA),B=PositionVector(IB),C=PositionVector(IC); + const FVector E1=B-A,E2=C-A,P=FVector::CrossProduct(Direction,E2); + const double Determinant=FVector::DotProduct(E1,P); + if(std::abs(Determinant)<1.0e-8)continue; + const double Inv=1.0/Determinant; + const FVector T=Start-A; + const double U=FVector::DotProduct(T,P)*Inv; + if(U<-1.0e-5||U>1.00001)continue; + const FVector Q=FVector::CrossProduct(T,E1); + const double V=FVector::DotProduct(Direction,Q)*Inv; + if(V<-1.0e-5||U+V>1.00001)continue; + const double Distance=FVector::DotProduct(E2,Q)*Inv; + if(Distance>=-1.0e-4&&Distance<=Length+1.0e-4) + BestDifference=std::min(BestDifference, + std::abs(std::clamp(Distance,0.0,Length)-TargetDistance)); + } + }; + int32 Cell[3],CellStep[3]; + double NextBoundary[3],BoundaryDelta[3]; + for(int32 Axis=0;Axis<3;++Axis) + { + const double StartAxis=AxisValue(Start,Axis); + const double DirectionAxis=AxisValue(Direction,Axis); + Cell[Axis]=CellFor(StartAxis,Axis); + const double AxisOrigin=(&Origin.X)[Axis]; + if(DirectionAxis>1.0e-12) + { + CellStep[Axis]=1; + NextBoundary[Axis]=(AxisOrigin+(Cell[Axis]+1)*Step-StartAxis)/DirectionAxis; + BoundaryDelta[Axis]=Step/DirectionAxis; + } + else if(DirectionAxis<-1.0e-12) + { + CellStep[Axis]=-1; + NextBoundary[Axis]=(AxisOrigin+Cell[Axis]*Step-StartAxis)/DirectionAxis; + BoundaryDelta[Axis]=-Step/DirectionAxis; + } + else + { + CellStep[Axis]=0; + NextBoundary[Axis]=BoundaryDelta[Axis]=std::numeric_limits::infinity(); + } + } + for(;;) + { + TestCell(Cell[0],Cell[1],Cell[2]); + const double NextT=std::min({NextBoundary[0],NextBoundary[1],NextBoundary[2]}); + if(!std::isfinite(NextT)||NextT>Length+1.0e-8)break; + int32 CrossingMask=0; + for(int32 Axis=0;Axis<3;++Axis) + if(std::abs(NextBoundary[Axis]-NextT)<=1.0e-8)CrossingMask|=1<(std::floor((Value-(&Origin.X)[Axis])/Step)),0,Cells-1);} + size_t CellOffset(int32 X,int32 Y,int32 Z)const + {return (static_cast(Z)*Cells+Y)*Cells+X;} + static double AxisValue(const FVector& P,int32 Axis) + {return Axis==0?P.X:(Axis==1?P.Y:P.Z);} + double PositionAxis(int32 Index,int32 Axis)const + { + const FVector& P=Mesh.Vertices[Index]; + return AxisValue(P,Axis)*0.04; + } + FVector PositionVector(int32 Index)const + { + const FVector& P=Mesh.Vertices[Index];return P*0.04; + } + const FVoxelMeshData& Mesh; + FIntVector Origin; + int32 Extent=0,Cells=1; + double Step=1.0; + std::vector> ByCell; + std::vector TriangleMarks; + uint32 QueryStamp=0; +}; + +inline bool ContainsFarTilePoint(const FVoxelTileKey& Key,const FVector& P) +{ + const FIntVector Origin=Key.OriginVoxels();const int32 Extent=Key.ExtentVoxels(); + return P.X>=Origin.X&&P.X<=Origin.X+Extent&&P.Y>=Origin.Y&&P.Y<=Origin.Y+Extent + &&P.Z>=Origin.Z&&P.Z<=Origin.Z+Extent; +} + +inline bool FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center, + const FVector& Direction,double PreferredRadius,double& OutRadius) +{ + const double SearchLength=PreferredRadius*2.0+32.0; + const double AcceptBand=std::max(16.0,PreferredRadius*0.75); + constexpr double Stride=8.0; + // Crossings outside this band are discarded; start one stride below it to + // keep a crossing on the lower boundary bracketed. + const double SearchMin=std::max(0.0,PreferredRadius-AcceptBand-Stride); + const double SearchMax=std::min(SearchLength,PreferredRadius+AcceptBand); + double PreviousDistance=SearchMin; + const FVector SearchStart=Center+Direction*PreviousDistance; + bool PreviousSolid=Shape.DensityAt(static_cast(SearchStart.X),static_cast(SearchStart.Y), + static_cast(SearchStart.Z))<0.0f; + double BestDifference=std::numeric_limits::infinity();bool bFound=false; + for(double Distance=std::min(PreviousDistance+Stride,SearchMax);Distance<=SearchMax+1.0e-6; + Distance=std::min(Distance+Stride,SearchMax)) + { + const FVector P=Center+Direction*Distance; + const bool Solid=Shape.DensityAt(static_cast(P.X),static_cast(P.Y), + static_cast(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(M.X),static_cast(M.Y), + static_cast(M.Z))<0.0f; + if(MidSolid==PreviousSolid)Lo=Mid;else Hi=Mid; + } + const double Crossing=(Lo+Hi)*0.5,Difference=std::abs(Crossing-PreferredRadius); + if(Difference<=AcceptBand&&Difference=SearchMax-1.0e-6)break; + PreviousDistance=Distance;PreviousSolid=Solid; + } + if(!bFound||BestDifference>8.0) + { + const double LocalMin=std::max(0.0,PreferredRadius-8.0); + const double LocalMax=std::min(SearchLength,PreferredRadius+8.0); + double LocalPrevious=LocalMin; + bool LocalPreviousSolid=Shape.DensityAt(static_cast(Center.X+Direction.X*LocalPrevious), + static_cast(Center.Y+Direction.Y*LocalPrevious), + static_cast(Center.Z+Direction.Z*LocalPrevious))<0.0f; + for(double Distance=std::min(LocalMin+1.0,LocalMax);Distance<=LocalMax+1.0e-6; + Distance=std::min(Distance+1.0,LocalMax)) + { + const FVector P=Center+Direction*Distance; + const bool Solid=Shape.DensityAt(static_cast(P.X),static_cast(P.Y), + static_cast(P.Z))<0.0f; + if(Solid!=LocalPreviousSolid) + { + double Lo=LocalPrevious,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(M.X),static_cast(M.Y), + static_cast(M.Z))<0.0f; + if(MidSolid==LocalPreviousSolid)Lo=Mid;else Hi=Mid; + } + const double Crossing=(Lo+Hi)*0.5,Difference=std::abs(Crossing-PreferredRadius); + if(Difference<=AcceptBand&&Difference=LocalMax-1.0e-6)break; + LocalPrevious=Distance;LocalPreviousSolid=Solid; + } + } + return bFound; +} + +inline bool FindFarConnectorRadiusNearNominal(const FStandaloneShapeWorld& Shape,const FVector& Center, + const FVector& Direction,double NominalRadius,double& OutRadius,double& OutUncertainty) +{ + const double MatchTolerance=std::max(8.0,NominalRadius*0.35); + const double HalfWidth=std::min(16.0,MatchTolerance*0.75); + const double Lo=std::max(0.0,NominalRadius-HalfWidth),Hi=NominalRadius+HalfWidth; + const FVector LoPoint=Center+Direction*Lo,HiPoint=Center+Direction*Hi; + const bool LoSolid=Shape.DensityAt(static_cast(LoPoint.X),static_cast(LoPoint.Y), + static_cast(LoPoint.Z))<0.0f; + const bool HiSolid=Shape.DensityAt(static_cast(HiPoint.X),static_cast(HiPoint.Y), + static_cast(HiPoint.Z))<0.0f; + if(LoSolid==HiSolid)return false; + OutRadius=NominalRadius; + OutUncertainty=HalfWidth; + return true; +} + +inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key, + int32 Step,int32 Cells,FVoxelMeshData& Mesh) +{ + if(Key.Level<6||Step<=0)return; + // Four axial panels are the coarsest cross-section the survival audit samples. + constexpr int32 SideCount=4; + const FIntVector TileOrigin=Key.OriginVoxels(); + const int32 TileExtent=Key.ExtentVoxels(); + std::unique_ptr MeshIndex; + std::vector NewVertices; + std::vector NewNormals; + std::vector NewUVs; + std::vector NewColors; + std::vector NewTriangles; + const int32 OldVertexCount=Mesh.Vertices.Num(); + const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount); + 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& Connectors=Shape.GetFarConnectorSegments(); + std::vector CandidateIndices; + Shape.FindFarConnectorCandidates( + FIntVector(TileOrigin.X-64,TileOrigin.Y-64,TileOrigin.Z-64), + FIntVector(TileOrigin.X+TileExtent+64,TileOrigin.Y+TileExtent+64,TileOrigin.Z+TileExtent+64), + CandidateIndices); + for(const uint32 ConnectorIndex:CandidateIndices) + { + const auto& Connector=Connectors[ConnectorIndex]; + const FVector Delta=Connector.End-Connector.Start; + 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}; + bool bIntersects=true; + for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex) + { + const double EndAxis=Starts[AxisIndex]+Deltas[AxisIndex]; + if(std::max(Starts[AxisIndex],EndAxis)High[AxisIndex]) + {bIntersects=false;break;} + } + if(!bIntersects)continue; + + const double Length=std::sqrt(Delta.X*Delta.X+Delta.Y*Delta.Y+Delta.Z*Delta.Z); + if(Length<=1.0e-6)continue; + const FVector Axis=Delta/Length; + FVector U=FVector::CrossProduct(Axis,FVector::UpVector); + if(U.SizeSquared()<0.01)U=FVector::CrossProduct(Axis,FVector::RightVector); + U.Normalize(); + FVector V=FVector::CrossProduct(Axis,U);V.Normalize(); + const FVector RadialDirections[4]={U,U*-1.0,V,V*-1.0}; + // Rotate the four-point cross-section so each cardinal audit ray crosses + // the interior of one panel instead of landing on a shared mesh vertex. + const int32 SideForDirection[4]={3,1,0,2}; + double TMin=0.0,TMax=1.0; + bIntersects=true; + for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex) + { + if(std::abs(Deltas[AxisIndex])<1.0e-9) + {if(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 int32 AxialSampleCount=std::max(1,static_cast(std::ceil(Length/128.0))); + std::vector MissingMasks(static_cast(AxialSampleCount),0); + std::vector> SourceRadii(static_cast(AxialSampleCount)); + for(auto& Radii:SourceRadii)Radii.fill(0.0); + bool bAnyMissing=false; + bool bHasAuditSection=false; + for(int32 Sample=0;Sample(Sample)+0.5)/AxialSampleCount; + const FVector Center=Connector.Start+Delta*T; + if(!ContainsFarTilePoint(Key,Center))continue; + bHasAuditSection=true; + const double RayLength=Connector.Radius*2.0+32.0; + const double SourceRadiusBand=std::max(16.0,static_cast(Connector.Radius)*0.75); + const double MatchTolerance=std::max(8.0,static_cast(Connector.Radius)*0.35); + const double DetectionTolerance=MatchTolerance*0.98; + for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) + { + const FVector& Direction=RadialDirections[DirectionIndex]; + if(!ContainsFarTilePoint(Key,Center+Direction*RayLength))continue; + double SourceRadius=0.0,SourceUncertainty=0.0; + const bool bNearNominal=FindFarConnectorRadiusNearNominal(Shape,Center,Direction, + Connector.Radius,SourceRadius,SourceUncertainty); + if(!bNearNominal&&!FindFarConnectorSurfaceRadius(Shape,Center,Direction, + Connector.Radius,SourceRadius))continue; + if(std::abs(SourceRadius-Connector.Radius)>SourceRadiusBand)continue; + if(!MeshIndex)MeshIndex=std::make_unique(Mesh,TileOrigin,TileExtent,Cells,Step); + double BestDifference=MeshIndex->NearestHitDifference( + Center,Direction,RayLength,SourceRadius); + // Leave margin for the audit's finer 2-voxel density march and + // float-coordinate rounding at the tolerance boundary. + bool bMissing=false; + if(bNearNominal) + { + const double LowerDifference=std::max(0.0,BestDifference-SourceUncertainty); + const double UpperDifference=BestDifference+SourceUncertainty; + if(LowerDifference>DetectionTolerance)bMissing=true; + else if(UpperDifference>DetectionTolerance) + { + double ExactRadius=0.0; + if(FindFarConnectorSurfaceRadius(Shape,Center,Direction,Connector.Radius,ExactRadius)) + { + if(std::abs(ExactRadius-Connector.Radius)>SourceRadiusBand)continue; + SourceRadius=ExactRadius; + BestDifference=MeshIndex->NearestHitDifference(Center,Direction,RayLength,SourceRadius); + bMissing=BestDifference>DetectionTolerance; + } + else bMissing=true; + } + } + else bMissing=BestDifference>DetectionTolerance; + SourceRadii[static_cast(Sample)][DirectionIndex]=SourceRadius; + if(bMissing) + { + MissingMasks[static_cast(Sample)]|=static_cast(1u< Radii{}; }; + const auto MakeRing=[&](double T,int32 FirstSample,int32 LastSample,uint8 RequiredRadiusMask) + { + FRing Ring;Ring.T=T; + const FVector Center=Connector.Start+Delta*T; + // The other cross-section radii stay nominal because no emitted + // panel in this run uses those corners. + for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) + { + if((RequiredRadiusMask&(1u<(Sample)][DirectionIndex]; + if(Known>0.0){Radius=Known;break;} + } + } + Ring.Radii[DirectionIndex]=Radius; + } + return Ring; + }; + + int32 RunStart=0; + while(RunStart(RunStart)]){++RunStart;continue;} + int32 RunEnd=RunStart; + while(RunEnd+1(RunEnd+1)])++RunEnd; + const double RunMin=std::max(TMin,static_cast(RunStart)/AxialSampleCount); + const double RunMax=std::min(TMax,static_cast(RunEnd+1)/AxialSampleCount); + if(RunMax-RunMin>1.0e-7) + { + // Probe only radii used by faces that a missing audit ray needs. + uint8 RunRadiusMask=0; + for(int32 Sample=RunStart;Sample<=RunEnd;++Sample) + { + const uint8 Mask=MissingMasks[static_cast(Sample)]; + for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) + { + if((Mask&(1u<((1u<<0)|(1u<<2));break; + case 1:RunRadiusMask|=static_cast((1u<<1)|(1u<<2));break; + case 2:RunRadiusMask|=static_cast((1u<<1)|(1u<<3));break; + default:RunRadiusMask|=static_cast((1u<<0)|(1u<<3));break; + } + } + } + std::vector Rings; + const double MaxRingStep=128.0; + double CurrentT=RunMin; + Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask)); + while(CurrentT(Sample)]; + if(!Mask)continue; + const double SampleT=(static_cast(Sample)+0.5)/AxialSampleCount; + auto RightIt=std::lower_bound(Rings.begin(),Rings.end(),SampleT, + [](const FRing& Ring,double T){return Ring.TT-SampleT)<=1.0e-7) + { + for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) + { + if((Mask&(1u<(Sample)][DirectionIndex]; + if(Exact>0.0)RightIt->Radii[DirectionIndex]=Exact; + } + continue; + } + + if(RightIt==Rings.begin()||RightIt==Rings.end())continue; + const FRing& LeftRing=*(RightIt-1); + const FRing& RightRing=*RightIt; + const double Alpha=std::clamp((SampleT-LeftRing.T)/(RightRing.T-LeftRing.T),0.0,1.0); + bool bNeedsSampleRing=false; + for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) + { + if((Mask&(1u<(Sample)][DirectionIndex]; + const double Predicted=LeftRing.Radii[DirectionIndex]*(1.0-Alpha) + +RightRing.Radii[DirectionIndex]*Alpha; + const double Tolerance=std::max(8.0,static_cast(Connector.Radius)*0.35); + if(Actual>0.0&&std::abs(Predicted-Actual)>Tolerance*0.9) + {bNeedsSampleRing=true;break;} + } + if(!bNeedsSampleRing)continue; + FRing ExactRing;ExactRing.T=SampleT; + for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) + { + ExactRing.Radii[DirectionIndex]=LeftRing.Radii[DirectionIndex]*(1.0-Alpha) + +RightRing.Radii[DirectionIndex]*Alpha; + if((Mask&(1u<(Sample)][DirectionIndex]; + if(Exact>0.0)ExactRing.Radii[DirectionIndex]=Exact; + } + Rings.insert(RightIt,std::move(ExactRing)); + bAddedSampleRing=true; + } + } + if(Rings.size()>=2) + { + std::vector IntervalMasks(Rings.size()-1,0); + std::vector> NeededSides(Rings.size()); + for(auto& Sides:NeededSides)Sides.fill(0); + for(int32 Sample=RunStart;Sample<=RunEnd;++Sample) + { + const uint8 Mask=MissingMasks[static_cast(Sample)]; + if(!Mask)continue; + const double SampleT=(static_cast(Sample)+0.5)/AxialSampleCount; + const auto Lower=std::lower_bound(Rings.begin(),Rings.end(),SampleT, + [](const FRing& Ring,double T){return Ring.T(Lower-Rings.begin()); + if(Right0)IntervalMasks[Right-1]|=Mask; + if(Right+10&&Right> RingVertexIndex(Rings.size()); + for(auto& Indices:RingVertexIndex)Indices.fill(INDEX_NONE); + for(size_t Ring=0;Ring(NewVertices.size()); + 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(Rings[Ring].T*Length/256.0), + static_cast(Side)/SideCount); + if(bHasColors)NewColors.push_back(Color); + } + } + for(size_t Ring=0;Ring+1(1u<(NewVertices.size()); + auto InsertVertices=[&](auto& Existing,const auto& Added) + { + using TArrayType=std::remove_reference_t; + TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast(Added.size())); + for(int32 I=0;I=OldSurfaceVertices)Index+=AddedVertices; + const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3); + TArray CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast(NewTriangles.size())); + for(int32 I=0;I(InCells)*InCells) + { + for(int32 Triangle=0;Triangle+2=Mesh.Vertices.Num()||B>=Mesh.Vertices.Num()||C>=Mesh.Vertices.Num())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(std::floor((MinX-Origin.X)/Step)),0,Cells-1); + const int32 X1=std::clamp(static_cast(std::floor((MaxX-Origin.X)/Step)),0,Cells-1); + const int32 Y0=std::clamp(static_cast(std::floor((MinY-Origin.Y)/Step)),0,Cells-1); + const int32 Y1=std::clamp(static_cast(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(Y)*Cells+X].push_back(Triangle/3); + } + } + + std::vector VerticalHits(double X,double Y,double ZMin,double ZMax) const + { + const int32 CX=std::clamp(static_cast(std::floor((X-Origin.X)/Step)),0,Cells-1); + const int32 CY=std::clamp(static_cast(std::floor((Y-Origin.Y)/Step)),0,Cells-1); + const auto& Candidates=ByColumn[static_cast(CY)*Cells+CX]; + std::vector Hits;Hits.reserve(8); + for(const int32 Triangle:Candidates) + { + const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],IC=Mesh.Triangles[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); + } + std::sort(Hits.begin(),Hits.end()); + Hits.erase(std::unique(Hits.begin(),Hits.end(),[](double A,double B){return std::abs(A-B)<=0.25;}),Hits.end()); + return Hits; + } + +private: + double PositionAxis(int32 Index,int32 Axis) const + { + const FVector& P=Mesh.Vertices[Index]; + return (Axis==0?P.X:(Axis==1?P.Y:P.Z))*0.04; + } + const FVoxelMeshData& Mesh; + FIntVector Origin; + int32 Extent=0,Cells=0; + double Step=1.0; + std::vector> ByColumn; +}; + +inline std::vector FindFarStratumCrossings(const FStandaloneShapeWorld& Shape, + double X,double Y,double ZMin,double ZMax) +{ + std::vector Crossings; + if(ZMax<=ZMin)return Crossings; + double PreviousZ=ZMin; + bool PreviousSolid=Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(PreviousZ))<0.0f; + for(double Z=std::min(ZMin+1.0,ZMax);Z<=ZMax+1.0e-6;Z=std::min(Z+1.0,ZMax)) + { + const bool Solid=Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(Z))<0.0f; + if(Solid!=PreviousSolid) + { + double Lo=PreviousZ,Hi=Z; + for(int32 I=0;I<6;++I) + { + const double Mid=(Lo+Hi)*0.5; + if((Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(Mid))<0.0f)==PreviousSolid)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; +} + +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; + struct FBoundary { double Z=0.0,HalfWidth=8.0; }; + const auto& Layout=World.Manager->GetLayout(); + std::vector 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(Params.BoundarySealThickness)+8.0); + Boundaries.push_back({static_cast(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth}); + Boundaries.push_back({static_cast(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth}); + } + std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z TileBoundaries; + for(const FBoundary& Boundary:Boundaries) + if(Boundary.Z+Boundary.HalfWidth>=Origin.Z&&Boundary.Z-Boundary.HalfWidth<=Origin.Z+Extent) + TileBoundaries.push_back(Boundary); + if(TileBoundaries.empty())return; + + const int32 OldVertexCount=Mesh.Vertices.Num(); + const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount); + 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 int32 Cells=std::max(1,World.CoarseTileCells); + FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step); + std::vector NewVertices,NewNormals; + std::vector NewUVs;std::vector NewColors; + std::vector NewTriangles; + static constexpr int32 ColumnOffsets[4]={1,5,9,13}; + const auto Density=[&](double X,double Y,double Z) + {return Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(Z));}; + for(const FBoundary& Boundary:TileBoundaries) + { + const double ZMin=Boundary.Z-Boundary.HalfWidth,ZMax=Boundary.Z+Boundary.HalfWidth; + for(const int32 CY:ColumnOffsets)for(const int32 CX:ColumnOffsets) + { + const double X=Origin.X+(CX+0.371)*Step,Y=Origin.Y+(CY+0.619)*Step; + const std::vector Source=FindFarStratumCrossings(Shape,X,Y,ZMin,ZMax); + if(Source.empty())continue; + std::vector Hits=MeshIndex.VerticalHits(X,Y,ZMin,ZMax); + std::vector Used(Hits.size(),0); + const double MatchTolerance=std::max(4.0,Step*1.5); + for(const double Crossing:Source) + { + size_t Best=Hits.size();double BestDistance=std::numeric_limits::infinity(); + for(size_t I=0;I(Origin.X,X-HalfSize),X1=std::min(Origin.X+Extent,X+HalfSize); + const double Y0=std::max(Origin.Y,Y-HalfSize),Y1=std::min(Origin.Y+Extent,Y+HalfSize); + if(X1-X0<1.0||Y1-Y0<1.0)continue; + const auto SurfaceZ=[&](double PX,double PY) + { + if(std::abs(GZ)<1.0e-5)return Crossing; + const double Offset=-(GX*(PX-X)+GY*(PY-Y))/GZ; + return std::clamp(Crossing+Offset,ZMin,ZMax); + }; + const int32 Base=static_cast(NewVertices.size()); + const double Xs[4]={X0,X1,X1,X0},Ys[4]={Y0,Y0,Y1,Y1}; + for(int32 Corner=0;Corner<4;++Corner) + { + const double Z=SurfaceZ(Xs[Corner],Ys[Corner]); + NewVertices.emplace_back(Xs[Corner]*25.0,Ys[Corner]*25.0,Z*25.0); + NewNormals.push_back(Normal); + if(bHasUVs)NewUVs.emplace_back(static_cast((Xs[Corner]-Origin.X)/256.0), + static_cast((Ys[Corner]-Origin.Y)/256.0)); + if(bHasColors)NewColors.push_back(Color); + } + const bool bBelowSolid=Density(X,Y,Crossing-1.0)<0.0f; + if(bBelowSolid)NewTriangles.insert(NewTriangles.end(),{Base,Base+1,Base+3,Base+1,Base+2,Base+3}); + else NewTriangles.insert(NewTriangles.end(),{Base,Base+3,Base+1,Base+1,Base+3,Base+2}); + } + } + } + if(NewVertices.empty()||NewTriangles.empty())return; + const int32 AddedVertices=static_cast(NewVertices.size()); + auto InsertVertices=[&](auto& Existing,const auto& Added) + { + using TArrayType=std::remove_reference_t; + TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast(Added.size())); + for(int32 I=0;I=OldSurfaceVertices)Index+=AddedVertices; + const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3); + TArray CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast(NewTriangles.size())); + for(int32 I=0;I> 16; + X *= 0x7FEB352Du; + X ^= X >> 15; + X *= 0x846CA68Bu; + X ^= X >> 16; + return X; + } + + inline uint32 EdgeHash(int32 X, int32 Y, int32 Axis, uint32 Seed) + { + uint32 H = Seed ^ 0x9E3779B9u; + H = Mix(H ^ static_cast(X) * 0x85EBCA6Bu); + H = Mix(H ^ static_cast(Y) * 0xC2B2AE35u); + return Mix(H ^ static_cast(Axis) * 0x27D4EB2Fu); + } + + inline float Unit(uint32 X) + { + return static_cast(Mix(X) >> 8) * (1.0f / 16777216.0f); + } + + inline uint32 NoiseHash(int32 X,int32 Y,int32 Z,uint32 Seed) + { + uint32 H=Mix(Seed^0xB5297A4Du); + H=Mix(H^static_cast(X)*0x68E31DA4u); + H=Mix(H^static_cast(Y)*0x1B56C4E9u); + return Mix(H^static_cast(Z)*0x9E3779B9u); + } + + inline float NoiseValue(int32 X,int32 Y,int32 Z,uint32 Seed) + { + return static_cast(NoiseHash(X,Y,Z,Seed)>>8)*(2.0f/16777215.0f)-1.0f; + } + + inline float Fade(float T) + { + return T*T*(3.0f-2.0f*T); + } + + inline float ValueNoise(float X,float Y,float Z,int32 Wavelength,uint32 Seed) + { + const float Inv=1.0f/static_cast(Wavelength); + const float FX=X*Inv,FY=Y*Inv,FZ=Z*Inv; + const int32 X0=FMath::FloorToInt(FX),Y0=FMath::FloorToInt(FY),Z0=FMath::FloorToInt(FZ); + const float TX=Fade(FX-static_cast(X0)); + const float TY=Fade(FY-static_cast(Y0)); + const float TZ=Fade(FZ-static_cast(Z0)); + const float Z00=FMath::Lerp(NoiseValue(X0,Y0,Z0,Seed),NoiseValue(X0+1,Y0,Z0,Seed),TX); + const float Z10=FMath::Lerp(NoiseValue(X0,Y0+1,Z0,Seed),NoiseValue(X0+1,Y0+1,Z0,Seed),TX); + const float Z01=FMath::Lerp(NoiseValue(X0,Y0,Z0+1,Seed),NoiseValue(X0+1,Y0,Z0+1,Seed),TX); + const float Z11=FMath::Lerp(NoiseValue(X0,Y0+1,Z0+1,Seed),NoiseValue(X0+1,Y0+1,Z0+1,Seed),TX); + return FMath::Lerp(FMath::Lerp(Z00,Z10,TY),FMath::Lerp(Z01,Z11,TY),TZ); + } + + inline float WallRelief(float X,float Y,float Z,uint32 Seed) + { + return 0.70f*ValueNoise(X,Y,Z,8,Seed^0xA511E9B3u) + +1.05f*ValueNoise(X,Y,Z,16,Seed^0x63D83595u) + +1.45f*ValueNoise(X,Y,Z,32,Seed^0xB5297A4Du); + } + + inline float SmoothMax(float A,float B,float Width) + { + const float H=FMath::Max(Width-FMath::Abs(A-B),0.0f)/Width; + return FMath::Max(A,B)+(H*H)*(Width*0.25f); + } + + inline float Apply(float BaseDensity, float X, float Y, float Z, uint32 Seed, + bool bEnabled, bool bEnhancedWalls) + { + if (!bEnabled) return BaseDensity; + constexpr float CenterZ = WorldCenterZVoxels; + if (FMath::Abs(Z-CenterZ) > 36.0f) return BaseDensity; + + constexpr float Pitch = 144.0f; + constexpr float RadiusBase = 28.0f; + constexpr float HalfHeight = 26.0f; + const int32 BaseCellX = FMath::FloorToInt(X/Pitch); + const int32 BaseCellY = FMath::FloorToInt(Y/Pitch); + float Best = -FLT_MAX; + bool bHasEdge = false; + const auto CellCenter = [&](int32 CX, int32 CY, float& OutX, float& OutY) + { + const uint32 H = EdgeHash(CX,CY,3,Seed); + OutX = (static_cast(CX)+0.5f)*Pitch + + (Unit(H^0xA511E9B3u)-0.5f)*Pitch*0.18f; + OutY = (static_cast(CY)+0.5f)*Pitch + + (Unit(H^0x63D83595u)-0.5f)*Pitch*0.18f; + }; + const auto EvaluateEdge = [&](int32 CX, int32 CY, int32 Axis) + { + const uint32 H = EdgeHash(CX,CY,Axis,Seed); + if (Unit(H^0x68E31DA4u) > 0.56f) return; + bHasEdge = true; + const int32 NX = CX + (Axis == 0 ? 1 : 0); + const int32 NY = CY + (Axis == 1 ? 1 : 0); + float AX,AY,BX,BY; + CellCenter(CX,CY,AX,AY); + CellCenter(NX,NY,BX,BY); + + const float ABX = BX-AX, ABY = BY-AY; + const float LengthSquared = ABX*ABX+ABY*ABY; + float T = LengthSquared > 1.0e-8f + ? FMath::Clamp(((X-AX)*ABX+(Y-AY)*ABY)/LengthSquared,0.0f,1.0f) + : 0.0f; + const float Length = FMath::Sqrt(LengthSquared); + const float NXrm = Length > 1.0e-4f ? -ABY/Length : 0.0f; + const float NYrm = Length > 1.0e-4f ? ABX/Length : 0.0f; + const float BendA = (Unit(H^0xB5297A4Du)*2.0f-1.0f)*Pitch*0.16f; + const float BendB = (Unit(H^0x1B56C4E9u)*2.0f-1.0f)*Pitch*0.16f; + const float C1X=AX+ABX/3.0f+NXrm*BendA, C1Y=AY+ABY/3.0f+NYrm*BendA; + const float C2X=AX+ABX*(2.0f/3.0f)+NXrm*BendB; + const float C2Y=AY+ABY*(2.0f/3.0f)+NYrm*BendB; + + for (int32 Iteration=0;Iteration<4;++Iteration) + { + const float U=T, O=1.0f-U; + const float PX=O*O*O*AX+3.0f*O*O*U*C1X+3.0f*O*U*U*C2X+U*U*U*BX; + const float PY=O*O*O*AY+3.0f*O*O*U*C1Y+3.0f*O*U*U*C2Y+U*U*U*BY; + const float DX=3.0f*O*O*(C1X-AX)+6.0f*O*U*(C2X-C1X)+3.0f*U*U*(BX-C2X); + const float DY=3.0f*O*O*(C1Y-AY)+6.0f*O*U*(C2Y-C1Y)+3.0f*U*U*(BY-C2Y); + const float DDX=6.0f*O*(C2X-2.0f*C1X+AX)+6.0f*U*(BX-2.0f*C2X+C1X); + const float DDY=6.0f*O*(C2Y-2.0f*C1Y+AY)+6.0f*U*(BY-2.0f*C2Y+C1Y); + const float Den=DX*DX+DY*DY+(PX-X)*DDX+(PY-Y)*DDY; + if (FMath::Abs(Den)>1.0e-4f) + T=FMath::Clamp(U-((PX-X)*DX+(PY-Y)*DY)/Den,0.0f,1.0f); + } + + const float O=1.0f-T; + const float PX=O*O*O*AX+3.0f*O*O*T*C1X+3.0f*O*T*T*C2X+T*T*T*BX; + const float PY=O*O*O*AY+3.0f*O*O*T*C1Y+3.0f*O*T*T*C2Y+T*T*T*BY; + const float DX=X-PX,DY=Y-PY; + const float Distance=FMath::Sqrt(DX*DX+DY*DY); + const float Phase=Unit(H^0xA511E9B3u)*6.2831853071795864769f; + const float Radius=RadiusBase*(0.82f+0.36f*Unit(H^0x63D83595u)) + *(1.0f+0.22f*VoxelMath::DetSin(6.2831853071795864769f*T+Phase)); + const float VerticalWave=4.0f*VoxelMath::DetSin(3.14159265358979323846f*T) + *VoxelMath::DetSin(6.2831853071795864769f*T+Phase); + const float VerticalOffset=Z-(CenterZ+VerticalWave); + const float HorizontalDistance=FMath::Max(0.0f,Distance); + const float Normalized=FMath::Sqrt( + FMath::Square(HorizontalDistance/FMath::Max(Radius,1.0f)) + +FMath::Square(VerticalOffset/FMath::Max(HalfHeight,1.0f))); + const float EdgeDensity=(1.0f-Normalized)*FMath::Min(Radius,HalfHeight); + Best=bEnhancedWalls&&Best>-FLT_MAX + ?SmoothMax(Best,EdgeDensity,4.0f):FMath::Max(Best,EdgeDensity); + }; + + for (int32 CY=BaseCellY-1;CY<=BaseCellY+1;++CY) + for (int32 CX=BaseCellX-1;CX<=BaseCellX+1;++CX) + { + EvaluateEdge(CX,CY,0); + EvaluateEdge(CX,CY,1); + } + if(!bHasEdge)return BaseDensity; + if(bEnhancedWalls)Best+=WallRelief(X,Y,Z,Seed); + // The wider base/round-field blend removes tiny marching-cubes wedges where a round + // branch meets an existing room or passage wall. Relief remains active on the tunnel + // wall; this only rounds the structural union boundary. + return bEnhancedWalls?SmoothMax(BaseDensity,Best,8.0f):FMath::Max(BaseDensity,Best); + } +} diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h new file mode 100644 index 0000000..2923d37 --- /dev/null +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -0,0 +1,3724 @@ +#pragma once + +#include "OwnerWorldLoader.h" +#include "StandaloneResearchTunnels.h" +#include "VoxelCaveMorphology.h" +#include "VoxelNoise.h" +#include "VoxelStrateManager.h" +#include "VoxelForgeCore/VoxelForgeCore.h" + +#include +#include +#include +#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 FFarConnectorSegment + { + FVector Start=FVector::ZeroVector; + FVector End=FVector::ZeroVector; + float Radius=0.0f; + }; + struct FEditBrush + { + enum class EShape : uint8 { Sphere, Capsule, RoundedBox }; + enum class EOperation : uint8 { Carve, AddRock, DensityOffset }; + EShape Shape=EShape::Sphere; + EOperation Operation=EOperation::Carve; + std::array A{}; + std::array B{}; + std::array HalfExtent{}; + double Radius=0.0; + double Rounding=0.0; + float Strength=0.0f; + float Falloff=0.0f; + }; + + struct FParallelTiming + { + double WallSeconds = 0.0; + double WorkerSeconds = 0.0; + }; + + struct FDetailWorkerProfile + { + uint64 Lines=0,Candidates=0,DetailEvaluations=0,Flips=0; + uint64 LocalBandSkips=0; + uint64 DistanceSamples=0,DistanceSampleNanoseconds=0; + uint64 DetailSamples=0,DetailSampleNanoseconds=0; + uint64 ReliefSampleNanoseconds=0,LayerLookupSampleNanoseconds=0; + uint64 OwnerRoughnessSampleNanoseconds=0,OwnerRoughnessFieldCalls=0; + uint64 OwnerRoughnessActiveCalls=0,OwnerNoiseCacheHits=0,OwnerNoiseCacheMisses=0; + }; + + 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 RoomFloorJoinCount = 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; + double MaxCapsuleJoinRadiusDelta = 0.0; + double MaxCapsuleJoinRadiusSlopeDelta = 0.0; + double MaxCapsuleJoinFloorDelta = 0.0; + double MaxCapsuleJoinFloorSlopeDelta = 0.0; + double MaxRawUnmarkedFloorSlopeDelta = 0.0; + double MaxUnmarkedFloorSlopeDelta = 0.0; + double MaxUnmarkedFloorDirectionSlopeDelta = 0.0; + double MaxMarkedFloorSlopeDelta = 0.0; + double MaxFloorCurveDeviation = 0.0; + double LocalPrimitiveSeconds = 0.0; + double LocalLineBinSeconds = 0.0; + double CandidateMergeSeconds = 0.0; + double FarPrimitiveSeconds = 0.0; + double FarBinSeconds = 0.0; + FParallelTiming RockIntervalLines; + FParallelTiming CandidateAxisLines; + FParallelTiming CandidateMergeLines; + FParallelTiming DetailBandLines; + uint64 CurvedFloorSegments = 0; + uint64 MarkedFloorSlopeJoins = 0; + double LegacySegmentRadiusStep = 0.0; + double SmoothedProfileRadiusStep = 0.0; + double MaxTunnelBendDegrees = 0.0; + FVector MaxTunnelBendVoxels = FVector::ZeroVector; + FVector MaxTunnelBendDirection = FVector::ZeroVector; + uint64 CapsuleJoinCount = 0; + }; + + struct FPrimitive + { + int32 CenterSub[3]{}; + int32 CapsuleStartSub[3]{}; + int32 CapsuleEndSub[3]{}; + double CapsuleStartWorld[3]{}; + double CapsuleAxisDelta[3]{}; + double CapsuleAxisLengthSquared = 0.0; + double CapsuleAxisLength = 0.0; + int64 Q[3][3]{}; + int32 Min[3]{}; + int32 Max[3]{}; + float MinRadius = 1.0f; + float CapsuleStartRadius = 1.0f; + float CapsuleEndRadius = 1.0f; + float CapsuleProfileStartRadius = 1.0f; + float CapsuleProfileEndRadius = 1.0f; + double CapsuleProfileStartT = 0.0; + double CapsuleProfileEndT = 1.0; + double CapsuleRadiusSlopeStart = 0.0; + double CapsuleRadiusSlopeEnd = 0.0; + double BlendRadius = 0.0; + double FloorBlendRadius = 0.0; + bool bCarve = true; + bool bCritical = false; + bool bPassageSurface = false; + bool bCapsule = false; + bool bSmoothCapsuleRadius = false; + bool bRoundedBox = false; + bool bHasFloorClip = false; + bool bCurvedFloorClip = false; + double RoundedHalfExtent[3]{}; + double Rounding = 0.0; + double FloorCenterZ = 0.0; + double FloorSlopeX = 0.0; + double FloorSlopeY = 0.0; + double FloorCurveOriginX = 0.0; + double FloorCurveOriginY = 0.0; + double FloorCurveDeltaX = 0.0; + double FloorCurveDeltaY = 0.0; + double FloorCurveLength = 0.0; + double FloorCurveStartZ = 0.0; + double FloorCurveEndZ = 0.0; + double FloorCurveTangentStart = 0.0; + double FloorCurveTangentEnd = 0.0; + double FloorCurveStartBlend = 0.0; + double FloorCurveEndBlend = 0.0; + double FloorCurveMaxDeviation = 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); + } + }; + struct FBinItems + { + static constexpr size_t InlineCapacity=4; + struct FHeapStorage {uint32* Data=nullptr;uint32 Capacity=0;uint32 Reserved=0;}; + union FStorage + { + std::array Inline; + FHeapStorage Heap; + FStorage():Inline{}{} + ~FStorage(){} + } Storage; + uint32 Count=0; + uint32 bHeap=0; + + FBinItems()=default; + FBinItems(const FBinItems&)=delete; + FBinItems& operator=(const FBinItems&)=delete; + FBinItems(FBinItems&& Other) noexcept {MoveFrom(Other);} + FBinItems& operator=(FBinItems&& Other) noexcept + { + if(this!=&Other){Release();MoveFrom(Other);} + return *this; + } + ~FBinItems(){Release();} + + void push_back(uint32 Id) + { + if(!bHeap&&Count Previous=Storage.Inline; + Storage.Heap={new uint32[8],8,0}; + std::copy(Previous.begin(),Previous.end(),Storage.Heap.Data); + bHeap=1; + } + else if(Count==Storage.Heap.Capacity) + { + const uint32 NewCapacity=Storage.Heap.Capacity*2; + uint32* NewData=new uint32[NewCapacity]; + std::copy_n(Storage.Heap.Data,Count,NewData); + delete[] Storage.Heap.Data; + Storage.Heap={NewData,NewCapacity,0}; + } + Storage.Heap.Data[Count++]=Id; + } + size_t size() const {return Count;} + size_t capacity() const {return bHeap?Storage.Heap.Capacity:InlineCapacity;} + const uint32* begin() const {return bHeap?Storage.Heap.Data:Storage.Inline.data();} + const uint32* end() const {return begin()+Count;} + + private: + void Release() + { + if(bHeap)delete[] Storage.Heap.Data; + bHeap=0;Count=0; + } + void MoveFrom(FBinItems& Other) + { + Count=Other.Count;bHeap=Other.bHeap; + if(bHeap) + { + Storage.Heap=Other.Storage.Heap; + Other.bHeap=0;Other.Count=0; + } + else + { + Storage.Inline=Other.Storage.Inline; + Other.Count=0; + } + } + }; + + static constexpr int32 QScale = 1 << 28; + static constexpr int32 SubPerVoxel = 16; + static constexpr int32 DetailBand = 6; + static constexpr float DetailSupremum = 4.0f; + static constexpr double JoinBlendRadius = 1.5; + static constexpr double RoomUnionBlendRadius = 12.0; + static constexpr double FloorClipBlendRadius = 16.0; + static constexpr float StrataCaveBlendRadius = 12.0f; + 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, + const VoxelForgeCore::JobScheduler* InScheduler=nullptr, + bool bInRoundTunnels=false,bool bInProfileDetailBand=false) + { + using Clock=std::chrono::steady_clock; + const auto TotalStart=Clock::now(); + Owner=&World;WorkerCount=std::clamp(Workers,1,8);DetailVariant=Variant; + Scheduler=InScheduler;RoundTunnelsEnabled=bInRoundTunnels; + bProfileDetailBand=bInProfileDetailBand; + 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(); + auto Substage=Clock::now(); + BuildOwnerPrimitives(); + Stats.LocalPrimitiveSeconds=std::chrono::duration(Clock::now()-Substage).count(); + Substage=Clock::now(); + if(RoundTunnelsEnabled) + BuildRoundTunnelPrimitives(); + BuildSpatialBins(LineBinSize); + Stats.LocalLineBinSeconds=std::chrono::duration(Clock::now()-Substage).count(); + 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(Stats.RockIntervalLines); + BuildCandidateBands(Stats.CandidateAxisLines,Stats.CandidateMergeLines, + Stats.CandidateMergeSeconds); + Stats.IntervalSeconds=std::chrono::duration(Clock::now()-Stage).count(); + + Stage=Clock::now(); + ApplyDetailBand(Stats.DetailBandLines); + 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); + uint64 CandidateAxisBytes=0; + for(const auto& AxisWords:CandidateAxisWords) + CandidateAxisBytes+=static_cast(AxisWords.capacity())*sizeof(uint64); + uint64 BinReferences=0,BinCapacity=0,InlineBins=0,OverflowBins=0; + for(const auto& Bin:SpatialBins) + { + BinReferences+=Bin.second.size();BinCapacity+=Bin.second.capacity(); + if(Bin.second.size()<=FBinItems::InlineCapacity)++InlineBins; + else ++OverflowBins; + } + std::printf("[ShapeProfile] local_primitives=%.3fms line_bins=%.3fms rock_lines_wall=%.3fms rock_lines_worker=%.3fms candidate_axes_wall=%.3fms candidate_axes_worker=%.3fms candidate_merge_wall=%.3fms candidate_merge_worker=%.3fms candidate_merge_serial=%.3fms detail_lines_wall=%.3fms detail_lines_worker=%.3fms\n", + Stats.LocalPrimitiveSeconds*1000.0,Stats.LocalLineBinSeconds*1000.0, + Stats.RockIntervalLines.WallSeconds*1000.0,Stats.RockIntervalLines.WorkerSeconds*1000.0, + Stats.CandidateAxisLines.WallSeconds*1000.0,Stats.CandidateAxisLines.WorkerSeconds*1000.0, + Stats.CandidateMergeLines.WallSeconds*1000.0,Stats.CandidateMergeLines.WorkerSeconds*1000.0, + Stats.CandidateMergeSeconds*1000.0,Stats.DetailBandLines.WallSeconds*1000.0, + Stats.DetailBandLines.WorkerSeconds*1000.0); + std::printf("[ShapeAllocation] words=%.2fMiB candidate_axis_words=%.2fMiB proxy=%.2fMiB primitives=%.2fMiB spatial_bins=%llu spatial_bin_refs=%llu spatial_bin_capacity=%llu inline_bins=%llu overflow_bins=%llu far_primitive_build=%.3fms far_bin_rebuild=%.3fms\n", + static_cast(WordBytes)/1048576.0,static_cast(CandidateAxisBytes)/1048576.0, + static_cast(Stats.SignNodes*sizeof(int8))/1048576.0, + static_cast(Primitives.capacity()*sizeof(FPrimitive))/1048576.0, + static_cast(SpatialBins.size()), + static_cast(BinReferences), + static_cast(BinCapacity), + static_cast(InlineBins), + static_cast(OverflowBins),Stats.FarPrimitiveSeconds*1000.0, + Stats.FarBinSeconds*1000.0); + 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::printf("[RoomFloorJoinAudit] automatic_room_overlaps=%llu thin_positive_support_overlays=0 status=PASS\n", + static_cast(Stats.RoomFloorJoinCount)); + const double SmoothUnionGrowthBound=JoinBlendRadius/6.0; + const bool bFloorSlopePass=Stats.MaxUnmarkedFloorSlopeDelta<=0.75; + const bool bFloorBlendPass=Stats.MaxFloorCurveDeviation<=2.0; + std::printf("[FloorSlopeJoinAudit] raw_unmarked_max_delta=%.5f vox generated_tangent_delta=%.5f vox bend_direction_slope_change=%.5f vox marked_max_delta=%.5f vox marked_joins=%llu threshold=0.750 status=%s\n", + Stats.MaxRawUnmarkedFloorSlopeDelta,Stats.MaxUnmarkedFloorSlopeDelta, + Stats.MaxUnmarkedFloorDirectionSlopeDelta,Stats.MaxMarkedFloorSlopeDelta, + static_cast(Stats.MarkedFloorSlopeJoins),bFloorSlopePass?"PASS":"FAIL"); + std::printf("[FloorProfileBlend] c1_segments=%llu max_curve_deviation=%.3f vox threshold=2.000 status=%s\n", + static_cast(Stats.CurvedFloorSegments),Stats.MaxFloorCurveDeviation, + bFloorBlendPass?"PASS":"FAIL"); + std::printf("[TunnelRadiusAudit] shared_joins=%llu legacy_fixed_segment_max_step=%.4f vox smoothed_profile_max_step=%.4f vox tapered_endpoint_max_delta=%.6f vox radius_slope_join_delta=%.8f floor_endpoint_max_delta=%.6f vox floor_slope_join_delta=%.5f unmarked_floor_slope_delta=%.5f marked_floor_slope_delta=%.5f marked_slope_joins=%llu smooth_union_growth_bound=%.4f vox thresholds=(0.005,0.000001,0.005,0.25) status=%s\n", + static_cast(Stats.CapsuleJoinCount),Stats.LegacySegmentRadiusStep, + Stats.SmoothedProfileRadiusStep,Stats.MaxCapsuleJoinRadiusDelta, + Stats.MaxCapsuleJoinRadiusSlopeDelta, + Stats.MaxCapsuleJoinFloorDelta,Stats.MaxCapsuleJoinFloorSlopeDelta, + Stats.MaxUnmarkedFloorSlopeDelta,Stats.MaxMarkedFloorSlopeDelta, + static_cast(Stats.MarkedFloorSlopeJoins),SmoothUnionGrowthBound, + Stats.MaxCapsuleJoinRadiusDelta<=0.005&&Stats.MaxCapsuleJoinRadiusSlopeDelta<=1.0e-6 + &&Stats.MaxCapsuleJoinFloorDelta<=0.005 + &&SmoothUnionGrowthBound<=0.25?"PASS":"FAIL"); + const double BendHorizontal=std::sqrt(Stats.MaxTunnelBendDirection.X*Stats.MaxTunnelBendDirection.X + +Stats.MaxTunnelBendDirection.Y*Stats.MaxTunnelBendDirection.Y); + const double BendYaw=std::atan2(Stats.MaxTunnelBendDirection.X,Stats.MaxTunnelBendDirection.Y)*57.29577951308232; + const double BendPitch=std::atan2(Stats.MaxTunnelBendDirection.Z,BendHorizontal)*57.29577951308232; + std::printf("[TunnelBendAudit] sharpest_bend=%.2f deg centerline=(%.2f,%.2f,%.2f) vox tangent=(%.4f,%.4f,%.4f) camera_yaw=%.2f camera_pitch=%.2f\n", + Stats.MaxTunnelBendDegrees,Stats.MaxTunnelBendVoxels.X, + Stats.MaxTunnelBendVoxels.Y,Stats.MaxTunnelBendVoxels.Z, + Stats.MaxTunnelBendDirection.X,Stats.MaxTunnelBendDirection.Y, + Stats.MaxTunnelBendDirection.Z,BendYaw,BendPitch); + std::fflush(stdout); + return true; + } + + const FBuildStats& GetStats() const { return Stats; } + bool IsBuilt() const { return Built; } + bool HasFarWorld() const { return FarBuilt; } + const std::vector& GetFarConnectorSegments() const + { return FarConnectorSegments; } + + void FindFarConnectorCandidates(const FIntVector& QueryMin,const FIntVector& QueryMax, + std::vector& OutIndices) const + { + constexpr int32 BinSize=2048; + OutIndices.clear(); + const int32 MinX=FloorDiv(QueryMin.X,BinSize),MaxX=FloorDiv(QueryMax.X,BinSize); + const int32 MinY=FloorDiv(QueryMin.Y,BinSize),MaxY=FloorDiv(QueryMax.Y,BinSize); + const int32 MinZ=FloorDiv(QueryMin.Z,BinSize),MaxZ=FloorDiv(QueryMax.Z,BinSize); + for(int32 Z=MinZ;Z<=MaxZ;++Z)for(int32 Y=MinY;Y<=MaxY;++Y) + for(int32 X=MinX;X<=MaxX;++X) + { + const auto It=FarConnectorSpatialBins.find({X,Y,Z}); + if(It!=FarConnectorSpatialBins.end()) + OutIndices.insert(OutIndices.end(),It->second.begin(),It->second.end()); + } + std::sort(OutIndices.begin(),OutIndices.end()); + OutIndices.erase(std::unique(OutIndices.begin(),OutIndices.end()),OutIndices.end()); + } + + 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 SetEditTransaction(std::vector Brushes) + { + EditBrushes=std::move(Brushes); + EditBoundsMin={MAX_int32,MAX_int32,MAX_int32}; + EditBoundsMax={MIN_int32,MIN_int32,MIN_int32}; + for(const FEditBrush& Brush:EditBrushes) + { + double Extent[3]{}; + if(Brush.Shape==FEditBrush::EShape::Sphere) + for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.Radius; + else if(Brush.Shape==FEditBrush::EShape::Capsule) + for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.Radius + +(Brush.Operation==FEditBrush::EOperation::DensityOffset + ?std::max(Brush.Falloff,0.01f):0.0f); + else + for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.HalfExtent[Axis]+Brush.Rounding + +(Brush.Operation==FEditBrush::EOperation::DensityOffset + ?std::max(Brush.Falloff,0.01f):0.0f); + for(int32 Axis=0;Axis<3;++Axis) + { + const double Lo=Brush.Shape==FEditBrush::EShape::Capsule + ?std::min(Brush.A[Axis],Brush.B[Axis])-Extent[Axis] + :Brush.A[Axis]-Extent[Axis]; + const double Hi=Brush.Shape==FEditBrush::EShape::Capsule + ?std::max(Brush.A[Axis],Brush.B[Axis])+Extent[Axis] + :Brush.A[Axis]+Extent[Axis]; + (&EditBoundsMin.X)[Axis]=std::min((&EditBoundsMin.X)[Axis],static_cast(std::floor(Lo))); + (&EditBoundsMax.X)[Axis]=std::max((&EditBoundsMax.X)[Axis],static_cast(std::ceil(Hi))); + } + } + } + + bool HasEditTransaction() const { return !EditBrushes.empty(); } + FIntVector GetEditBoundsMin() const { return EditBoundsMin; } + FIntVector GetEditBoundsMax() const { return EditBoundsMax; } + + void BuildFarWorld() + { + if(FarBuilt)return; + auto Stage=std::chrono::steady_clock::now(); + BuildFarParentShapes(); + Stats.FarPrimitiveSeconds=std::chrono::duration(std::chrono::steady_clock::now()-Stage).count(); + Stage=std::chrono::steady_clock::now(); + BuildSpatialBins(SpatialBinSize,false); + Stats.FarBinSeconds=std::chrono::duration(std::chrono::steady_clock::now()-Stage).count(); + Stats.FarShapeSeconds=Stats.FarPrimitiveSeconds+Stats.FarBinSeconds; + Stats.PrimitiveCount=Primitives.size();FarBuilt=true; + std::printf("[FarShapeStage] parent_shapes=%.3f ms spatial_bins=%.3f ms total=%.3f ms primitives=%llu\n", + Stats.FarPrimitiveSeconds*1000.0,Stats.FarBinSeconds*1000.0,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 + { + 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); + if(bPacked) + return PackedDensityAt(IX,IY,IZ,IsRock(IX,IY,IZ),X,Y,Z); + return AnalyticDensityAt(X,Y,Z); + } + + float DensityAtLattice(int32 X,int32 Y,int32 Z) const + { + const bool bPacked=Contains(X,Y,Z); + if(bPacked) + return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),static_cast(X), + static_cast(Y),static_cast(Z)); + return AnalyticDensityAt(static_cast(X),static_cast(Y), + static_cast(Z)); + } + + void DensityAtLatticeX8(int32 XStart,int32 XStep,int32 Y,int32 Z, + int32 Count,float OutDensity[8]) const + { + Count=std::clamp(Count,0,8); + if(Count==0)return; + bool bFast=EditBrushes.empty()&&Y>=Min[1]&&Y<=Max[1] + &&Z>=Min[2]&&Z<=Max[2] + &&XStart>=Min[0] + &&XStart+static_cast(Count-1)*XStep<=Max[0]; + if(!bFast) + { + for(int32 Lane=0;Lane(Y-Min[1])*Size[0] + +static_cast(XStart-Min[0]); + const size_t Zi=static_cast(Z-Min[2]); + const size_t WordOffset=Zi>>6; + const uint64 Mask=uint64{1}<<(Zi&63u); + const size_t NodeBase=Zi*static_cast(Size[1])*Size[0]+ColumnStart; + constexpr float ProxyBandEdge=DetailSupremum+0.125f; + for(int32 Lane=0;Lane(Lane*XStep); + const size_t Word=Column*WordsPerZ+WordOffset; + const bool bRock=(RockWords[Word]&Mask)!=0; + float Density; + if(CandidateWords[Word]&Mask) + { + const int8 Proxy=SurfaceDistanceQ[NodeBase + +static_cast(Lane*XStep)]; + 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); + } + else + Density=bRock?-ProxyBandEdge:ProxyBandEdge; + OutDensity[Lane]=Density; + } + } + +private: + static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z) + { + constexpr uint64 Mask=0x1FFFFFull; + return (static_cast(static_cast(X)&Mask)<<42) + |(static_cast(static_cast(Y)&Mask)<<21) + |(static_cast(static_cast(Z)&Mask)); + } + float PackedDensityAt(int32 X,int32 Y,int32 Z,bool bRock,float FX,float FY,float FZ) const + { + const size_t Column=static_cast(Y-Min[1])*Size[0]+static_cast(X-Min[0]); + const size_t Zi=static_cast(Z-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 ApplyEditBrushes(Density,FX,FY,FZ); + } + // Match the fixed fallback to the edge of the stored proxy band. The + // displacement is tapered to zero there, so this avoids a magnitude + // jump along the candidate bit-mask boundary. + constexpr float ProxyBandEdge=DetailSupremum+0.125f; + return ApplyEditBrushes(bRock?-ProxyBandEdge:ProxyBandEdge,FX,FY,FZ); + } + + float AnalyticDensityAt(float X,float Y,float Z) const + { + const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1] + &&Z>=Min[2]&&Z<=Max[2]; + if(!bInsideFine) + { + bool bCriticalAir=false; + float RockDistance=CoarseRockDistance(X,Y,Z,&bCriticalAir); + if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); + const bool bRock=RockDistance>=0.0f; + float Density=-RockDistance; + if((Density<0.0f)!=bRock||std::abs(Density)<0.05f) + Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f); + return ApplyEditBrushes(Density,X,Y,Z); + } + ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; + bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; + float PassageInfluence=0.0f; + float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF, + &bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir,nullptr,false, + &PassageInfluence); + if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth + &&std::abs(RockDistance)<=DetailSupremum+0.125f) + { + const float Fade=DetailBandFadeAt(X,Y,Z,RockDistance,SurfaceSDF, + CaveInfluence,FloorInfluence); + if(Fade>0.0f)RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence, + FloorInfluence,PassageInfluence)*Fade; + } + if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); + const bool bRock=RockDistance>=0.0f; + float Density=-RockDistance; + if((Density<0.0f)!=bRock||std::abs(Density)<0.05f) + Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f); + return ApplyEditBrushes(Density,X,Y,Z); + } + +public: + 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}; + if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0] + &&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1] + &&EditBoundsMin.Z<=BMax[2]&&EditBoundsMax.Z>=BMin[2]) + return EVoxelTileClass::Mixed; + 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; + const bool bFamilies=AuditSurfaceBandFamilies(); + 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&&bFamilies?"PASS":"FAIL",bCoherent?"PASS":"FAIL"); + return bFlips&&bCoherent&&bFamilies; + } + + bool AuditSurfaceBandFamilies(bool bPassageOnly=false) const + { + struct FPatchKey + { + int32 X=0,Y=0,Z=0;uint8 Family=0; + bool operator==(const FPatchKey&)const=default; + }; + struct FPatchHash + { + size_t operator()(const FPatchKey& 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^=static_cast(K.Family)*0x94D049BB133111EBull; + H^=H>>29;H*=0xBF58476D1CE4E5B9ull;H^=H>>31; + return static_cast(H); + } + }; + struct FPatchStats {uint64 Count=0;double Sum=0.0,SumSq=0.0;}; + struct FFamilyStats {uint64 Count=0,NearZero=0;double Sum=0.0,SumSq=0.0;}; + std::unordered_map Patches; + std::array Families{}; + FFamilyStats PassageWalls,OrdinaryCaveWalls; + for(size_t Line=0;Line(Size[0])*Size[1];++Line) + { + const int32 X=Min[0]+static_cast(Line%Size[0]); + const int32 Y=Min[1]+static_cast(Line/Size[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); + ESurfaceFamily Family=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; + bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; + float PassageInfluence=0.0f; + const float Raw=EffectiveRockDistance(X,Y,Z,&Family,&SurfaceSDF, + &bFloorSurface,&CaveInfluence,&FloorInfluence,nullptr,nullptr,false, + &PassageInfluence); + if(std::abs(Raw)>=0.5f)continue; + const float Fade=DetailBandFadeAt(X,Y,Z,Raw,SurfaceSDF, + CaveInfluence,FloorInfluence); + const float Detail=Fade>0.0f + ?DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence, + PassageInfluence)*Fade:0.0f; + const size_t FamilyIndex=static_cast(Family); + FFamilyStats& F=Families[FamilyIndex];++F.Count;F.Sum+=Detail;F.SumSq+=Detail*Detail; + if(std::abs(Detail)<0.05f)++F.NearZero; + if(PassageInfluence>=0.5f) + {++PassageWalls.Count;PassageWalls.Sum+=Detail;PassageWalls.SumSq+=Detail*Detail;} + else if(Family==ESurfaceFamily::Cave&&PassageInfluence<0.1f) + {++OrdinaryCaveWalls.Count;OrdinaryCaveWalls.Sum+=Detail;OrdinaryCaveWalls.SumSq+=Detail*Detail;} + const FPatchKey Key{FloorDiv(X,16),FloorDiv(Y,16),FloorDiv(Z,16), + static_cast(FamilyIndex)}; + FPatchStats& P=Patches[Key];++P.Count;P.Sum+=Detail;P.SumSq+=Detail*Detail; + } + } + } + std::array FlatCounts{};uint64 LargestFlatPatch=0; + for(const auto& Entry:Patches) + { + const FPatchStats& P=Entry.second;if(P.Count<16)continue; + const double Mean=P.Sum/P.Count; + const double StdDev=std::sqrt(std::max(0.0,P.SumSq/P.Count-Mean*Mean)); + if(StdDev<0.10) + { + FlatCounts[Entry.first.Family]+=P.Count; + LargestFlatPatch=std::max(LargestFlatPatch,P.Count); + } + } + bool bPass=true;uint64 Tested=0,FlatTotal=0,Total=0; + for(size_t I=0;I(F.NearZero)/F.Count; + const bool FamilyPass=StdDev>=0.15&&ZeroShare<0.60&&FlatCounts[I](F.Count),StdDev,ZeroShare*100.0, + static_cast(FlatCounts[I]),FamilyPass?"PASS":"FAIL"); + } + const double FlatShare=Total?100.0*static_cast(FlatTotal)/Total:100.0; + std::printf("[FlatPatchAudit] patch_width=16 vox min_patch_samples=16 flat_stddev_lt=0.10 flat_share=%.1f%% largest_flat_patch=%llu status=%s\n", + FlatShare,static_cast(LargestFlatPatch), + Tested>0&&FlatShare<20.0?"PASS":"FAIL"); + const double PassageRms=PassageWalls.Count + ?std::sqrt(PassageWalls.SumSq/PassageWalls.Count):0.0; + const double OrdinaryCaveRms=OrdinaryCaveWalls.Count + ?std::sqrt(OrdinaryCaveWalls.SumSq/OrdinaryCaveWalls.Count):0.0; + const double PassageRmsRatio=OrdinaryCaveRms>1.0e-9 + ?PassageRms/OrdinaryCaveRms:0.0; + const bool bHasPassageSamples=PassageWalls.Count>0; + const bool bRatioPass=PassageWalls.Count>=32&&OrdinaryCaveWalls.Count>=32 + &&PassageRmsRatio>=0.75&&PassageRmsRatio<=1.25; + const bool bPassageDetail=bPassageOnly?bRatioPass:(!bHasPassageSamples||bRatioPass); + const char* PassageStatus=!bHasPassageSamples + ?(bPassageOnly?"FAIL":"SKIP"):(bPassageDetail?"PASS":"FAIL"); + std::printf("[PassageWallDetailAudit] passage_samples=%llu passage_rms=%.4f cave_samples=%llu cave_rms=%.4f ratio=%.3f range=(0.750,1.250) status=%s\n", + static_cast(PassageWalls.Count),PassageRms, + static_cast(OrdinaryCaveWalls.Count),OrdinaryCaveRms, + PassageRmsRatio,PassageStatus); + return bPassageOnly?bPassageDetail:(Tested>0&&FlatShare<20.0&&bPass&&bPassageDetail); + } + + bool AuditViewSurface(double CameraX,double CameraY,double CameraZ, + float Yaw,float Pitch) const + { + struct FFamilyStats + { + uint64 Hits=0,NearZero=0,AxisLocked=0; + double Sum=0.0,SumSq=0.0; + }; + std::array FamilyStats{}; + constexpr std::array YawOffsets={-36.0f,-30.0f,-24.0f,-18.0f,-12.0f,-6.0f, + 0.0f,6.0f,12.0f,18.0f,24.0f,30.0f,36.0f}; + constexpr std::array PitchOffsets={-80.0f,-70.0f,-60.0f,-50.0f,-40.0f, + -30.0f,-20.0f,-10.0f,0.0f,10.0f,20.0f}; + const double OX=CameraX*4.0,OY=CameraY*4.0,OZ=CameraZ*4.0; + auto Distance=[&](double X,double Y,double Z,ESurfaceFamily* OutFamily=nullptr, + float* OutRaw=nullptr,float* OutDetail=nullptr, + bool* OutFloorSurface=nullptr,float* OutPassageInfluence=nullptr) + { + ESurfaceFamily Family=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; + bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; + float PassageInfluence=0.0f; + float D=EffectiveRockDistance(X,Y,Z,&Family,&SurfaceSDF, + &bFloorSurface,&CaveInfluence,&FloorInfluence,nullptr,nullptr,false, + &PassageInfluence); + float Detail=0.0f; + const bool bInside=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1] + &&Z>=Min[2]&&Z<=Max[2]; + const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z); + if(bInside&&DetailVariant!=EDetailVariant::Smooth&&std::abs(D)<=DetailSupremum+0.125f) + { + const float Fade=DetailBandFadeAt(X,Y,Z,D,SurfaceSDF,CaveInfluence,FloorInfluence); + if(Fade>0.0f) + {Detail=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence, + PassageInfluence)*Fade;D+=Detail;} + } + if(bCriticalAir)D=std::min(D,-1.0f/16.0f); + if(OutFamily)*OutFamily=Family; + if(OutRaw)*OutRaw=D-Detail; + if(OutDetail)*OutDetail=Detail; + if(OutFloorSurface)*OutFloorSurface=bFloorSurface; + if(OutPassageInfluence)*OutPassageInfluence=PassageInfluence; + return D; + }; + uint64 TotalHits=0,TotalAxisLocked=0,TotalNearZero=0,FloorTaggedHits=0,FloorAxisLocked=0; + uint64 FamilyTransitionPairs=0,StableFamilyPairs=0; + uint64 ThinDetachedSheets=0; + double ThinnestDetachedSheet=std::numeric_limits::max(); + double FamilyTransitionGradientSq=0.0,StableFamilyGradientSq=0.0; + for(float YawOffset:YawOffsets)for(float PitchOffset:PitchOffsets) + { + const double YawRad=Yaw+YawOffset*0.017453292519943295; + const double PitchRad=Pitch+PitchOffset*0.017453292519943295; + const double CP=std::cos(PitchRad),DX=std::sin(YawRad)*CP, + DY=std::cos(YawRad)*CP,DZ=std::sin(PitchRad); + double PreviousT=0.0; + float PreviousD=Distance(OX,OY,OZ+0.001); + bool bFound=false; + double PrimaryHitT=-1.0; + for(double T=0.5;T<=256.0;T+=0.5) + { + const double X=OX+DX*T,Y=OY+DY*T,Z=OZ+DZ*T; + const float D=Distance(X,Y,Z); + if(PreviousD<0.0f&&D>=0.0f) + { + double Lo=PreviousT,Hi=T; + for(int32 It=0;It<10;++It) + { + const double Mid=(Lo+Hi)*0.5; + if(Distance(OX+DX*Mid,OY+DY*Mid,OZ+DZ*Mid)<0.0f)Lo=Mid;else Hi=Mid; + } + const double HitT=(Lo+Hi)*0.5; + PrimaryHitT=HitT; + const double XH=OX+DX*HitT,YH=OY+DY*HitT,ZH=OZ+DZ*HitT; + ESurfaceFamily Family=ESurfaceFamily::Strata;float Raw=0.0f,Detail=0.0f; + bool bFloorSurface=false;float PassageInfluence=0.0f; + Distance(XH,YH,ZH,&Family,&Raw,&Detail,&bFloorSurface,&PassageInfluence); + const double GX=Distance(XH+0.5,YH,ZH)-Distance(XH-0.5,YH,ZH); + const double GY=Distance(XH,YH+0.5,ZH)-Distance(XH,YH-0.5,ZH); + const double GZ=Distance(XH,YH,ZH+0.5)-Distance(XH,YH,ZH-0.5); + const double GL=std::sqrt(GX*GX+GY*GY+GZ*GZ); + const double AxisDot=GL>1.0e-8?std::max({std::abs(GX),std::abs(GY),std::abs(GZ)})/GL:1.0; + for(int32 Axis=0;Axis<3;++Axis) + { + double AP[3]={XH,YH,ZH},AM[3]={XH,YH,ZH}; + AP[Axis]+=0.5;AM[Axis]-=0.5; + ESurfaceFamily FamilyP=ESurfaceFamily::Strata,FamilyM=ESurfaceFamily::Strata; + float DetailP=0.0f,DetailM=0.0f;bool bFloorP=false,bFloorM=false; + Distance(AP[0],AP[1],AP[2],&FamilyP,nullptr,&DetailP,&bFloorP); + Distance(AM[0],AM[1],AM[2],&FamilyM,nullptr,&DetailM,&bFloorM); + const double Gradient=static_cast(DetailP-DetailM); + if(FamilyP!=FamilyM||bFloorP!=bFloorM) + {FamilyTransitionGradientSq+=Gradient*Gradient;++FamilyTransitionPairs;} + else + {StableFamilyGradientSq+=Gradient*Gradient;++StableFamilyPairs;} + } + constexpr double ScanRadius=16.0; + constexpr int32 ScanSteps=128; + double ScanT=std::max(0.0,HitT-ScanRadius); + ESurfaceFamily ScanFamily=ESurfaceFamily::Strata; + bool bScanFloor=false;float ScanRaw=0.0f,ScanDetail=0.0f; + Distance(OX+DX*ScanT,OY+DY*ScanT,OZ+DZ*ScanT, + &ScanFamily,&ScanRaw,&ScanDetail,&bScanFloor); + for(int32 Step=1;Step<=ScanSteps;++Step) + { + const double T=std::min(HitT+ScanRadius,ScanT+0.25); + ESurfaceFamily NextFamily=ESurfaceFamily::Strata; + bool bNextFloor=false;float NextRaw=0.0f,NextDetail=0.0f; + Distance(OX+DX*T,OY+DY*T,OZ+DZ*T, + &NextFamily,&NextRaw,&NextDetail,&bNextFloor); + if(std::abs(ScanRaw)<=5.0f&&std::abs(NextRaw)<=5.0f) + { + const double Gradient=static_cast(NextDetail-ScanDetail); + if(ScanFamily!=NextFamily||bScanFloor!=bNextFloor) + {FamilyTransitionGradientSq+=Gradient*Gradient;++FamilyTransitionPairs;} + else + {StableFamilyGradientSq+=Gradient*Gradient;++StableFamilyPairs;} + } + ScanT=T;ScanFamily=NextFamily;bScanFloor=bNextFloor; + ScanRaw=NextRaw;ScanDetail=NextDetail; + } + FFamilyStats& S=FamilyStats[static_cast(Family)]; + ++S.Hits;++TotalHits;S.Sum+=Detail;S.SumSq+=Detail*Detail; + if(std::abs(Detail)<0.05f){++S.NearZero;++TotalNearZero;} + if(AxisDot>0.985){++S.AxisLocked;++TotalAxisLocked;} + if(bFloorSurface){++FloorTaggedHits;if(AxisDot>0.985)++FloorAxisLocked;} + if(YawOffset==0.0f&&PitchOffset==0.0f) + { + const char* Name=Family==ESurfaceFamily::Strata?"strata": + Family==ESurfaceFamily::Cave?"cave":"solid_overlay"; + std::printf("[SurfaceRay] t=%.2f vox hit=(%.2f,%.2f,%.2f) family=%s floor_surface=%s passage_influence=%.3f raw_sdf=%.4f detail=%.4f axis_dot=%.4f\n", + HitT,XH,YH,ZH,Name,bFloorSurface?"yes":"no",PassageInfluence,Raw,Detail,AxisDot); + } + bFound=true;break; + } + PreviousT=T;PreviousD=D; + } + if(PrimaryHitT>=0.0) + { + constexpr double SheetStep=0.25; + constexpr double MinimumAirGap=3.0; + constexpr double MaximumSheetThickness=1.0; + double T=PrimaryHitT+SheetStep; + float Prev=Distance(OX+DX*T,OY+DY*T,OZ+DZ*T); + bool bInSolid=Prev>=0.0f; + double SolidStart=T,LastSolidExit=PrimaryHitT; + bool bSeparatedFromPrevious=false; + for(T+=SheetStep;T<=256.0;T+=SheetStep) + { + const float D=Distance(OX+DX*T,OY+DY*T,OZ+DZ*T); + if(!bInSolid&&Prev<0.0f&&D>=0.0f) + { + SolidStart=T; + bSeparatedFromPrevious=(T-LastSolidExit)>=MinimumAirGap; + bInSolid=true; + } + else if(bInSolid&&Prev>=0.0f&&D<0.0f) + { + const double Thickness=T-SolidStart; + if(bSeparatedFromPrevious&&Thickness(TotalNearZero)/TotalHits:1.0; + const double AxisShare=TotalHits?static_cast(TotalAxisLocked)/TotalHits:1.0; + const double FloorAxisShare=FloorTaggedHits?static_cast(FloorAxisLocked)/FloorTaggedHits:1.0; + const double TransitionRMS=FamilyTransitionPairs + ?std::sqrt(FamilyTransitionGradientSq/FamilyTransitionPairs):0.0; + const double StableRMS=StableFamilyPairs + ?std::sqrt(StableFamilyGradientSq/StableFamilyPairs):0.0; + const double TransitionRatio=StableRMS>1.0e-6?TransitionRMS/StableRMS:1000000.0; + double Sum=0.0,SumSq=0.0; + for(const FFamilyStats& S:FamilyStats){Sum+=S.Sum;SumSq+=S.SumSq;} + const double Mean=TotalHits?Sum/TotalHits:0.0; + const double Variance=TotalHits?std::max(0.0,SumSq/TotalHits-Mean*Mean):0.0; + const double SurfaceDetailRMS=std::sqrt(Variance); + const double RelativeJoinStep=SurfaceDetailRMS>1.0e-6?TransitionRMS/SurfaceDetailRMS:1000000.0; + const bool bDetailPass=TotalHits>=24&&std::sqrt(Variance)>=0.15&&NearZeroShare<0.50; + constexpr double GridStairThreshold=0.40; + const bool bJoinCoverage=FloorTaggedHits>=8 + &&(FamilyTransitionPairs==0||(FamilyTransitionPairs>=8&&RelativeJoinStep=24&&AxisShare<0.60 + &&bJoinCoverage; + const bool bThinSheetPass=ThinDetachedSheets==0; + std::printf("[FlatSurfaceAudit] rays=%llu detail_stddev=%.4f vox near_zero_share=%.1f%% threshold=50.0%% status=%s\n", + static_cast(TotalHits),std::sqrt(Variance),NearZeroShare*100.0, + bDetailPass?"PASS":"FAIL"); + std::printf("[GridStairAudit] rays=%llu axis_locked_normals=%.1f%% threshold=60.0%% floor_tagged_hits=%llu floor_axis_locked=%.1f%% family_transition_pairs=%llu transition_rms=%.4f vox stable_rms=%.4f vox surface_detail_rms=%.4f vox relative_join_step=%.3f threshold=%.3f legacy_join_ratio=%.3f status=%s\n", + static_cast(TotalHits),AxisShare*100.0, + static_cast(FloorTaggedHits),FloorAxisShare*100.0, + static_cast(FamilyTransitionPairs),TransitionRMS,StableRMS, + SurfaceDetailRMS,RelativeJoinStep,GridStairThreshold,TransitionRatio,bGridPass?"PASS":"FAIL"); + std::printf("[ThinSheetAudit] rays=%llu isolated_sheets=%llu thickness_lt=1.000 vox air_gap_ge=3.000 vox thinnest=%.3f vox status=%s\n", + static_cast(TotalHits),static_cast(ThinDetachedSheets), + ThinDetachedSheets?ThinnestDetachedSheet:0.0,bThinSheetPass?"PASS":"FAIL"); + for(size_t I=0;I(S.Hits),M,V, + static_cast(S.NearZero),static_cast(S.AxisLocked)); + } + const bool bFloorProfilePass=Stats.MaxUnmarkedFloorSlopeDelta<=0.75 + &&Stats.MaxFloorCurveDeviation<=2.0; + return bDetailPass&&bGridPass&&bThinSheetPass&&bFloorProfilePass; + } + +private: + float ApplyEditBrushes(float Density,double X,double Y,double Z) const + { + if(EditBrushes.empty()||XEditBoundsMax.X + ||YEditBoundsMax.Y||ZEditBoundsMax.Z) + return Density; + const double P[3]={X,Y,Z}; + for(const FEditBrush& Brush:EditBrushes) + { + double Distance=0.0; + if(Brush.Shape==FEditBrush::EShape::Sphere) + { + const double DX=X-Brush.A[0],DY=Y-Brush.A[1],DZ=Z-Brush.A[2]; + Distance=DeterministicSqrt(DX*DX+DY*DY+DZ*DZ)-Brush.Radius; + } + else if(Brush.Shape==FEditBrush::EShape::Capsule) + { + double AB[3]{},AP[3]{},LengthSq=0.0,Dot=0.0; + for(int32 Axis=0;Axis<3;++Axis) + {AB[Axis]=Brush.B[Axis]-Brush.A[Axis];AP[Axis]=P[Axis]-Brush.A[Axis];LengthSq+=AB[Axis]*AB[Axis];Dot+=AB[Axis]*AP[Axis];} + const double T=LengthSq>1.0e-12?std::clamp(Dot/LengthSq,0.0,1.0):0.0; + double D2=0.0; + for(int32 Axis=0;Axis<3;++Axis) + {const double D=AP[Axis]-T*AB[Axis];D2+=D*D;} + Distance=DeterministicSqrt(D2)-Brush.Radius; + } + else + { + double OutsideSq=0.0,MaxQ=-std::numeric_limits::infinity(); + for(int32 Axis=0;Axis<3;++Axis) + { + const double Q=std::abs(P[Axis]-Brush.A[Axis])-Brush.HalfExtent[Axis]; + const double Outside=std::max(Q,0.0); + OutsideSq+=Outside*Outside;MaxQ=std::max(MaxQ,Q); + } + Distance=DeterministicSqrt(OutsideSq)+std::min(MaxQ,0.0)-Brush.Rounding; + } + if(Brush.Operation==FEditBrush::EOperation::DensityOffset) + { + double Influence=0.0; + if(Brush.Shape==FEditBrush::EShape::Sphere) + { + const double Radius=std::max(Brush.Radius,1.0e-12); + const double T=std::clamp((Distance+Brush.Radius)/Radius,0.0,1.0); + if(Distance+Brush.Radius>=Brush.Radius)continue; + Influence=1.0-T*T*(3.0-2.0*T); + } + else + { + const double Band=std::max(static_cast(Brush.Falloff),0.01); + if(Distance>=Band)continue; + const double T=std::clamp(std::max(Distance,0.0)/Band,0.0,1.0); + Influence=1.0-T*T*(3.0-2.0*T); + } + Density+=Brush.Strength*static_cast(Influence); + continue; + } + if(Brush.Operation==FEditBrush::EOperation::Carve) + Density=std::max(Density,static_cast(-Distance)); + else + Density=std::min(Density,static_cast(Distance)); + } + return Density; + } + + struct FAxisBins { int32 MinA=0,MinB=0,CountA=0,CountB=0; std::vector> Items; }; + struct FLayerField + { + int32 Lo=0,Hi=-1; + int32 StrateIndex=INDEX_NONE; + FStrateGenerationParams Params; + }; + enum class ESurfaceFamily : uint8 { Strata, Cave, SolidOverlay }; + + 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; + } + + static double DeterministicSqrt(double Value) + { + if(!(Value>0.0))return 0.0; + return _mm_cvtsd_f64(_mm_sqrt_sd(_mm_setzero_pd(),_mm_set_sd(Value))); + } + + 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);} + static float SmoothMinDistance(float A,float B,float Width) + { + if(Width<=0.0f)return std::min(A,B); + const float H=std::max(Width-std::abs(A-B),0.0f)/Width; + return std::min(A,B)-(H*H*H)*(Width/6.0f); + } + static float SmoothMaxDistance(float A,float B,float Width) + {return -SmoothMinDistance(-A,-B,Width);} + static float SmoothUnit(float V) + { + V=std::clamp(V,0.0f,1.0f); + return V*V*(3.0f-2.0f*V); + } + static float DetailBandFade(float SignedDistance) + { + // Keep the larger candidate mask conservative around smooth family unions; + // visible relief still fades to zero beyond its 4.125-voxel support radius. + constexpr float Edge=DetailSupremum+0.125f; + constexpr float Width=1.5f; + return SmoothUnit((Edge-std::abs(SignedDistance))/Width); + } + static constexpr float DetailBandInnerRadius=1.5f; + static float DetailBandFade(float SignedDistance,float Edge) + { + const float Distance=std::abs(SignedDistance); + if(Distance<=DetailBandInnerRadius)return 1.0f; + const float Width=Edge-DetailBandInnerRadius; + return Width>0.0f?SmoothUnit((Edge-Distance)/Width):0.0f; + } + static float LocalDetailBandEdge(float MacroValue,float SurfaceSDF, + float CaveInfluence,float FloorInfluence,const FLayerField* Field) + { + float OwnerBound=0.0f; + if(Field) + { + const FStrateGenerationParams& Params=Field->Params; + const float Depth=Params.SurfaceRoughness*2.0f; + if(Depth>0.0f&&std::abs(SurfaceSDF)0.0f) + { + const float Fade=1.0f-std::abs(SurfaceSDF)/Depth; + // Perlin is bounded by 2; the squared ridged fold by 2.25. This + // uses the largest type bound and remains valid under domain warp. + OwnerBound=3.15f*VOXEL_NOISE_SCALE*Params.SurfaceRoughness*Fade*Fade; + } + } + const float ReliefBound=std::abs(MacroValue) + +(0.80f+0.25f*MacroValue)*(0.85f+0.18f); + const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) + +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); + const float LocalSupremum=std::min(3.9f,(ReliefBound+0.5f*OwnerBound)*Contrast); + return std::clamp(LocalSupremum+0.125f,DetailBandInnerRadius,DetailSupremum+0.125f); + } + + float DetailBandFadeAt(double X,double Y,double Z,float SignedDistance,float SurfaceSDF, + float CaveInfluence,float FloorInfluence) const + { + if(DetailVariant!=EDetailVariant::Coherent + ||std::abs(SignedDistance)<=DetailBandInnerRadius) + return DetailBandFade(SignedDistance); + const float MacroValue=FStandaloneResearchTunnels::ValueNoise( + static_cast(X),static_cast(Y),static_cast(Z),32, + static_cast(Owner->Seed)^0xB5297A4Du); + const FLayerField* Field=FindLayerField(Z); + return DetailBandFade(SignedDistance,LocalDetailBandEdge(MacroValue,SurfaceSDF, + CaveInfluence,FloorInfluence,Field)); + } + + void BuildLayerBounds() + { + LayerRanges.clear();LayerFields.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(!Slot.Definition)continue; + const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2; + FLayerField Field; + Field.Lo=Slot.BottomChunkZ*CHUNK_SIZE; + Field.Hi=(Slot.TopChunkZ+1)*CHUNK_SIZE; + Field.StrateIndex=Slot.StrateIndex; + Field.Params=Owner->Manager->GetGenerationParams(FIntVector(0,0,MidChunkZ)); + LayerFields.push_back(std::move(Field)); + } + 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.BlendRadius=ActiveBlendRadius; + 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, + double FloorBlendRadius=0.0) + { + P.bHasFloorClip=true;P.FloorCenterZ=FloorCenterZ-0.5; + P.FloorSlopeX=SlopeX;P.FloorSlopeY=SlopeY;P.FloorBlendRadius=FloorBlendRadius; + } + + 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) + { + AddLegacyEllipsoidCapsule(A,B,Radius,bCarve,bCritical,bFloorClip,FloorA,FloorB); + } + + static FVector ApproximateUnwarp(const FVector& Target,const FStrateGenerationParams& Params, + uint32 Seed) + { + FVector WorldPoint=Target; + const float ZScale=Params.VerticalScale>0.0f?Params.VerticalScale:1.0f; + if(ZScale!=1.0f)WorldPoint.Z*=ZScale; + if(Params.CaveWarpStrength<=0.0f)return WorldPoint; + for(int32 Iteration=0;Iteration<8;++Iteration) + { + const FVector Mapped=VoxelCaveMorphology::ApplyCaveWarp(WorldPoint,Params,Seed); + const FVector Error=Target-Mapped; + if(!Error.ContainsNaN()&&VoxelMath::IsFinite(Error.X) + &&VoxelMath::IsFinite(Error.Y)&&VoxelMath::IsFinite(Error.Z)) + { + WorldPoint.X+=Error.X;WorldPoint.Y+=Error.Y;WorldPoint.Z+=Error.Z*ZScale; + } + } + return WorldPoint; + } + + void AddLegacyEllipsoidCapsule(const FVector& A,const FVector& B,float Radius,bool bCarve, + bool bCritical,bool bFloorClip,double FloorA,double FloorB) + { + const auto D=Sub(B,A);const double Length=std::sqrt(Dot(D,D)); + const double PreviousBlendRadius=ActiveBlendRadius;ActiveBlendRadius=0.0; + 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),0.0,0.0, + 0.0); + 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,0.0); + else ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB),0.0,0.0, + 0.0); + } + if(bCritical)for(size_t J=Before;J1.0e-4)) + { + AddEllipsoid(A,{RadiusA,RadiusA,RadiusA},{1,0,0},{0,1,0},{0,0,1},bCarve); + Primitives.back().bCritical=bCritical; + if(bFloorClip)ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB),0.0,0.0, + ActiveFloorBlendRadius); + return; + } + ExtendStart=std::clamp(ExtendStart,0.0,Length*0.4); + ExtendEnd=std::clamp(ExtendEnd,0.0,Length*0.4); + const double RadiusSlope=(static_cast(RadiusB)-RadiusA)/Length; + const float ExtendedRadiusA=static_cast(std::max(1.0,RadiusA-RadiusSlope*ExtendStart)); + const float ExtendedRadiusB=static_cast(std::max(1.0,RadiusB+RadiusSlope*ExtendEnd)); + const double Start[3]={A.X-D[0]*(ExtendStart/Length),A.Y-D[1]*(ExtendStart/Length),A.Z-D[2]*(ExtendStart/Length)}; + const double End[3]={B.X+D[0]*(ExtendEnd/Length),B.Y+D[1]*(ExtendEnd/Length),B.Z+D[2]*(ExtendEnd/Length)}; + FPrimitive P; + P.bCapsule=true;P.bCarve=bCarve;P.bCritical=bCritical; + P.CapsuleStartRadius=ExtendedRadiusA;P.CapsuleEndRadius=ExtendedRadiusB; + P.MinRadius=std::min(ExtendedRadiusA,ExtendedRadiusB);P.BlendRadius=ActiveBlendRadius; + const double R=std::max(ExtendedRadiusA,ExtendedRadiusB); + double AxisLengthSquared=0.0; + for(int32 Axis=0;Axis<3;++Axis) + { + P.CapsuleStartSub[Axis]=static_cast(std::llround(Start[Axis]*SubPerVoxel)); + P.CapsuleEndSub[Axis]=static_cast(std::llround(End[Axis]*SubPerVoxel)); + P.CapsuleStartWorld[Axis]=P.CapsuleStartSub[Axis]/static_cast(SubPerVoxel); + const double CapsuleEndWorld=P.CapsuleEndSub[Axis]/static_cast(SubPerVoxel); + P.CapsuleAxisDelta[Axis]=CapsuleEndWorld-P.CapsuleStartWorld[Axis]; + AxisLengthSquared+=P.CapsuleAxisDelta[Axis]*P.CapsuleAxisDelta[Axis]; + P.CenterSub[Axis]=static_cast(std::llround((Start[Axis]+End[Axis])*0.5*SubPerVoxel)); + P.Min[Axis]=static_cast(std::floor(std::min(Start[Axis],End[Axis])-R))-1; + P.Max[Axis]=static_cast(std::ceil(std::max(Start[Axis],End[Axis])+R))+1; + } + P.CapsuleAxisLengthSquared=AxisLengthSquared; + P.CapsuleAxisLength=std::sqrt(std::max(0.0,AxisLengthSquared)); + if(bFloorClip) + { + const double FloorSlope=(FloorB-FloorA)/Length; + const double ExtendedFloorA=FloorA-FloorSlope*ExtendStart; + const double ExtendedFloorB=FloorB+FloorSlope*ExtendEnd; + const double ExtendedD[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; + const double HorizontalSq=ExtendedD[0]*ExtendedD[0]+ExtendedD[1]*ExtendedD[1]; + if(HorizontalSq>1.0e-6) + ApplyFloorClip(P,(ExtendedFloorA+ExtendedFloorB)*0.5, + (ExtendedFloorB-ExtendedFloorA)*ExtendedD[0]/HorizontalSq, + (ExtendedFloorB-ExtendedFloorA)*ExtendedD[1]/HorizontalSq,ActiveFloorBlendRadius); + else ApplyFloorClip(P,std::min(ExtendedFloorA,ExtendedFloorB),0.0,0.0,ActiveFloorBlendRadius); + } + Primitives.push_back(P); + } + + static double ContinuousJoinExtension(float RadiusA,float RadiusB,double SegmentLength) + { + return std::min(std::max(0.0,SegmentLength*0.35), + JoinBlendRadius+0.5*static_cast(std::max(RadiusA,RadiusB))); + } + + static float CapsuleRadiusAt(const FPrimitive& P,const FVector& Point) + { + if(!P.bCapsule)return P.MinRadius; + const double Start[3]={P.CapsuleStartSub[0]/static_cast(SubPerVoxel), + P.CapsuleStartSub[1]/static_cast(SubPerVoxel), + P.CapsuleStartSub[2]/static_cast(SubPerVoxel)}; + const double End[3]={P.CapsuleEndSub[0]/static_cast(SubPerVoxel), + P.CapsuleEndSub[1]/static_cast(SubPerVoxel), + P.CapsuleEndSub[2]/static_cast(SubPerVoxel)}; + const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]}; + const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; + const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2]; + const double T=D2>1.0e-12?(V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2:0.0; + return CapsuleRadiusAtT(P,T,std::sqrt(std::max(0.0,D2))); + } + + static void FitCapsuleRadiiAtEndpoints(FPrimitive& P,const FVector& PointA,float RadiusA, + const FVector& PointB,float RadiusB) + { + const double Start[3]={P.CapsuleStartSub[0]/static_cast(SubPerVoxel), + P.CapsuleStartSub[1]/static_cast(SubPerVoxel), + P.CapsuleStartSub[2]/static_cast(SubPerVoxel)}; + const double End[3]={P.CapsuleEndSub[0]/static_cast(SubPerVoxel), + P.CapsuleEndSub[1]/static_cast(SubPerVoxel), + P.CapsuleEndSub[2]/static_cast(SubPerVoxel)}; + const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; + const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2]; + if(D2<=1.0e-12)return; + const auto Parameter=[&](const FVector& Point) + { + const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]}; + return (V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2; + }; + const double TA=Parameter(PointA),TB=Parameter(PointB); + if(std::abs(TB-TA)<=1.0e-9)return; + const double Slope=(RadiusB-RadiusA)/(TB-TA); + P.CapsuleStartRadius=static_cast(RadiusA-Slope*TA); + P.CapsuleEndRadius=static_cast(P.CapsuleStartRadius+Slope); + P.MinRadius=std::min(P.CapsuleStartRadius,P.CapsuleEndRadius); + } + + static void SetCapsuleRadiusProfile(FPrimitive& P,const FVector& PointA,float RadiusA, + double SlopeA,const FVector& PointB,float RadiusB,double SlopeB) + { + const double Start[3]={P.CapsuleStartSub[0]/static_cast(SubPerVoxel), + P.CapsuleStartSub[1]/static_cast(SubPerVoxel), + P.CapsuleStartSub[2]/static_cast(SubPerVoxel)}; + const double End[3]={P.CapsuleEndSub[0]/static_cast(SubPerVoxel), + P.CapsuleEndSub[1]/static_cast(SubPerVoxel), + P.CapsuleEndSub[2]/static_cast(SubPerVoxel)}; + const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; + const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2]; + if(D2<=1.0e-12)return; + const auto Parameter=[&](const FVector& Point) + { + const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]}; + return std::clamp((V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2,0.0,1.0); + }; + P.CapsuleProfileStartT=Parameter(PointA); + P.CapsuleProfileEndT=Parameter(PointB); + P.CapsuleProfileStartRadius=RadiusA; + P.CapsuleProfileEndRadius=RadiusB; + P.CapsuleRadiusSlopeStart=SlopeA; + P.CapsuleRadiusSlopeEnd=SlopeB; + P.bSmoothCapsuleRadius=P.CapsuleProfileEndT-P.CapsuleProfileStartT>1.0e-6; + } + + static float SmoothedControlRadius(const TArray& Radii,int32 Index,float Fallback, + float Minimum) + { + const int32 Count=Radii.Num(); + const float Current=std::max(Minimum,Radii.IsValidIndex(Index)?Radii[Index]:Fallback); + if(Index<=0||Index+1>=Count)return Current; + const auto At=[&](int32 I) + {return std::max(Minimum,Radii[std::clamp(I,0,Count-1)]);}; + return (At(Index-4)+8.0f*At(Index-3)+28.0f*At(Index-2)+56.0f*At(Index-1) + +70.0f*Current+56.0f*At(Index+1)+28.0f*At(Index+2)+8.0f*At(Index+3) + +At(Index+4))*(1.0f/256.0f); + } + + static double ControlRadiusSlopeAt(const TArray& Points,const TArray& Radii, + int32 Index,float Fallback,float Minimum) + { + if(Points.Num()<2||Index<0||Index>=Points.Num())return 0.0; + const auto RadiusAt=[&](int32 I) + {return static_cast(SmoothedControlRadius(Radii,I,Fallback,Minimum));}; + const auto LengthAt=[&](int32 I) + { + const FVector& A=Points[I];const FVector& B=Points[I+1]; + const double DX=B.X-A.X,DY=B.Y-A.Y,DZ=B.Z-A.Z; + return std::sqrt(DX*DX+DY*DY+DZ*DZ); + }; + if(Index==0) + { + const double Length=LengthAt(0); + return Length>1.0e-9?(RadiusAt(1)-RadiusAt(0))/Length:0.0; + } + if(Index+1==Points.Num()) + { + const double Length=LengthAt(Index-1); + return Length>1.0e-9?(RadiusAt(Index)-RadiusAt(Index-1))/Length:0.0; + } + const double PrevLength=LengthAt(Index-1),NextLength=LengthAt(Index); + if(PrevLength<=1.0e-9||NextLength<=1.0e-9)return 0.0; + const double PrevSlope=(RadiusAt(Index)-RadiusAt(Index-1))/PrevLength; + const double NextSlope=(RadiusAt(Index+1)-RadiusAt(Index))/NextLength; + if(PrevSlope*NextSlope<=0.0)return 0.0; + const double W1=2.0*NextLength+PrevLength,W2=NextLength+2.0*PrevLength; + const double Harmonic=(W1+W2)/(W1/PrevSlope+W2/NextSlope); + const double Limit=3.0*std::min(std::abs(PrevSlope),std::abs(NextSlope)); + return std::copysign(std::min(std::abs(Harmonic),Limit),Harmonic); + } + + static double EvaluateFloorProfile(double A,double B,double TangentA,double TangentB, + double Length,double StartBlend,double EndBlend,double S) + { + const double Secant=(B-A)/Length; + if(S<0.0)return A+TangentA*S; + if(S>Length)return B+TangentB*(S-Length); + double Result=std::lerp(A,B,S/Length); + const auto Correction=[](double DeltaSlope,double Span,double T) + { + if(!(Span>0.0)||T<0.0||T>=1.0)return 0.0; + return DeltaSlope*Span*(T-2.0*T*T+T*T*T); + }; + if(S(X),static_cast(Y),0.0f,32,Seed^0x7F4A7C15u); + const float Mid=FStandaloneResearchTunnels::ValueNoise( + static_cast(X),static_cast(Y),0.0f,16,Seed^0x94D049BBu); + return 0.80*Broad+0.35*Mid; + } + + static std::vector BuildFloorTangents(const TArray& Points, + const std::vector& Floor,const std::vector& MarkedSegments) + { + const size_t Count=Floor.size();std::vector Tangent(Count,0.0); + if(Count<2||Count!=static_cast(Points.Num())||MarkedSegments.size()+1!=Count)return Tangent; + std::vector Length(Count-1,0.0),Slope(Count-1,0.0); + for(size_t I=0;I+1(I)];const FVector& B=Points[static_cast(I+1)]; + Length[I]=std::hypot(B.X-A.X,B.Y-A.Y); + if(Length[I]>1.0e-6)Slope[I]=(Floor[I+1]-Floor[I])/Length[I]; + } + for(size_t I=0;I0&&Length[I-1]>1.0e-6; + const bool bHasNext=I+11.0e-6; + if(I==0){Tangent[I]=MarkedSegments[0]?0.0:Slope[0];continue;} + if(I+1==Count){Tangent[I]=MarkedSegments[I-1]?0.0:Slope[I-1];continue;} + const bool bPrevMarked=MarkedSegments[I-1]!=0,bNextMarked=MarkedSegments[I]!=0; + if(bPrevMarked||bNextMarked) + { + if(!bPrevMarked&&bHasPrev)Tangent[I]=Slope[I-1]; + else if(!bNextMarked&&bHasNext)Tangent[I]=Slope[I]; + continue; + } + if(!bHasPrev||!bHasNext||Slope[I-1]*Slope[I]<=0.0)continue; + const double W0=2.0*Length[I]+Length[I-1],W1=Length[I]+2.0*Length[I-1]; + const double Denominator=W0/Slope[I-1]+W1/Slope[I]; + if(std::abs(Denominator)>1.0e-12) + Tangent[I]=(W0+W1)/Denominator; + } + return Tangent; + } + + static void SetCurvedFloorClip(FPrimitive& P,const FVector& A,const FVector& B, + double FloorA,double FloorB,double TangentA,double TangentB) + { + if(!P.bHasFloorClip)return; + const double DX=B.X-A.X,DY=B.Y-A.Y,Length=std::hypot(DX,DY); + if(Length<=1.0e-6)return; + P.bCurvedFloorClip=true;P.FloorCurveOriginX=A.X;P.FloorCurveOriginY=A.Y; + P.FloorCurveDeltaX=DX;P.FloorCurveDeltaY=DY;P.FloorCurveLength=Length; + P.FloorCurveStartZ=FloorA;P.FloorCurveEndZ=FloorB; + P.FloorCurveTangentStart=TangentA;P.FloorCurveTangentEnd=TangentB; + P.FloorCurveStartBlend=std::min(8.0,Length*0.35); + P.FloorCurveEndBlend=std::min(8.0,Length*0.35); + for(int32 I=1;I<64;++I) + { + const double T=I/64.0; + const double Curve=EvaluateFloorProfile(FloorA,FloorB,TangentA,TangentB, + Length,P.FloorCurveStartBlend,P.FloorCurveEndBlend,Length*T); + const double Linear=std::lerp(FloorA,FloorB,T); + P.FloorCurveMaxDeviation=std::max(P.FloorCurveMaxDeviation,std::abs(Curve-Linear)); + } + } + + double FloorAt(const FPrimitive& P,double X,double Y) const + { + double Base=0.0; + if(P.bCurvedFloorClip) + { + const double T=((X-P.FloorCurveOriginX)*P.FloorCurveDeltaX + +(Y-P.FloorCurveOriginY)*P.FloorCurveDeltaY) + /(P.FloorCurveLength*P.FloorCurveLength); + Base=EvaluateFloorProfile(P.FloorCurveStartZ,P.FloorCurveEndZ, + P.FloorCurveTangentStart,P.FloorCurveTangentEnd,P.FloorCurveLength, + P.FloorCurveStartBlend,P.FloorCurveEndBlend,T*P.FloorCurveLength); + } + else + Base=P.FloorCenterZ+P.FloorSlopeX*(X-P.CenterSub[0]/16.0) + +P.FloorSlopeY*(Y-P.CenterSub[1]/16.0); + return Base; + } + + double FloorSlopeAtEndpoint(const FPrimitive& P,bool bEnd) const + { + const double DX=P.bCurvedFloorClip?P.FloorCurveDeltaX + :P.CapsuleEndSub[0]-P.CapsuleStartSub[0]; + const double DY=P.bCurvedFloorClip?P.FloorCurveDeltaY + :P.CapsuleEndSub[1]-P.CapsuleStartSub[1]; + const double Length=P.bCurvedFloorClip?P.FloorCurveLength + :std::hypot(DX,DY)/SubPerVoxel; + if(Length<=1.0e-9)return 0.0; + const double OriginX=P.bCurvedFloorClip?P.FloorCurveOriginX + :P.CapsuleStartSub[0]/static_cast(SubPerVoxel); + const double OriginY=P.bCurvedFloorClip?P.FloorCurveOriginY + :P.CapsuleStartSub[1]/static_cast(SubPerVoxel); + const double X=OriginX+(bEnd?DX/(P.bCurvedFloorClip?1.0:SubPerVoxel):0.0); + const double Y=OriginY+(bEnd?DY/(P.bCurvedFloorClip?1.0:SubPerVoxel):0.0); + constexpr double Epsilon=0.125; + const double UX=DX/(Length*(P.bCurvedFloorClip?1.0:SubPerVoxel)); + const double UY=DY/(Length*(P.bCurvedFloorClip?1.0:SubPerVoxel)); + return (FloorAt(P,X+UX*Epsilon,Y+UY*Epsilon) + -FloorAt(P,X-UX*Epsilon,Y-UY*Epsilon))/(2.0*Epsilon); + } + + 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); + ActiveBlendRadius=0.0; + ActiveFloorBlendRadius=0.0; + 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(); + const FVector RoomCenter=ApproximateUnwarp(R.Center,Params, + static_cast(Owner->Seed)); + if(R.ShapeType==2) + AddCapsule(ApproximateUnwarp(R.ShapeA,Params,static_cast(Owner->Seed)), + ApproximateUnwarp(R.ShapeB,Params,static_cast(Owner->Seed)), + std::max(1.0f,R.ShapeR),true); + else if(R.ShapeType==1) + { + AddRoundedBox(RoomCenter,{std::abs(R.ShapeA.X),std::abs(R.ShapeA.Y),std::abs(R.ShapeA.Z)}, + std::max(0.0f,R.ShapeR),true); + } + else + AddEllipsoid(RoomCenter,{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); + const double FloorZ=ApproximateUnwarp( + FVector(R.Center.X,R.Center.Y,R.FloorCutZ-Relief),Params, + static_cast(Owner->Seed)).Z; + for(size_t I=RoomPrimitiveStart;I(Cache.RoomFloorJoins.Num()); + const double RoomBlendRadius=ActiveBlendRadius; + ActiveBlendRadius=JoinBlendRadius; + ActiveFloorBlendRadius=FloorClipBlendRadius; + for(const FCachedTunnel& T:Cache.Tunnels) + { + ++Stats.TunnelCount; + const bool bHasWorldChain=T.WorldControlPoints.Num()>1 + &&T.WorldControlRadii.Num()==T.WorldControlPoints.Num(); + const TArray& Points=bHasWorldChain?T.WorldControlPoints:T.ControlPoints; + const TArray& Radii=bHasWorldChain?T.WorldControlRadii:T.ControlRadii; + const TArray& FloorProfiles=bHasWorldChain + &&T.WorldFloorProfiles.Num()==Points.Num()-1?T.WorldFloorProfiles:T.FloorProfiles; + std::vector RawFloor(static_cast(Points.Num())); + std::vector MarkedFloorSegments(Points.Num()>0?static_cast(Points.Num()-1):0,0); + for(int32 I=0;I(I)]=static_cast(Points[I].Z + -SmoothedControlRadius(Radii,I,T.RadiusA,2.0f)); + if(FloorProfiles.Num()==Points.Num()-1) + { + for(int32 I=0;I+1(I)]=0.5f*(FloorProfiles[I-1].EndFloorZ+Profile.StartFloorZ); + RawFloor[static_cast(I+1)]=Profile.EndFloorZ; + MarkedFloorSegments[static_cast(I)]=Profile.bLedgeTransition?1:0; + } + } + for(int32 I=0;I(I-1)]; + const bool bNextUnmarked=I+1>=Points.Num() + ||!MarkedFloorSegments[static_cast(I)]; + if(bPreviousUnmarked&&bNextUnmarked) + RawFloor[static_cast(I)]+=static_cast(FloorSurfaceRelief( + Points[I].X,Points[I].Y,static_cast(Owner->Seed))); + } + const std::vector FloorTangents=BuildFloorTangents( + Points,RawFloor,MarkedFloorSegments); + for(int32 Join=1;Join+11.0e-6&&BL>1.0e-6) + { + const double Dot=(A.X*B.X+A.Y*B.Y+A.Z*B.Z)/(AL*BL); + const double Angle=std::acos(std::clamp(Dot,-1.0,1.0))*57.29577951308232; + if(Angle>Stats.MaxTunnelBendDegrees) + { + Stats.MaxTunnelBendDegrees=Angle;Stats.MaxTunnelBendVoxels=Points[Join]; + const double DX=A.X/AL+B.X/BL,DY=A.Y/AL+B.Y/BL,DZ=A.Z/AL+B.Z/BL; + const double DL=std::sqrt(DX*DX+DY*DY+DZ*DZ); + if(DL>1.0e-6)Stats.MaxTunnelBendDirection=FVector(DX/DL,DY/DL,DZ/DL); + } + } + const float RPrev=std::max(2.0f,Radii.IsValidIndex(Join-1)?Radii[Join-1]:T.RadiusA); + const float RHere=std::max(2.0f,Radii.IsValidIndex(Join)?Radii[Join]:T.RadiusA); + const float RNext=std::max(2.0f,Radii.IsValidIndex(Join+1)?Radii[Join+1]:T.RadiusB); + Stats.LegacySegmentRadiusStep=std::max(Stats.LegacySegmentRadiusStep, + static_cast(std::abs(std::max(RPrev,RHere)-std::max(RHere,RNext)))); + if(FloorProfiles.IsValidIndex(Join-1)&&FloorProfiles.IsValidIndex(Join)) + { + Stats.MaxCapsuleJoinFloorDelta=std::max(Stats.MaxCapsuleJoinFloorDelta, + static_cast(std::abs(FloorProfiles[Join-1].EndFloorZ + -FloorProfiles[Join].StartFloorZ))); + const FVector PrevD=Points[Join]-Points[Join-1]; + const FVector NextD=Points[Join+1]-Points[Join]; + const double PrevXY=std::sqrt(PrevD.X*PrevD.X+PrevD.Y*PrevD.Y); + const double NextXY=std::sqrt(NextD.X*NextD.X+NextD.Y*NextD.Y); + const double RawPrevSlope=PrevXY>1.0e-6 + ?(FloorProfiles[Join-1].EndFloorZ-FloorProfiles[Join-1].StartFloorZ)/PrevXY:0.0; + const double RawNextSlope=NextXY>1.0e-6 + ?(FloorProfiles[Join].EndFloorZ-FloorProfiles[Join].StartFloorZ)/NextXY:0.0; + const double RawSlopeDelta=std::abs(RawPrevSlope-RawNextSlope); + if(FloorProfiles[Join-1].bLedgeTransition||FloorProfiles[Join].bLedgeTransition) + { + ++Stats.MarkedFloorSlopeJoins; + Stats.MaxMarkedFloorSlopeDelta=std::max(Stats.MaxMarkedFloorSlopeDelta,RawSlopeDelta); + } + else + { + Stats.MaxRawUnmarkedFloorSlopeDelta=std::max(Stats.MaxRawUnmarkedFloorSlopeDelta,RawSlopeDelta); + } + } + ++Stats.CapsuleJoinCount; + } + if(Points.Num()>1) + { + const size_t SegmentPrimitiveStart=Primitives.size(); + for(int32 I=0;I+1(std::abs(R1-R0))); + const FVector Segment=Points[I+1]-Points[I]; + const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z); + const double Extension=ContinuousJoinExtension(R0,R1,SegmentLength); + const float ZA=RawFloor[static_cast(I)]; + const float ZB=RawFloor[static_cast(I+1)]; + AddTaperedCapsule(Points[I],Points[I+1],R0,R1,true,false,true,ZA,ZB, + I>0?Extension:0.0,I+2(I)]) + { + SetCurvedFloorClip(Primitives.back(),Points[I],Points[I+1],ZA,ZB, + FloorTangents[static_cast(I)],FloorTangents[static_cast(I+1)]); + ++Stats.CurvedFloorSegments; + Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation, + Primitives.back().FloorCurveMaxDeviation); + } + FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],R0,Points[I+1],R1); + SetCapsuleRadiusProfile(Primitives.back(),Points[I],R0,RadiusSlope0, + Points[I+1],R1,RadiusSlope1); + } + for(int32 Join=1;Join+1(Join-1)]; + const FPrimitive& Next=Primitives[SegmentPrimitiveStart+static_cast(Join)]; + const float PreviousEnd=CapsuleRadiusAt(Previous,Points[Join]); + const float NextStart=CapsuleRadiusAt(Next,Points[Join]); + Stats.MaxCapsuleJoinRadiusDelta=std::max(Stats.MaxCapsuleJoinRadiusDelta, + static_cast(std::abs(PreviousEnd-NextStart))); + Stats.MaxCapsuleJoinRadiusSlopeDelta=std::max( + Stats.MaxCapsuleJoinRadiusSlopeDelta, + std::abs(Previous.CapsuleRadiusSlopeEnd-Next.CapsuleRadiusSlopeStart)); + if(!MarkedFloorSegments[static_cast(Join-1)] + &&!MarkedFloorSegments[static_cast(Join)]) + { + const double TangentDelta=Previous.bCurvedFloorClip&&Next.bCurvedFloorClip + ?std::abs(Previous.FloorCurveTangentEnd-Next.FloorCurveTangentStart) + :std::abs(FloorSlopeAtEndpoint(Previous,true)-FloorSlopeAtEndpoint(Next,false)); + const double DirectionalSlopeChange=std::abs(FloorSlopeAtEndpoint(Previous,true) + -FloorSlopeAtEndpoint(Next,false)); + Stats.MaxCapsuleJoinFloorSlopeDelta=std::max( + Stats.MaxCapsuleJoinFloorSlopeDelta,TangentDelta); + Stats.MaxUnmarkedFloorSlopeDelta=std::max( + Stats.MaxUnmarkedFloorSlopeDelta,TangentDelta); + Stats.MaxUnmarkedFloorDirectionSlopeDelta=std::max( + Stats.MaxUnmarkedFloorDirectionSlopeDelta,DirectionalSlopeChange); + } + } + } + else AddCapsule(T.EndpointA,T.EndpointB,std::max(T.RadiusA,T.RadiusB),true); + } + ActiveBlendRadius=RoomBlendRadius; + ActiveFloorBlendRadius=0.0; + for(const FCachedPit& P:Cache.Pits) + AddCapsule(FVector(P.CenterX,P.CenterY,P.TopZ-P.Depth), + FVector(P.CenterX,P.CenterY,P.TopZ),std::max(2.0f,P.Radius),true); + for(const FCachedChimney& C:Cache.Chimneys) + AddCapsule(FVector(C.CenterX,C.CenterY,C.BottomZ), + FVector(C.CenterX,C.CenterY,C.BottomZ+C.Height),std::max(2.0f,C.Radius),true); + for(const FCachedColumn& C:Cache.Columns) + { + FVector Center(C.CenterX,C.CenterY,(LayerRanges[0].Lo+LayerRanges[0].Hi)*0.5); + const double HalfZ=std::max(8.0,(LayerRanges[0].Hi-LayerRanges[0].Lo)*0.48); + AddEllipsoid(Center,{std::max(2.0f,C.Radius),std::max(2.0f,C.Radius),HalfZ}, + {1,0,0},{0,1,0},{0,0,1},false); + } + } + const double PassageJoinBlend=ActiveBlendRadius;ActiveBlendRadius=JoinBlendRadius; + ActiveFloorBlendRadius=FloorClipBlendRadius; + for(const FVoxelPassage& P:Owner->Manager->GetPassages()) + { + ++Stats.PassageCount; + const TArray& Points=P.ControlPoints; + const TArray& Radii=P.ControlRadii; + const size_t PassagePrimitiveStart=Primitives.size(); + std::vector RawFloor(static_cast(Points.Num())); + std::vector MarkedFloorSegments(Points.Num()>0?static_cast(Points.Num()-1):0,0); + for(int32 I=0;I(I)]=static_cast(Points[I].Z + -SmoothedControlRadius(Radii,I,P.Radius,6.0f)); + if(P.NativeFloorProfileZ.Num()==Points.Num()) + for(int32 I=0;I(I)]=P.NativeFloorProfileZ[I]; + for(int32 I=0;I(I)]+=static_cast(FloorSurfaceRelief( + Points[I].X,Points[I].Y,static_cast(Owner->Seed))); + const std::vector FloorTangents=BuildFloorTangents( + Points,RawFloor,MarkedFloorSegments); + for(int32 Join=1;Join+1(std::abs(std::max(RPrev,RHere)-std::max(RHere,RNext)))); + if(P.NativeFloorProfileZ.IsValidIndex(Join-1)&&P.NativeFloorProfileZ.IsValidIndex(Join)) + { + const FVector PrevD=Points[Join]-Points[Join-1]; + const FVector NextD=Points[Join+1]-Points[Join]; + const double PrevXY=std::sqrt(PrevD.X*PrevD.X+PrevD.Y*PrevD.Y); + const double NextXY=std::sqrt(NextD.X*NextD.X+NextD.Y*NextD.Y); + const double PrevSlope=PrevXY>1.0e-6 + ?(P.NativeFloorProfileZ[Join]-P.NativeFloorProfileZ[Join-1])/PrevXY:0.0; + const double NextSlope=NextXY>1.0e-6 + ?(P.NativeFloorProfileZ[Join+1]-P.NativeFloorProfileZ[Join])/NextXY:0.0; + const double RawSlopeDelta=std::abs(PrevSlope-NextSlope); + Stats.MaxRawUnmarkedFloorSlopeDelta=std::max(Stats.MaxRawUnmarkedFloorSlopeDelta,RawSlopeDelta); + } + ++Stats.CapsuleJoinCount; + } + if(Points.Num()>1) + { + for(int32 I=0;I+1(std::abs(RadiusB-RadiusA))); + const FVector Segment=Points[I+1]-Points[I]; + const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z); + const float ZA=RawFloor[static_cast(I)]; + const float ZB=RawFloor[static_cast(I+1)]; + const double Extension=ContinuousJoinExtension(RadiusA,RadiusB,SegmentLength); + const double CoreExtension=ContinuousJoinExtension(std::min(2.5f,RadiusA*0.45f), + std::min(2.5f,RadiusB*0.45f),SegmentLength); + const double ExtendStart=I>0?Extension:0.0,ExtendEnd=I+20?CoreExtension:0.0,CoreExtendEnd=I+2(I)],FloorTangents[static_cast(I+1)]); + ++Stats.CurvedFloorSegments; + Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation, + Primitives.back().FloorCurveMaxDeviation); + FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],RadiusA,Points[I+1],RadiusB); + SetCapsuleRadiusProfile(Primitives.back(),Points[I],RadiusA,RadiusSlopeA, + Points[I+1],RadiusB,RadiusSlopeB); + const float CoreRadiusA=std::min(2.5f,RadiusA*0.45f); + const float CoreRadiusB=std::min(2.5f,RadiusB*0.45f); + const double CoreSlopeA=CoreRadiusA<2.5f-1.0e-4?RadiusSlopeA*0.45:0.0; + const double CoreSlopeB=CoreRadiusB<2.5f-1.0e-4?RadiusSlopeB*0.45:0.0; + AddTaperedCapsule(Points[I],Points[I+1],CoreRadiusA, + CoreRadiusB,true,true,true,ZA,ZB, + CoreExtendStart,CoreExtendEnd); + SetCurvedFloorClip(Primitives.back(),Points[I],Points[I+1],ZA,ZB, + FloorTangents[static_cast(I)],FloorTangents[static_cast(I+1)]); + ++Stats.CurvedFloorSegments; + Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation, + Primitives.back().FloorCurveMaxDeviation); + FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],CoreRadiusA, + Points[I+1],CoreRadiusB); + SetCapsuleRadiusProfile(Primitives.back(),Points[I],CoreRadiusA,CoreSlopeA, + Points[I+1],CoreRadiusB,CoreSlopeB); + } + for(int32 Join=1;Join+1(Join-1)*2; + const size_t NextBase=PassagePrimitiveStart+static_cast(Join)*2; + for(size_t Tube=0;Tube<2;++Tube) + { + const FPrimitive& Previous=Primitives[PreviousBase+Tube]; + const FPrimitive& Next=Primitives[NextBase+Tube]; + const float PreviousEnd=CapsuleRadiusAt(Previous,Points[Join]); + const float NextStart=CapsuleRadiusAt(Next,Points[Join]); + Stats.MaxCapsuleJoinRadiusDelta=std::max(Stats.MaxCapsuleJoinRadiusDelta, + static_cast(std::abs(PreviousEnd-NextStart))); + Stats.MaxCapsuleJoinRadiusSlopeDelta=std::max( + Stats.MaxCapsuleJoinRadiusSlopeDelta, + std::abs(Previous.CapsuleRadiusSlopeEnd-Next.CapsuleRadiusSlopeStart)); + if(Tube==0) + { + const double TangentDelta=Previous.bCurvedFloorClip&&Next.bCurvedFloorClip + ?std::abs(Previous.FloorCurveTangentEnd-Next.FloorCurveTangentStart) + :std::abs(FloorSlopeAtEndpoint(Previous,true)-FloorSlopeAtEndpoint(Next,false)); + const double DirectionalSlopeChange=std::abs(FloorSlopeAtEndpoint(Previous,true) + -FloorSlopeAtEndpoint(Next,false)); + Stats.MaxCapsuleJoinFloorSlopeDelta=std::max( + Stats.MaxCapsuleJoinFloorSlopeDelta,TangentDelta); + Stats.MaxUnmarkedFloorSlopeDelta=std::max( + Stats.MaxUnmarkedFloorSlopeDelta,TangentDelta); + Stats.MaxUnmarkedFloorDirectionSlopeDelta=std::max( + Stats.MaxUnmarkedFloorDirectionSlopeDelta,DirectionalSlopeChange); + } + } + } + } + else + { + AddCapsule(P.UpperPoint,P.LowerPoint,std::max(6.0f,P.Radius),true,true); + Primitives.back().bPassageSurface=true; + } + AddLanding(P.UpperLanding,true); + AddLanding(P.LowerLanding,true); + } + ActiveBlendRadius=PassageJoinBlend;ActiveFloorBlendRadius=0.0; + if(Owner->Settings->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); + const FVector A(Last.first[0],Last.first[1],Last.first[2]); + const FVector B(Next.first[0],Next.first[1],Next.first[2]); + const FVector Segment=B-A; + const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z); + const double Extension=ContinuousJoinExtension(static_cast(Last.second), + static_cast(Next.second),SegmentLength); + AddTaperedCapsule(A,B,static_cast(Last.second),static_cast(Next.second), + true,false,false,0.0,0.0,I>1?Extension:0.0,I(Primitives.size()*BinsPerPrimitive,1u)); + if(!bBuildAxisBins) + { + CriticalAirBins.reserve(Primitives.size()/16+1); + } + if(bBuildAxisBins)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) + { + const FBinKey Key{X,Y,Z}; + SpatialBins[Key].push_back(Id); + if(bCriticalAir)CriticalAirBins[Key].push_back(Id); + } + } + } + + void BuildFarParentShapes() + { + constexpr int32 ParentPitch=2048; + FarConnectorSegments.clear(); + 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; + AddTaperedCapsule(A,B,64.0f,64.0f,true); + Primitives.back().BlendRadius=0.0; + FarConnectorSegments.push_back({A,B,64.0f}); + } + } + BuildFarConnectorSpatialBins(); + 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]); + } + + void BuildFarConnectorSpatialBins() + { + constexpr int32 BinSize=2048; + FarConnectorSpatialBins.clear(); + FarConnectorSpatialBins.reserve(FarConnectorSegments.size()); + for(uint32 Index=0;Index(std::floor( + (std::min(Connector.Start.X,Connector.End.X)-Connector.Radius)/BinSize)); + const int32 MaxX=static_cast(std::floor( + (std::max(Connector.Start.X,Connector.End.X)+Connector.Radius)/BinSize)); + const int32 MinY=static_cast(std::floor( + (std::min(Connector.Start.Y,Connector.End.Y)-Connector.Radius)/BinSize)); + const int32 MaxY=static_cast(std::floor( + (std::max(Connector.Start.Y,Connector.End.Y)+Connector.Radius)/BinSize)); + const int32 MinZ=static_cast(std::floor( + (std::min(Connector.Start.Z,Connector.End.Z)-Connector.Radius)/BinSize)); + const int32 MaxZ=static_cast(std::floor( + (std::max(Connector.Start.Z,Connector.End.Z)+Connector.Radius)/BinSize)); + for(int32 Z=MinZ;Z<=MaxZ;++Z)for(int32 Y=MinY;Y<=MaxY;++Y) + for(int32 X=MinX;X<=MaxX;++X) + FarConnectorSpatialBins[{X,Y,Z}].push_back(Index); + } + } + + 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.bCapsule) + { + const double Position[3]={static_cast(X),static_cast(Y),static_cast(Z)}; + double V[3],D[3];double D2=0.0,VD=0.0,V2=0.0; + for(int32 A=0;A<3;++A) + { + const double Start=P.CapsuleStartSub[A]/static_cast(SubPerVoxel); + const double End=P.CapsuleEndSub[A]/static_cast(SubPerVoxel); + V[A]=Position[A]-Start;D[A]=End-Start; + D2+=D[A]*D[A];VD+=V[A]*D[A];V2+=V[A]*V[A]; + } + const double R0=P.CapsuleStartRadius,R1=P.CapsuleEndRadius,DR=R1-R0; + const double QA=D2-DR*DR,QB=-2.0*(VD+R0*DR),QC=V2-R0*R0; + double Minimum=std::min(QC,QA+QB+QC); + if(QA>1.0e-12) + { + const double T=std::clamp(-QB/(2.0*QA),0.0,1.0); + Minimum=std::min(Minimum,(QA*T+QB)*T+QC); + } + bInside=Minimum<=0.0; + } + else 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=FloorAt(P,X,Y); + if(P.FloorBlendRadius<=0.0)return Z>=Floor; + return SmoothMaxDistance(ShapeDistance(P,X,Y,Z),static_cast(Floor-Z), + static_cast(P.FloorBlendRadius))<=0.0f; + } + return true; + } + + static float CapsuleRadiusAtT(const FPrimitive& P,double T,double AxisLength) + { + T=std::clamp(T,0.0,1.0); + if(!P.bSmoothCapsuleRadius) + return static_cast(P.CapsuleStartRadius + +(P.CapsuleEndRadius-P.CapsuleStartRadius)*T); + const double T0=P.CapsuleProfileStartT,T1=P.CapsuleProfileEndT; + if(T(P.CapsuleProfileStartRadius + +P.CapsuleRadiusSlopeStart*AxisLength*(T-T0)); + if(T>T1)return static_cast(P.CapsuleProfileEndRadius + +P.CapsuleRadiusSlopeEnd*AxisLength*(T-T1)); + const double SpanT=std::max(1.0e-9,T1-T0); + const double U=std::clamp((T-T0)/SpanT,0.0,1.0),U2=U*U,U3=U2*U; + const double SpanDistance=AxisLength*SpanT; + const double H00=2.0*U3-3.0*U2+1.0; + const double H10=U3-2.0*U2+U; + const double H01=-2.0*U3+3.0*U2; + const double H11=U3-U2; + return static_cast(H00*P.CapsuleProfileStartRadius + +H10*SpanDistance*P.CapsuleRadiusSlopeStart + +H01*P.CapsuleProfileEndRadius + +H11*SpanDistance*P.CapsuleRadiusSlopeEnd); + } + + float ShapeDistance(const FPrimitive& P,double X,double Y,double Z) const + { + if(P.bCapsule) + { + const double Position[3]={X,Y,Z}; + double V[3],D[3],D2=0.0,VD=0.0; + for(int32 A=0;A<3;++A) + { + V[A]=Position[A]-P.CapsuleStartWorld[A]; + D[A]=P.CapsuleAxisDelta[A]; + VD+=V[A]*D[A]; + } + D2=P.CapsuleAxisLengthSquared; + const double T=D2>1.0e-12?std::clamp(VD/D2,0.0,1.0):0.0; + double DistanceSquared=0.0; + for(int32 A=0;A<3;++A) + {const double E=V[A]-T*D[A];DistanceSquared+=E*E;} + const double Radius=CapsuleRadiusAtT(P,T,P.CapsuleAxisLength); + return static_cast(std::sqrt(DistanceSquared)-Radius); + } + 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&&!P.bCapsule) + { + 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&&(!P.bCurvedFloorClip||Axis==2)) + { + 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=FloorAt(P,X0,Y0); + const double SmoothAllowance=P.FloorBlendRadius>0.0?P.FloorBlendRadius/6.0:0.0; + const double Margin0=Z0-(Floor0-SmoothAllowance); + 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(P.bCapsule) + { + double V2=0.0,VD=0.0,D2=0.0; + for(int32 J=0;J<3;++J)if(J!=Axis) + { + const double Start=P.CapsuleStartSub[J]/static_cast(SubPerVoxel); + const double End=P.CapsuleEndSub[J]/static_cast(SubPerVoxel); + const double V=Fixed[J]-Start,D=End-Start; + V2+=V*V;VD+=V*D;D2+=D*D; + } + const double R0=P.CapsuleStartRadius,DR=P.CapsuleEndRadius-R0; + const double QA=D2-DR*DR,QB=-2.0*(VD+R0*DR),QC=V2-R0*R0; + const auto Value=[&](double T){return (QA*T+QB)*T+QC;}; + double TBest=Value(1.0)1.0e-12) + { + const double T=std::clamp(-QB/(2.0*QA),0.0,1.0); + if(Value(T)0.0)return false; + const double Start=P.CapsuleStartSub[Axis]/static_cast(SubPerVoxel); + const double End=P.CapsuleEndSub[Axis]/static_cast(SubPerVoxel); + Mid=std::clamp(static_cast(std::llround(Start+(End-Start)*TBest)),L,H); + } + if(!ShapeContains(P,Axis==0?Mid:Fixed[0],Axis==1?Mid:Fixed[1],Axis==2?Mid:Fixed[2])) + { + bool Found=false; + const int32 SearchLimit=P.bCapsule?std::min(H-L,8):2; + for(int32 Delta=1;Delta<=SearchLimit&&!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&&P.bCapsule) + { + for(int32 Sample=L;Sample<=H;Sample+=2) + if(ShapeContains(P,Axis==0?Sample:Fixed[0],Axis==1?Sample:Fixed[1],Axis==2?Sample:Fixed[2])) + {Mid=Sample;Found=true;break;} + if(!Found&&((H-L)&1) + &&ShapeContains(P,Axis==0?H:Fixed[0],Axis==1?H:Fixed[1],Axis==2?H:Fixed[2])) + {Mid=H;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(FParallelTiming& Timing) + { + const int32 NX=Size[0],NY=Size[1],NZ=Size[2]; + CandidateAxisWords[2].assign(static_cast(NX)*NY*WordsPerZ,0); + struct FIntervalScratch + { + std::vector Carve,Solid,ProtectedAir; + }; + std::array WorkerScratch; + for(int32 Worker=0;Worker(Worker)].Carve.reserve(24); + WorkerScratch[static_cast(Worker)].Solid.reserve(8); + WorkerScratch[static_cast(Worker)].ProtectedAir.reserve(4); + } + ParallelLines(static_cast(NX)*NY,[&](size_t Line,int32 Worker) + { + const int32 X=Min[0]+static_cast(Line%NX),Y=Min[1]+static_cast(Line/NX); + FIntervalScratch& Scratch=WorkerScratch[static_cast(Worker)]; + std::vector& Carve=Scratch.Carve; + std::vector& Solid=Scratch.Solid; + std::vector& ProtectedAir=Scratch.ProtectedAir; + Carve.clear();Solid.clear();ProtectedAir.clear(); + 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)) + { + if(!P.bCritical) + { + const size_t CandidateBase=Line*WordsPerZ; + const int32 CandidateLo=std::max(Min[2],R.Lo-DetailBand); + const int32 CandidateHi=std::min(Max[2],R.Lo+DetailBand); + const int32 CandidateLo2=std::max(Min[2],R.Hi-DetailBand); + const int32 CandidateHi2=std::min(Max[2],R.Hi+DetailBand); + FillBits(CandidateAxisWords[2],CandidateBase, + CandidateLo-Min[2],CandidateHi-Min[2],true); + FillBits(CandidateAxisWords[2],CandidateBase, + CandidateLo2-Min[2],CandidateHi2-Min[2],true); + } + 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); + },&Timing,nullptr,16); + } + + void BuildCandidateBands(FParallelTiming& AxisTiming,FParallelTiming& MergeTiming, + double& MergeSeconds) + { + const int32 N[3]={Size[0],Size[1],Size[2]}; + for(int32 Axis=0;Axis<3;++Axis) + { + if(Axis==2)continue; + 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.bCritical)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); + } + },&AxisTiming,nullptr,16); + } + const auto MergeStart=std::chrono::steady_clock::now(); + CandidateWords=std::move(CandidateAxisWords[2]); + for(int32 Axis=0;Axis<2;++Axis) + { + const int32 A=(Axis+1)%3,B=(Axis+2)%3; + const size_t Words=(static_cast(Size[Axis])+63u)/64u; + ParallelLines(static_cast(Size[A])*Size[B],[&,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]); + 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]); + std::atomic_ref(CandidateWords[Column*WordsPerZ+(Zi>>6)]) + .fetch_or(uint64{1}<<(Zi&63u),std::memory_order_relaxed); + } + } + },&MergeTiming,nullptr,16); + } + const int32 LayerLo=LayerRanges.empty()?1:LayerRanges[0].Lo; + const int32 LayerHi=LayerRanges.empty()?0:LayerRanges[0].Hi; + if(LayerLo<=LayerHi) + { + for(size_t Line=0;Line(Size[0])*Size[1];++Line) + { + const size_t Base=Line*WordsPerZ; + const int32 Lo0=std::max(Min[2],LayerLo-DetailBand)-Min[2]; + const int32 Hi0=std::min(Max[2],LayerLo+DetailBand)-Min[2]; + const int32 Lo1=std::max(Min[2],LayerHi-DetailBand)-Min[2]; + const int32 Hi1=std::min(Max[2],LayerHi+DetailBand)-Min[2]; + FillBits(CandidateWords,Base,Lo0,Hi0,true); + FillBits(CandidateWords,Base,Lo1,Hi1,true); + } + } + const double MergeElapsed=std::chrono::duration( + std::chrono::steady_clock::now()-MergeStart).count(); + MergeSeconds=std::max(0.0,MergeElapsed-MergeTiming.WallSeconds); + } + + void ApplyDetailBand(FParallelTiming& Timing) + { + using Clock=std::chrono::steady_clock; + const bool bProfile=bProfileDetailBand; + std::array WorkerProfile{}; + std::array WorkerActiveSeconds{}; + std::atomic CandidateCount{0},ProxyCount{0},FlipCount{0}; + ParallelLines(static_cast(Size[0])*Size[1],[&](size_t Line,int32 Worker) + { + const int32 X=Min[0]+static_cast(Line%Size[0]); + const int32 Y=Min[1]+static_cast(Line/Size[0]); + FDetailNoiseLineContext NoiseLine(static_cast(X),static_cast(Y), + static_cast(Owner->Seed)); + const FLayerField* LineField=nullptr; + bool bLineFieldKnown=false; + int32 LineFieldLo=0,LineFieldHi=-1; + const auto FindLineLayerField=[&](double Z)->const FLayerField* + { + if(bLineFieldKnown&&LineField&&Z>=LineFieldLo&&Z<=LineFieldHi) + return LineField; + LineField=FindLayerField(Z); + bLineFieldKnown=LineField!=nullptr; + if(LineField){LineFieldLo=LineField->Lo;LineFieldHi=LineField->Hi;} + return LineField; + }; + const int32 QueryBinX=FloorDiv(X,ActiveSpatialBinSize); + const int32 QueryBinY=FloorDiv(Y,ActiveSpatialBinSize); + int32 CachedQueryBinZ=std::numeric_limits::min(); + const FBinItems* CachedQueryBinItems=nullptr; + FDetailWorkerProfile& Profile=WorkerProfile[static_cast(Worker)]; + if(bProfile)++Profile.Lines; + uint64 LocalCandidates=0,LocalProxies=0,LocalFlips=0; + const size_t WordBase=Line*WordsPerZ; + for(size_t W=0;W::min(); + const FBinItems* BatchBinItems=nullptr; + const auto ProcessBatch=[&]() + { + if(BatchCount==0)return; + FEffectiveRockSample Samples[8]; + int32 ProfileDistanceSamples=0; + if(bProfile) + { + for(int32 Lane=0;Lane0 + ?Clock::now():Clock::time_point{}; + EffectiveRockDistanceZ8(X,Y,BatchZ,BatchCount,BatchBinItems,Samples); + if(ProfileDistanceSamples>0) + { + Profile.DistanceSampleNanoseconds+=static_cast( + std::chrono::duration_cast( + Clock::now()-DistanceStart).count()); + Profile.DistanceSamples+=static_cast(BatchCount); + } + for(int32 Lane=0;Lane(Z-Min[2]); + ++LocalCandidates; + const FEffectiveRockSample& Sample=Samples[Lane]; + const float SDF=Sample.Distance; + if(std::abs(SDF)>DetailSupremum+0.125f)continue; + const float SurfaceSDF=Sample.SurfaceSDF; + const float CaveInfluence=Sample.CaveInfluence; + const float FloorInfluence=Sample.FloorInfluence; + const float PassageInfluence=Sample.PassageInfluence; + const bool bCriticalAir=Sample.bCriticalAir; + float DetailFade=DetailBandFade(SDF); + float MacroValue=0.0f; + bool bMacroKnown=false; + const FLayerField* DetailField=nullptr; + bool bDetailFieldKnown=false; + if(DetailVariant==EDetailVariant::Coherent + &&std::abs(SDF)>DetailBandInnerRadius) + { + MacroValue=NoiseLine.Macro.Sample(static_cast(Z)); + bMacroKnown=true; + DetailField=FindLineLayerField(Z); + bDetailFieldKnown=true; + DetailFade=DetailBandFade(SDF,LocalDetailBandEdge(MacroValue, + SurfaceSDF,CaveInfluence,FloorInfluence,DetailField)); + } + if(DetailFade<=0.0f&&bProfile)++Profile.LocalBandSkips; + const bool bTimeDetail=DetailFade>0.0f&&bProfile + &&((Profile.DetailEvaluations&63u)==0); + const auto DetailStart=bTimeDetail?Clock::now():Clock::time_point{}; + float Displaced=SDF; + if(DetailFade>0.0f) + { + if(bProfile)++Profile.DetailEvaluations; + Displaced+=DetailNoiseOnLine(X,Y,Z,Sample.SurfaceSDF, + CaveInfluence,FloorInfluence,PassageInfluence,NoiseLine,bTimeDetail?&Profile:nullptr, + DetailField,bDetailFieldKnown,bMacroKnown,MacroValue)*DetailFade; + } + if(bTimeDetail) + { + Profile.DetailSampleNanoseconds+=static_cast( + std::chrono::duration_cast( + Clock::now()-DetailStart).count()); + ++Profile.DetailSamples; + } + if(bCriticalAir)Displaced=std::min(Displaced,-1.0f/16.0f); + 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; + if(bProfile)++Profile.Flips; + } + 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}<<(Zi&63u); + ++LocalProxies; + } + BatchCount=0; + }; + while(Bits) + { + const uint32 Bit=std::countr_zero(Bits);Bits&=Bits-1; + const size_t Zi=W*64+Bit;if(Zi>=static_cast(Size[2]))continue; + const int32 Z=Min[2]+static_cast(Zi); + const int32 QueryBinZ=FloorDiv(Z,ActiveSpatialBinSize); + if(BatchCount>0&&QueryBinZ!=BatchQueryBinZ)ProcessBatch(); + if(QueryBinZ!=CachedQueryBinZ) + { + const auto Bin=SpatialBins.find({QueryBinX,QueryBinY,QueryBinZ}); + CachedQueryBinItems=Bin==SpatialBins.end()?nullptr:&Bin->second; + CachedQueryBinZ=QueryBinZ; + } + if(BatchCount==0)BatchQueryBinZ=QueryBinZ,BatchBinItems=CachedQueryBinItems; + BatchZ[BatchCount++]=Z; + if(BatchCount==8)ProcessBatch(); + } + ProcessBatch(); + if(OriginalBits!=ValidBits)CandidateWords[WordBase+W]=ValidBits; + } + CandidateCount.fetch_add(LocalCandidates,std::memory_order_relaxed); + ProxyCount.fetch_add(LocalProxies,std::memory_order_relaxed); + FlipCount.fetch_add(LocalFlips,std::memory_order_relaxed); + },&Timing,&WorkerActiveSeconds); + Stats.SurfaceCandidates=CandidateCount.load(std::memory_order_relaxed); + Stats.SurfaceProxyNodes=ProxyCount.load(std::memory_order_relaxed); + Stats.DetailFlips=FlipCount.load(std::memory_order_relaxed); + if(bProfile) + { + uint64 DistanceCalls=0,DetailCalls=0,DistanceSamples=0,DistanceSampleNs=0; + uint64 DetailSamples=0,DetailSampleNs=0,ReliefSampleNs=0,LocalBandSkips=0; + uint64 LayerLookupSampleNs=0,OwnerSampleNs=0,OwnerFieldCalls=0,OwnerActiveCalls=0; + uint64 OwnerCacheHits=0,OwnerCacheMisses=0; + for(int32 Worker=0;Worker(Worker)]; + DistanceCalls+=P.Candidates;DetailCalls+=P.DetailEvaluations; + DistanceSamples+=P.DistanceSamples;DistanceSampleNs+=P.DistanceSampleNanoseconds; + DetailSamples+=P.DetailSamples;DetailSampleNs+=P.DetailSampleNanoseconds; + LocalBandSkips+=P.LocalBandSkips; + ReliefSampleNs+=P.ReliefSampleNanoseconds; + LayerLookupSampleNs+=P.LayerLookupSampleNanoseconds; + OwnerSampleNs+=P.OwnerRoughnessSampleNanoseconds; + OwnerFieldCalls+=P.OwnerRoughnessFieldCalls; + OwnerActiveCalls+=P.OwnerRoughnessActiveCalls; + OwnerCacheHits+=P.OwnerNoiseCacheHits; + OwnerCacheMisses+=P.OwnerNoiseCacheMisses; + } + const double DistanceAverageNs=DistanceSamples?static_cast(DistanceSampleNs)/DistanceSamples:0.0; + const double DetailAverageNs=DetailSamples?static_cast(DetailSampleNs)/DetailSamples:0.0; + const double CandidateWorkerNs=Stats.SurfaceCandidates + ?Timing.WorkerSeconds*1.0e9/static_cast(Stats.SurfaceCandidates):0.0; + std::printf("[DetailBandProfile] candidates=%llu distance_evaluations=%llu detail_noise_evaluations=%llu proxies=%llu flips=%llu candidate_worker_ns=%.1f distance_sample_ns=%.1f distance_estimated_worker_ms=%.1f detail_sample_ns=%.1f detail_estimated_worker_ms=%.1f worker_sum_ms=%.3f wall_ms=%.3f sampling=(distance_1_of_256,detail_1_of_64)\n", + static_cast(Stats.SurfaceCandidates), + static_cast(DistanceCalls),static_cast(DetailCalls), + static_cast(Stats.SurfaceProxyNodes),static_cast(Stats.DetailFlips), + CandidateWorkerNs,DistanceAverageNs,DistanceAverageNs*static_cast(DistanceCalls)/1.0e6, + DetailAverageNs,DetailAverageNs*static_cast(DetailCalls)/1.0e6, + Timing.WorkerSeconds*1000.0,Timing.WallSeconds*1000.0); + std::printf("[DetailBandSubprofile] relief_sample_ns=%.1f relief_estimated_worker_ms=%.1f layer_lookup_sample_ns=%.1f layer_lookup_estimated_worker_ms=%.1f owner_sample_ns=%.1f owner_estimated_worker_ms=%.1f owner_sampled_field_calls=%llu owner_sampled_noise_calls=%llu owner_sampled_core_cache_hits=%llu owner_sampled_core_cache_misses=%llu local_band_skipped=%llu\n", + DetailSamples?static_cast(ReliefSampleNs)/DetailSamples:0.0, + DetailSamples?static_cast(ReliefSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, + DetailSamples?static_cast(LayerLookupSampleNs)/DetailSamples:0.0, + DetailSamples?static_cast(LayerLookupSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, + DetailSamples?static_cast(OwnerSampleNs)/DetailSamples:0.0, + DetailSamples?static_cast(OwnerSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, + static_cast(OwnerFieldCalls), + static_cast(OwnerActiveCalls), + static_cast(OwnerCacheHits), + static_cast(OwnerCacheMisses), + static_cast(LocalBandSkips)); + for(int32 Worker=0;Worker(Worker)]; + std::printf("[DetailBandWorker] id=%d active_ms=%.3f lines=%llu candidates=%llu detail_evaluations=%llu flips=%llu\n", + Worker,WorkerActiveSeconds[static_cast(Worker)]*1000.0, + static_cast(P.Lines),static_cast(P.Candidates), + static_cast(P.DetailEvaluations),static_cast(P.Flips)); + } + } + } + + void StoreSurfaceDistance(size_t Node,float Density) + { + int32 Q=static_cast(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=CriticalAirBins.find(PointBin(X,Y,Z)); + if(It==CriticalAirBins.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; + } + + float SolidOverlayDistance(double X,double Y,double Z) const + { + const auto It=SpatialBins.find(PointBin(X,Y,Z)); + if(It==SpatialBins.end())return -100000.0f; + float Best=-100000.0f; + 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; + Best=std::max(Best,-ShapeDistance(P,X,Y,Z)); + } + return Best; + } + + bool IsInsideCriticalAir(double X,double Y,double Z) const + { + const auto It=CriticalAirBins.find(PointBin(X,Y,Z)); + if(It==CriticalAirBins.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; + } + + float CaveDistance(double X,double Y,double Z,bool* OutFloorSurface=nullptr, + float* OutFloorInfluence=nullptr) const + { + const auto It=SpatialBins.find(PointBin(X,Y,Z)); + if(It==SpatialBins.end()) + { + if(OutFloorSurface)*OutFloorSurface=false; + if(OutFloorInfluence)*OutFloorInfluence=0.0f; + return 100000.0f; + } + float Best=100000.0f; + float BestFloorInfluence=0.0f; + for(uint32 Id:It->second) + { + 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); + float PrimitiveFloorInfluence=0.0f; + if(P.bHasFloorClip) + { + const double Floor=FloorAt(P,X,Y); + const float FloorT=(static_cast(std::abs(Z-Floor))-static_cast(P.FloorBlendRadius)) + /static_cast(std::max(DetailBand*2,1)); + PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT); + Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z), + static_cast(P.FloorBlendRadius)); + } + const float MetadataWidth=static_cast(std::max(P.BlendRadius,6.0)); + const float PrimitiveWeight=SmoothUnit(0.5f+(Best-Distance)/(2.0f*MetadataWidth)); + const float Combined=SmoothMinDistance(Best,Distance,static_cast(P.BlendRadius)); + BestFloorInfluence=std::lerp(BestFloorInfluence,PrimitiveFloorInfluence,PrimitiveWeight); + Best=Combined; + } + if(OutFloorInfluence)*OutFloorInfluence=BestFloorInfluence; + if(OutFloorSurface)*OutFloorSurface=BestFloorInfluence>=0.5f; + return Best; + } + + float StrataDistance(double Z) const + { + if(LayerRanges.empty())return -100000.0f; + return static_cast(std::min(Z-LayerRanges[0].Lo,LayerRanges[0].Hi-Z)); + } + + const FLayerField* FindLayerField(double Z) const + { + for(const FLayerField& Field:LayerFields) + if(Z>=Field.Lo&&Z<=Field.Hi)return &Field; + return nullptr; + } + + float EffectiveRockDistance(double X,double Y,double Z, + ESurfaceFamily* OutFamily=nullptr, + float* OutSurfaceSDF=nullptr, + bool* OutFloorSurface=nullptr, + float* OutCaveInfluence=nullptr, + float* OutFloorInfluence=nullptr, + bool* OutCriticalAir=nullptr, + const FBinItems* KnownBinItems=nullptr, + bool bKnownBin=false, + float* OutPassageInfluence=nullptr) const + { + const float Base=StrataDistance(Z); + float Cave=100000.0f; + float Overlay=-100000.0f; + float CaveFloorInfluence=0.0f; + float CavePassageInfluence=0.0f; + bool bCriticalAir=false; + const FBinItems* BinItems=KnownBinItems; + if(!bKnownBin) + { + const auto Bin=SpatialBins.find(PointBin(X,Y,Z)); + if(Bin!=SpatialBins.end())BinItems=&Bin->second; + } + if(BinItems) + { + for(uint32 Id:*BinItems) + { + const FPrimitive& P=Primitives[Id]; + if(P.bCarve) + { + if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] + &&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P, + static_cast(X),static_cast(Y),static_cast(Z))) + bCriticalAir=true; + if(XP.Max[0]+DetailBand + ||YP.Max[1]+DetailBand + ||ZP.Max[2]+DetailBand)continue; + float Distance=ShapeDistance(P,X,Y,Z); + float PrimitiveFloorInfluence=0.0f; + if(P.bHasFloorClip) + { + const double Floor=FloorAt(P,X,Y); + const float FloorT=(static_cast(std::abs(Z-Floor)) + -static_cast(P.FloorBlendRadius)) + /static_cast(std::max(DetailBand*2,1)); + PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT); + Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z), + static_cast(P.FloorBlendRadius)); + } + const float MetadataWidth=static_cast(std::max(P.BlendRadius,6.0)); + const float PrimitiveWeight=SmoothUnit(0.5f+(Cave-Distance)/(2.0f*MetadataWidth)); + const float Combined=SmoothMinDistance(Cave,Distance,static_cast(P.BlendRadius)); + CaveFloorInfluence=std::lerp(CaveFloorInfluence,PrimitiveFloorInfluence,PrimitiveWeight); + if(OutPassageInfluence&&(CavePassageInfluence>0.0f||P.bPassageSurface)) + CavePassageInfluence=std::lerp(CavePassageInfluence, + P.bPassageSurface?1.0f:0.0f,PrimitiveWeight); + Cave=Combined; + } + else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] + &&Z>=P.Min[2]&&Z<=P.Max[2]) + Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z)); + } + } + if(OutCriticalAir)*OutCriticalAir=bCriticalAir; + const float CarveOrOverlay=std::max(Cave,Overlay); + const ESurfaceFamily Family=Base<=CarveOrOverlay?ESurfaceFamily::Strata: + (Cave>=Overlay?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay); + constexpr float FamilyBlendWidth=6.0f; + const float CaveVsOverlay=SmoothUnit(0.5f+(Cave-Overlay)/(2.0f*FamilyBlendWidth)); + const float CarveInfluence=SmoothUnit(0.5f+(Base-CarveOrOverlay)/(2.0f*FamilyBlendWidth)); + const float CaveInfluence=CarveInfluence*CaveVsOverlay; + const float FloorInfluence=CaveInfluence*CaveFloorInfluence; + const float PassageInfluence=CaveInfluence*CavePassageInfluence; + const float RockDistance=SmoothMinDistance(Base,CarveOrOverlay,StrataCaveBlendRadius); + if(OutFamily) + *OutFamily=Family; + if(OutSurfaceSDF) + *OutSurfaceSDF=Family==ESurfaceFamily::Strata?Base: + (Family==ESurfaceFamily::Cave?Cave:Overlay); + if(OutCaveInfluence)*OutCaveInfluence=CaveInfluence; + if(OutFloorInfluence)*OutFloorInfluence=FloorInfluence; + if(OutPassageInfluence)*OutPassageInfluence=PassageInfluence; + if(OutFloorSurface)*OutFloorSurface=FloorInfluence>=0.5f; + return RockDistance; + } + + struct FEffectiveRockSample + { + float Distance=0.0f; + float SurfaceSDF=0.0f; + float CaveInfluence=0.0f; + float FloorInfluence=0.0f; + float PassageInfluence=0.0f; + ESurfaceFamily Family=ESurfaceFamily::Strata; + bool bFloorSurface=false; + bool bCriticalAir=false; + }; + + void ShapeDistanceZ8(const FPrimitive& P,int32 X,int32 Y,const int32 Z[8],int32 Count, + float OutDistance[8]) const + { + const __m256d Zero=_mm256_setzero_pd(); + const __m256d One=_mm256_set1_pd(1.0); + const __m256d SignMask=_mm256_set1_pd(-0.0); + for(int32 Base=0;Base(Z[Base+std::min(Lane,Lanes-1)]); + const __m256d VZ=_mm256_setr_pd(ZLane[0],ZLane[1],ZLane[2],ZLane[3]); + __m256d Distance; + if(P.bCapsule) + { + const double VX=static_cast(X)-P.CapsuleStartWorld[0]; + const double VY=static_cast(Y)-P.CapsuleStartWorld[1]; + const __m256d VXv=_mm256_set1_pd(VX); + const __m256d VYv=_mm256_set1_pd(VY); + const __m256d VZv=_mm256_sub_pd(VZ,_mm256_set1_pd(P.CapsuleStartWorld[2])); + __m256d VD=_mm256_mul_pd(VXv,_mm256_set1_pd(P.CapsuleAxisDelta[0])); + VD=_mm256_add_pd(VD,_mm256_mul_pd(VYv,_mm256_set1_pd(P.CapsuleAxisDelta[1]))); + VD=_mm256_add_pd(VD,_mm256_mul_pd(VZv,_mm256_set1_pd(P.CapsuleAxisDelta[2]))); + const __m256d T=P.CapsuleAxisLengthSquared>1.0e-12 + ?_mm256_max_pd(Zero,_mm256_min_pd(One, + _mm256_div_pd(VD,_mm256_set1_pd(P.CapsuleAxisLengthSquared)))) + :Zero; + const __m256d EX=_mm256_sub_pd(VXv, + _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[0]))); + const __m256d EY=_mm256_sub_pd(VYv, + _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[1]))); + const __m256d EZ=_mm256_sub_pd(VZv, + _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[2]))); + __m256d DistanceSquared=_mm256_mul_pd(EX,EX); + DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EY,EY)); + DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EZ,EZ)); + const __m256d Root=_mm256_sqrt_pd(DistanceSquared); + double TValues[4],RootValues[4]; + _mm256_storeu_pd(TValues,T); + _mm256_storeu_pd(RootValues,Root); + for(int32 Lane=0;Lane(RootValues[Lane] + -CapsuleRadiusAtT(P,TValues[Lane],P.CapsuleAxisLength)); + continue; + } + if(P.bRoundedBox) + { + const double DX=std::abs(static_cast(X)-P.CenterSub[0]/16.0) + -P.RoundedHalfExtent[0]; + const double DY=std::abs(static_cast(Y)-P.CenterSub[1]/16.0) + -P.RoundedHalfExtent[1]; + const double DZCenter=P.CenterSub[2]/16.0; + const __m256d DZv=_mm256_sub_pd(_mm256_andnot_pd(SignMask, + _mm256_sub_pd(VZ,_mm256_set1_pd(DZCenter))), + _mm256_set1_pd(P.RoundedHalfExtent[2])); + const __m256d DXv=_mm256_set1_pd(DX),DYv=_mm256_set1_pd(DY); + const __m256d Ox=_mm256_max_pd(DXv,Zero),Oy=_mm256_max_pd(DYv,Zero); + const __m256d Oz=_mm256_max_pd(DZv,Zero); + __m256d OutsideSquared=_mm256_mul_pd(Ox,Ox); + OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oy,Oy)); + OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oz,Oz)); + const __m256d MaxXY=_mm256_max_pd(DXv,DYv); + const __m256d MaxQ=_mm256_max_pd(MaxXY,DZv); + const __m256d Inside=_mm256_min_pd(MaxQ,Zero); + Distance=_mm256_sub_pd(_mm256_add_pd(_mm256_sqrt_pd(OutsideSquared),Inside), + _mm256_set1_pd(P.Rounding)); + } + else + { + const __m256d D[3]={ + _mm256_set1_pd(static_cast(X)-P.CenterSub[0]/16.0), + _mm256_set1_pd(static_cast(Y)-P.CenterSub[1]/16.0), + _mm256_sub_pd(VZ,_mm256_set1_pd(P.CenterSub[2]/16.0))}; + __m256d Q=_mm256_setzero_pd(); + for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J) + { + const double Coefficient=static_cast(P.Q[I][J])/QScale; + __m256d Term=_mm256_mul_pd(_mm256_set1_pd(Coefficient),D[I]); + Term=_mm256_mul_pd(Term,D[J]); + Term=_mm256_mul_pd(Term,_mm256_set1_pd(256.0)); + Q=_mm256_add_pd(Q,Term); + } + Distance=_mm256_mul_pd(_mm256_sub_pd( + _mm256_sqrt_pd(_mm256_max_pd(Q,Zero)),_mm256_set1_pd(1.0)), + _mm256_set1_pd(P.MinRadius)); + } + double Values[4]; + _mm256_storeu_pd(Values,Distance); + for(int32 Lane=0;Lane(Values[Lane]); + } + } + + void EffectiveRockDistanceZ8(int32 X,int32 Y,const int32 Z[8],int32 Count, + const FBinItems* BinItems,FEffectiveRockSample Out[8]) const + { + float Base[8],Cave[8],Overlay[8],CaveFloorInfluence[8],CavePassageInfluence[8]; + bool CriticalAir[8]{}; + for(int32 Lane=0;Lane=P.Min[0]&&X<=P.Max[0] + &&Y>=P.Min[1]&&Y<=P.Max[1] + &&ZValue>=P.Min[2]&&ZValue<=P.Max[2] + &&ShapeContains(P,X,Y,ZValue)) + CriticalAir[Lane]=true; + if(XP.Max[0]+DetailBand + ||YP.Max[1]+DetailBand + ||ZValueP.Max[2]+DetailBand) + continue; + Eligible[Lane]=true; + AnyEligible=true; + } + } + else + { + for(int32 Lane=0;Lane=P.Min[0]&&X<=P.Max[0] + &&Y>=P.Min[1]&&Y<=P.Max[1] + &&ZValue>=P.Min[2]&&ZValue<=P.Max[2]; + AnyEligible=AnyEligible||Eligible[Lane]; + } + } + if(!AnyEligible)continue; + + float Distances[8]; + ShapeDistanceZ8(P,X,Y,Z,Count,Distances); + if(P.bCarve) + { + const double Floor=P.bHasFloorClip?FloorAt(P,X,Y):0.0; + const float MetadataWidth=static_cast(std::max(P.BlendRadius,6.0)); + for(int32 Lane=0;Lane(std::abs(Z[Lane]-Floor)) + -static_cast(P.FloorBlendRadius)) + /static_cast(std::max(DetailBand*2,1)); + PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT); + Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z[Lane]), + static_cast(P.FloorBlendRadius)); + } + const float PrimitiveWeight=SmoothUnit(0.5f + +(Cave[Lane]-Distance)/(2.0f*MetadataWidth)); + const float Combined=SmoothMinDistance(Cave[Lane],Distance, + static_cast(P.BlendRadius)); + CaveFloorInfluence[Lane]=std::lerp(CaveFloorInfluence[Lane], + PrimitiveFloorInfluence,PrimitiveWeight); + if(CavePassageInfluence[Lane]>0.0f||P.bPassageSurface) + CavePassageInfluence[Lane]=std::lerp(CavePassageInfluence[Lane], + P.bPassageSurface?1.0f:0.0f,PrimitiveWeight); + Cave[Lane]=Combined; + } + } + else + { + for(int32 Lane=0;Lane=Overlay[Lane]?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay); + constexpr float FamilyBlendWidth=6.0f; + const float CaveVsOverlay=SmoothUnit(0.5f + +(Cave[Lane]-Overlay[Lane])/(2.0f*FamilyBlendWidth)); + const float CarveInfluence=SmoothUnit(0.5f + +(Base[Lane]-CarveOrOverlay)/(2.0f*FamilyBlendWidth)); + const float CaveInfluence=CarveInfluence*CaveVsOverlay; + const float FloorInfluence=CaveInfluence*CaveFloorInfluence[Lane]; + const float PassageInfluence=CaveInfluence*CavePassageInfluence[Lane]; + Out[Lane].Distance=SmoothMinDistance(Base[Lane],CarveOrOverlay,StrataCaveBlendRadius); + Out[Lane].Family=Family; + Out[Lane].SurfaceSDF=Family==ESurfaceFamily::Strata?Base[Lane]: + (Family==ESurfaceFamily::Cave?Cave[Lane]:Overlay[Lane]); + Out[Lane].CaveInfluence=CaveInfluence; + Out[Lane].FloorInfluence=FloorInfluence; + Out[Lane].PassageInfluence=PassageInfluence; + Out[Lane].bFloorSurface=FloorInfluence>=0.5f; + Out[Lane].bCriticalAir=CriticalAir[Lane]; + } + } + + float CoarseRockDistance(double X,double Y,double Z,bool* OutCriticalAir) const + { + const float Base=StrataDistance(Z); + float Cave=100000.0f,Overlay=-100000.0f; + bool bCriticalAir=false; + const FBinKey QueryKey=PointBin(X,Y,Z); + const auto QueryBin=SpatialBins.find(QueryKey); + const FBinItems* QueryItems=QueryBin==SpatialBins.end()?nullptr:&QueryBin->second; + if(QueryItems) + { + for(uint32 Id:*QueryItems) + { + const FPrimitive& P=Primitives[Id]; + if(P.bCarve) + { + if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] + &&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P, + static_cast(X),static_cast(Y),static_cast(Z))) + bCriticalAir=true; + 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=FloorAt(P,X,Y); + Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z), + static_cast(P.FloorBlendRadius)); + } + Cave=SmoothMinDistance(Cave,Distance,static_cast(P.BlendRadius)); + } + else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] + &&Z>=P.Min[2]&&Z<=P.Max[2]) + Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z)); + } + } + if(OutCriticalAir)*OutCriticalAir=bCriticalAir; + return SmoothMinDistance(Base,std::max(Cave,Overlay),StrataCaveBlendRadius); + } + + struct FOwnerNoiseLineContext + { + const FLayerField* Field=nullptr; + int32 GridZ0=MIN_int32; + float LowerCore=0.0f,UpperCore=0.0f; + }; + + float OwnerCaveRoughnessCore(double X,double Y,double Z, + const FStrateGenerationParams& Params) const + { + const uint32 Seed=static_cast(Owner->Seed); + const float RF=Params.RoughnessFrequency; + const float NoiseX=static_cast(X); + const float NoiseY=static_cast(Y); + const float EffectiveZ=(Params.VerticalScale>0.0f&&Params.VerticalScale!=1.0f) + ?static_cast(Z)/Params.VerticalScale:static_cast(Z); + FVector3f Main(NoiseX*RF+VoxelHash::SeedOffset(Seed,11.3f), + NoiseY*RF+VoxelHash::SeedOffset(Seed,13.7f), + EffectiveZ*RF+VoxelHash::SeedOffset(Seed,17.1f)); + FVector3f Fine(NoiseX*RF*3.0f+VoxelHash::SeedOffset(Seed,19.1f)+2000.0f, + NoiseY*RF*3.0f+VoxelHash::SeedOffset(Seed,23.7f)+2500.0f, + EffectiveZ*RF*3.0f+VoxelHash::SeedOffset(Seed,29.3f)+3000.0f); + if(Params.DomainWarpStrength>0.0f) + { + const float WF=Params.DomainWarpFrequency,WS=Params.DomainWarpStrength; + float WX=0.0f,WY=0.0f,WZ=0.0f; + VoxelNoise::Perlin3D_x3( + NoiseX*WF+VoxelHash::SeedOffset(Seed,5.2f), + NoiseY*WF+VoxelHash::SeedOffset(Seed,1.3f), + EffectiveZ*WF+VoxelHash::SeedOffset(Seed,9.7f), + NoiseX*WF+100.0f+VoxelHash::SeedOffset(Seed,7.7f), + NoiseY*WF+200.0f+VoxelHash::SeedOffset(Seed,3.1f), + EffectiveZ*WF+300.0f, + NoiseX*WF+400.0f, + NoiseY*WF+500.0f+VoxelHash::SeedOffset(Seed,11.9f), + EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(Seed,13.3f),WX,WY,WZ); + WX*=VOXEL_NOISE_SCALE*WS;WY*=VOXEL_NOISE_SCALE*WS;WZ*=VOXEL_NOISE_SCALE*WS; + Main+=FVector3f(WX,WY,WZ);Fine+=FVector3f(WX,WY,WZ); + } + float Rough=0.0f,FineValue=0.0f; + switch(Params.RoughnessNoiseType) + { + case EVoxelNoiseType::Ridged: + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(Main.X,Main.Y,Main.Z,3, + Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged); + Rough=MainRidged;FineValue=FineRidged; + break; + } + case EVoxelNoiseType::Mixed: + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(Main.X,Main.Y,Main.Z,3, + Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged); + Rough=MainFBM*0.5f+MainRidged*0.5f; + FineValue=FineFBM*0.5f+FineRidged*0.5f; + break; + } + case EVoxelNoiseType::Cellular: + Rough=VoxelNoise::Cellular3D(Main);FineValue=VoxelNoise::Cellular3D(Fine);break; + case EVoxelNoiseType::FBM: + default: + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(Main.X,Main.Y,Main.Z,3, + Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged); + Rough=MainFBM;FineValue=FineFBM; + break; + } + } + return (Rough+FineValue*0.4f)*VOXEL_NOISE_SCALE*Params.SurfaceRoughness; + } + + float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF, + const FLayerField& Field,float CaveInfluence, + FOwnerNoiseLineContext& LineContext, + FDetailWorkerProfile* Profile=nullptr, + int32 AnchorStep=8) const + { + const FStrateGenerationParams& Params=Field.Params; + const float Depth=Params.SurfaceRoughness*2.0f; + if(!(Depth>0.0f)||std::abs(SurfaceSDF)>=Depth||!(Params.RoughnessFrequency>0.0f))return 0.0f; + if(Profile)++Profile->OwnerRoughnessActiveCalls; + const int32 ZStep=std::max(1,AnchorStep); + const int32 GridZ0=static_cast(std::floor(Z/static_cast(ZStep)))*ZStep; + const int32 SampleZ0=std::max(GridZ0,Field.Lo); + const int32 SampleZ1=std::min(GridZ0+ZStep,Field.Hi); + float Total=0.0f; + if(ZStep==1) + Total=OwnerCaveRoughnessCore(X,Y,Z,Params); + else if(SampleZ1>SampleZ0) + { + const bool bCacheHit=LineContext.Field==&Field&&LineContext.GridZ0==GridZ0; + if(Profile) + { + if(bCacheHit)++Profile->OwnerNoiseCacheHits; + else ++Profile->OwnerNoiseCacheMisses; + } + if(!bCacheHit) + { + LineContext.Field=&Field; + LineContext.GridZ0=GridZ0; + LineContext.LowerCore=OwnerCaveRoughnessCore(X,Y,SampleZ0,Params); + LineContext.UpperCore=OwnerCaveRoughnessCore(X,Y,SampleZ1,Params); + } + const float Alpha=static_cast((Z-SampleZ0)/(SampleZ1-SampleZ0)); + Total=FMath::Lerp(LineContext.LowerCore,LineContext.UpperCore,Alpha); + } + else + Total=OwnerCaveRoughnessCore(X,Y,Z,Params); + if(SurfaceSDF<0.0f) + Total=std::lerp(Total,std::min(Total,0.0f),std::clamp(CaveInfluence,0.0f,1.0f)); + const float Fade=1.0f-std::abs(SurfaceSDF)/Depth; + return Total*Fade*Fade; + } + + float OwnerCaveRoughnessForDetail(double X,double Y,double Z,float SurfaceSDF, + const FLayerField& Field,float CaveInfluence, + float PassageInfluence, + FOwnerNoiseLineContext& LineContext, + FDetailWorkerProfile* Profile=nullptr) const + { + constexpr float DirectStart=0.40f,DirectEnd=0.60f; + constexpr int32 PassageAnchorStep=2; + const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f); + if(PassageWeight>=DirectEnd) + return OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field,CaveInfluence, + LineContext,Profile,PassageAnchorStep); + + const float Anchored=OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field, + CaveInfluence,LineContext,Profile); + if(PassageWeight<=DirectStart)return Anchored; + + const float Direct=OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field, + CaveInfluence,LineContext,Profile,PassageAnchorStep); + const float DirectWeight=SmoothUnit((PassageWeight-DirectStart) + /(DirectEnd-DirectStart)); + const float AnchoredWeight=1.0f-DirectWeight; + const float Normalization=1.0f/std::sqrt( + AnchoredWeight*AnchoredWeight+DirectWeight*DirectWeight); + return (AnchoredWeight*Anchored+DirectWeight*Direct)*Normalization; + } + + static float SurfaceRelief(float X,float Y,float Z,uint32 Seed) + { + // The low band carries the broad rock shapes and also gently varies mid-band + // amplitude. The quieter 6-voxel octave keeps fine relief without restoring + // the same tight bubble pattern on every exposed surface. + const float Macro=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,32,Seed^0xB5297A4Du); + const float Mid=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,16,Seed^0x63D83595u); + const float Fine=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,6,Seed^0xA511E9B3u); + return 1.00f*Macro+(0.80f+0.25f*Macro)*(0.85f*Mid+0.18f*Fine); + } + + struct FValueNoiseLineContext + { + int32 X0=0,Y0=0,Z0=std::numeric_limits::min(); + int32 Wavelength=1; + uint32 Seed=0; + float Inv=1.0f,TX=0.0f,TY=0.0f; + std::array Corners{}; + + FValueNoiseLineContext(float X,float Y,int32 InWavelength,uint32 InSeed) + :Wavelength(InWavelength),Seed(InSeed),Inv(1.0f/static_cast(InWavelength)) + { + const float FX=X*Inv,FY=Y*Inv; + X0=FMath::FloorToInt(FX);Y0=FMath::FloorToInt(FY); + TX=FStandaloneResearchTunnels::Fade(FX-static_cast(X0)); + TY=FStandaloneResearchTunnels::Fade(FY-static_cast(Y0)); + } + + void SetPlane(int32 Z,bool bUpper) + { + const size_t I=bUpper?4u:0u; + Corners[I+0]=FStandaloneResearchTunnels::NoiseValue(X0,Y0,Z,Seed); + Corners[I+1]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0,Z,Seed); + Corners[I+2]=FStandaloneResearchTunnels::NoiseValue(X0,Y0+1,Z,Seed); + Corners[I+3]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0+1,Z,Seed); + } + + float Sample(float Z) + { + const float FZ=Z*Inv; + const int32 NewZ0=FMath::FloorToInt(FZ); + const float TZ=FStandaloneResearchTunnels::Fade(FZ-static_cast(NewZ0)); + if(NewZ0!=Z0) + { + if(NewZ0==Z0+1) + { + Corners[0]=Corners[4];Corners[2]=Corners[6]; + Corners[1]=Corners[5];Corners[3]=Corners[7]; + SetPlane(NewZ0+1,true); + } + else + { + SetPlane(NewZ0,false);SetPlane(NewZ0+1,true); + } + Z0=NewZ0; + } + const float Z00=FMath::Lerp(Corners[0],Corners[1],TX); + const float Z10=FMath::Lerp(Corners[2],Corners[3],TX); + const float Z01=FMath::Lerp(Corners[4],Corners[5],TX); + const float Z11=FMath::Lerp(Corners[6],Corners[7],TX); + return FMath::Lerp(FMath::Lerp(Z00,Z10,TY),FMath::Lerp(Z01,Z11,TY),TZ); + } + }; + + struct FDetailNoiseLineContext + { + FValueNoiseLineContext Macro,Mid,Fine; + FOwnerNoiseLineContext OwnerNoise; + FDetailNoiseLineContext(float X,float Y,uint32 Seed) + :Macro(X,Y,32,Seed^0xB5297A4Du),Mid(X,Y,16,Seed^0x63D83595u), + Fine(X,Y,6,Seed^0xA511E9B3u) + {} + + float SurfaceReliefAt(float Z,bool bMacroKnown=false,float KnownMacro=0.0f) + { + const float MacroValue=bMacroKnown?KnownMacro:Macro.Sample(Z); + const float MidValue=Mid.Sample(Z); + const float FineValue=Fine.Sample(Z); + return 1.00f*MacroValue+(0.80f+0.25f*MacroValue) + *(0.85f*MidValue+0.18f*FineValue); + } + + }; + + float DetailNoise(double X,double Y,double Z,float SurfaceSDF, + float CaveInfluence,float FloorInfluence,float PassageInfluence) 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; + } + const float Relief=SurfaceRelief(static_cast(X),static_cast(Y), + static_cast(Z),static_cast(Owner->Seed)); + const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f); + const FLayerField* Field=FindLayerField(Z); + FOwnerNoiseLineContext OwnerNoise; + float OwnerRelief=0.0f; + if(Field) + OwnerRelief=OwnerCaveRoughnessForDetail(X,Y,Z,SurfaceSDF,*Field, + CaveInfluence,PassageWeight,OwnerNoise); + const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) + +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); + return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f); + } + + float DetailNoiseOnLine(double X,double Y,double Z,float SurfaceSDF, + float CaveInfluence,float FloorInfluence,float PassageInfluence, + FDetailNoiseLineContext& NoiseLine, + FDetailWorkerProfile* Profile=nullptr, + const FLayerField* KnownField=nullptr, + bool bFieldKnown=false,bool bMacroKnown=false, + float KnownMacro=0.0f) const + { + if(DetailVariant!=EDetailVariant::Coherent) + return DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence,PassageInfluence); + using Clock=std::chrono::steady_clock; + const auto ReliefStart=Profile?Clock::now():Clock::time_point{}; + const float Relief=NoiseLine.SurfaceReliefAt(static_cast(Z), + bMacroKnown,KnownMacro); + if(Profile) + Profile->ReliefSampleNanoseconds+=static_cast( + std::chrono::duration_cast(Clock::now()-ReliefStart).count()); + const auto LookupStart=Profile?Clock::now():Clock::time_point{}; + const FLayerField* Field=bFieldKnown?KnownField:FindLayerField(Z); + if(Profile) + Profile->LayerLookupSampleNanoseconds+=static_cast( + std::chrono::duration_cast(Clock::now()-LookupStart).count()); + const auto OwnerStart=Profile?Clock::now():Clock::time_point{}; + if(Profile&&Field)++Profile->OwnerRoughnessFieldCalls; + const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f); + float OwnerRelief=0.0f; + if(Field) + OwnerRelief=OwnerCaveRoughnessForDetail(X,Y,Z,SurfaceSDF,*Field, + CaveInfluence,PassageWeight,NoiseLine.OwnerNoise,Profile); + if(Profile) + Profile->OwnerRoughnessSampleNanoseconds+=static_cast( + std::chrono::duration_cast(Clock::now()-OwnerStart).count()); + const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) + +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); + return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f); + } + + bool EvaluateFast(double X,double Y,double Z) const + { + ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; + bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; + float PassageInfluence=0.0f; + float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF, + &bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir,nullptr,false, + &PassageInfluence); + 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(RockDistance)<=DetailSupremum+0.125f) + { + const float Fade=DetailBandFadeAt(X,Y,Z,RockDistance,SurfaceSDF, + CaveInfluence,FloorInfluence); + if(Fade>0.0f)RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence, + FloorInfluence,PassageInfluence)*Fade; + } + if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); + return RockDistance>=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 SurfaceRelief(static_cast(X),static_cast(Y),static_cast(Z),11); + }; + for(int32 Z=0;Z void ParallelLines(size_t Count,Fn&& Work,FParallelTiming* Timing=nullptr, + std::array* WorkerActiveSeconds=nullptr,size_t WorkBatch=1) const + { + const auto WallStart=std::chrono::steady_clock::now(); + if(WorkerCount<=1||Count<512||!Scheduler) + { + const auto WorkerStart=std::chrono::steady_clock::now(); + for(size_t I=0;I)Work(I,0); + else Work(I); + } + if(WorkerActiveSeconds) + (*WorkerActiveSeconds)[0]=std::chrono::duration( + std::chrono::steady_clock::now()-WorkerStart).count(); + if(Timing) + { + const double WorkerSeconds=std::chrono::duration(std::chrono::steady_clock::now()-WorkerStart).count(); + Timing->WallSeconds+=std::chrono::duration(std::chrono::steady_clock::now()-WallStart).count(); + Timing->WorkerSeconds+=WorkerSeconds; + } + return; + } + WorkBatch=std::max(WorkBatch,1); + using WorkType=std::remove_reference_t; + struct FDispatchContext { WorkType* Work; } Dispatch{&Work}; + const auto Invoke=[](void* Opaque,size_t Index,size_t Worker) + { + auto& Context=*static_cast(Opaque); + if constexpr(std::is_invocable_v) + (*Context.Work)(Index,static_cast(Worker)); + else + (*Context.Work)(Index); + }; + std::array ActiveSeconds{}; + const double WorkerSeconds=Scheduler->parallel_for(Count,WorkBatch, + static_cast(WorkerCount),&Dispatch,Invoke,ActiveSeconds.data(),ActiveSeconds.size()); + if(WorkerActiveSeconds)*WorkerActiveSeconds=ActiveSeconds; + if(Timing) + { + Timing->WallSeconds+=std::chrono::duration(std::chrono::steady_clock::now()-WallStart).count(); + Timing->WorkerSeconds+=WorkerSeconds; + } + } + + FStandaloneOwnerWorld* Owner=nullptr; + int32 WorkerCount=6; + const VoxelForgeCore::JobScheduler* Scheduler=nullptr; + bool RoundTunnelsEnabled=false; + bool bProfileDetailBand=false; + int32 Min[3]{},Max[3]{},ClipMin[3]{},ClipMax[3]{},Size[3]{}; + FIntVector EditBoundsMin=FIntVector(MAX_int32,MAX_int32,MAX_int32); + FIntVector EditBoundsMax=FIntVector(MIN_int32,MIN_int32,MIN_int32); + int32 ActiveSpatialBinSize=LineBinSize; + double ActiveBlendRadius=0.0; + double ActiveFloorBlendRadius=0.0; + size_t WordsPerZ=0; + EDetailVariant DetailVariant=EDetailVariant::Coherent; + bool Built=false; + bool FarBuilt=false; + std::vector LayerRanges; + std::vector LayerFields; + std::vector Primitives; + std::vector FarConnectorSegments; + std::unordered_map,FBinHash> FarConnectorSpatialBins; + std::vector EditBrushes; + std::array AxisBins; + std::unordered_map SpatialBins; + std::unordered_map CriticalAirBins; + std::vector RockWords; + std::unique_ptr SurfaceDistanceQ; + std::array,3> CandidateAxisWords; + std::vector CandidateWords; + FBuildStats Stats; +}; diff --git a/Standalone/Core/src/StandaloneSnapshotAdapter.h b/Standalone/Core/src/StandaloneSnapshotAdapter.h new file mode 100644 index 0000000..bb879d5 --- /dev/null +++ b/Standalone/Core/src/StandaloneSnapshotAdapter.h @@ -0,0 +1,12 @@ +#pragma once + +#include "OwnerWorldLoader.h" +#include "VoxelForgeCore/VoxelForgeCore.h" + +#include + +namespace VoxelForgeCore +{ + // Private standalone adapter: keeps the compatibility holders behind the public value API. + ResolvedWorldSnapshot MakeStandaloneSnapshot(std::shared_ptr owner); +} diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp new file mode 100644 index 0000000..c0b82a3 --- /dev/null +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -0,0 +1,484 @@ +#include "StandaloneSnapshotAdapter.h" + +#include "FarShellsInternal.h" +#include "VoxelClipmapDesiredTiles.h" +#include "VoxelMarchingCubesMesher.h" +#include "VoxelForgeCore/VoxelForgeCore.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace VoxelForgeCore +{ + struct ResolvedWorldSnapshot::Data + { + std::shared_ptr owner; + WorldConfiguration configuration; + }; + + namespace + { + FIntVector ToNative(Vec3i value) { return {value.x,value.y,value.z}; } + FVector ToNative(Vec3d value) { return {value.x,value.y,value.z}; } + Vec3d FromNative(const FVector& value) { return {value.X,value.Y,value.Z}; } + FVoxelTileKey ToNative(const TileKey& value) + { return FVoxelTileKey(ToNative(value.coordinate),value.lod); } + TileKey FromNative(const FVoxelTileKey& value) + { return {{value.Coord.X,value.Coord.Y,value.Coord.Z},value.Level}; } + + WorldConfiguration MakeConfiguration(const FStandaloneOwnerWorld& owner) + { + WorldConfiguration out; + out.seed=owner.Seed; + out.clipRadius=owner.ClipRadius; + out.maxLod=owner.MaxClipLevel; + out.fullResolutionLodCount=owner.FullResClipLevels; + out.coarseTileCells=owner.CoarseTileCells; + out.renderDistanceChunks=owner.RenderDistanceChunks; + out.contentCutMinLod=owner.StrateContentCutMinLevel; + out.skirtCells=owner.SkirtCells; + out.lodOctaveDrop=owner.LODOctaveDrop; + out.viewDistanceUp=owner.Settings->ViewDistanceUp; + out.viewDistanceDown=owner.Settings->ViewDistanceDown; + out.strateViewMarginChunks=owner.Settings->StrateViewMarginChunks; + out.clampViewToStrate=owner.Settings->bClampViewToStrate; + out.generateSkirts=owner.bGenerateSkirts; + out.viewerStartFound=owner.ViewerStartFound; + out.viewerStartVoxels=FromNative(owner.ViewerStartVoxels); + return out; + } + + double TileDistanceSquared(const TileKey& tile,Vec3i center) + { + const double scale=static_cast(1<(tile.coordinate.x)+0.5)*scale-center.x; + const double dy=(static_cast(tile.coordinate.y)+0.5)*scale-center.y; + const double dz=(static_cast(tile.coordinate.z)+0.5)*scale-center.z; + return dx*dx+dy*dy+dz*dz; + } + + void AddWord(std::uint64_t& hash,std::uint64_t word,int bytes) + { + for(int i=0;i(word>>(i*8)); + hash*=1099511628211ull; + } + } + + std::uint64_t HashPayload(const FVoxelMeshData& mesh) + { + std::uint64_t hash=14695981039346656037ull; + AddWord(hash,static_cast(mesh.Vertices.Num()),8); + for(const FVector& p:mesh.Vertices) + { AddWord(hash,std::bit_cast(p.X),8); AddWord(hash,std::bit_cast(p.Y),8); AddWord(hash,std::bit_cast(p.Z),8); } + AddWord(hash,static_cast(mesh.Normals.Num()),8); + for(const FVector& n:mesh.Normals) + { AddWord(hash,std::bit_cast(n.X),8); AddWord(hash,std::bit_cast(n.Y),8); AddWord(hash,std::bit_cast(n.Z),8); } + AddWord(hash,static_cast(mesh.UVs.Num()),8); + for(const auto& uv:mesh.UVs) + { AddWord(hash,std::bit_cast(static_cast(uv.X)),8); + AddWord(hash,std::bit_cast(static_cast(uv.Y)),8); } + AddWord(hash,static_cast(mesh.Colors.Num()),8); + for(const FColor& color:mesh.Colors) + { AddWord(hash,color.R,1); AddWord(hash,color.G,1); AddWord(hash,color.B,1); AddWord(hash,color.A,1); } + AddWord(hash,static_cast(mesh.Triangles.Num()),8); + for(const int32 index:mesh.Triangles)AddWord(hash,static_cast(index),4); + AddWord(hash,static_cast(mesh.NumSurfaceVertices),4); + AddWord(hash,static_cast(mesh.NumSkirtTriangles),4); + AddWord(hash,static_cast(mesh.NumCeilingTriangles),4); + return hash; + } + + std::uint64_t HashTopology(const FVoxelMeshData& mesh) + { + const auto vertexKey=[&](int32 index) + { + std::uint64_t hash=14695981039346656037ull; + if(index<0||index>=mesh.Vertices.Num()||index>=mesh.Normals.Num()) + { AddWord(hash,UINT64_MAX,8); return hash; } + const FVector& p=mesh.Vertices[index]; const FVector& n=mesh.Normals[index]; + for(double v:{p.X,p.Y,p.Z,n.X,n.Y,n.Z})AddWord(hash,std::bit_cast(v),8); + return hash; + }; + std::vector> triangles; + triangles.reserve(static_cast(mesh.Triangles.Num()/3)); + for(int32 i=0;i+2(triangles.size()),8); + for(const auto& triangle:triangles)for(const auto key:triangle)AddWord(hash,key,8); + return hash; + } + + std::uint64_t HashViewerMesh(const FVoxelMeshData& mesh) + { + std::uint64_t hash=14695981039346656037ull; + AddWord(hash,static_cast(mesh.Vertices.Num()),8); + for(int32 i=0;i(p.X*0.01),static_cast(p.Y*0.01),static_cast(p.Z*0.01), + static_cast(n.X),static_cast(n.Y),static_cast(n.Z)}) + AddWord(hash,std::bit_cast(value),4); + } + AddWord(hash,static_cast(mesh.Triangles.Num()),8); + for(const int32 index:mesh.Triangles)AddWord(hash,static_cast(index),4); + return hash; + } + + std::uint64_t HashBrushTransaction(const EditTransaction& transaction) + { + std::uint64_t hash=14695981039346656037ull; + AddWord(hash,transaction.sequence,8); + AddWord(hash,transaction.brushes.size(),8); + for(const EditBrush& brush:transaction.brushes) + { + AddWord(hash,static_cast(brush.shape),1); + AddWord(hash,static_cast(brush.operation),1); + for(double v:{brush.a.x,brush.a.y,brush.a.z,brush.b.x,brush.b.y,brush.b.z, + brush.halfExtent.x,brush.halfExtent.y,brush.halfExtent.z, + brush.radius,brush.rounding}) + AddWord(hash,std::bit_cast(v),8); + AddWord(hash,std::bit_cast(brush.strength),4); + AddWord(hash,std::bit_cast(brush.falloff),4); + } + return hash; + } + + bool IsFiniteBrush(const EditBrush& brush) + { + const std::array values={brush.a.x,brush.a.y,brush.a.z,brush.b.x,brush.b.y,brush.b.z, + brush.halfExtent.x,brush.halfExtent.y,brush.halfExtent.z,brush.radius,brush.rounding}; + return std::all_of(values.begin(),values.end(),[](double v){return std::isfinite(v);}) + &&brush.radius>=0.0&&brush.rounding>=0.0 + &&brush.halfExtent.x>=0.0&&brush.halfExtent.y>=0.0&&brush.halfExtent.z>=0.0 + &&std::isfinite(brush.strength)&&std::isfinite(brush.falloff)&&brush.falloff>=0.0f; + } + + FStandaloneShapeWorld::EDetailVariant ToNative(DetailVariant value) + { + switch(value) + { + case DetailVariant::smooth:return FStandaloneShapeWorld::EDetailVariant::Smooth; + case DetailVariant::perVoxel:return FStandaloneShapeWorld::EDetailVariant::PerVoxel; + default:return FStandaloneShapeWorld::EDetailVariant::Coherent; + } + } + } + + struct detail_StandaloneSnapshotAdapter + { + static ResolvedWorldSnapshot FromOwner(std::shared_ptr owner) + { + if(!owner||!owner->Settings||!owner->Manager||!owner->Generator)return {}; + auto data=std::make_shared(); + data->configuration=MakeConfiguration(*owner); + data->owner=std::move(owner); + return ResolvedWorldSnapshot(std::move(data)); + } + }; + + ResolvedWorldSnapshot MakeStandaloneSnapshot(std::shared_ptr owner) + { return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner)); } + + std::optional ResolvedWorldSnapshot::FromJson( + std::string_view json,std::string& error) + { + auto owner=std::make_shared(); + if(!StandaloneOwnerLoader::LoadJson(json,*owner,error))return std::nullopt; + return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner)); + } + + const WorldConfiguration& ResolvedWorldSnapshot::configuration() const + { + static const WorldConfiguration empty{}; + return data_?data_->configuration:empty; + } + + struct World::Impl + { + std::shared_ptr owner; + WorldConfiguration configuration; + std::unique_ptr shape; + std::vector editLog; + std::map transactions; + bool hasLastSequence=false; + std::uint64_t lastSequence=0; + }; + + World::World(ResolvedWorldSnapshot snapshot):impl_(std::make_unique()) + { + if(snapshot.data_) + { + impl_->owner=snapshot.data_->owner; + impl_->configuration=snapshot.data_->configuration; + } + } + World::~World()=default; + World::World(World&&) noexcept=default; + World& World::operator=(World&&) noexcept=default; + + bool World::compile(const ShapeBuildRequest& request,const JobScheduler& scheduler,std::string& error) + { + if(!impl_->owner){error="resolved world snapshot is empty";return false;} + auto shape=std::make_unique(); + const bool ok=shape->Build(*impl_->owner,ToNative(request.minVoxel),ToNative(request.maxVoxel), + ToNative(request.clipMinVoxel),ToNative(request.clipMaxVoxel), + static_cast(std::clamp(request.workerLimit,1,8)),ToNative(request.detail), + request.includeFarWorld,error,&scheduler,request.roundTunnels,request.profileDetailBand); + if(ok)impl_->shape=std::move(shape); + return ok; + } + + void World::buildFarWorld() + { if(impl_->shape)impl_->shape->BuildFarWorld(); } + bool World::isCompiled() const noexcept { return impl_&&impl_->shape&&impl_->shape->IsBuilt(); } + bool World::hasFarWorld() const noexcept { return impl_&&impl_->shape&&impl_->shape->HasFarWorld(); } + + ShapeBuildStats World::buildStats() const + { + ShapeBuildStats out; + if(!impl_||!impl_->shape)return out; + const auto& stats=impl_->shape->GetStats(); + out.shapeSeconds=stats.ShapeSeconds;out.intervalSeconds=stats.IntervalSeconds; + out.surfaceSeconds=stats.SurfaceSeconds;out.farShapeSeconds=stats.FarShapeSeconds; + out.primitiveCount=stats.PrimitiveCount;out.surfaceCandidates=stats.SurfaceCandidates; + out.detailFlips=stats.DetailFlips;out.walkSamples=stats.WalkSamples;out.walkFailures=stats.WalkFailures; + return out; + } + + float World::densityAt(Vec3d position) const + { return impl_&&impl_->shape?impl_->shape->DensityAt(static_cast(position.x),static_cast(position.y),static_cast(position.z)):0.0f; } + float World::densityAtLattice(Vec3i position) const + { return impl_&&impl_->shape?impl_->shape->DensityAtLattice(position.x,position.y,position.z):0.0f; } + bool World::isSolidAt(Vec3d position) const { return densityAt(position)<0.0f; } + + bool World::isSolidAlongSegment(Vec3d start,Vec3d end,double spacing) const + { + if(!impl_||!impl_->shape||!std::isfinite(spacing)||spacing<=0.0)return false; + const double dx=end.x-start.x,dy=end.y-start.y,dz=end.z-start.z; + const double length=std::sqrt(dx*dx+dy*dy+dz*dz); + const std::size_t intervals=std::max(1,static_cast(std::ceil(length/spacing))); + for(std::size_t i=0;i<=intervals;++i) + { + const double t=static_cast(i)/static_cast(intervals); + if(isSolidAt({start.x+dx*t,start.y+dy*t,start.z+dz*t}))return true; + } + return false; + } + + TileClass World::classifyBox(Vec3i origin,std::int32_t step,std::int32_t cells) const + { + if(!impl_||!impl_->shape)return TileClass::mixed; + const EVoxelTileClass result=impl_->shape->ClassifyBox(ToNative(origin),step,cells); + return result==EVoxelTileClass::AllSolid?TileClass::allSolid: + result==EVoxelTileClass::AllAir?TileClass::allAir:TileClass::mixed; + } + + std::vector World::prioritizeTiles(std::span tiles,Vec3i centerChunk) + { + std::vector output(tiles.begin(),tiles.end()); + std::sort(output.begin(),output.end(),[&](const TileKey& a,const TileKey& b) + { + const double da=TileDistanceSquared(a,centerChunk),db=TileDistanceSquared(b,centerChunk); + if(da!=db)return da World::desiredTiles(Vec3d cameraPosition) const + { + if(!impl_||!impl_->owner)return {}; + const auto& world=*impl_->owner; + const FIntVector center(FMath::FloorToInt(cameraPosition.x/CHUNK_SIZE), + FMath::FloorToInt(cameraPosition.y/CHUNK_SIZE),FMath::FloorToInt(cameraPosition.z/CHUNK_SIZE)); + bool hasStrate=false;int32 top=0,bottom=0; + if(world.Settings->bClampViewToStrate)hasStrate=world.Manager->GetStrateChunkZBounds(center.Z,top,bottom); + int32 minZ=0,maxZ=0; + VoxelClipmapDesiredTiles::ResolveVerticalBounds(center.Z,world.Settings->ViewDistanceUp, + world.Settings->ViewDistanceDown,world.Settings->StrateViewMarginChunks, + world.Settings->bClampViewToStrate,hasStrate,top,bottom,minZ,maxZ); + VoxelClipmapDesiredTiles::FParameters parameters; + parameters.ClipRadius=world.ClipRadius;parameters.MaxClipLevel=FMath::Clamp(world.MaxClipLevel,0,8); + parameters.RenderDistanceChunks=world.RenderDistanceChunks; + TArray nativeTiles; + VoxelClipmapDesiredTiles::Build(center,minZ,maxZ,parameters,nativeTiles); + std::vector tiles;tiles.reserve(static_cast(nativeTiles.Num())); + for(const FVoxelTileKey& key:nativeTiles)tiles.push_back(FromNative(key)); + std::sort(tiles.begin(),tiles.end(),[&](const TileKey& a,const TileKey& b) + { + const double da=TileDistanceSquared(a,{center.X,center.Y,center.Z}); + const double db=TileDistanceSquared(b,{center.X,center.Y,center.Z}); + return da==db?a.lod World::generateTile(const TileKey& key,const TileGenerationOptions& options, + std::string& error) const + { + if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt()) + { error="world must be compiled before generating tiles";return std::nullopt; } + const auto& world=*impl_->owner; + UVoxelMarchingCubesMesher mesher; + mesher.SetGenerator(world.Generator.get()); + mesher.SetStandaloneShapeWorld(impl_->shape.get()); + mesher.bStandaloneSkipUvAndColors=options.skipUvAndColors; + mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors; + mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups; + mesher.bGenerateSkirts=world.bGenerateSkirts;mesher.SkirtCells=static_cast(world.SkirtCells); + mesher.LODOctaveDrop=world.LODOctaveDrop; + const int32 level=key.lod,extent=CHUNK_SIZE< densityValues; + const FVoxelTileKey nativeKey=ToNative(key); + const FIntVector origin=nativeKey.OriginVoxels(); + FVoxelMeshData mesh=mesher.GenerateMesh(origin,step,cells,nullptr, + options.bandMinZVoxel,options.bandMaxZVoxel,&sampleCount,nullptr,nullptr, + (options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr); + if(impl_->shape->HasFarWorld()) + { + AppendFarStratumShells(world,*impl_->shape,nativeKey,step,mesh); + AppendFarConnectorShells(*impl_->shape,nativeKey,step,world.CoarseTileCells,mesh); + } + + TileMesh out;out.key=key;out.densitySampleCount=static_cast(std::max(0,sampleCount)); + out.meshPayloadAllocatedBytes=static_cast(mesh.Vertices.GetAllocatedSize()) + +static_cast(mesh.Normals.GetAllocatedSize()) + +static_cast(mesh.UVs.GetAllocatedSize()) + +static_cast(mesh.Colors.GetAllocatedSize()) + +static_cast(mesh.Triangles.GetAllocatedSize()); + out.groups.surfaceVertexCount=static_cast(mesh.NumSurfaceVertices); + out.groups.ceilingTriangleCount=static_cast(mesh.NumCeilingTriangles); + out.groups.groundTriangleCount=static_cast( + std::max(0,mesh.Triangles.Num()/3-mesh.NumCeilingTriangles)); + out.groups.skirtTriangleCount=static_cast(mesh.NumSkirtTriangles); + out.vertices.reserve(static_cast(mesh.Vertices.Num())); + for(int32 i=0;i(mesh.Triangles.Num())); + for(const int32 index:mesh.Triangles)out.indices.push_back(static_cast(index)); + if(options.captureDensitySamples)out.debugDensitySamples=std::move(densityValues); + if(options.hashDensity) + { + std::uint64_t densityHash=14695981039346656037ull; + const auto& values=options.captureDensitySamples?out.debugDensitySamples:densityValues; + for(const float value:values) + { densityHash^=std::bit_cast(value);densityHash*=1099511628211ull; } + out.densityHash=densityHash;out.meshHash=HashViewerMesh(mesh); + out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh); + out.hasDensityHash=out.hasMeshHash=out.hasPayloadHash=out.hasTopologyHash=true; + } + return out; + } + + std::optional World::applyEditTransaction(const EditTransaction& transaction, + std::span desired,std::string& error) + { + if(!impl_||!impl_->shape){error="world must be compiled before edits can be applied";return std::nullopt;} + const std::uint64_t digest=HashBrushTransaction(transaction); + if(const auto found=impl_->transactions.find(transaction.sequence);found!=impl_->transactions.end()) + { + if(found->second.digest!=digest){error="edit sequence was reused with different brush data";return std::nullopt;} + return found->second; + } + if(impl_->hasLastSequence&&transaction.sequence<=impl_->lastSequence) + { error="edit transactions must be applied in increasing sequence order";return std::nullopt; } + for(const EditBrush& brush:transaction.brushes) + { + if(!IsFiniteBrush(brush)){error="edit brush contains non-finite or negative dimensions";return std::nullopt;} + if(static_cast(brush.shape)>static_cast(EditBrush::Shape::roundedBox) + ||static_cast(brush.operation)>static_cast(EditBrush::Operation::densityOffset)) + {error="edit brush has an unknown shape or operation";return std::nullopt;} + } + std::vector native; + native.reserve(transaction.brushes.size()); + for(const EditBrush& brush:transaction.brushes) + { + FStandaloneShapeWorld::FEditBrush output; + output.Shape=static_cast(brush.shape); + output.Operation=static_cast(brush.operation); + output.A={brush.a.x,brush.a.y,brush.a.z};output.B={brush.b.x,brush.b.y,brush.b.z}; + output.HalfExtent={brush.halfExtent.x,brush.halfExtent.y,brush.halfExtent.z}; + output.Radius=brush.radius;output.Rounding=brush.rounding; + output.Strength=brush.strength;output.Falloff=brush.falloff; + native.push_back(output); + } + impl_->editLog.insert(impl_->editLog.end(),native.begin(),native.end()); + impl_->shape->SetEditTransaction(impl_->editLog); + const FIntVector min=impl_->shape->GetEditBoundsMin(),max=impl_->shape->GetEditBoundsMax(); + EditResult result;result.digest=digest; + result.affectedByLod.resize(static_cast(std::max(impl_->configuration.maxLod,0)+1)); + for(const TileKey& tile:desired) + { + if(tile.lod<0||tile.lod>=static_cast(result.affectedByLod.size()))continue; + const FIntVector origin=ToNative(tile).OriginVoxels(); + const int32 extent=ToNative(tile).ExtentVoxels(); + const int32 pad=std::max(2,ToNative(tile).StepVoxels()); + if(origin.X-pad>max.X||origin.X+extent+padmax.Y||origin.Y+extent+padmax.Z||origin.Z+extent+pad(tile.lod)].push_back(tile); + } + impl_->transactions.emplace(transaction.sequence,result); + impl_->hasLastSequence=true;impl_->lastSequence=transaction.sequence; + return result; + } + + std::vector World::farConnectorSegments() const + { + std::vector result; + if(!impl_||!impl_->shape)return result; + const auto& source=impl_->shape->GetFarConnectorSegments();result.reserve(source.size()); + for(const auto& segment:source)result.push_back({FromNative(segment.Start),FromNative(segment.End),segment.Radius}); + return result; + } + + std::vector World::farConnectorCandidates(Vec3i minVoxel,Vec3i maxVoxel) const + { + std::vector result; + if(impl_&&impl_->shape)impl_->shape->FindFarConnectorCandidates(ToNative(minVoxel),ToNative(maxVoxel),result); + return result; + } + + bool World::auditDetailPassing() const { return impl_&&impl_->shape&&impl_->shape->IsDetailAuditPassing(); } + bool World::auditSurfaceFamilies(bool passageOnly) const + { return impl_&&impl_->shape&&impl_->shape->AuditSurfaceBandFamilies(passageOnly); } + bool World::auditViewSurface(Vec3d camera,double yaw,double pitch) const + { return impl_&&impl_->shape&&impl_->shape->AuditViewSurface(camera.x*0.25,camera.y*0.25, + camera.z*0.25,static_cast(yaw),static_cast(pitch)); } + bool World::debugCriticalAirAt(Vec3d position) const + { return impl_&&impl_->shape&&impl_->shape->DebugCriticalAirAt(position.x,position.y,position.z); } + bool World::debugSolidOverlayAt(Vec3d position) const + { return impl_&&impl_->shape&&impl_->shape->DebugSolidOverlayAt(position.x,position.y,position.z); } + const WorldConfiguration& World::configuration() const + { return impl_?impl_->configuration:ResolvedWorldSnapshot{}.configuration(); } +} + +std::size_t VoxelForgeCore::TileKeyHash::operator()(const TileKey& key) const noexcept +{ + std::size_t h=static_cast(key.coordinate.x)*0x9E3779B1u; + h^=static_cast(key.coordinate.y)+0x9E3779B9u+(h<<6)+(h>>2); + h^=static_cast(key.coordinate.z)+0x9E3779B9u+(h<<6)+(h>>2); + h^=static_cast(key.lod)+0x9E3779B9u+(h<<6)+(h>>2); + return h; +} diff --git a/Standalone/README.md b/Standalone/README.md index 3d96188..22c595f 100644 --- a/Standalone/README.md +++ b/Standalone/README.md @@ -1,31 +1,33 @@ # VoxelForge standalone world -This C++20 program loads the owner snapshot from `data/owner_world.json`, runs the copied -VoxelForge density pipeline and marching-cubes mesher, streams the clipmap with eight worker threads -by default, and displays it through Direct3D 11. Unreal is not linked or needed at run time. +This C++20 program loads the owner snapshot from `data/owner_world.json`, runs the VoxelForge CPU +pipeline, streams the clipmap with up to eight host worker threads, and displays it through Direct3D +11. `Core/` contains the Unreal-free `VoxelForgeCore` API and static-library target used by the +standalone shape-world path. Unreal is not linked or needed at run time. ## Build and launch From the plugin root, run: ```powershell -.\Standalone\Build.ps1 -.\Standalone\bin\VoxelForgeStandalone.exe +.\Standalone\Build.ps1 -Target All +.\Standalone\bin\VoxelForgeCoreStandalone.exe --shape-world ``` -The build uses the configured VS 2022 `vcvars64.bat`, MSVC `/std:c++20 /O2 /arch:AVX2 /fp:precise`. -`Build.ps1` starts MSVC at BelowNormal priority and does not enable `/MP`. -Pass another executable name to build beside any viewer that may be open: -`.\Standalone\Build.ps1 -OutputName VoxelForgeStandalone_test.exe`. The executable finds the -JSON beside itself under `Standalone\data\`. +`Build.ps1 -Target Core` builds only `Build\VoxelForgeCore\VoxelForgeCore.lib`; `-Target All` +builds that library and the viewer. `Core\core_sources.txt` is the shared translation-unit +manifest. The build uses the configured VS 2022 `vcvars64.bat`, MSVC `/std:c++20`, `/O2`, +`/arch:AVX2`, `/fp:precise` and `/MP8`, BelowNormal priority and affinity `0xFF00`. Pass another +executable name to build beside any viewer that may be open by setting `-OutputName`, for example +`VoxelForgeCoreStandalone_test.exe`. The executable finds the JSON under `Standalone\data\`. ### Daytime resource limit -Run tests and benchmarks at Idle or BelowNormal priority and use no more than 8 of this PC's 16 -logical CPUs. The default viewer uses eight workers. Use `Build\LaunchBelowNormal.ps1` for any test or -benchmark process; it starts the child at BelowNormal, applies affinity `0xFF00`, and verifies both. -Builds use no more than four compiler processes (the current build is serial) and retain -`/fp:precise`. The all-core 16-worker runs documented below were the owner's one-night exception. +Run audits and benchmarks at BelowNormal priority and use no more than 8 of this PC's 16 logical +CPUs. The viewer clamps worker requests to eight. `Build.ps1` bounds MSVC with `/MP8`, sets the +build process to BelowNormal and affinity `0xFF00`, and retains `/fp:precise`. The verification +runner in `Core\verification\RunMachine.ps1` applies the same limits to viewer runs and refuses to +overwrite an existing log stem. ## Controls diff --git a/Standalone/SHARED_CPP_LIBRARY_PLAN.md b/Standalone/SHARED_CPP_LIBRARY_PLAN.md index e39a2ae..c668669 100644 --- a/Standalone/SHARED_CPP_LIBRARY_PLAN.md +++ b/Standalone/SHARED_CPP_LIBRARY_PLAN.md @@ -1,74 +1,134 @@ -# Shared C++ library plan +# VoxelForgeCore shared-library plan -This round keeps the standalone copy in place. The next extraction should make one implementation -serve both hosts; it should not turn the compatibility layer into a second public API. +This plan describes the extracted shape-world pipeline and the next Unreal integration. The +standalone implementation is under `Standalone/Core`; no file in the plugin's `Source` tree was +changed. -## Library contents +## Current layout and build -Move the deterministic CPU pipeline into `VoxelForgeCore`: +- `Core/include/VoxelForgeCore/VoxelForgeCore.h` is the public C++20 API. It includes only standard + C++ headers and value types. +- `Core/src/VoxelForgeCore.cpp` adapts the copied density evaluator and marching-cubes mesher to the + public API. `ShapeWorldInternal.h` owns shape compilation, density/detail evaluation, edits and + audits. `FarShellsInternal.h` owns far-parent repairs and strata/connector shells. +- `Core/core_sources.txt` is the translation-unit manifest. `Build.ps1` reads it for both + `-Target Core` and `-Target All`; the latter links the viewer against + `Build/VoxelForgeCore/VoxelForgeCore.lib`. Use `-OutputName` to keep another executable open. + Builds use `/std:c++20 /O2 /arch:AVX2 /fp:precise /MP8`, BelowNormal priority and affinity + `0xFF00`. +- `Source/StandaloneShapeWorld.h` and `Source/StandaloneResearchTunnels.h` are forwarding headers + for old standalone include sites. `StreamingWorld.h` remains host code: it owns worker threads, + queued jobs, caches, cancellation, D3D upload state and per-frame upload budgets. When a core + world is supplied, it gets desired tiles and their priority from the library and submits each + tile to `World::generateTile`. Shape-world `--stream-benchmark`, `--stream-pipeline`, edit, + audit, and `--benchmark-level` runs use the Core world. Explicit evaluator and owner-comparison + modes retain the compatibility generator only as the comparison side of those diagnostics. +- The build manifest compiles the copied CPU generator and mesher with the private compatibility + headers under `Compat`. Those headers and their Unreal-shaped stand-ins are implementation + details. They are absent from the public include tree and public API. -- Plain voxel, tile, vector, bounds, and resolved-world input types. -- Deterministic math, noise, density operations, cave and passage fields, strate composition, and - the room/SDF caches they use. -- Marching-cubes tables and meshing, including the density-lattice gradient normals. -- Clipmap desired-set calculation and the shared density evaluation entry points. +## API and ownership -Keep JSON and Unreal asset loading, material/actor creation, D3D11 and Unreal rendering, collision, -worker scheduling, cancellation policy, and per-frame mesh upload budgets in the host applications. -The cache-region traversal found in this round belongs with the density/meshing implementation. -The worker-local room graph cache is keyed by exact XY bounds and stratum. Grouping samples by the -legacy cache regions reduced a profiled LOD3 tile from 1,084 graph builds to 229. At coarser sizes, -plane-first traversal still thrashes the same XY windows across a tall Z run, so tiles wider than 16 -chunks keep each XY column's Z samples contiguous. The profiled 64-tile LOD6 case fell from 36,480 -graph builds (13.189 s in graph construction) to 12,160 (5.210 s), with the same density hash. +`ResolvedWorldSnapshot::FromJson` takes the normalized UTF-8 schema-v1 JSON produced by the +standalone owner export. It resolves the input into owned read-only data. `World` owns the compiled +shape field and edit state for that snapshot. See [Core/API.md](Core/API.md) for signatures, units, +and examples. -Cache capacity is also scale-dependent: a 32-chunk LOD5 tile benefits from two cached stratum -windows (26,926 to 9,700 builds across 64 tiles), while a 64-chunk LOD6 tile regresses with two -slots (12,160 to 23,104), so the current generator selects two only for 32-chunk tile contexts and -one otherwise. Replacing the room-graph `std::function` recursion then reduced profiled LOD5 -RoomGraphBuild from 4.531 to 4.417 s and LOD6 from 5.210 to 5.038 s across the same 64 tiles, -without changing graph build counts or density hashes. These timings are profiling evidence, not a -cross-host benchmark. The shared API -should own the traversal and cache policy, with parity tests covering both traversal paths while -writing the same original density-grid indices. +The host supplies a `JobScheduler`. The core calls its synchronous `parallel_for` during shape +compilation; the host chooses the pool, fibers, task graph or serial execution. Core code starts no +threads. After compilation, hosts may run independent tile generation and query calls on their own +workers. Serialize compilation and edit application against reads/generation of the same `World`; +there is no internal lock or cancellation policy. -The compatibility-map and allocator change did not move the original LOD0 run; those changes remove -known linear-lookup and heap-growth gaps, but stage profiling put the cost in density evaluation and -room-graph cache churn. The hot room-graph walks also used an owning `std::function` compatibility -alias for recursion; replacing them with generic self-recursive lambdas kept the same depth-first -order and removed repeated type-erasure/allocation overhead. Keep that code in the core rather than -carrying the UE-style callback alias into it. Viewer lighting and its palette remain host renderer -choices and stay outside the shared generator. +Tile meshes own positions, normals, UVs, colors and indices. Positions are centimetres; query and +tile coordinates are voxels. Group counts expose the ground index run, ceiling index run, and skirt +triangle subset. Density/sign, point-solid, segment-solid, and tile-classification queries support +host gameplay and collision checks without returning engine objects. -## Compatibility boundary +Edit transactions contain an authority-supplied increasing sequence and an ordered brush list. The +core hashes those exact values, accepts an identical replay idempotently, rejects sequence reuse +with different data and out-of-order transactions, and returns affected desired tiles grouped by +LOD. The host schedules `generateTile` for those keys and drops results made stale by its own +cancellation/version policy. Every client must apply the same sequence and brush order. -The core should expose standard C++ value types, explicit immutable resolved inputs, and owned mesh -results. No Unreal headers, reflection macros, `UObject`, Unreal containers, renderer types, or host -allocator should cross the boundary. The plugin adapter converts settings and loaded assets into -the resolved input snapshot, then converts the returned vertices, indices, and normals into Unreal -mesh types. The standalone JSON loader maps its exported snapshot into the same input snapshot. +## Unreal integration: UBT module -Do not move scheduling into the library. Each host should pass an immutable generation context to a -worker and receive an owned result; this lets the plugin retain its task admission and game-thread -rules while the viewer retains its worker pool and bounded D3D11 upload pump. +Add a `VoxelForgeCore` runtime module to the plugin. Its `Build.cs` sets +`CppStandardVersion.Cpp20`, exports only `Standalone/Core/include`, and keeps these private include +roots available to the core implementation: -## Plugin integration +- `Standalone/Core/src` +- `Standalone/Compat` +- `Standalone/Source` +- `Standalone/Source/VoxelForge/Public` +- `Standalone/Source/VoxelForge/Private` -Add a `VoxelForgeCore` C++20 target and make both builds compile or link that target from the same -source files. For Unreal, first prototype the Unreal Build Tool integration: either a small core -module that compiles the library sources or a platform-built static library with explicit build -rules. Prefer source integration if packaging a binary per Unreal platform would add maintenance. -The plugin then replaces its copied evaluator and mesher calls with the core API behind a thin -adapter. Preserve plugin-owned UObject resolution, task cancellation, collision, game-thread mesh -application, and upload budgets. The standalone keeps its JSON loader and D3D11 renderer. +UBT discovers translation units under a module's `Source` tree. Add one thin `.cpp` include shim per +row of `Core/core_sources.txt`, with each shim including exactly one canonical source file by a +relative path into `Standalone`. Keeping one included source per shim preserves the translation +unit boundaries used by `Build.ps1`. A manifest check in the UBT module should fail when a manifest +row lacks a shim or a shim names a different file. Do not copy implementation files into the +module. The standalone and UBT builds then compile the same source bytes. -## Preventing drift before extraction +The existing `VoxelForge` module owns the adapter and calls the new module. Its adapter runs on the +game thread, resolves soft references and asset defaults, preserves array order, and serializes +settings, biome and terrain-op records, strata layouts, passages, seed, voxel size, clip limits and +viewer start into the exact schema-v1 document accepted by `FromJson`. It must not serialize +UObject pointers or depend on unordered map iteration. Snapshot creation returns a descriptive +error for unresolved or unsupported assets. Once created, workers only see the immutable core +snapshot and standard C++ results. -Until the common target exists, keep a manifest of mirrored files and a sync/check script that names -the canonical source for each file, compares hashes, and refuses when both copies have changed. -Avoid hand-editing only one copy without recording that the edit is pending synchronization. After -each density or meshing change, run the 3,436-sample bitwise corpus and the selected-tile density -hash check. Keep `/fp:precise`; do not accept evaluator changes that alter density bits as a speed -shortcut. A standalone-only optimization must be marked as pending synchronization until it is -ported or explicitly rejected for the canonical core. Delete the duplicate standalone source only -after both host builds use the common target and the parity corpus passes through each adapter. +The plugin supplies a `JobScheduler` backed by its chosen task system. The adapter submits shape +build work and tile jobs to that scheduler, observes generation IDs for cancellation, and discards +stale results. It copies completed `TileMesh` data into `FVoxelMeshData` or the plugin's current +mesh payload on the host side, preserving centimetre positions, normals, UVs, colors, indices and +ground/ceiling/skirt group counts. UObject creation, collision cooking, render-resource changes, +game-thread application, residency and upload budgets remain in the plugin. + +## What the plugin pipeline replaces + +The current plugin's `AVoxelWorld::GenerateTileResult` worker path performs proofs and +`UVoxelGenerator::ClassifyTile`, calls `UVoxelMarchingCubesMesher::GenerateMesh`, builds +`RealtimeMesh` streams with `BuildTileStreamSet`, then publishes an epoch-tagged `FChunkResult` for +game-thread upload/collision handling. `AVoxelWorld::CarveAtPosition`, `CarveBox`, and +`CarveCapsule` route soft `FVoxelModification` brushes through `UVoxelDiffLayer::ApplyModification` +and remesh the affected chunks. The adapter replaces those CPU-field and edit operations as +follows: + +| Current call or state | Core API replacement | Keep in the plugin | +| --- | --- | --- | +| Shape interval/detail-band build and any active shape-world dispatch | `World::compile` with explicit bounds/detail options | Asset resolution, build admission and cancellation | +| `GetDensityAt`, lattice sign tests, and point/segment checks in meshing/gameplay | `densityAt`, `densityAtLattice`, `isSolidAt`, `isSolidAlongSegment` | Gameplay policy and physics objects | +| `ClassifyTile` / tile proof | `classifyBox` | Clipmap residency and tile lifetime | +| `GenerateMesh` plus shape-world far-parent repair and shell stitching | `generateTile` | Convert owned result to `FVoxelMeshData`, call `BuildTileStreamSet`, and upload it | +| `VoxelClipmapDesiredTiles::Build` tile-set calculation | `desiredTiles` and `prioritizeTiles` | Decide when to request, cancel, retain or evict jobs | +| `UVoxelDiffLayer::ApplyModification` and affected-chunk scan | `applyEditTransaction` with `densityOffset` brushes carrying signed strength and falloff; regenerate returned LOD keys | Brush budgets, authority sequence, replication, and result acceptance | + +Delete plugin-side active-world dispatch when routing these calls; the `World` context is an +explicit object passed to each operation. The plugin's epoch/cancellation checks still reject stale +results, but cannot interrupt an executing core call. Because edit application replaces field edit +state, the host must drain in-flight core calls before applying a transaction and then submit the +affected tiles under a new epoch. Do not move worker admission, collision cooking, actor or material +creation, or rendering into the library. + +## Acceptance record + +The retained Round 11 executable is the reference at checkout `631dd57`. The final Core build +matched its 11 hash rows (full detail, whole view and LOD0–8) at both W1 and W8. It retained the +5,642/5,642 route audit, passed the shape/look and far-survival audits, and the damaged-shell control +still reports 512 removed triangles, 16 missing boundary surfaces, 16 holes, and exit 14. Six +fixed 1280×800 captures compare byte-for-byte with the Round 11 references. The final single-tile +`--shape-world --benchmark-level=0` path reports `generator=VoxelForgeCore`. + +The final no-hash, cold W8 direct full-payload benchmark was interleaved +reference/core/core/reference with `--stream-benchmark --shape-world --force-generate --workers=8 +--clip-radius=3`. Process wall means were 1.041 s for the reference (1.035, 1.047) and 1.0245 s +for Core (1.037, 1.012), 16.5 ms faster. Each run generated 2,226 tiles with the full 110.90 MiB +payload and zero cache hits. + +The separate neighborhood-first `--stream-pipeline` benchmark was repeated in eight interleaved +processes per build. Whole-view-ready means were 0.887 s for the reference (0.878–0.899 s) and +0.894 s for Core (0.880–0.932 s). The 7 ms mean difference is smaller than the 54 ms combined +run-to-run range; Core reached the 27-tile neighborhood and 960-tile full-detail milestones at +comparable times. Direct benchmark, pipeline, audit, and capture logs are retained in +`Core/verification`. diff --git a/Standalone/Source/FarSurvivalAudit.h b/Standalone/Source/FarSurvivalAudit.h index 8e0ae21..7730c84 100644 --- a/Standalone/Source/FarSurvivalAudit.h +++ b/Standalone/Source/FarSurvivalAudit.h @@ -1,5 +1,7 @@ #pragma once +#include "VoxelForgeCore/VoxelForgeCore.h" + // 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. @@ -31,7 +33,7 @@ struct FFarSurvivalAudit uint64 CorruptedTriangles=0; }; - FFarSurvivalAudit(FStandaloneOwnerWorld& InWorld,const FStandaloneShapeWorld& InShape, + FFarSurvivalAudit(FStandaloneOwnerWorld& InWorld,const VoxelForgeCore::World& InShape, const std::vector& DesiredTiles,bool bInDropOneShell) :World(InWorld),Shape(InShape),bDropOneShell(bInDropOneShell) { @@ -257,7 +259,7 @@ private: void BuildConnectorSamples(const std::vector& DesiredTiles) { - const auto& Connectors=Shape.GetFarConnectorSegments(); + const auto Connectors=Shape.farConnectorSegments(); ConnectorInView.assign(Connectors.size(),0); ConnectorHasSamples.assign(Connectors.size(),0); ConnectorHasGoodSample.assign(Connectors.size(),0); @@ -267,9 +269,10 @@ private: for(size_t I=0;I(Connector.start.x),static_cast(Connector.start.y),static_cast(Connector.start.z)}; + const V3 B{static_cast(Connector.end.x),static_cast(Connector.end.y),static_cast(Connector.end.z)}; const V3 Delta=Sub(B,A);const float Length=LengthV(Delta); - if(Length<=2.0f*Connector.Radius)continue; + 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}); @@ -286,12 +289,12 @@ private: 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; + 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(RayLength))))) Mask|=static_cast(1u< Crossings; if(ZMax<=ZMin)return Crossings; double PreviousZ=ZMin; - bool PreviousSolid=Shape.DensityAt(static_cast(X),static_cast(Y), - static_cast(PreviousZ))<0.0f; + bool PreviousSolid=Shape.densityAt({static_cast(X),static_cast(Y), + static_cast(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(X),static_cast(Y), - static_cast(Z))<0.0f; + const bool Solid=Shape.densityAt({static_cast(X),static_cast(Y), + static_cast(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(X),static_cast(Y), - static_cast(Mid))<0.0f; + const bool MidSolid=Shape.densityAt({static_cast(X),static_cast(Y), + static_cast(Mid)})<0.0f; if(MidSolid==LoSolid)Lo=Mid;else Hi=Mid; } Crossings.push_back((Lo+Hi)*0.5); @@ -451,12 +454,12 @@ private: { std::vector Crossings; double PreviousDistance=0.0; - bool PreviousSolid=Shape.DensityAt(Start.X,Start.Y,Start.Z)<0.0f; + 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(Distance))); - const bool Solid=Shape.DensityAt(P.X,P.Y,P.Z)<0.0f; + const bool Solid=Shape.densityAt({P.X,P.Y,P.Z})<0.0f; if(Solid!=PreviousSolid) { double Lo=PreviousDistance,Hi=Distance; @@ -464,7 +467,7 @@ private: { const double Mid=(Lo+Hi)*0.5; const V3 M=AddV(Start,MulV(Direction,static_cast(Mid))); - const bool MidSolid=Shape.DensityAt(M.X,M.Y,M.Z)<0.0f; + 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); @@ -511,7 +514,7 @@ private: } FStandaloneOwnerWorld& World; - const FStandaloneShapeWorld& Shape; + const VoxelForgeCore::World& Shape; static constexpr int32 AuditMinLevel=6; bool bDropOneShell=false,bCorruptionApplied=false; std::vector Boundaries; diff --git a/Standalone/Source/OwnerWorldLoader.h b/Standalone/Source/OwnerWorldLoader.h index 444378e..3fb8a6f 100644 --- a/Standalone/Source/OwnerWorldLoader.h +++ b/Standalone/Source/OwnerWorldLoader.h @@ -242,10 +242,8 @@ namespace StandaloneOwnerLoader return Raw; } - inline bool Load(const std::string& Path, FStandaloneOwnerWorld& Out, std::string& Error) + inline bool Resolve(const VFJson::Value& Root, FStandaloneOwnerWorld& Out, std::string& Error) { - VFJson::Value Root; - if (!VFJson::Load(Path,Root,Error)) return false; if (Root.Get("voxel_size_cm").Number() != VOXEL_SIZE) { Error = "export voxel_size_cm does not match the standalone VOXEL_SIZE"; @@ -320,4 +318,18 @@ namespace StandaloneOwnerLoader Out.Generator->SetStrateManager(Out.Manager.get()); return true; } + + inline bool LoadJson(std::string_view Json,FStandaloneOwnerWorld& Out,std::string& Error) + { + VFJson::Value Root; + if(!VFJson::Parser(std::string(Json)).Parse(Root,Error))return false; + return Resolve(Root,Out,Error); + } + + inline bool Load(const std::string& Path,FStandaloneOwnerWorld& Out,std::string& Error) + { + VFJson::Value Root; + if(!VFJson::Load(Path,Root,Error))return false; + return Resolve(Root,Out,Error); + } } diff --git a/Standalone/Source/StandaloneCoreScheduler.h b/Standalone/Source/StandaloneCoreScheduler.h new file mode 100644 index 0000000..d2ed5a7 --- /dev/null +++ b/Standalone/Source/StandaloneCoreScheduler.h @@ -0,0 +1,52 @@ +#pragma once + +#include "VoxelForgeCore/VoxelForgeCore.h" + +#include +#include +#include +#include +#include + +// This executor belongs to the standalone host. VoxelForgeCore submits indexed work but never +// creates or owns worker threads. +class FStandaloneCoreScheduler final : public VoxelForgeCore::JobScheduler +{ +public: + double parallel_for(std::size_t count,std::size_t batchSize,std::size_t workerLimit, + void* context,Task task,double* workerActiveSeconds, + std::size_t workerActiveCapacity) const override + { + if(count==0)return 0.0; + const std::size_t workers=std::clamp(workerLimit,1, + std::min(8,std::max(1,count))); + const std::size_t grain=std::max(1,batchSize); + std::atomic next{0}; + std::vector active(workers,0.0); + std::vector threads; + threads.reserve(workers); + for(std::size_t worker=0;worker=count)break; + const std::size_t end=first+std::min(grain,count-first); + for(std::size_t index=first;index( + std::chrono::steady_clock::now()-started).count(); + }); + for(std::thread& thread:threads)thread.join(); + double total=0.0; + for(std::size_t worker=0;worker - -// Standalone-only port of the research field's round, winding tunnel profile. -// The cubic centerline, variable radius and elliptical vertical section follow -// GeneratedTunnelEdgeFromColumnProfile in VoxelRedefinedDensityField.cpp. -namespace FStandaloneResearchTunnels -{ - inline std::atomic Enabled{false}; - inline std::atomic EnhancedWalls{true}; - inline constexpr float WorldCenterZVoxels=-80.0f; - - inline uint32 Mix(uint32 X) - { - X ^= X >> 16; - X *= 0x7FEB352Du; - X ^= X >> 15; - X *= 0x846CA68Bu; - X ^= X >> 16; - return X; - } - - inline uint32 EdgeHash(int32 X, int32 Y, int32 Axis, uint32 Seed) - { - uint32 H = Seed ^ 0x9E3779B9u; - H = Mix(H ^ static_cast(X) * 0x85EBCA6Bu); - H = Mix(H ^ static_cast(Y) * 0xC2B2AE35u); - return Mix(H ^ static_cast(Axis) * 0x27D4EB2Fu); - } - - inline float Unit(uint32 X) - { - return static_cast(Mix(X) >> 8) * (1.0f / 16777216.0f); - } - - inline void Set(bool bEnabled, bool bEnhancedWalls = true) - { - EnhancedWalls.store(bEnhancedWalls,std::memory_order_relaxed); - Enabled.store(bEnabled,std::memory_order_release); - } - - inline uint32 NoiseHash(int32 X,int32 Y,int32 Z,uint32 Seed) - { - uint32 H=Mix(Seed^0xB5297A4Du); - H=Mix(H^static_cast(X)*0x68E31DA4u); - H=Mix(H^static_cast(Y)*0x1B56C4E9u); - return Mix(H^static_cast(Z)*0x9E3779B9u); - } - - inline float NoiseValue(int32 X,int32 Y,int32 Z,uint32 Seed) - { - return static_cast(NoiseHash(X,Y,Z,Seed)>>8)*(2.0f/16777215.0f)-1.0f; - } - - inline float Fade(float T) - { - return T*T*(3.0f-2.0f*T); - } - - inline float ValueNoise(float X,float Y,float Z,int32 Wavelength,uint32 Seed) - { - const float Inv=1.0f/static_cast(Wavelength); - const float FX=X*Inv,FY=Y*Inv,FZ=Z*Inv; - const int32 X0=FMath::FloorToInt(FX),Y0=FMath::FloorToInt(FY),Z0=FMath::FloorToInt(FZ); - const float TX=Fade(FX-static_cast(X0)); - const float TY=Fade(FY-static_cast(Y0)); - const float TZ=Fade(FZ-static_cast(Z0)); - const float Z00=FMath::Lerp(NoiseValue(X0,Y0,Z0,Seed),NoiseValue(X0+1,Y0,Z0,Seed),TX); - const float Z10=FMath::Lerp(NoiseValue(X0,Y0+1,Z0,Seed),NoiseValue(X0+1,Y0+1,Z0,Seed),TX); - const float Z01=FMath::Lerp(NoiseValue(X0,Y0,Z0+1,Seed),NoiseValue(X0+1,Y0,Z0+1,Seed),TX); - const float Z11=FMath::Lerp(NoiseValue(X0,Y0+1,Z0+1,Seed),NoiseValue(X0+1,Y0+1,Z0+1,Seed),TX); - return FMath::Lerp(FMath::Lerp(Z00,Z10,TY),FMath::Lerp(Z01,Z11,TY),TZ); - } - - inline float WallRelief(float X,float Y,float Z,uint32 Seed) - { - return 0.70f*ValueNoise(X,Y,Z,8,Seed^0xA511E9B3u) - +1.05f*ValueNoise(X,Y,Z,16,Seed^0x63D83595u) - +1.45f*ValueNoise(X,Y,Z,32,Seed^0xB5297A4Du); - } - - inline float SmoothMax(float A,float B,float Width) - { - const float H=FMath::Max(Width-FMath::Abs(A-B),0.0f)/Width; - return FMath::Max(A,B)+(H*H)*(Width*0.25f); - } - - inline float Apply(float BaseDensity, float X, float Y, float Z, uint32 Seed) - { - if (!Enabled.load(std::memory_order_acquire)) return BaseDensity; - constexpr float CenterZ = WorldCenterZVoxels; - const bool bEnhancedWalls=EnhancedWalls.load(std::memory_order_relaxed); - if (FMath::Abs(Z-CenterZ) > 36.0f) return BaseDensity; - - constexpr float Pitch = 144.0f; - constexpr float RadiusBase = 28.0f; - constexpr float HalfHeight = 26.0f; - const int32 BaseCellX = FMath::FloorToInt(X/Pitch); - const int32 BaseCellY = FMath::FloorToInt(Y/Pitch); - float Best = -FLT_MAX; - bool bHasEdge = false; - const auto CellCenter = [&](int32 CX, int32 CY, float& OutX, float& OutY) - { - const uint32 H = EdgeHash(CX,CY,3,Seed); - OutX = (static_cast(CX)+0.5f)*Pitch - + (Unit(H^0xA511E9B3u)-0.5f)*Pitch*0.18f; - OutY = (static_cast(CY)+0.5f)*Pitch - + (Unit(H^0x63D83595u)-0.5f)*Pitch*0.18f; - }; - const auto EvaluateEdge = [&](int32 CX, int32 CY, int32 Axis) - { - const uint32 H = EdgeHash(CX,CY,Axis,Seed); - if (Unit(H^0x68E31DA4u) > 0.56f) return; - bHasEdge = true; - const int32 NX = CX + (Axis == 0 ? 1 : 0); - const int32 NY = CY + (Axis == 1 ? 1 : 0); - float AX,AY,BX,BY; - CellCenter(CX,CY,AX,AY); - CellCenter(NX,NY,BX,BY); - - const float ABX = BX-AX, ABY = BY-AY; - const float LengthSquared = ABX*ABX+ABY*ABY; - float T = LengthSquared > 1.0e-8f - ? FMath::Clamp(((X-AX)*ABX+(Y-AY)*ABY)/LengthSquared,0.0f,1.0f) - : 0.0f; - const float Length = FMath::Sqrt(LengthSquared); - const float NXrm = Length > 1.0e-4f ? -ABY/Length : 0.0f; - const float NYrm = Length > 1.0e-4f ? ABX/Length : 0.0f; - const float BendA = (Unit(H^0xB5297A4Du)*2.0f-1.0f)*Pitch*0.16f; - const float BendB = (Unit(H^0x1B56C4E9u)*2.0f-1.0f)*Pitch*0.16f; - const float C1X=AX+ABX/3.0f+NXrm*BendA, C1Y=AY+ABY/3.0f+NYrm*BendA; - const float C2X=AX+ABX*(2.0f/3.0f)+NXrm*BendB; - const float C2Y=AY+ABY*(2.0f/3.0f)+NYrm*BendB; - - for (int32 Iteration=0;Iteration<4;++Iteration) - { - const float U=T, O=1.0f-U; - const float PX=O*O*O*AX+3.0f*O*O*U*C1X+3.0f*O*U*U*C2X+U*U*U*BX; - const float PY=O*O*O*AY+3.0f*O*O*U*C1Y+3.0f*O*U*U*C2Y+U*U*U*BY; - const float DX=3.0f*O*O*(C1X-AX)+6.0f*O*U*(C2X-C1X)+3.0f*U*U*(BX-C2X); - const float DY=3.0f*O*O*(C1Y-AY)+6.0f*O*U*(C2Y-C1Y)+3.0f*U*U*(BY-C2Y); - const float DDX=6.0f*O*(C2X-2.0f*C1X+AX)+6.0f*U*(BX-2.0f*C2X+C1X); - const float DDY=6.0f*O*(C2Y-2.0f*C1Y+AY)+6.0f*U*(BY-2.0f*C2Y+C1Y); - const float Den=DX*DX+DY*DY+(PX-X)*DDX+(PY-Y)*DDY; - if (FMath::Abs(Den)>1.0e-4f) - T=FMath::Clamp(U-((PX-X)*DX+(PY-Y)*DY)/Den,0.0f,1.0f); - } - - const float O=1.0f-T; - const float PX=O*O*O*AX+3.0f*O*O*T*C1X+3.0f*O*T*T*C2X+T*T*T*BX; - const float PY=O*O*O*AY+3.0f*O*O*T*C1Y+3.0f*O*T*T*C2Y+T*T*T*BY; - const float DX=X-PX,DY=Y-PY; - const float Distance=FMath::Sqrt(DX*DX+DY*DY); - const float Phase=Unit(H^0xA511E9B3u)*6.2831853071795864769f; - const float Radius=RadiusBase*(0.82f+0.36f*Unit(H^0x63D83595u)) - *(1.0f+0.22f*VoxelMath::DetSin(6.2831853071795864769f*T+Phase)); - const float VerticalWave=4.0f*VoxelMath::DetSin(3.14159265358979323846f*T) - *VoxelMath::DetSin(6.2831853071795864769f*T+Phase); - const float VerticalOffset=Z-(CenterZ+VerticalWave); - const float HorizontalDistance=FMath::Max(0.0f,Distance); - const float Normalized=FMath::Sqrt( - FMath::Square(HorizontalDistance/FMath::Max(Radius,1.0f)) - +FMath::Square(VerticalOffset/FMath::Max(HalfHeight,1.0f))); - const float EdgeDensity=(1.0f-Normalized)*FMath::Min(Radius,HalfHeight); - Best=bEnhancedWalls&&Best>-FLT_MAX - ?SmoothMax(Best,EdgeDensity,4.0f):FMath::Max(Best,EdgeDensity); - }; - - for (int32 CY=BaseCellY-1;CY<=BaseCellY+1;++CY) - for (int32 CX=BaseCellX-1;CX<=BaseCellX+1;++CX) - { - EvaluateEdge(CX,CY,0); - EvaluateEdge(CX,CY,1); - } - if(!bHasEdge)return BaseDensity; - if(bEnhancedWalls)Best+=WallRelief(X,Y,Z,Seed); - // The wider base/round-field blend removes tiny marching-cubes wedges where a round - // branch meets an existing room or passage wall. Relief remains active on the tunnel - // wall; this only rounds the structural union boundary. - return bEnhancedWalls?SmoothMax(BaseDensity,Best,8.0f):FMath::Max(BaseDensity,Best); - } -} +#include "../Core/src/ResearchTunnelsInternal.h" diff --git a/Standalone/Source/StandaloneShapeWorld.h b/Standalone/Source/StandaloneShapeWorld.h index ab0e4c9..7fa23d4 100644 --- a/Standalone/Source/StandaloneShapeWorld.h +++ b/Standalone/Source/StandaloneShapeWorld.h @@ -1,3766 +1,3 @@ #pragma once -#include "OwnerWorldLoader.h" -#include "StandaloneResearchTunnels.h" -#include "VoxelCaveMorphology.h" -#include "VoxelNoise.h" -#include "VoxelStrateManager.h" - -#include -#include -#include -#include -#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 FFarConnectorSegment - { - FVector Start=FVector::ZeroVector; - FVector End=FVector::ZeroVector; - float Radius=0.0f; - }; - struct FEditBrush - { - enum class EShape : uint8 { Sphere, Capsule, RoundedBox }; - enum class EOperation : uint8 { Carve, AddRock }; - EShape Shape=EShape::Sphere; - EOperation Operation=EOperation::Carve; - std::array A{}; - std::array B{}; - std::array HalfExtent{}; - double Radius=0.0; - double Rounding=0.0; - }; - - struct FParallelTiming - { - double WallSeconds = 0.0; - double WorkerSeconds = 0.0; - }; - - struct FDetailWorkerProfile - { - uint64 Lines=0,Candidates=0,DetailEvaluations=0,Flips=0; - uint64 LocalBandSkips=0; - uint64 DistanceSamples=0,DistanceSampleNanoseconds=0; - uint64 DetailSamples=0,DetailSampleNanoseconds=0; - uint64 ReliefSampleNanoseconds=0,LayerLookupSampleNanoseconds=0; - uint64 OwnerRoughnessSampleNanoseconds=0,OwnerRoughnessFieldCalls=0; - uint64 OwnerRoughnessActiveCalls=0,OwnerNoiseCacheHits=0,OwnerNoiseCacheMisses=0; - }; - - 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 RoomFloorJoinCount = 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; - double MaxCapsuleJoinRadiusDelta = 0.0; - double MaxCapsuleJoinRadiusSlopeDelta = 0.0; - double MaxCapsuleJoinFloorDelta = 0.0; - double MaxCapsuleJoinFloorSlopeDelta = 0.0; - double MaxRawUnmarkedFloorSlopeDelta = 0.0; - double MaxUnmarkedFloorSlopeDelta = 0.0; - double MaxUnmarkedFloorDirectionSlopeDelta = 0.0; - double MaxMarkedFloorSlopeDelta = 0.0; - double MaxFloorCurveDeviation = 0.0; - double LocalPrimitiveSeconds = 0.0; - double LocalLineBinSeconds = 0.0; - double CandidateMergeSeconds = 0.0; - double FarPrimitiveSeconds = 0.0; - double FarBinSeconds = 0.0; - FParallelTiming RockIntervalLines; - FParallelTiming CandidateAxisLines; - FParallelTiming CandidateMergeLines; - FParallelTiming DetailBandLines; - uint64 CurvedFloorSegments = 0; - uint64 MarkedFloorSlopeJoins = 0; - double LegacySegmentRadiusStep = 0.0; - double SmoothedProfileRadiusStep = 0.0; - double MaxTunnelBendDegrees = 0.0; - FVector MaxTunnelBendVoxels = FVector::ZeroVector; - FVector MaxTunnelBendDirection = FVector::ZeroVector; - uint64 CapsuleJoinCount = 0; - }; - - struct FPrimitive - { - int32 CenterSub[3]{}; - int32 CapsuleStartSub[3]{}; - int32 CapsuleEndSub[3]{}; - double CapsuleStartWorld[3]{}; - double CapsuleAxisDelta[3]{}; - double CapsuleAxisLengthSquared = 0.0; - double CapsuleAxisLength = 0.0; - int64 Q[3][3]{}; - int32 Min[3]{}; - int32 Max[3]{}; - float MinRadius = 1.0f; - float CapsuleStartRadius = 1.0f; - float CapsuleEndRadius = 1.0f; - float CapsuleProfileStartRadius = 1.0f; - float CapsuleProfileEndRadius = 1.0f; - double CapsuleProfileStartT = 0.0; - double CapsuleProfileEndT = 1.0; - double CapsuleRadiusSlopeStart = 0.0; - double CapsuleRadiusSlopeEnd = 0.0; - double BlendRadius = 0.0; - double FloorBlendRadius = 0.0; - bool bCarve = true; - bool bCritical = false; - bool bPassageSurface = false; - bool bCapsule = false; - bool bSmoothCapsuleRadius = false; - bool bRoundedBox = false; - bool bHasFloorClip = false; - bool bCurvedFloorClip = false; - double RoundedHalfExtent[3]{}; - double Rounding = 0.0; - double FloorCenterZ = 0.0; - double FloorSlopeX = 0.0; - double FloorSlopeY = 0.0; - double FloorCurveOriginX = 0.0; - double FloorCurveOriginY = 0.0; - double FloorCurveDeltaX = 0.0; - double FloorCurveDeltaY = 0.0; - double FloorCurveLength = 0.0; - double FloorCurveStartZ = 0.0; - double FloorCurveEndZ = 0.0; - double FloorCurveTangentStart = 0.0; - double FloorCurveTangentEnd = 0.0; - double FloorCurveStartBlend = 0.0; - double FloorCurveEndBlend = 0.0; - double FloorCurveMaxDeviation = 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); - } - }; - struct FBinItems - { - static constexpr size_t InlineCapacity=4; - struct FHeapStorage {uint32* Data=nullptr;uint32 Capacity=0;uint32 Reserved=0;}; - union FStorage - { - std::array Inline; - FHeapStorage Heap; - FStorage():Inline{}{} - ~FStorage(){} - } Storage; - uint32 Count=0; - uint32 bHeap=0; - - FBinItems()=default; - FBinItems(const FBinItems&)=delete; - FBinItems& operator=(const FBinItems&)=delete; - FBinItems(FBinItems&& Other) noexcept {MoveFrom(Other);} - FBinItems& operator=(FBinItems&& Other) noexcept - { - if(this!=&Other){Release();MoveFrom(Other);} - return *this; - } - ~FBinItems(){Release();} - - void push_back(uint32 Id) - { - if(!bHeap&&Count Previous=Storage.Inline; - Storage.Heap={new uint32[8],8,0}; - std::copy(Previous.begin(),Previous.end(),Storage.Heap.Data); - bHeap=1; - } - else if(Count==Storage.Heap.Capacity) - { - const uint32 NewCapacity=Storage.Heap.Capacity*2; - uint32* NewData=new uint32[NewCapacity]; - std::copy_n(Storage.Heap.Data,Count,NewData); - delete[] Storage.Heap.Data; - Storage.Heap={NewData,NewCapacity,0}; - } - Storage.Heap.Data[Count++]=Id; - } - size_t size() const {return Count;} - size_t capacity() const {return bHeap?Storage.Heap.Capacity:InlineCapacity;} - const uint32* begin() const {return bHeap?Storage.Heap.Data:Storage.Inline.data();} - const uint32* end() const {return begin()+Count;} - - private: - void Release() - { - if(bHeap)delete[] Storage.Heap.Data; - bHeap=0;Count=0; - } - void MoveFrom(FBinItems& Other) - { - Count=Other.Count;bHeap=Other.bHeap; - if(bHeap) - { - Storage.Heap=Other.Storage.Heap; - Other.bHeap=0;Other.Count=0; - } - else - { - Storage.Inline=Other.Storage.Inline; - Other.Count=0; - } - } - }; - - static constexpr int32 QScale = 1 << 28; - static constexpr int32 SubPerVoxel = 16; - static constexpr int32 DetailBand = 6; - static constexpr float DetailSupremum = 4.0f; - static constexpr double JoinBlendRadius = 1.5; - static constexpr double RoomUnionBlendRadius = 12.0; - static constexpr double FloorClipBlendRadius = 16.0; - static constexpr float StrataCaveBlendRadius = 12.0f; - static constexpr int32 LineBinSize = 32; - static constexpr int32 SpatialBinSize = 256; - - static void SetDetailProfileEnabled(bool bEnabled) - {ProfileDetailBand.store(bEnabled,std::memory_order_release);} - - 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,16);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(); - auto Substage=Clock::now(); - BuildOwnerPrimitives(); - Stats.LocalPrimitiveSeconds=std::chrono::duration(Clock::now()-Substage).count(); - Substage=Clock::now(); - if(FStandaloneResearchTunnels::Enabled.load(std::memory_order_acquire)) - BuildRoundTunnelPrimitives(); - BuildSpatialBins(LineBinSize); - Stats.LocalLineBinSeconds=std::chrono::duration(Clock::now()-Substage).count(); - 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(Stats.RockIntervalLines); - BuildCandidateBands(Stats.CandidateAxisLines,Stats.CandidateMergeLines, - Stats.CandidateMergeSeconds); - Stats.IntervalSeconds=std::chrono::duration(Clock::now()-Stage).count(); - - Stage=Clock::now(); - ApplyDetailBand(Stats.DetailBandLines); - 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); - uint64 CandidateAxisBytes=0; - for(const auto& AxisWords:CandidateAxisWords) - CandidateAxisBytes+=static_cast(AxisWords.capacity())*sizeof(uint64); - uint64 BinReferences=0,BinCapacity=0,InlineBins=0,OverflowBins=0; - for(const auto& Bin:SpatialBins) - { - BinReferences+=Bin.second.size();BinCapacity+=Bin.second.capacity(); - if(Bin.second.size()<=FBinItems::InlineCapacity)++InlineBins; - else ++OverflowBins; - } - std::printf("[ShapeProfile] local_primitives=%.3fms line_bins=%.3fms rock_lines_wall=%.3fms rock_lines_worker=%.3fms candidate_axes_wall=%.3fms candidate_axes_worker=%.3fms candidate_merge_wall=%.3fms candidate_merge_worker=%.3fms candidate_merge_serial=%.3fms detail_lines_wall=%.3fms detail_lines_worker=%.3fms\n", - Stats.LocalPrimitiveSeconds*1000.0,Stats.LocalLineBinSeconds*1000.0, - Stats.RockIntervalLines.WallSeconds*1000.0,Stats.RockIntervalLines.WorkerSeconds*1000.0, - Stats.CandidateAxisLines.WallSeconds*1000.0,Stats.CandidateAxisLines.WorkerSeconds*1000.0, - Stats.CandidateMergeLines.WallSeconds*1000.0,Stats.CandidateMergeLines.WorkerSeconds*1000.0, - Stats.CandidateMergeSeconds*1000.0,Stats.DetailBandLines.WallSeconds*1000.0, - Stats.DetailBandLines.WorkerSeconds*1000.0); - std::printf("[ShapeAllocation] words=%.2fMiB candidate_axis_words=%.2fMiB proxy=%.2fMiB primitives=%.2fMiB spatial_bins=%llu spatial_bin_refs=%llu spatial_bin_capacity=%llu inline_bins=%llu overflow_bins=%llu far_primitive_build=%.3fms far_bin_rebuild=%.3fms\n", - static_cast(WordBytes)/1048576.0,static_cast(CandidateAxisBytes)/1048576.0, - static_cast(Stats.SignNodes*sizeof(int8))/1048576.0, - static_cast(Primitives.capacity()*sizeof(FPrimitive))/1048576.0, - static_cast(SpatialBins.size()), - static_cast(BinReferences), - static_cast(BinCapacity), - static_cast(InlineBins), - static_cast(OverflowBins),Stats.FarPrimitiveSeconds*1000.0, - Stats.FarBinSeconds*1000.0); - 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::printf("[RoomFloorJoinAudit] automatic_room_overlaps=%llu thin_positive_support_overlays=0 status=PASS\n", - static_cast(Stats.RoomFloorJoinCount)); - const double SmoothUnionGrowthBound=JoinBlendRadius/6.0; - const bool bFloorSlopePass=Stats.MaxUnmarkedFloorSlopeDelta<=0.75; - const bool bFloorBlendPass=Stats.MaxFloorCurveDeviation<=2.0; - std::printf("[FloorSlopeJoinAudit] raw_unmarked_max_delta=%.5f vox generated_tangent_delta=%.5f vox bend_direction_slope_change=%.5f vox marked_max_delta=%.5f vox marked_joins=%llu threshold=0.750 status=%s\n", - Stats.MaxRawUnmarkedFloorSlopeDelta,Stats.MaxUnmarkedFloorSlopeDelta, - Stats.MaxUnmarkedFloorDirectionSlopeDelta,Stats.MaxMarkedFloorSlopeDelta, - static_cast(Stats.MarkedFloorSlopeJoins),bFloorSlopePass?"PASS":"FAIL"); - std::printf("[FloorProfileBlend] c1_segments=%llu max_curve_deviation=%.3f vox threshold=2.000 status=%s\n", - static_cast(Stats.CurvedFloorSegments),Stats.MaxFloorCurveDeviation, - bFloorBlendPass?"PASS":"FAIL"); - std::printf("[TunnelRadiusAudit] shared_joins=%llu legacy_fixed_segment_max_step=%.4f vox smoothed_profile_max_step=%.4f vox tapered_endpoint_max_delta=%.6f vox radius_slope_join_delta=%.8f floor_endpoint_max_delta=%.6f vox floor_slope_join_delta=%.5f unmarked_floor_slope_delta=%.5f marked_floor_slope_delta=%.5f marked_slope_joins=%llu smooth_union_growth_bound=%.4f vox thresholds=(0.005,0.000001,0.005,0.25) status=%s\n", - static_cast(Stats.CapsuleJoinCount),Stats.LegacySegmentRadiusStep, - Stats.SmoothedProfileRadiusStep,Stats.MaxCapsuleJoinRadiusDelta, - Stats.MaxCapsuleJoinRadiusSlopeDelta, - Stats.MaxCapsuleJoinFloorDelta,Stats.MaxCapsuleJoinFloorSlopeDelta, - Stats.MaxUnmarkedFloorSlopeDelta,Stats.MaxMarkedFloorSlopeDelta, - static_cast(Stats.MarkedFloorSlopeJoins),SmoothUnionGrowthBound, - Stats.MaxCapsuleJoinRadiusDelta<=0.005&&Stats.MaxCapsuleJoinRadiusSlopeDelta<=1.0e-6 - &&Stats.MaxCapsuleJoinFloorDelta<=0.005 - &&SmoothUnionGrowthBound<=0.25?"PASS":"FAIL"); - const double BendHorizontal=std::sqrt(Stats.MaxTunnelBendDirection.X*Stats.MaxTunnelBendDirection.X - +Stats.MaxTunnelBendDirection.Y*Stats.MaxTunnelBendDirection.Y); - const double BendYaw=std::atan2(Stats.MaxTunnelBendDirection.X,Stats.MaxTunnelBendDirection.Y)*57.29577951308232; - const double BendPitch=std::atan2(Stats.MaxTunnelBendDirection.Z,BendHorizontal)*57.29577951308232; - std::printf("[TunnelBendAudit] sharpest_bend=%.2f deg centerline=(%.2f,%.2f,%.2f) vox tangent=(%.4f,%.4f,%.4f) camera_yaw=%.2f camera_pitch=%.2f\n", - Stats.MaxTunnelBendDegrees,Stats.MaxTunnelBendVoxels.X, - Stats.MaxTunnelBendVoxels.Y,Stats.MaxTunnelBendVoxels.Z, - Stats.MaxTunnelBendDirection.X,Stats.MaxTunnelBendDirection.Y, - Stats.MaxTunnelBendDirection.Z,BendYaw,BendPitch); - std::fflush(stdout); - return true; - } - - const FBuildStats& GetStats() const { return Stats; } - bool IsBuilt() const { return Built; } - bool HasFarWorld() const { return FarBuilt; } - const std::vector& GetFarConnectorSegments() const - { return FarConnectorSegments; } - - void FindFarConnectorCandidates(const FIntVector& QueryMin,const FIntVector& QueryMax, - std::vector& OutIndices) const - { - constexpr int32 BinSize=2048; - OutIndices.clear(); - const int32 MinX=FloorDiv(QueryMin.X,BinSize),MaxX=FloorDiv(QueryMax.X,BinSize); - const int32 MinY=FloorDiv(QueryMin.Y,BinSize),MaxY=FloorDiv(QueryMax.Y,BinSize); - const int32 MinZ=FloorDiv(QueryMin.Z,BinSize),MaxZ=FloorDiv(QueryMax.Z,BinSize); - for(int32 Z=MinZ;Z<=MaxZ;++Z)for(int32 Y=MinY;Y<=MaxY;++Y) - for(int32 X=MinX;X<=MaxX;++X) - { - const auto It=FarConnectorSpatialBins.find({X,Y,Z}); - if(It!=FarConnectorSpatialBins.end()) - OutIndices.insert(OutIndices.end(),It->second.begin(),It->second.end()); - } - std::sort(OutIndices.begin(),OutIndices.end()); - OutIndices.erase(std::unique(OutIndices.begin(),OutIndices.end()),OutIndices.end()); - } - - class FScopedForcedFarAirSamples - { - public: - explicit FScopedForcedFarAirSamples(const std::unordered_set* AirSamples) - :PreviousAir(ForcedFarAirSamples) - { ForcedFarAirSamples=AirSamples; } - ~FScopedForcedFarAirSamples(){ForcedFarAirSamples=PreviousAir;} - FScopedForcedFarAirSamples(const FScopedForcedFarAirSamples&)=delete; - FScopedForcedFarAirSamples& operator=(const FScopedForcedFarAirSamples&)=delete; - private: - const std::unordered_set* PreviousAir=nullptr; - }; - 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 SetEditTransaction(std::vector Brushes) - { - EditBrushes=std::move(Brushes); - EditBoundsMin={MAX_int32,MAX_int32,MAX_int32}; - EditBoundsMax={MIN_int32,MIN_int32,MIN_int32}; - for(const FEditBrush& Brush:EditBrushes) - { - double Extent[3]{}; - if(Brush.Shape==FEditBrush::EShape::Sphere) - for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.Radius; - else if(Brush.Shape==FEditBrush::EShape::Capsule) - for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.Radius; - else - for(int32 Axis=0;Axis<3;++Axis)Extent[Axis]=Brush.HalfExtent[Axis]+Brush.Rounding; - for(int32 Axis=0;Axis<3;++Axis) - { - const double Lo=Brush.Shape==FEditBrush::EShape::Capsule - ?std::min(Brush.A[Axis],Brush.B[Axis])-Extent[Axis] - :Brush.A[Axis]-Extent[Axis]; - const double Hi=Brush.Shape==FEditBrush::EShape::Capsule - ?std::max(Brush.A[Axis],Brush.B[Axis])+Extent[Axis] - :Brush.A[Axis]+Extent[Axis]; - (&EditBoundsMin.X)[Axis]=std::min((&EditBoundsMin.X)[Axis],static_cast(std::floor(Lo))); - (&EditBoundsMax.X)[Axis]=std::max((&EditBoundsMax.X)[Axis],static_cast(std::ceil(Hi))); - } - } - } - - bool HasEditTransaction() const { return !EditBrushes.empty(); } - FIntVector GetEditBoundsMin() const { return EditBoundsMin; } - FIntVector GetEditBoundsMax() const { return EditBoundsMax; } - - void BuildFarWorld() - { - if(FarBuilt)return; - auto Stage=std::chrono::steady_clock::now(); - BuildFarParentShapes(); - Stats.FarPrimitiveSeconds=std::chrono::duration(std::chrono::steady_clock::now()-Stage).count(); - Stage=std::chrono::steady_clock::now(); - BuildSpatialBins(SpatialBinSize,false); - Stats.FarBinSeconds=std::chrono::duration(std::chrono::steady_clock::now()-Stage).count(); - Stats.FarShapeSeconds=Stats.FarPrimitiveSeconds+Stats.FarBinSeconds; - Stats.PrimitiveCount=Primitives.size();FarBuilt=true; - std::printf("[FarShapeStage] parent_shapes=%.3f ms spatial_bins=%.3f ms total=%.3f ms primitives=%llu\n", - Stats.FarPrimitiveSeconds*1000.0,Stats.FarBinSeconds*1000.0,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 - { - int32 IX=0,IY=0,IZ=0; - 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); - if(bPacked) - return PackedDensityAt(IX,IY,IZ,IsRock(IX,IY,IZ),X,Y,Z); - return AnalyticDensityAt(X,Y,Z); - } - - float DensityAtLattice(int32 X,int32 Y,int32 Z) const - { - if(IsForcedFarAirSample(X,Y,Z))return 0.05f; - const bool bPacked=Contains(X,Y,Z); - if(bPacked) - return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),static_cast(X), - static_cast(Y),static_cast(Z)); - return AnalyticDensityAt(static_cast(X),static_cast(Y), - static_cast(Z)); - } - - void DensityAtLatticeX8(int32 XStart,int32 XStep,int32 Y,int32 Z, - int32 Count,float OutDensity[8]) const - { - Count=std::clamp(Count,0,8); - if(Count==0)return; - bool bFast=EditBrushes.empty()&&Y>=Min[1]&&Y<=Max[1] - &&Z>=Min[2]&&Z<=Max[2] - &&XStart>=Min[0] - &&XStart+static_cast(Count-1)*XStep<=Max[0]; - if(bFast) - { - for(int32 Lane=0;Lane(Y-Min[1])*Size[0] - +static_cast(XStart-Min[0]); - const size_t Zi=static_cast(Z-Min[2]); - const size_t WordOffset=Zi>>6; - const uint64 Mask=uint64{1}<<(Zi&63u); - const size_t NodeBase=Zi*static_cast(Size[1])*Size[0]+ColumnStart; - constexpr float ProxyBandEdge=DetailSupremum+0.125f; - for(int32 Lane=0;Lane(Lane*XStep); - const size_t Word=Column*WordsPerZ+WordOffset; - const bool bRock=(RockWords[Word]&Mask)!=0; - float Density; - if(CandidateWords[Word]&Mask) - { - const int8 Proxy=SurfaceDistanceQ[NodeBase - +static_cast(Lane*XStep)]; - 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); - } - else - Density=bRock?-ProxyBandEdge:ProxyBandEdge; - OutDensity[Lane]=Density; - } - } - -private: - static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z) - { - constexpr uint64 Mask=0x1FFFFFull; - return (static_cast(static_cast(X)&Mask)<<42) - |(static_cast(static_cast(Y)&Mask)<<21) - |(static_cast(static_cast(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 - { - const size_t Column=static_cast(Y-Min[1])*Size[0]+static_cast(X-Min[0]); - const size_t Zi=static_cast(Z-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 ApplyEditBrushes(Density,FX,FY,FZ); - } - // Match the fixed fallback to the edge of the stored proxy band. The - // displacement is tapered to zero there, so this avoids a magnitude - // jump along the candidate bit-mask boundary. - constexpr float ProxyBandEdge=DetailSupremum+0.125f; - return ApplyEditBrushes(bRock?-ProxyBandEdge:ProxyBandEdge,FX,FY,FZ); - } - - float AnalyticDensityAt(float X,float Y,float Z) const - { - const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1] - &&Z>=Min[2]&&Z<=Max[2]; - if(!bInsideFine) - { - bool bCriticalAir=false; - float RockDistance=CoarseRockDistance(X,Y,Z,&bCriticalAir); - if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); - const bool bRock=RockDistance>=0.0f; - float Density=-RockDistance; - if((Density<0.0f)!=bRock||std::abs(Density)<0.05f) - Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f); - return ApplyEditBrushes(Density,X,Y,Z); - } - ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; - bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; - float PassageInfluence=0.0f; - float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF, - &bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir,nullptr,false, - &PassageInfluence); - if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth - &&std::abs(RockDistance)<=DetailSupremum+0.125f) - { - const float Fade=DetailBandFadeAt(X,Y,Z,RockDistance,SurfaceSDF, - CaveInfluence,FloorInfluence); - if(Fade>0.0f)RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence, - FloorInfluence,PassageInfluence)*Fade; - } - if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); - const bool bRock=RockDistance>=0.0f; - float Density=-RockDistance; - if((Density<0.0f)!=bRock||std::abs(Density)<0.05f) - Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f); - return ApplyEditBrushes(Density,X,Y,Z); - } - -public: - 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}; - if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0] - &&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1] - &&EditBoundsMin.Z<=BMax[2]&&EditBoundsMax.Z>=BMin[2]) - return EVoxelTileClass::Mixed; - 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; - const bool bFamilies=AuditSurfaceBandFamilies(); - 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&&bFamilies?"PASS":"FAIL",bCoherent?"PASS":"FAIL"); - return bFlips&&bCoherent&&bFamilies; - } - - bool AuditSurfaceBandFamilies(bool bPassageOnly=false) const - { - struct FPatchKey - { - int32 X=0,Y=0,Z=0;uint8 Family=0; - bool operator==(const FPatchKey&)const=default; - }; - struct FPatchHash - { - size_t operator()(const FPatchKey& 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^=static_cast(K.Family)*0x94D049BB133111EBull; - H^=H>>29;H*=0xBF58476D1CE4E5B9ull;H^=H>>31; - return static_cast(H); - } - }; - struct FPatchStats {uint64 Count=0;double Sum=0.0,SumSq=0.0;}; - struct FFamilyStats {uint64 Count=0,NearZero=0;double Sum=0.0,SumSq=0.0;}; - std::unordered_map Patches; - std::array Families{}; - FFamilyStats PassageWalls,OrdinaryCaveWalls; - for(size_t Line=0;Line(Size[0])*Size[1];++Line) - { - const int32 X=Min[0]+static_cast(Line%Size[0]); - const int32 Y=Min[1]+static_cast(Line/Size[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); - ESurfaceFamily Family=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; - bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; - float PassageInfluence=0.0f; - const float Raw=EffectiveRockDistance(X,Y,Z,&Family,&SurfaceSDF, - &bFloorSurface,&CaveInfluence,&FloorInfluence,nullptr,nullptr,false, - &PassageInfluence); - if(std::abs(Raw)>=0.5f)continue; - const float Fade=DetailBandFadeAt(X,Y,Z,Raw,SurfaceSDF, - CaveInfluence,FloorInfluence); - const float Detail=Fade>0.0f - ?DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence, - PassageInfluence)*Fade:0.0f; - const size_t FamilyIndex=static_cast(Family); - FFamilyStats& F=Families[FamilyIndex];++F.Count;F.Sum+=Detail;F.SumSq+=Detail*Detail; - if(std::abs(Detail)<0.05f)++F.NearZero; - if(PassageInfluence>=0.5f) - {++PassageWalls.Count;PassageWalls.Sum+=Detail;PassageWalls.SumSq+=Detail*Detail;} - else if(Family==ESurfaceFamily::Cave&&PassageInfluence<0.1f) - {++OrdinaryCaveWalls.Count;OrdinaryCaveWalls.Sum+=Detail;OrdinaryCaveWalls.SumSq+=Detail*Detail;} - const FPatchKey Key{FloorDiv(X,16),FloorDiv(Y,16),FloorDiv(Z,16), - static_cast(FamilyIndex)}; - FPatchStats& P=Patches[Key];++P.Count;P.Sum+=Detail;P.SumSq+=Detail*Detail; - } - } - } - std::array FlatCounts{};uint64 LargestFlatPatch=0; - for(const auto& Entry:Patches) - { - const FPatchStats& P=Entry.second;if(P.Count<16)continue; - const double Mean=P.Sum/P.Count; - const double StdDev=std::sqrt(std::max(0.0,P.SumSq/P.Count-Mean*Mean)); - if(StdDev<0.10) - { - FlatCounts[Entry.first.Family]+=P.Count; - LargestFlatPatch=std::max(LargestFlatPatch,P.Count); - } - } - bool bPass=true;uint64 Tested=0,FlatTotal=0,Total=0; - for(size_t I=0;I(F.NearZero)/F.Count; - const bool FamilyPass=StdDev>=0.15&&ZeroShare<0.60&&FlatCounts[I](F.Count),StdDev,ZeroShare*100.0, - static_cast(FlatCounts[I]),FamilyPass?"PASS":"FAIL"); - } - const double FlatShare=Total?100.0*static_cast(FlatTotal)/Total:100.0; - std::printf("[FlatPatchAudit] patch_width=16 vox min_patch_samples=16 flat_stddev_lt=0.10 flat_share=%.1f%% largest_flat_patch=%llu status=%s\n", - FlatShare,static_cast(LargestFlatPatch), - Tested>0&&FlatShare<20.0?"PASS":"FAIL"); - const double PassageRms=PassageWalls.Count - ?std::sqrt(PassageWalls.SumSq/PassageWalls.Count):0.0; - const double OrdinaryCaveRms=OrdinaryCaveWalls.Count - ?std::sqrt(OrdinaryCaveWalls.SumSq/OrdinaryCaveWalls.Count):0.0; - const double PassageRmsRatio=OrdinaryCaveRms>1.0e-9 - ?PassageRms/OrdinaryCaveRms:0.0; - const bool bHasPassageSamples=PassageWalls.Count>0; - const bool bRatioPass=PassageWalls.Count>=32&&OrdinaryCaveWalls.Count>=32 - &&PassageRmsRatio>=0.75&&PassageRmsRatio<=1.25; - const bool bPassageDetail=bPassageOnly?bRatioPass:(!bHasPassageSamples||bRatioPass); - const char* PassageStatus=!bHasPassageSamples - ?(bPassageOnly?"FAIL":"SKIP"):(bPassageDetail?"PASS":"FAIL"); - std::printf("[PassageWallDetailAudit] passage_samples=%llu passage_rms=%.4f cave_samples=%llu cave_rms=%.4f ratio=%.3f range=(0.750,1.250) status=%s\n", - static_cast(PassageWalls.Count),PassageRms, - static_cast(OrdinaryCaveWalls.Count),OrdinaryCaveRms, - PassageRmsRatio,PassageStatus); - return bPassageOnly?bPassageDetail:(Tested>0&&FlatShare<20.0&&bPass&&bPassageDetail); - } - - bool AuditViewSurface(double CameraX,double CameraY,double CameraZ, - float Yaw,float Pitch) const - { - struct FFamilyStats - { - uint64 Hits=0,NearZero=0,AxisLocked=0; - double Sum=0.0,SumSq=0.0; - }; - std::array FamilyStats{}; - constexpr std::array YawOffsets={-36.0f,-30.0f,-24.0f,-18.0f,-12.0f,-6.0f, - 0.0f,6.0f,12.0f,18.0f,24.0f,30.0f,36.0f}; - constexpr std::array PitchOffsets={-80.0f,-70.0f,-60.0f,-50.0f,-40.0f, - -30.0f,-20.0f,-10.0f,0.0f,10.0f,20.0f}; - const double OX=CameraX*4.0,OY=CameraY*4.0,OZ=CameraZ*4.0; - auto Distance=[&](double X,double Y,double Z,ESurfaceFamily* OutFamily=nullptr, - float* OutRaw=nullptr,float* OutDetail=nullptr, - bool* OutFloorSurface=nullptr,float* OutPassageInfluence=nullptr) - { - ESurfaceFamily Family=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; - bool bFloorSurface=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; - float PassageInfluence=0.0f; - float D=EffectiveRockDistance(X,Y,Z,&Family,&SurfaceSDF, - &bFloorSurface,&CaveInfluence,&FloorInfluence,nullptr,nullptr,false, - &PassageInfluence); - float Detail=0.0f; - const bool bInside=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1] - &&Z>=Min[2]&&Z<=Max[2]; - const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z); - if(bInside&&DetailVariant!=EDetailVariant::Smooth&&std::abs(D)<=DetailSupremum+0.125f) - { - const float Fade=DetailBandFadeAt(X,Y,Z,D,SurfaceSDF,CaveInfluence,FloorInfluence); - if(Fade>0.0f) - {Detail=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence, - PassageInfluence)*Fade;D+=Detail;} - } - if(bCriticalAir)D=std::min(D,-1.0f/16.0f); - if(OutFamily)*OutFamily=Family; - if(OutRaw)*OutRaw=D-Detail; - if(OutDetail)*OutDetail=Detail; - if(OutFloorSurface)*OutFloorSurface=bFloorSurface; - if(OutPassageInfluence)*OutPassageInfluence=PassageInfluence; - return D; - }; - uint64 TotalHits=0,TotalAxisLocked=0,TotalNearZero=0,FloorTaggedHits=0,FloorAxisLocked=0; - uint64 FamilyTransitionPairs=0,StableFamilyPairs=0; - uint64 ThinDetachedSheets=0; - double ThinnestDetachedSheet=std::numeric_limits::max(); - double FamilyTransitionGradientSq=0.0,StableFamilyGradientSq=0.0; - for(float YawOffset:YawOffsets)for(float PitchOffset:PitchOffsets) - { - const double YawRad=Yaw+YawOffset*0.017453292519943295; - const double PitchRad=Pitch+PitchOffset*0.017453292519943295; - const double CP=std::cos(PitchRad),DX=std::sin(YawRad)*CP, - DY=std::cos(YawRad)*CP,DZ=std::sin(PitchRad); - double PreviousT=0.0; - float PreviousD=Distance(OX,OY,OZ+0.001); - bool bFound=false; - double PrimaryHitT=-1.0; - for(double T=0.5;T<=256.0;T+=0.5) - { - const double X=OX+DX*T,Y=OY+DY*T,Z=OZ+DZ*T; - const float D=Distance(X,Y,Z); - if(PreviousD<0.0f&&D>=0.0f) - { - double Lo=PreviousT,Hi=T; - for(int32 It=0;It<10;++It) - { - const double Mid=(Lo+Hi)*0.5; - if(Distance(OX+DX*Mid,OY+DY*Mid,OZ+DZ*Mid)<0.0f)Lo=Mid;else Hi=Mid; - } - const double HitT=(Lo+Hi)*0.5; - PrimaryHitT=HitT; - const double XH=OX+DX*HitT,YH=OY+DY*HitT,ZH=OZ+DZ*HitT; - ESurfaceFamily Family=ESurfaceFamily::Strata;float Raw=0.0f,Detail=0.0f; - bool bFloorSurface=false;float PassageInfluence=0.0f; - Distance(XH,YH,ZH,&Family,&Raw,&Detail,&bFloorSurface,&PassageInfluence); - const double GX=Distance(XH+0.5,YH,ZH)-Distance(XH-0.5,YH,ZH); - const double GY=Distance(XH,YH+0.5,ZH)-Distance(XH,YH-0.5,ZH); - const double GZ=Distance(XH,YH,ZH+0.5)-Distance(XH,YH,ZH-0.5); - const double GL=std::sqrt(GX*GX+GY*GY+GZ*GZ); - const double AxisDot=GL>1.0e-8?std::max({std::abs(GX),std::abs(GY),std::abs(GZ)})/GL:1.0; - for(int32 Axis=0;Axis<3;++Axis) - { - double AP[3]={XH,YH,ZH},AM[3]={XH,YH,ZH}; - AP[Axis]+=0.5;AM[Axis]-=0.5; - ESurfaceFamily FamilyP=ESurfaceFamily::Strata,FamilyM=ESurfaceFamily::Strata; - float DetailP=0.0f,DetailM=0.0f;bool bFloorP=false,bFloorM=false; - Distance(AP[0],AP[1],AP[2],&FamilyP,nullptr,&DetailP,&bFloorP); - Distance(AM[0],AM[1],AM[2],&FamilyM,nullptr,&DetailM,&bFloorM); - const double Gradient=static_cast(DetailP-DetailM); - if(FamilyP!=FamilyM||bFloorP!=bFloorM) - {FamilyTransitionGradientSq+=Gradient*Gradient;++FamilyTransitionPairs;} - else - {StableFamilyGradientSq+=Gradient*Gradient;++StableFamilyPairs;} - } - constexpr double ScanRadius=16.0; - constexpr int32 ScanSteps=128; - double ScanT=std::max(0.0,HitT-ScanRadius); - ESurfaceFamily ScanFamily=ESurfaceFamily::Strata; - bool bScanFloor=false;float ScanRaw=0.0f,ScanDetail=0.0f; - Distance(OX+DX*ScanT,OY+DY*ScanT,OZ+DZ*ScanT, - &ScanFamily,&ScanRaw,&ScanDetail,&bScanFloor); - for(int32 Step=1;Step<=ScanSteps;++Step) - { - const double T=std::min(HitT+ScanRadius,ScanT+0.25); - ESurfaceFamily NextFamily=ESurfaceFamily::Strata; - bool bNextFloor=false;float NextRaw=0.0f,NextDetail=0.0f; - Distance(OX+DX*T,OY+DY*T,OZ+DZ*T, - &NextFamily,&NextRaw,&NextDetail,&bNextFloor); - if(std::abs(ScanRaw)<=5.0f&&std::abs(NextRaw)<=5.0f) - { - const double Gradient=static_cast(NextDetail-ScanDetail); - if(ScanFamily!=NextFamily||bScanFloor!=bNextFloor) - {FamilyTransitionGradientSq+=Gradient*Gradient;++FamilyTransitionPairs;} - else - {StableFamilyGradientSq+=Gradient*Gradient;++StableFamilyPairs;} - } - ScanT=T;ScanFamily=NextFamily;bScanFloor=bNextFloor; - ScanRaw=NextRaw;ScanDetail=NextDetail; - } - FFamilyStats& S=FamilyStats[static_cast(Family)]; - ++S.Hits;++TotalHits;S.Sum+=Detail;S.SumSq+=Detail*Detail; - if(std::abs(Detail)<0.05f){++S.NearZero;++TotalNearZero;} - if(AxisDot>0.985){++S.AxisLocked;++TotalAxisLocked;} - if(bFloorSurface){++FloorTaggedHits;if(AxisDot>0.985)++FloorAxisLocked;} - if(YawOffset==0.0f&&PitchOffset==0.0f) - { - const char* Name=Family==ESurfaceFamily::Strata?"strata": - Family==ESurfaceFamily::Cave?"cave":"solid_overlay"; - std::printf("[SurfaceRay] t=%.2f vox hit=(%.2f,%.2f,%.2f) family=%s floor_surface=%s passage_influence=%.3f raw_sdf=%.4f detail=%.4f axis_dot=%.4f\n", - HitT,XH,YH,ZH,Name,bFloorSurface?"yes":"no",PassageInfluence,Raw,Detail,AxisDot); - } - bFound=true;break; - } - PreviousT=T;PreviousD=D; - } - if(PrimaryHitT>=0.0) - { - constexpr double SheetStep=0.25; - constexpr double MinimumAirGap=3.0; - constexpr double MaximumSheetThickness=1.0; - double T=PrimaryHitT+SheetStep; - float Prev=Distance(OX+DX*T,OY+DY*T,OZ+DZ*T); - bool bInSolid=Prev>=0.0f; - double SolidStart=T,LastSolidExit=PrimaryHitT; - bool bSeparatedFromPrevious=false; - for(T+=SheetStep;T<=256.0;T+=SheetStep) - { - const float D=Distance(OX+DX*T,OY+DY*T,OZ+DZ*T); - if(!bInSolid&&Prev<0.0f&&D>=0.0f) - { - SolidStart=T; - bSeparatedFromPrevious=(T-LastSolidExit)>=MinimumAirGap; - bInSolid=true; - } - else if(bInSolid&&Prev>=0.0f&&D<0.0f) - { - const double Thickness=T-SolidStart; - if(bSeparatedFromPrevious&&Thickness(TotalNearZero)/TotalHits:1.0; - const double AxisShare=TotalHits?static_cast(TotalAxisLocked)/TotalHits:1.0; - const double FloorAxisShare=FloorTaggedHits?static_cast(FloorAxisLocked)/FloorTaggedHits:1.0; - const double TransitionRMS=FamilyTransitionPairs - ?std::sqrt(FamilyTransitionGradientSq/FamilyTransitionPairs):0.0; - const double StableRMS=StableFamilyPairs - ?std::sqrt(StableFamilyGradientSq/StableFamilyPairs):0.0; - const double TransitionRatio=StableRMS>1.0e-6?TransitionRMS/StableRMS:1000000.0; - double Sum=0.0,SumSq=0.0; - for(const FFamilyStats& S:FamilyStats){Sum+=S.Sum;SumSq+=S.SumSq;} - const double Mean=TotalHits?Sum/TotalHits:0.0; - const double Variance=TotalHits?std::max(0.0,SumSq/TotalHits-Mean*Mean):0.0; - const double SurfaceDetailRMS=std::sqrt(Variance); - const double RelativeJoinStep=SurfaceDetailRMS>1.0e-6?TransitionRMS/SurfaceDetailRMS:1000000.0; - const bool bDetailPass=TotalHits>=24&&std::sqrt(Variance)>=0.15&&NearZeroShare<0.50; - constexpr double GridStairThreshold=0.40; - const bool bJoinCoverage=FloorTaggedHits>=8 - &&(FamilyTransitionPairs==0||(FamilyTransitionPairs>=8&&RelativeJoinStep=24&&AxisShare<0.60 - &&bJoinCoverage; - const bool bThinSheetPass=ThinDetachedSheets==0; - std::printf("[FlatSurfaceAudit] rays=%llu detail_stddev=%.4f vox near_zero_share=%.1f%% threshold=50.0%% status=%s\n", - static_cast(TotalHits),std::sqrt(Variance),NearZeroShare*100.0, - bDetailPass?"PASS":"FAIL"); - std::printf("[GridStairAudit] rays=%llu axis_locked_normals=%.1f%% threshold=60.0%% floor_tagged_hits=%llu floor_axis_locked=%.1f%% family_transition_pairs=%llu transition_rms=%.4f vox stable_rms=%.4f vox surface_detail_rms=%.4f vox relative_join_step=%.3f threshold=%.3f legacy_join_ratio=%.3f status=%s\n", - static_cast(TotalHits),AxisShare*100.0, - static_cast(FloorTaggedHits),FloorAxisShare*100.0, - static_cast(FamilyTransitionPairs),TransitionRMS,StableRMS, - SurfaceDetailRMS,RelativeJoinStep,GridStairThreshold,TransitionRatio,bGridPass?"PASS":"FAIL"); - std::printf("[ThinSheetAudit] rays=%llu isolated_sheets=%llu thickness_lt=1.000 vox air_gap_ge=3.000 vox thinnest=%.3f vox status=%s\n", - static_cast(TotalHits),static_cast(ThinDetachedSheets), - ThinDetachedSheets?ThinnestDetachedSheet:0.0,bThinSheetPass?"PASS":"FAIL"); - for(size_t I=0;I(S.Hits),M,V, - static_cast(S.NearZero),static_cast(S.AxisLocked)); - } - const bool bFloorProfilePass=Stats.MaxUnmarkedFloorSlopeDelta<=0.75 - &&Stats.MaxFloorCurveDeviation<=2.0; - return bDetailPass&&bGridPass&&bThinSheetPass&&bFloorProfilePass; - } - -private: - float ApplyEditBrushes(float Density,double X,double Y,double Z) const - { - if(EditBrushes.empty()||XEditBoundsMax.X - ||YEditBoundsMax.Y||ZEditBoundsMax.Z) - return Density; - const double P[3]={X,Y,Z}; - for(const FEditBrush& Brush:EditBrushes) - { - double Distance=0.0; - if(Brush.Shape==FEditBrush::EShape::Sphere) - { - const double DX=X-Brush.A[0],DY=Y-Brush.A[1],DZ=Z-Brush.A[2]; - Distance=DeterministicSqrt(DX*DX+DY*DY+DZ*DZ)-Brush.Radius; - } - else if(Brush.Shape==FEditBrush::EShape::Capsule) - { - double AB[3]{},AP[3]{},LengthSq=0.0,Dot=0.0; - for(int32 Axis=0;Axis<3;++Axis) - {AB[Axis]=Brush.B[Axis]-Brush.A[Axis];AP[Axis]=P[Axis]-Brush.A[Axis];LengthSq+=AB[Axis]*AB[Axis];Dot+=AB[Axis]*AP[Axis];} - const double T=LengthSq>1.0e-12?std::clamp(Dot/LengthSq,0.0,1.0):0.0; - double D2=0.0; - for(int32 Axis=0;Axis<3;++Axis) - {const double D=AP[Axis]-T*AB[Axis];D2+=D*D;} - Distance=DeterministicSqrt(D2)-Brush.Radius; - } - else - { - double OutsideSq=0.0,MaxQ=-std::numeric_limits::infinity(); - for(int32 Axis=0;Axis<3;++Axis) - { - const double Q=std::abs(P[Axis]-Brush.A[Axis])-Brush.HalfExtent[Axis]; - const double Outside=std::max(Q,0.0); - OutsideSq+=Outside*Outside;MaxQ=std::max(MaxQ,Q); - } - Distance=DeterministicSqrt(OutsideSq)+std::min(MaxQ,0.0)-Brush.Rounding; - } - if(Brush.Operation==FEditBrush::EOperation::Carve) - Density=std::max(Density,static_cast(-Distance)); - else - Density=std::min(Density,static_cast(Distance)); - } - return Density; - } - - struct FAxisBins { int32 MinA=0,MinB=0,CountA=0,CountB=0; std::vector> Items; }; - struct FLayerField - { - int32 Lo=0,Hi=-1; - int32 StrateIndex=INDEX_NONE; - FStrateGenerationParams Params; - }; - enum class ESurfaceFamily : uint8 { Strata, Cave, SolidOverlay }; - - 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; - } - - static double DeterministicSqrt(double Value) - { - if(!(Value>0.0))return 0.0; - return _mm_cvtsd_f64(_mm_sqrt_sd(_mm_setzero_pd(),_mm_set_sd(Value))); - } - - 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);} - static float SmoothMinDistance(float A,float B,float Width) - { - if(Width<=0.0f)return std::min(A,B); - const float H=std::max(Width-std::abs(A-B),0.0f)/Width; - return std::min(A,B)-(H*H*H)*(Width/6.0f); - } - static float SmoothMaxDistance(float A,float B,float Width) - {return -SmoothMinDistance(-A,-B,Width);} - static float SmoothUnit(float V) - { - V=std::clamp(V,0.0f,1.0f); - return V*V*(3.0f-2.0f*V); - } - static float DetailBandFade(float SignedDistance) - { - // Keep the larger candidate mask conservative around smooth family unions; - // visible relief still fades to zero beyond its 4.125-voxel support radius. - constexpr float Edge=DetailSupremum+0.125f; - constexpr float Width=1.5f; - return SmoothUnit((Edge-std::abs(SignedDistance))/Width); - } - static constexpr float DetailBandInnerRadius=1.5f; - static float DetailBandFade(float SignedDistance,float Edge) - { - const float Distance=std::abs(SignedDistance); - if(Distance<=DetailBandInnerRadius)return 1.0f; - const float Width=Edge-DetailBandInnerRadius; - return Width>0.0f?SmoothUnit((Edge-Distance)/Width):0.0f; - } - static float LocalDetailBandEdge(float MacroValue,float SurfaceSDF, - float CaveInfluence,float FloorInfluence,const FLayerField* Field) - { - float OwnerBound=0.0f; - if(Field) - { - const FStrateGenerationParams& Params=Field->Params; - const float Depth=Params.SurfaceRoughness*2.0f; - if(Depth>0.0f&&std::abs(SurfaceSDF)0.0f) - { - const float Fade=1.0f-std::abs(SurfaceSDF)/Depth; - // Perlin is bounded by 2; the squared ridged fold by 2.25. This - // uses the largest type bound and remains valid under domain warp. - OwnerBound=3.15f*VOXEL_NOISE_SCALE*Params.SurfaceRoughness*Fade*Fade; - } - } - const float ReliefBound=std::abs(MacroValue) - +(0.80f+0.25f*MacroValue)*(0.85f+0.18f); - const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) - +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); - const float LocalSupremum=std::min(3.9f,(ReliefBound+0.5f*OwnerBound)*Contrast); - return std::clamp(LocalSupremum+0.125f,DetailBandInnerRadius,DetailSupremum+0.125f); - } - - float DetailBandFadeAt(double X,double Y,double Z,float SignedDistance,float SurfaceSDF, - float CaveInfluence,float FloorInfluence) const - { - if(DetailVariant!=EDetailVariant::Coherent - ||std::abs(SignedDistance)<=DetailBandInnerRadius) - return DetailBandFade(SignedDistance); - const float MacroValue=FStandaloneResearchTunnels::ValueNoise( - static_cast(X),static_cast(Y),static_cast(Z),32, - static_cast(Owner->Seed)^0xB5297A4Du); - const FLayerField* Field=FindLayerField(Z); - return DetailBandFade(SignedDistance,LocalDetailBandEdge(MacroValue,SurfaceSDF, - CaveInfluence,FloorInfluence,Field)); - } - - void BuildLayerBounds() - { - LayerRanges.clear();LayerFields.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(!Slot.Definition)continue; - const int32 MidChunkZ=(Slot.TopChunkZ+Slot.BottomChunkZ)/2; - FLayerField Field; - Field.Lo=Slot.BottomChunkZ*CHUNK_SIZE; - Field.Hi=(Slot.TopChunkZ+1)*CHUNK_SIZE; - Field.StrateIndex=Slot.StrateIndex; - Field.Params=Owner->Manager->GetGenerationParams(FIntVector(0,0,MidChunkZ)); - LayerFields.push_back(std::move(Field)); - } - 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.BlendRadius=ActiveBlendRadius; - 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, - double FloorBlendRadius=0.0) - { - P.bHasFloorClip=true;P.FloorCenterZ=FloorCenterZ-0.5; - P.FloorSlopeX=SlopeX;P.FloorSlopeY=SlopeY;P.FloorBlendRadius=FloorBlendRadius; - } - - 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) - { - AddLegacyEllipsoidCapsule(A,B,Radius,bCarve,bCritical,bFloorClip,FloorA,FloorB); - } - - static FVector ApproximateUnwarp(const FVector& Target,const FStrateGenerationParams& Params, - uint32 Seed) - { - FVector WorldPoint=Target; - const float ZScale=Params.VerticalScale>0.0f?Params.VerticalScale:1.0f; - if(ZScale!=1.0f)WorldPoint.Z*=ZScale; - if(Params.CaveWarpStrength<=0.0f)return WorldPoint; - for(int32 Iteration=0;Iteration<8;++Iteration) - { - const FVector Mapped=VoxelCaveMorphology::ApplyCaveWarp(WorldPoint,Params,Seed); - const FVector Error=Target-Mapped; - if(!Error.ContainsNaN()&&VoxelMath::IsFinite(Error.X) - &&VoxelMath::IsFinite(Error.Y)&&VoxelMath::IsFinite(Error.Z)) - { - WorldPoint.X+=Error.X;WorldPoint.Y+=Error.Y;WorldPoint.Z+=Error.Z*ZScale; - } - } - return WorldPoint; - } - - void AddLegacyEllipsoidCapsule(const FVector& A,const FVector& B,float Radius,bool bCarve, - bool bCritical,bool bFloorClip,double FloorA,double FloorB) - { - const auto D=Sub(B,A);const double Length=std::sqrt(Dot(D,D)); - const double PreviousBlendRadius=ActiveBlendRadius;ActiveBlendRadius=0.0; - 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),0.0,0.0, - 0.0); - 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,0.0); - else ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB),0.0,0.0, - 0.0); - } - if(bCritical)for(size_t J=Before;J1.0e-4)) - { - AddEllipsoid(A,{RadiusA,RadiusA,RadiusA},{1,0,0},{0,1,0},{0,0,1},bCarve); - Primitives.back().bCritical=bCritical; - if(bFloorClip)ApplyFloorClip(Primitives.back(),std::min(FloorA,FloorB),0.0,0.0, - ActiveFloorBlendRadius); - return; - } - ExtendStart=std::clamp(ExtendStart,0.0,Length*0.4); - ExtendEnd=std::clamp(ExtendEnd,0.0,Length*0.4); - const double RadiusSlope=(static_cast(RadiusB)-RadiusA)/Length; - const float ExtendedRadiusA=static_cast(std::max(1.0,RadiusA-RadiusSlope*ExtendStart)); - const float ExtendedRadiusB=static_cast(std::max(1.0,RadiusB+RadiusSlope*ExtendEnd)); - const double Start[3]={A.X-D[0]*(ExtendStart/Length),A.Y-D[1]*(ExtendStart/Length),A.Z-D[2]*(ExtendStart/Length)}; - const double End[3]={B.X+D[0]*(ExtendEnd/Length),B.Y+D[1]*(ExtendEnd/Length),B.Z+D[2]*(ExtendEnd/Length)}; - FPrimitive P; - P.bCapsule=true;P.bCarve=bCarve;P.bCritical=bCritical; - P.CapsuleStartRadius=ExtendedRadiusA;P.CapsuleEndRadius=ExtendedRadiusB; - P.MinRadius=std::min(ExtendedRadiusA,ExtendedRadiusB);P.BlendRadius=ActiveBlendRadius; - const double R=std::max(ExtendedRadiusA,ExtendedRadiusB); - double AxisLengthSquared=0.0; - for(int32 Axis=0;Axis<3;++Axis) - { - P.CapsuleStartSub[Axis]=static_cast(std::llround(Start[Axis]*SubPerVoxel)); - P.CapsuleEndSub[Axis]=static_cast(std::llround(End[Axis]*SubPerVoxel)); - P.CapsuleStartWorld[Axis]=P.CapsuleStartSub[Axis]/static_cast(SubPerVoxel); - const double CapsuleEndWorld=P.CapsuleEndSub[Axis]/static_cast(SubPerVoxel); - P.CapsuleAxisDelta[Axis]=CapsuleEndWorld-P.CapsuleStartWorld[Axis]; - AxisLengthSquared+=P.CapsuleAxisDelta[Axis]*P.CapsuleAxisDelta[Axis]; - P.CenterSub[Axis]=static_cast(std::llround((Start[Axis]+End[Axis])*0.5*SubPerVoxel)); - P.Min[Axis]=static_cast(std::floor(std::min(Start[Axis],End[Axis])-R))-1; - P.Max[Axis]=static_cast(std::ceil(std::max(Start[Axis],End[Axis])+R))+1; - } - P.CapsuleAxisLengthSquared=AxisLengthSquared; - P.CapsuleAxisLength=std::sqrt(std::max(0.0,AxisLengthSquared)); - if(bFloorClip) - { - const double FloorSlope=(FloorB-FloorA)/Length; - const double ExtendedFloorA=FloorA-FloorSlope*ExtendStart; - const double ExtendedFloorB=FloorB+FloorSlope*ExtendEnd; - const double ExtendedD[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; - const double HorizontalSq=ExtendedD[0]*ExtendedD[0]+ExtendedD[1]*ExtendedD[1]; - if(HorizontalSq>1.0e-6) - ApplyFloorClip(P,(ExtendedFloorA+ExtendedFloorB)*0.5, - (ExtendedFloorB-ExtendedFloorA)*ExtendedD[0]/HorizontalSq, - (ExtendedFloorB-ExtendedFloorA)*ExtendedD[1]/HorizontalSq,ActiveFloorBlendRadius); - else ApplyFloorClip(P,std::min(ExtendedFloorA,ExtendedFloorB),0.0,0.0,ActiveFloorBlendRadius); - } - Primitives.push_back(P); - } - - static double ContinuousJoinExtension(float RadiusA,float RadiusB,double SegmentLength) - { - return std::min(std::max(0.0,SegmentLength*0.35), - JoinBlendRadius+0.5*static_cast(std::max(RadiusA,RadiusB))); - } - - static float CapsuleRadiusAt(const FPrimitive& P,const FVector& Point) - { - if(!P.bCapsule)return P.MinRadius; - const double Start[3]={P.CapsuleStartSub[0]/static_cast(SubPerVoxel), - P.CapsuleStartSub[1]/static_cast(SubPerVoxel), - P.CapsuleStartSub[2]/static_cast(SubPerVoxel)}; - const double End[3]={P.CapsuleEndSub[0]/static_cast(SubPerVoxel), - P.CapsuleEndSub[1]/static_cast(SubPerVoxel), - P.CapsuleEndSub[2]/static_cast(SubPerVoxel)}; - const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]}; - const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; - const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2]; - const double T=D2>1.0e-12?(V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2:0.0; - return CapsuleRadiusAtT(P,T,std::sqrt(std::max(0.0,D2))); - } - - static void FitCapsuleRadiiAtEndpoints(FPrimitive& P,const FVector& PointA,float RadiusA, - const FVector& PointB,float RadiusB) - { - const double Start[3]={P.CapsuleStartSub[0]/static_cast(SubPerVoxel), - P.CapsuleStartSub[1]/static_cast(SubPerVoxel), - P.CapsuleStartSub[2]/static_cast(SubPerVoxel)}; - const double End[3]={P.CapsuleEndSub[0]/static_cast(SubPerVoxel), - P.CapsuleEndSub[1]/static_cast(SubPerVoxel), - P.CapsuleEndSub[2]/static_cast(SubPerVoxel)}; - const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; - const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2]; - if(D2<=1.0e-12)return; - const auto Parameter=[&](const FVector& Point) - { - const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]}; - return (V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2; - }; - const double TA=Parameter(PointA),TB=Parameter(PointB); - if(std::abs(TB-TA)<=1.0e-9)return; - const double Slope=(RadiusB-RadiusA)/(TB-TA); - P.CapsuleStartRadius=static_cast(RadiusA-Slope*TA); - P.CapsuleEndRadius=static_cast(P.CapsuleStartRadius+Slope); - P.MinRadius=std::min(P.CapsuleStartRadius,P.CapsuleEndRadius); - } - - static void SetCapsuleRadiusProfile(FPrimitive& P,const FVector& PointA,float RadiusA, - double SlopeA,const FVector& PointB,float RadiusB,double SlopeB) - { - const double Start[3]={P.CapsuleStartSub[0]/static_cast(SubPerVoxel), - P.CapsuleStartSub[1]/static_cast(SubPerVoxel), - P.CapsuleStartSub[2]/static_cast(SubPerVoxel)}; - const double End[3]={P.CapsuleEndSub[0]/static_cast(SubPerVoxel), - P.CapsuleEndSub[1]/static_cast(SubPerVoxel), - P.CapsuleEndSub[2]/static_cast(SubPerVoxel)}; - const double D[3]={End[0]-Start[0],End[1]-Start[1],End[2]-Start[2]}; - const double D2=D[0]*D[0]+D[1]*D[1]+D[2]*D[2]; - if(D2<=1.0e-12)return; - const auto Parameter=[&](const FVector& Point) - { - const double V[3]={Point.X-Start[0],Point.Y-Start[1],Point.Z-Start[2]}; - return std::clamp((V[0]*D[0]+V[1]*D[1]+V[2]*D[2])/D2,0.0,1.0); - }; - P.CapsuleProfileStartT=Parameter(PointA); - P.CapsuleProfileEndT=Parameter(PointB); - P.CapsuleProfileStartRadius=RadiusA; - P.CapsuleProfileEndRadius=RadiusB; - P.CapsuleRadiusSlopeStart=SlopeA; - P.CapsuleRadiusSlopeEnd=SlopeB; - P.bSmoothCapsuleRadius=P.CapsuleProfileEndT-P.CapsuleProfileStartT>1.0e-6; - } - - static float SmoothedControlRadius(const TArray& Radii,int32 Index,float Fallback, - float Minimum) - { - const int32 Count=Radii.Num(); - const float Current=std::max(Minimum,Radii.IsValidIndex(Index)?Radii[Index]:Fallback); - if(Index<=0||Index+1>=Count)return Current; - const auto At=[&](int32 I) - {return std::max(Minimum,Radii[std::clamp(I,0,Count-1)]);}; - return (At(Index-4)+8.0f*At(Index-3)+28.0f*At(Index-2)+56.0f*At(Index-1) - +70.0f*Current+56.0f*At(Index+1)+28.0f*At(Index+2)+8.0f*At(Index+3) - +At(Index+4))*(1.0f/256.0f); - } - - static double ControlRadiusSlopeAt(const TArray& Points,const TArray& Radii, - int32 Index,float Fallback,float Minimum) - { - if(Points.Num()<2||Index<0||Index>=Points.Num())return 0.0; - const auto RadiusAt=[&](int32 I) - {return static_cast(SmoothedControlRadius(Radii,I,Fallback,Minimum));}; - const auto LengthAt=[&](int32 I) - { - const FVector& A=Points[I];const FVector& B=Points[I+1]; - const double DX=B.X-A.X,DY=B.Y-A.Y,DZ=B.Z-A.Z; - return std::sqrt(DX*DX+DY*DY+DZ*DZ); - }; - if(Index==0) - { - const double Length=LengthAt(0); - return Length>1.0e-9?(RadiusAt(1)-RadiusAt(0))/Length:0.0; - } - if(Index+1==Points.Num()) - { - const double Length=LengthAt(Index-1); - return Length>1.0e-9?(RadiusAt(Index)-RadiusAt(Index-1))/Length:0.0; - } - const double PrevLength=LengthAt(Index-1),NextLength=LengthAt(Index); - if(PrevLength<=1.0e-9||NextLength<=1.0e-9)return 0.0; - const double PrevSlope=(RadiusAt(Index)-RadiusAt(Index-1))/PrevLength; - const double NextSlope=(RadiusAt(Index+1)-RadiusAt(Index))/NextLength; - if(PrevSlope*NextSlope<=0.0)return 0.0; - const double W1=2.0*NextLength+PrevLength,W2=NextLength+2.0*PrevLength; - const double Harmonic=(W1+W2)/(W1/PrevSlope+W2/NextSlope); - const double Limit=3.0*std::min(std::abs(PrevSlope),std::abs(NextSlope)); - return std::copysign(std::min(std::abs(Harmonic),Limit),Harmonic); - } - - static double EvaluateFloorProfile(double A,double B,double TangentA,double TangentB, - double Length,double StartBlend,double EndBlend,double S) - { - const double Secant=(B-A)/Length; - if(S<0.0)return A+TangentA*S; - if(S>Length)return B+TangentB*(S-Length); - double Result=std::lerp(A,B,S/Length); - const auto Correction=[](double DeltaSlope,double Span,double T) - { - if(!(Span>0.0)||T<0.0||T>=1.0)return 0.0; - return DeltaSlope*Span*(T-2.0*T*T+T*T*T); - }; - if(S(X),static_cast(Y),0.0f,32,Seed^0x7F4A7C15u); - const float Mid=FStandaloneResearchTunnels::ValueNoise( - static_cast(X),static_cast(Y),0.0f,16,Seed^0x94D049BBu); - return 0.80*Broad+0.35*Mid; - } - - static std::vector BuildFloorTangents(const TArray& Points, - const std::vector& Floor,const std::vector& MarkedSegments) - { - const size_t Count=Floor.size();std::vector Tangent(Count,0.0); - if(Count<2||Count!=static_cast(Points.Num())||MarkedSegments.size()+1!=Count)return Tangent; - std::vector Length(Count-1,0.0),Slope(Count-1,0.0); - for(size_t I=0;I+1(I)];const FVector& B=Points[static_cast(I+1)]; - Length[I]=std::hypot(B.X-A.X,B.Y-A.Y); - if(Length[I]>1.0e-6)Slope[I]=(Floor[I+1]-Floor[I])/Length[I]; - } - for(size_t I=0;I0&&Length[I-1]>1.0e-6; - const bool bHasNext=I+11.0e-6; - if(I==0){Tangent[I]=MarkedSegments[0]?0.0:Slope[0];continue;} - if(I+1==Count){Tangent[I]=MarkedSegments[I-1]?0.0:Slope[I-1];continue;} - const bool bPrevMarked=MarkedSegments[I-1]!=0,bNextMarked=MarkedSegments[I]!=0; - if(bPrevMarked||bNextMarked) - { - if(!bPrevMarked&&bHasPrev)Tangent[I]=Slope[I-1]; - else if(!bNextMarked&&bHasNext)Tangent[I]=Slope[I]; - continue; - } - if(!bHasPrev||!bHasNext||Slope[I-1]*Slope[I]<=0.0)continue; - const double W0=2.0*Length[I]+Length[I-1],W1=Length[I]+2.0*Length[I-1]; - const double Denominator=W0/Slope[I-1]+W1/Slope[I]; - if(std::abs(Denominator)>1.0e-12) - Tangent[I]=(W0+W1)/Denominator; - } - return Tangent; - } - - static void SetCurvedFloorClip(FPrimitive& P,const FVector& A,const FVector& B, - double FloorA,double FloorB,double TangentA,double TangentB) - { - if(!P.bHasFloorClip)return; - const double DX=B.X-A.X,DY=B.Y-A.Y,Length=std::hypot(DX,DY); - if(Length<=1.0e-6)return; - P.bCurvedFloorClip=true;P.FloorCurveOriginX=A.X;P.FloorCurveOriginY=A.Y; - P.FloorCurveDeltaX=DX;P.FloorCurveDeltaY=DY;P.FloorCurveLength=Length; - P.FloorCurveStartZ=FloorA;P.FloorCurveEndZ=FloorB; - P.FloorCurveTangentStart=TangentA;P.FloorCurveTangentEnd=TangentB; - P.FloorCurveStartBlend=std::min(8.0,Length*0.35); - P.FloorCurveEndBlend=std::min(8.0,Length*0.35); - for(int32 I=1;I<64;++I) - { - const double T=I/64.0; - const double Curve=EvaluateFloorProfile(FloorA,FloorB,TangentA,TangentB, - Length,P.FloorCurveStartBlend,P.FloorCurveEndBlend,Length*T); - const double Linear=std::lerp(FloorA,FloorB,T); - P.FloorCurveMaxDeviation=std::max(P.FloorCurveMaxDeviation,std::abs(Curve-Linear)); - } - } - - double FloorAt(const FPrimitive& P,double X,double Y) const - { - double Base=0.0; - if(P.bCurvedFloorClip) - { - const double T=((X-P.FloorCurveOriginX)*P.FloorCurveDeltaX - +(Y-P.FloorCurveOriginY)*P.FloorCurveDeltaY) - /(P.FloorCurveLength*P.FloorCurveLength); - Base=EvaluateFloorProfile(P.FloorCurveStartZ,P.FloorCurveEndZ, - P.FloorCurveTangentStart,P.FloorCurveTangentEnd,P.FloorCurveLength, - P.FloorCurveStartBlend,P.FloorCurveEndBlend,T*P.FloorCurveLength); - } - else - Base=P.FloorCenterZ+P.FloorSlopeX*(X-P.CenterSub[0]/16.0) - +P.FloorSlopeY*(Y-P.CenterSub[1]/16.0); - return Base; - } - - double FloorSlopeAtEndpoint(const FPrimitive& P,bool bEnd) const - { - const double DX=P.bCurvedFloorClip?P.FloorCurveDeltaX - :P.CapsuleEndSub[0]-P.CapsuleStartSub[0]; - const double DY=P.bCurvedFloorClip?P.FloorCurveDeltaY - :P.CapsuleEndSub[1]-P.CapsuleStartSub[1]; - const double Length=P.bCurvedFloorClip?P.FloorCurveLength - :std::hypot(DX,DY)/SubPerVoxel; - if(Length<=1.0e-9)return 0.0; - const double OriginX=P.bCurvedFloorClip?P.FloorCurveOriginX - :P.CapsuleStartSub[0]/static_cast(SubPerVoxel); - const double OriginY=P.bCurvedFloorClip?P.FloorCurveOriginY - :P.CapsuleStartSub[1]/static_cast(SubPerVoxel); - const double X=OriginX+(bEnd?DX/(P.bCurvedFloorClip?1.0:SubPerVoxel):0.0); - const double Y=OriginY+(bEnd?DY/(P.bCurvedFloorClip?1.0:SubPerVoxel):0.0); - constexpr double Epsilon=0.125; - const double UX=DX/(Length*(P.bCurvedFloorClip?1.0:SubPerVoxel)); - const double UY=DY/(Length*(P.bCurvedFloorClip?1.0:SubPerVoxel)); - return (FloorAt(P,X+UX*Epsilon,Y+UY*Epsilon) - -FloorAt(P,X-UX*Epsilon,Y-UY*Epsilon))/(2.0*Epsilon); - } - - 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); - ActiveBlendRadius=0.0; - ActiveFloorBlendRadius=0.0; - 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(); - const FVector RoomCenter=ApproximateUnwarp(R.Center,Params, - static_cast(Owner->Seed)); - if(R.ShapeType==2) - AddCapsule(ApproximateUnwarp(R.ShapeA,Params,static_cast(Owner->Seed)), - ApproximateUnwarp(R.ShapeB,Params,static_cast(Owner->Seed)), - std::max(1.0f,R.ShapeR),true); - else if(R.ShapeType==1) - { - AddRoundedBox(RoomCenter,{std::abs(R.ShapeA.X),std::abs(R.ShapeA.Y),std::abs(R.ShapeA.Z)}, - std::max(0.0f,R.ShapeR),true); - } - else - AddEllipsoid(RoomCenter,{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); - const double FloorZ=ApproximateUnwarp( - FVector(R.Center.X,R.Center.Y,R.FloorCutZ-Relief),Params, - static_cast(Owner->Seed)).Z; - for(size_t I=RoomPrimitiveStart;I(Cache.RoomFloorJoins.Num()); - const double RoomBlendRadius=ActiveBlendRadius; - ActiveBlendRadius=JoinBlendRadius; - ActiveFloorBlendRadius=FloorClipBlendRadius; - for(const FCachedTunnel& T:Cache.Tunnels) - { - ++Stats.TunnelCount; - const bool bHasWorldChain=T.WorldControlPoints.Num()>1 - &&T.WorldControlRadii.Num()==T.WorldControlPoints.Num(); - const TArray& Points=bHasWorldChain?T.WorldControlPoints:T.ControlPoints; - const TArray& Radii=bHasWorldChain?T.WorldControlRadii:T.ControlRadii; - const TArray& FloorProfiles=bHasWorldChain - &&T.WorldFloorProfiles.Num()==Points.Num()-1?T.WorldFloorProfiles:T.FloorProfiles; - std::vector RawFloor(static_cast(Points.Num())); - std::vector MarkedFloorSegments(Points.Num()>0?static_cast(Points.Num()-1):0,0); - for(int32 I=0;I(I)]=static_cast(Points[I].Z - -SmoothedControlRadius(Radii,I,T.RadiusA,2.0f)); - if(FloorProfiles.Num()==Points.Num()-1) - { - for(int32 I=0;I+1(I)]=0.5f*(FloorProfiles[I-1].EndFloorZ+Profile.StartFloorZ); - RawFloor[static_cast(I+1)]=Profile.EndFloorZ; - MarkedFloorSegments[static_cast(I)]=Profile.bLedgeTransition?1:0; - } - } - for(int32 I=0;I(I-1)]; - const bool bNextUnmarked=I+1>=Points.Num() - ||!MarkedFloorSegments[static_cast(I)]; - if(bPreviousUnmarked&&bNextUnmarked) - RawFloor[static_cast(I)]+=static_cast(FloorSurfaceRelief( - Points[I].X,Points[I].Y,static_cast(Owner->Seed))); - } - const std::vector FloorTangents=BuildFloorTangents( - Points,RawFloor,MarkedFloorSegments); - for(int32 Join=1;Join+11.0e-6&&BL>1.0e-6) - { - const double Dot=(A.X*B.X+A.Y*B.Y+A.Z*B.Z)/(AL*BL); - const double Angle=std::acos(std::clamp(Dot,-1.0,1.0))*57.29577951308232; - if(Angle>Stats.MaxTunnelBendDegrees) - { - Stats.MaxTunnelBendDegrees=Angle;Stats.MaxTunnelBendVoxels=Points[Join]; - const double DX=A.X/AL+B.X/BL,DY=A.Y/AL+B.Y/BL,DZ=A.Z/AL+B.Z/BL; - const double DL=std::sqrt(DX*DX+DY*DY+DZ*DZ); - if(DL>1.0e-6)Stats.MaxTunnelBendDirection=FVector(DX/DL,DY/DL,DZ/DL); - } - } - const float RPrev=std::max(2.0f,Radii.IsValidIndex(Join-1)?Radii[Join-1]:T.RadiusA); - const float RHere=std::max(2.0f,Radii.IsValidIndex(Join)?Radii[Join]:T.RadiusA); - const float RNext=std::max(2.0f,Radii.IsValidIndex(Join+1)?Radii[Join+1]:T.RadiusB); - Stats.LegacySegmentRadiusStep=std::max(Stats.LegacySegmentRadiusStep, - static_cast(std::abs(std::max(RPrev,RHere)-std::max(RHere,RNext)))); - if(FloorProfiles.IsValidIndex(Join-1)&&FloorProfiles.IsValidIndex(Join)) - { - Stats.MaxCapsuleJoinFloorDelta=std::max(Stats.MaxCapsuleJoinFloorDelta, - static_cast(std::abs(FloorProfiles[Join-1].EndFloorZ - -FloorProfiles[Join].StartFloorZ))); - const FVector PrevD=Points[Join]-Points[Join-1]; - const FVector NextD=Points[Join+1]-Points[Join]; - const double PrevXY=std::sqrt(PrevD.X*PrevD.X+PrevD.Y*PrevD.Y); - const double NextXY=std::sqrt(NextD.X*NextD.X+NextD.Y*NextD.Y); - const double RawPrevSlope=PrevXY>1.0e-6 - ?(FloorProfiles[Join-1].EndFloorZ-FloorProfiles[Join-1].StartFloorZ)/PrevXY:0.0; - const double RawNextSlope=NextXY>1.0e-6 - ?(FloorProfiles[Join].EndFloorZ-FloorProfiles[Join].StartFloorZ)/NextXY:0.0; - const double RawSlopeDelta=std::abs(RawPrevSlope-RawNextSlope); - if(FloorProfiles[Join-1].bLedgeTransition||FloorProfiles[Join].bLedgeTransition) - { - ++Stats.MarkedFloorSlopeJoins; - Stats.MaxMarkedFloorSlopeDelta=std::max(Stats.MaxMarkedFloorSlopeDelta,RawSlopeDelta); - } - else - { - Stats.MaxRawUnmarkedFloorSlopeDelta=std::max(Stats.MaxRawUnmarkedFloorSlopeDelta,RawSlopeDelta); - } - } - ++Stats.CapsuleJoinCount; - } - if(Points.Num()>1) - { - const size_t SegmentPrimitiveStart=Primitives.size(); - for(int32 I=0;I+1(std::abs(R1-R0))); - const FVector Segment=Points[I+1]-Points[I]; - const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z); - const double Extension=ContinuousJoinExtension(R0,R1,SegmentLength); - const float ZA=RawFloor[static_cast(I)]; - const float ZB=RawFloor[static_cast(I+1)]; - AddTaperedCapsule(Points[I],Points[I+1],R0,R1,true,false,true,ZA,ZB, - I>0?Extension:0.0,I+2(I)]) - { - SetCurvedFloorClip(Primitives.back(),Points[I],Points[I+1],ZA,ZB, - FloorTangents[static_cast(I)],FloorTangents[static_cast(I+1)]); - ++Stats.CurvedFloorSegments; - Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation, - Primitives.back().FloorCurveMaxDeviation); - } - FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],R0,Points[I+1],R1); - SetCapsuleRadiusProfile(Primitives.back(),Points[I],R0,RadiusSlope0, - Points[I+1],R1,RadiusSlope1); - } - for(int32 Join=1;Join+1(Join-1)]; - const FPrimitive& Next=Primitives[SegmentPrimitiveStart+static_cast(Join)]; - const float PreviousEnd=CapsuleRadiusAt(Previous,Points[Join]); - const float NextStart=CapsuleRadiusAt(Next,Points[Join]); - Stats.MaxCapsuleJoinRadiusDelta=std::max(Stats.MaxCapsuleJoinRadiusDelta, - static_cast(std::abs(PreviousEnd-NextStart))); - Stats.MaxCapsuleJoinRadiusSlopeDelta=std::max( - Stats.MaxCapsuleJoinRadiusSlopeDelta, - std::abs(Previous.CapsuleRadiusSlopeEnd-Next.CapsuleRadiusSlopeStart)); - if(!MarkedFloorSegments[static_cast(Join-1)] - &&!MarkedFloorSegments[static_cast(Join)]) - { - const double TangentDelta=Previous.bCurvedFloorClip&&Next.bCurvedFloorClip - ?std::abs(Previous.FloorCurveTangentEnd-Next.FloorCurveTangentStart) - :std::abs(FloorSlopeAtEndpoint(Previous,true)-FloorSlopeAtEndpoint(Next,false)); - const double DirectionalSlopeChange=std::abs(FloorSlopeAtEndpoint(Previous,true) - -FloorSlopeAtEndpoint(Next,false)); - Stats.MaxCapsuleJoinFloorSlopeDelta=std::max( - Stats.MaxCapsuleJoinFloorSlopeDelta,TangentDelta); - Stats.MaxUnmarkedFloorSlopeDelta=std::max( - Stats.MaxUnmarkedFloorSlopeDelta,TangentDelta); - Stats.MaxUnmarkedFloorDirectionSlopeDelta=std::max( - Stats.MaxUnmarkedFloorDirectionSlopeDelta,DirectionalSlopeChange); - } - } - } - else AddCapsule(T.EndpointA,T.EndpointB,std::max(T.RadiusA,T.RadiusB),true); - } - ActiveBlendRadius=RoomBlendRadius; - ActiveFloorBlendRadius=0.0; - for(const FCachedPit& P:Cache.Pits) - AddCapsule(FVector(P.CenterX,P.CenterY,P.TopZ-P.Depth), - FVector(P.CenterX,P.CenterY,P.TopZ),std::max(2.0f,P.Radius),true); - for(const FCachedChimney& C:Cache.Chimneys) - AddCapsule(FVector(C.CenterX,C.CenterY,C.BottomZ), - FVector(C.CenterX,C.CenterY,C.BottomZ+C.Height),std::max(2.0f,C.Radius),true); - for(const FCachedColumn& C:Cache.Columns) - { - FVector Center(C.CenterX,C.CenterY,(LayerRanges[0].Lo+LayerRanges[0].Hi)*0.5); - const double HalfZ=std::max(8.0,(LayerRanges[0].Hi-LayerRanges[0].Lo)*0.48); - AddEllipsoid(Center,{std::max(2.0f,C.Radius),std::max(2.0f,C.Radius),HalfZ}, - {1,0,0},{0,1,0},{0,0,1},false); - } - } - const double PassageJoinBlend=ActiveBlendRadius;ActiveBlendRadius=JoinBlendRadius; - ActiveFloorBlendRadius=FloorClipBlendRadius; - for(const FVoxelPassage& P:Owner->Manager->GetPassages()) - { - ++Stats.PassageCount; - const TArray& Points=P.ControlPoints; - const TArray& Radii=P.ControlRadii; - const size_t PassagePrimitiveStart=Primitives.size(); - std::vector RawFloor(static_cast(Points.Num())); - std::vector MarkedFloorSegments(Points.Num()>0?static_cast(Points.Num()-1):0,0); - for(int32 I=0;I(I)]=static_cast(Points[I].Z - -SmoothedControlRadius(Radii,I,P.Radius,6.0f)); - if(P.NativeFloorProfileZ.Num()==Points.Num()) - for(int32 I=0;I(I)]=P.NativeFloorProfileZ[I]; - for(int32 I=0;I(I)]+=static_cast(FloorSurfaceRelief( - Points[I].X,Points[I].Y,static_cast(Owner->Seed))); - const std::vector FloorTangents=BuildFloorTangents( - Points,RawFloor,MarkedFloorSegments); - for(int32 Join=1;Join+1(std::abs(std::max(RPrev,RHere)-std::max(RHere,RNext)))); - if(P.NativeFloorProfileZ.IsValidIndex(Join-1)&&P.NativeFloorProfileZ.IsValidIndex(Join)) - { - const FVector PrevD=Points[Join]-Points[Join-1]; - const FVector NextD=Points[Join+1]-Points[Join]; - const double PrevXY=std::sqrt(PrevD.X*PrevD.X+PrevD.Y*PrevD.Y); - const double NextXY=std::sqrt(NextD.X*NextD.X+NextD.Y*NextD.Y); - const double PrevSlope=PrevXY>1.0e-6 - ?(P.NativeFloorProfileZ[Join]-P.NativeFloorProfileZ[Join-1])/PrevXY:0.0; - const double NextSlope=NextXY>1.0e-6 - ?(P.NativeFloorProfileZ[Join+1]-P.NativeFloorProfileZ[Join])/NextXY:0.0; - const double RawSlopeDelta=std::abs(PrevSlope-NextSlope); - Stats.MaxRawUnmarkedFloorSlopeDelta=std::max(Stats.MaxRawUnmarkedFloorSlopeDelta,RawSlopeDelta); - } - ++Stats.CapsuleJoinCount; - } - if(Points.Num()>1) - { - for(int32 I=0;I+1(std::abs(RadiusB-RadiusA))); - const FVector Segment=Points[I+1]-Points[I]; - const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z); - const float ZA=RawFloor[static_cast(I)]; - const float ZB=RawFloor[static_cast(I+1)]; - const double Extension=ContinuousJoinExtension(RadiusA,RadiusB,SegmentLength); - const double CoreExtension=ContinuousJoinExtension(std::min(2.5f,RadiusA*0.45f), - std::min(2.5f,RadiusB*0.45f),SegmentLength); - const double ExtendStart=I>0?Extension:0.0,ExtendEnd=I+20?CoreExtension:0.0,CoreExtendEnd=I+2(I)],FloorTangents[static_cast(I+1)]); - ++Stats.CurvedFloorSegments; - Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation, - Primitives.back().FloorCurveMaxDeviation); - FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],RadiusA,Points[I+1],RadiusB); - SetCapsuleRadiusProfile(Primitives.back(),Points[I],RadiusA,RadiusSlopeA, - Points[I+1],RadiusB,RadiusSlopeB); - const float CoreRadiusA=std::min(2.5f,RadiusA*0.45f); - const float CoreRadiusB=std::min(2.5f,RadiusB*0.45f); - const double CoreSlopeA=CoreRadiusA<2.5f-1.0e-4?RadiusSlopeA*0.45:0.0; - const double CoreSlopeB=CoreRadiusB<2.5f-1.0e-4?RadiusSlopeB*0.45:0.0; - AddTaperedCapsule(Points[I],Points[I+1],CoreRadiusA, - CoreRadiusB,true,true,true,ZA,ZB, - CoreExtendStart,CoreExtendEnd); - SetCurvedFloorClip(Primitives.back(),Points[I],Points[I+1],ZA,ZB, - FloorTangents[static_cast(I)],FloorTangents[static_cast(I+1)]); - ++Stats.CurvedFloorSegments; - Stats.MaxFloorCurveDeviation=std::max(Stats.MaxFloorCurveDeviation, - Primitives.back().FloorCurveMaxDeviation); - FitCapsuleRadiiAtEndpoints(Primitives.back(),Points[I],CoreRadiusA, - Points[I+1],CoreRadiusB); - SetCapsuleRadiusProfile(Primitives.back(),Points[I],CoreRadiusA,CoreSlopeA, - Points[I+1],CoreRadiusB,CoreSlopeB); - } - for(int32 Join=1;Join+1(Join-1)*2; - const size_t NextBase=PassagePrimitiveStart+static_cast(Join)*2; - for(size_t Tube=0;Tube<2;++Tube) - { - const FPrimitive& Previous=Primitives[PreviousBase+Tube]; - const FPrimitive& Next=Primitives[NextBase+Tube]; - const float PreviousEnd=CapsuleRadiusAt(Previous,Points[Join]); - const float NextStart=CapsuleRadiusAt(Next,Points[Join]); - Stats.MaxCapsuleJoinRadiusDelta=std::max(Stats.MaxCapsuleJoinRadiusDelta, - static_cast(std::abs(PreviousEnd-NextStart))); - Stats.MaxCapsuleJoinRadiusSlopeDelta=std::max( - Stats.MaxCapsuleJoinRadiusSlopeDelta, - std::abs(Previous.CapsuleRadiusSlopeEnd-Next.CapsuleRadiusSlopeStart)); - if(Tube==0) - { - const double TangentDelta=Previous.bCurvedFloorClip&&Next.bCurvedFloorClip - ?std::abs(Previous.FloorCurveTangentEnd-Next.FloorCurveTangentStart) - :std::abs(FloorSlopeAtEndpoint(Previous,true)-FloorSlopeAtEndpoint(Next,false)); - const double DirectionalSlopeChange=std::abs(FloorSlopeAtEndpoint(Previous,true) - -FloorSlopeAtEndpoint(Next,false)); - Stats.MaxCapsuleJoinFloorSlopeDelta=std::max( - Stats.MaxCapsuleJoinFloorSlopeDelta,TangentDelta); - Stats.MaxUnmarkedFloorSlopeDelta=std::max( - Stats.MaxUnmarkedFloorSlopeDelta,TangentDelta); - Stats.MaxUnmarkedFloorDirectionSlopeDelta=std::max( - Stats.MaxUnmarkedFloorDirectionSlopeDelta,DirectionalSlopeChange); - } - } - } - } - else - { - AddCapsule(P.UpperPoint,P.LowerPoint,std::max(6.0f,P.Radius),true,true); - Primitives.back().bPassageSurface=true; - } - AddLanding(P.UpperLanding,true); - AddLanding(P.LowerLanding,true); - } - ActiveBlendRadius=PassageJoinBlend;ActiveFloorBlendRadius=0.0; - if(Owner->Settings->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); - const FVector A(Last.first[0],Last.first[1],Last.first[2]); - const FVector B(Next.first[0],Next.first[1],Next.first[2]); - const FVector Segment=B-A; - const double SegmentLength=std::sqrt(Segment.X*Segment.X+Segment.Y*Segment.Y+Segment.Z*Segment.Z); - const double Extension=ContinuousJoinExtension(static_cast(Last.second), - static_cast(Next.second),SegmentLength); - AddTaperedCapsule(A,B,static_cast(Last.second),static_cast(Next.second), - true,false,false,0.0,0.0,I>1?Extension:0.0,I(Primitives.size()*BinsPerPrimitive,1u)); - if(!bBuildAxisBins) - { - CriticalAirBins.reserve(Primitives.size()/16+1); - } - if(bBuildAxisBins)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) - { - const FBinKey Key{X,Y,Z}; - SpatialBins[Key].push_back(Id); - if(bCriticalAir)CriticalAirBins[Key].push_back(Id); - } - } - } - - void BuildFarParentShapes() - { - constexpr int32 ParentPitch=2048; - FarConnectorSegments.clear(); - 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; - AddTaperedCapsule(A,B,64.0f,64.0f,true); - Primitives.back().BlendRadius=0.0; - FarConnectorSegments.push_back({A,B,64.0f}); - } - } - BuildFarConnectorSpatialBins(); - 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]); - } - - void BuildFarConnectorSpatialBins() - { - constexpr int32 BinSize=2048; - FarConnectorSpatialBins.clear(); - FarConnectorSpatialBins.reserve(FarConnectorSegments.size()); - for(uint32 Index=0;Index(std::floor( - (std::min(Connector.Start.X,Connector.End.X)-Connector.Radius)/BinSize)); - const int32 MaxX=static_cast(std::floor( - (std::max(Connector.Start.X,Connector.End.X)+Connector.Radius)/BinSize)); - const int32 MinY=static_cast(std::floor( - (std::min(Connector.Start.Y,Connector.End.Y)-Connector.Radius)/BinSize)); - const int32 MaxY=static_cast(std::floor( - (std::max(Connector.Start.Y,Connector.End.Y)+Connector.Radius)/BinSize)); - const int32 MinZ=static_cast(std::floor( - (std::min(Connector.Start.Z,Connector.End.Z)-Connector.Radius)/BinSize)); - const int32 MaxZ=static_cast(std::floor( - (std::max(Connector.Start.Z,Connector.End.Z)+Connector.Radius)/BinSize)); - for(int32 Z=MinZ;Z<=MaxZ;++Z)for(int32 Y=MinY;Y<=MaxY;++Y) - for(int32 X=MinX;X<=MaxX;++X) - FarConnectorSpatialBins[{X,Y,Z}].push_back(Index); - } - } - - 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.bCapsule) - { - const double Position[3]={static_cast(X),static_cast(Y),static_cast(Z)}; - double V[3],D[3];double D2=0.0,VD=0.0,V2=0.0; - for(int32 A=0;A<3;++A) - { - const double Start=P.CapsuleStartSub[A]/static_cast(SubPerVoxel); - const double End=P.CapsuleEndSub[A]/static_cast(SubPerVoxel); - V[A]=Position[A]-Start;D[A]=End-Start; - D2+=D[A]*D[A];VD+=V[A]*D[A];V2+=V[A]*V[A]; - } - const double R0=P.CapsuleStartRadius,R1=P.CapsuleEndRadius,DR=R1-R0; - const double QA=D2-DR*DR,QB=-2.0*(VD+R0*DR),QC=V2-R0*R0; - double Minimum=std::min(QC,QA+QB+QC); - if(QA>1.0e-12) - { - const double T=std::clamp(-QB/(2.0*QA),0.0,1.0); - Minimum=std::min(Minimum,(QA*T+QB)*T+QC); - } - bInside=Minimum<=0.0; - } - else 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=FloorAt(P,X,Y); - if(P.FloorBlendRadius<=0.0)return Z>=Floor; - return SmoothMaxDistance(ShapeDistance(P,X,Y,Z),static_cast(Floor-Z), - static_cast(P.FloorBlendRadius))<=0.0f; - } - return true; - } - - static float CapsuleRadiusAtT(const FPrimitive& P,double T,double AxisLength) - { - T=std::clamp(T,0.0,1.0); - if(!P.bSmoothCapsuleRadius) - return static_cast(P.CapsuleStartRadius - +(P.CapsuleEndRadius-P.CapsuleStartRadius)*T); - const double T0=P.CapsuleProfileStartT,T1=P.CapsuleProfileEndT; - if(T(P.CapsuleProfileStartRadius - +P.CapsuleRadiusSlopeStart*AxisLength*(T-T0)); - if(T>T1)return static_cast(P.CapsuleProfileEndRadius - +P.CapsuleRadiusSlopeEnd*AxisLength*(T-T1)); - const double SpanT=std::max(1.0e-9,T1-T0); - const double U=std::clamp((T-T0)/SpanT,0.0,1.0),U2=U*U,U3=U2*U; - const double SpanDistance=AxisLength*SpanT; - const double H00=2.0*U3-3.0*U2+1.0; - const double H10=U3-2.0*U2+U; - const double H01=-2.0*U3+3.0*U2; - const double H11=U3-U2; - return static_cast(H00*P.CapsuleProfileStartRadius - +H10*SpanDistance*P.CapsuleRadiusSlopeStart - +H01*P.CapsuleProfileEndRadius - +H11*SpanDistance*P.CapsuleRadiusSlopeEnd); - } - - float ShapeDistance(const FPrimitive& P,double X,double Y,double Z) const - { - if(P.bCapsule) - { - const double Position[3]={X,Y,Z}; - double V[3],D[3],D2=0.0,VD=0.0; - for(int32 A=0;A<3;++A) - { - V[A]=Position[A]-P.CapsuleStartWorld[A]; - D[A]=P.CapsuleAxisDelta[A]; - VD+=V[A]*D[A]; - } - D2=P.CapsuleAxisLengthSquared; - const double T=D2>1.0e-12?std::clamp(VD/D2,0.0,1.0):0.0; - double DistanceSquared=0.0; - for(int32 A=0;A<3;++A) - {const double E=V[A]-T*D[A];DistanceSquared+=E*E;} - const double Radius=CapsuleRadiusAtT(P,T,P.CapsuleAxisLength); - return static_cast(std::sqrt(DistanceSquared)-Radius); - } - 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&&!P.bCapsule) - { - 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&&(!P.bCurvedFloorClip||Axis==2)) - { - 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=FloorAt(P,X0,Y0); - const double SmoothAllowance=P.FloorBlendRadius>0.0?P.FloorBlendRadius/6.0:0.0; - const double Margin0=Z0-(Floor0-SmoothAllowance); - 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(P.bCapsule) - { - double V2=0.0,VD=0.0,D2=0.0; - for(int32 J=0;J<3;++J)if(J!=Axis) - { - const double Start=P.CapsuleStartSub[J]/static_cast(SubPerVoxel); - const double End=P.CapsuleEndSub[J]/static_cast(SubPerVoxel); - const double V=Fixed[J]-Start,D=End-Start; - V2+=V*V;VD+=V*D;D2+=D*D; - } - const double R0=P.CapsuleStartRadius,DR=P.CapsuleEndRadius-R0; - const double QA=D2-DR*DR,QB=-2.0*(VD+R0*DR),QC=V2-R0*R0; - const auto Value=[&](double T){return (QA*T+QB)*T+QC;}; - double TBest=Value(1.0)1.0e-12) - { - const double T=std::clamp(-QB/(2.0*QA),0.0,1.0); - if(Value(T)0.0)return false; - const double Start=P.CapsuleStartSub[Axis]/static_cast(SubPerVoxel); - const double End=P.CapsuleEndSub[Axis]/static_cast(SubPerVoxel); - Mid=std::clamp(static_cast(std::llround(Start+(End-Start)*TBest)),L,H); - } - if(!ShapeContains(P,Axis==0?Mid:Fixed[0],Axis==1?Mid:Fixed[1],Axis==2?Mid:Fixed[2])) - { - bool Found=false; - const int32 SearchLimit=P.bCapsule?std::min(H-L,8):2; - for(int32 Delta=1;Delta<=SearchLimit&&!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&&P.bCapsule) - { - for(int32 Sample=L;Sample<=H;Sample+=2) - if(ShapeContains(P,Axis==0?Sample:Fixed[0],Axis==1?Sample:Fixed[1],Axis==2?Sample:Fixed[2])) - {Mid=Sample;Found=true;break;} - if(!Found&&((H-L)&1) - &&ShapeContains(P,Axis==0?H:Fixed[0],Axis==1?H:Fixed[1],Axis==2?H:Fixed[2])) - {Mid=H;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(FParallelTiming& Timing) - { - const int32 NX=Size[0],NY=Size[1],NZ=Size[2]; - CandidateAxisWords[2].assign(static_cast(NX)*NY*WordsPerZ,0); - struct FIntervalScratch - { - std::vector Carve,Solid,ProtectedAir; - }; - std::array WorkerScratch; - for(int32 Worker=0;Worker(Worker)].Carve.reserve(24); - WorkerScratch[static_cast(Worker)].Solid.reserve(8); - WorkerScratch[static_cast(Worker)].ProtectedAir.reserve(4); - } - ParallelLines(static_cast(NX)*NY,[&](size_t Line,int32 Worker) - { - const int32 X=Min[0]+static_cast(Line%NX),Y=Min[1]+static_cast(Line/NX); - FIntervalScratch& Scratch=WorkerScratch[static_cast(Worker)]; - std::vector& Carve=Scratch.Carve; - std::vector& Solid=Scratch.Solid; - std::vector& ProtectedAir=Scratch.ProtectedAir; - Carve.clear();Solid.clear();ProtectedAir.clear(); - 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)) - { - if(!P.bCritical) - { - const size_t CandidateBase=Line*WordsPerZ; - const int32 CandidateLo=std::max(Min[2],R.Lo-DetailBand); - const int32 CandidateHi=std::min(Max[2],R.Lo+DetailBand); - const int32 CandidateLo2=std::max(Min[2],R.Hi-DetailBand); - const int32 CandidateHi2=std::min(Max[2],R.Hi+DetailBand); - FillBits(CandidateAxisWords[2],CandidateBase, - CandidateLo-Min[2],CandidateHi-Min[2],true); - FillBits(CandidateAxisWords[2],CandidateBase, - CandidateLo2-Min[2],CandidateHi2-Min[2],true); - } - 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); - },&Timing,nullptr,16); - } - - void BuildCandidateBands(FParallelTiming& AxisTiming,FParallelTiming& MergeTiming, - double& MergeSeconds) - { - const int32 N[3]={Size[0],Size[1],Size[2]}; - for(int32 Axis=0;Axis<3;++Axis) - { - if(Axis==2)continue; - 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.bCritical)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); - } - },&AxisTiming,nullptr,16); - } - const auto MergeStart=std::chrono::steady_clock::now(); - CandidateWords=std::move(CandidateAxisWords[2]); - for(int32 Axis=0;Axis<2;++Axis) - { - const int32 A=(Axis+1)%3,B=(Axis+2)%3; - const size_t Words=(static_cast(Size[Axis])+63u)/64u; - ParallelLines(static_cast(Size[A])*Size[B],[&,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]); - 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]); - std::atomic_ref(CandidateWords[Column*WordsPerZ+(Zi>>6)]) - .fetch_or(uint64{1}<<(Zi&63u),std::memory_order_relaxed); - } - } - },&MergeTiming,nullptr,16); - } - const int32 LayerLo=LayerRanges.empty()?1:LayerRanges[0].Lo; - const int32 LayerHi=LayerRanges.empty()?0:LayerRanges[0].Hi; - if(LayerLo<=LayerHi) - { - for(size_t Line=0;Line(Size[0])*Size[1];++Line) - { - const size_t Base=Line*WordsPerZ; - const int32 Lo0=std::max(Min[2],LayerLo-DetailBand)-Min[2]; - const int32 Hi0=std::min(Max[2],LayerLo+DetailBand)-Min[2]; - const int32 Lo1=std::max(Min[2],LayerHi-DetailBand)-Min[2]; - const int32 Hi1=std::min(Max[2],LayerHi+DetailBand)-Min[2]; - FillBits(CandidateWords,Base,Lo0,Hi0,true); - FillBits(CandidateWords,Base,Lo1,Hi1,true); - } - } - const double MergeElapsed=std::chrono::duration( - std::chrono::steady_clock::now()-MergeStart).count(); - MergeSeconds=std::max(0.0,MergeElapsed-MergeTiming.WallSeconds); - } - - void ApplyDetailBand(FParallelTiming& Timing) - { - using Clock=std::chrono::steady_clock; - const bool bProfile=ProfileDetailBand.load(std::memory_order_acquire); - std::array WorkerProfile{}; - std::array WorkerActiveSeconds{}; - std::atomic CandidateCount{0},ProxyCount{0},FlipCount{0}; - ParallelLines(static_cast(Size[0])*Size[1],[&](size_t Line,int32 Worker) - { - const int32 X=Min[0]+static_cast(Line%Size[0]); - const int32 Y=Min[1]+static_cast(Line/Size[0]); - FDetailNoiseLineContext NoiseLine(static_cast(X),static_cast(Y), - static_cast(Owner->Seed)); - const FLayerField* LineField=nullptr; - bool bLineFieldKnown=false; - int32 LineFieldLo=0,LineFieldHi=-1; - const auto FindLineLayerField=[&](double Z)->const FLayerField* - { - if(bLineFieldKnown&&LineField&&Z>=LineFieldLo&&Z<=LineFieldHi) - return LineField; - LineField=FindLayerField(Z); - bLineFieldKnown=LineField!=nullptr; - if(LineField){LineFieldLo=LineField->Lo;LineFieldHi=LineField->Hi;} - return LineField; - }; - const int32 QueryBinX=FloorDiv(X,ActiveSpatialBinSize); - const int32 QueryBinY=FloorDiv(Y,ActiveSpatialBinSize); - int32 CachedQueryBinZ=std::numeric_limits::min(); - const FBinItems* CachedQueryBinItems=nullptr; - FDetailWorkerProfile& Profile=WorkerProfile[static_cast(Worker)]; - if(bProfile)++Profile.Lines; - uint64 LocalCandidates=0,LocalProxies=0,LocalFlips=0; - const size_t WordBase=Line*WordsPerZ; - for(size_t W=0;W::min(); - const FBinItems* BatchBinItems=nullptr; - const auto ProcessBatch=[&]() - { - if(BatchCount==0)return; - FEffectiveRockSample Samples[8]; - int32 ProfileDistanceSamples=0; - if(bProfile) - { - for(int32 Lane=0;Lane0 - ?Clock::now():Clock::time_point{}; - EffectiveRockDistanceZ8(X,Y,BatchZ,BatchCount,BatchBinItems,Samples); - if(ProfileDistanceSamples>0) - { - Profile.DistanceSampleNanoseconds+=static_cast( - std::chrono::duration_cast( - Clock::now()-DistanceStart).count()); - Profile.DistanceSamples+=static_cast(BatchCount); - } - for(int32 Lane=0;Lane(Z-Min[2]); - ++LocalCandidates; - const FEffectiveRockSample& Sample=Samples[Lane]; - const float SDF=Sample.Distance; - if(std::abs(SDF)>DetailSupremum+0.125f)continue; - const float SurfaceSDF=Sample.SurfaceSDF; - const float CaveInfluence=Sample.CaveInfluence; - const float FloorInfluence=Sample.FloorInfluence; - const float PassageInfluence=Sample.PassageInfluence; - const bool bCriticalAir=Sample.bCriticalAir; - float DetailFade=DetailBandFade(SDF); - float MacroValue=0.0f; - bool bMacroKnown=false; - const FLayerField* DetailField=nullptr; - bool bDetailFieldKnown=false; - if(DetailVariant==EDetailVariant::Coherent - &&std::abs(SDF)>DetailBandInnerRadius) - { - MacroValue=NoiseLine.Macro.Sample(static_cast(Z)); - bMacroKnown=true; - DetailField=FindLineLayerField(Z); - bDetailFieldKnown=true; - DetailFade=DetailBandFade(SDF,LocalDetailBandEdge(MacroValue, - SurfaceSDF,CaveInfluence,FloorInfluence,DetailField)); - } - if(DetailFade<=0.0f&&bProfile)++Profile.LocalBandSkips; - const bool bTimeDetail=DetailFade>0.0f&&bProfile - &&((Profile.DetailEvaluations&63u)==0); - const auto DetailStart=bTimeDetail?Clock::now():Clock::time_point{}; - float Displaced=SDF; - if(DetailFade>0.0f) - { - if(bProfile)++Profile.DetailEvaluations; - Displaced+=DetailNoiseOnLine(X,Y,Z,Sample.SurfaceSDF, - CaveInfluence,FloorInfluence,PassageInfluence,NoiseLine,bTimeDetail?&Profile:nullptr, - DetailField,bDetailFieldKnown,bMacroKnown,MacroValue)*DetailFade; - } - if(bTimeDetail) - { - Profile.DetailSampleNanoseconds+=static_cast( - std::chrono::duration_cast( - Clock::now()-DetailStart).count()); - ++Profile.DetailSamples; - } - if(bCriticalAir)Displaced=std::min(Displaced,-1.0f/16.0f); - 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; - if(bProfile)++Profile.Flips; - } - 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}<<(Zi&63u); - ++LocalProxies; - } - BatchCount=0; - }; - while(Bits) - { - const uint32 Bit=std::countr_zero(Bits);Bits&=Bits-1; - const size_t Zi=W*64+Bit;if(Zi>=static_cast(Size[2]))continue; - const int32 Z=Min[2]+static_cast(Zi); - const int32 QueryBinZ=FloorDiv(Z,ActiveSpatialBinSize); - if(BatchCount>0&&QueryBinZ!=BatchQueryBinZ)ProcessBatch(); - if(QueryBinZ!=CachedQueryBinZ) - { - const auto Bin=SpatialBins.find({QueryBinX,QueryBinY,QueryBinZ}); - CachedQueryBinItems=Bin==SpatialBins.end()?nullptr:&Bin->second; - CachedQueryBinZ=QueryBinZ; - } - if(BatchCount==0)BatchQueryBinZ=QueryBinZ,BatchBinItems=CachedQueryBinItems; - BatchZ[BatchCount++]=Z; - if(BatchCount==8)ProcessBatch(); - } - ProcessBatch(); - if(OriginalBits!=ValidBits)CandidateWords[WordBase+W]=ValidBits; - } - CandidateCount.fetch_add(LocalCandidates,std::memory_order_relaxed); - ProxyCount.fetch_add(LocalProxies,std::memory_order_relaxed); - FlipCount.fetch_add(LocalFlips,std::memory_order_relaxed); - },&Timing,&WorkerActiveSeconds); - Stats.SurfaceCandidates=CandidateCount.load(std::memory_order_relaxed); - Stats.SurfaceProxyNodes=ProxyCount.load(std::memory_order_relaxed); - Stats.DetailFlips=FlipCount.load(std::memory_order_relaxed); - if(bProfile) - { - uint64 DistanceCalls=0,DetailCalls=0,DistanceSamples=0,DistanceSampleNs=0; - uint64 DetailSamples=0,DetailSampleNs=0,ReliefSampleNs=0,LocalBandSkips=0; - uint64 LayerLookupSampleNs=0,OwnerSampleNs=0,OwnerFieldCalls=0,OwnerActiveCalls=0; - uint64 OwnerCacheHits=0,OwnerCacheMisses=0; - for(int32 Worker=0;Worker(Worker)]; - DistanceCalls+=P.Candidates;DetailCalls+=P.DetailEvaluations; - DistanceSamples+=P.DistanceSamples;DistanceSampleNs+=P.DistanceSampleNanoseconds; - DetailSamples+=P.DetailSamples;DetailSampleNs+=P.DetailSampleNanoseconds; - LocalBandSkips+=P.LocalBandSkips; - ReliefSampleNs+=P.ReliefSampleNanoseconds; - LayerLookupSampleNs+=P.LayerLookupSampleNanoseconds; - OwnerSampleNs+=P.OwnerRoughnessSampleNanoseconds; - OwnerFieldCalls+=P.OwnerRoughnessFieldCalls; - OwnerActiveCalls+=P.OwnerRoughnessActiveCalls; - OwnerCacheHits+=P.OwnerNoiseCacheHits; - OwnerCacheMisses+=P.OwnerNoiseCacheMisses; - } - const double DistanceAverageNs=DistanceSamples?static_cast(DistanceSampleNs)/DistanceSamples:0.0; - const double DetailAverageNs=DetailSamples?static_cast(DetailSampleNs)/DetailSamples:0.0; - const double CandidateWorkerNs=Stats.SurfaceCandidates - ?Timing.WorkerSeconds*1.0e9/static_cast(Stats.SurfaceCandidates):0.0; - std::printf("[DetailBandProfile] candidates=%llu distance_evaluations=%llu detail_noise_evaluations=%llu proxies=%llu flips=%llu candidate_worker_ns=%.1f distance_sample_ns=%.1f distance_estimated_worker_ms=%.1f detail_sample_ns=%.1f detail_estimated_worker_ms=%.1f worker_sum_ms=%.3f wall_ms=%.3f sampling=(distance_1_of_256,detail_1_of_64)\n", - static_cast(Stats.SurfaceCandidates), - static_cast(DistanceCalls),static_cast(DetailCalls), - static_cast(Stats.SurfaceProxyNodes),static_cast(Stats.DetailFlips), - CandidateWorkerNs,DistanceAverageNs,DistanceAverageNs*static_cast(DistanceCalls)/1.0e6, - DetailAverageNs,DetailAverageNs*static_cast(DetailCalls)/1.0e6, - Timing.WorkerSeconds*1000.0,Timing.WallSeconds*1000.0); - std::printf("[DetailBandSubprofile] relief_sample_ns=%.1f relief_estimated_worker_ms=%.1f layer_lookup_sample_ns=%.1f layer_lookup_estimated_worker_ms=%.1f owner_sample_ns=%.1f owner_estimated_worker_ms=%.1f owner_sampled_field_calls=%llu owner_sampled_noise_calls=%llu owner_sampled_core_cache_hits=%llu owner_sampled_core_cache_misses=%llu local_band_skipped=%llu\n", - DetailSamples?static_cast(ReliefSampleNs)/DetailSamples:0.0, - DetailSamples?static_cast(ReliefSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, - DetailSamples?static_cast(LayerLookupSampleNs)/DetailSamples:0.0, - DetailSamples?static_cast(LayerLookupSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, - DetailSamples?static_cast(OwnerSampleNs)/DetailSamples:0.0, - DetailSamples?static_cast(OwnerSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, - static_cast(OwnerFieldCalls), - static_cast(OwnerActiveCalls), - static_cast(OwnerCacheHits), - static_cast(OwnerCacheMisses), - static_cast(LocalBandSkips)); - for(int32 Worker=0;Worker(Worker)]; - std::printf("[DetailBandWorker] id=%d active_ms=%.3f lines=%llu candidates=%llu detail_evaluations=%llu flips=%llu\n", - Worker,WorkerActiveSeconds[static_cast(Worker)]*1000.0, - static_cast(P.Lines),static_cast(P.Candidates), - static_cast(P.DetailEvaluations),static_cast(P.Flips)); - } - } - } - - void StoreSurfaceDistance(size_t Node,float Density) - { - int32 Q=static_cast(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=CriticalAirBins.find(PointBin(X,Y,Z)); - if(It==CriticalAirBins.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; - } - - float SolidOverlayDistance(double X,double Y,double Z) const - { - const auto It=SpatialBins.find(PointBin(X,Y,Z)); - if(It==SpatialBins.end())return -100000.0f; - float Best=-100000.0f; - 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; - Best=std::max(Best,-ShapeDistance(P,X,Y,Z)); - } - return Best; - } - - bool IsInsideCriticalAir(double X,double Y,double Z) const - { - const auto It=CriticalAirBins.find(PointBin(X,Y,Z)); - if(It==CriticalAirBins.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; - } - - float CaveDistance(double X,double Y,double Z,bool* OutFloorSurface=nullptr, - float* OutFloorInfluence=nullptr) const - { - const auto It=SpatialBins.find(PointBin(X,Y,Z)); - if(It==SpatialBins.end()) - { - if(OutFloorSurface)*OutFloorSurface=false; - if(OutFloorInfluence)*OutFloorInfluence=0.0f; - return 100000.0f; - } - float Best=100000.0f; - float BestFloorInfluence=0.0f; - for(uint32 Id:It->second) - { - 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); - float PrimitiveFloorInfluence=0.0f; - if(P.bHasFloorClip) - { - const double Floor=FloorAt(P,X,Y); - const float FloorT=(static_cast(std::abs(Z-Floor))-static_cast(P.FloorBlendRadius)) - /static_cast(std::max(DetailBand*2,1)); - PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT); - Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z), - static_cast(P.FloorBlendRadius)); - } - const float MetadataWidth=static_cast(std::max(P.BlendRadius,6.0)); - const float PrimitiveWeight=SmoothUnit(0.5f+(Best-Distance)/(2.0f*MetadataWidth)); - const float Combined=SmoothMinDistance(Best,Distance,static_cast(P.BlendRadius)); - BestFloorInfluence=std::lerp(BestFloorInfluence,PrimitiveFloorInfluence,PrimitiveWeight); - Best=Combined; - } - if(OutFloorInfluence)*OutFloorInfluence=BestFloorInfluence; - if(OutFloorSurface)*OutFloorSurface=BestFloorInfluence>=0.5f; - return Best; - } - - float StrataDistance(double Z) const - { - if(LayerRanges.empty())return -100000.0f; - return static_cast(std::min(Z-LayerRanges[0].Lo,LayerRanges[0].Hi-Z)); - } - - const FLayerField* FindLayerField(double Z) const - { - for(const FLayerField& Field:LayerFields) - if(Z>=Field.Lo&&Z<=Field.Hi)return &Field; - return nullptr; - } - - float EffectiveRockDistance(double X,double Y,double Z, - ESurfaceFamily* OutFamily=nullptr, - float* OutSurfaceSDF=nullptr, - bool* OutFloorSurface=nullptr, - float* OutCaveInfluence=nullptr, - float* OutFloorInfluence=nullptr, - bool* OutCriticalAir=nullptr, - const FBinItems* KnownBinItems=nullptr, - bool bKnownBin=false, - float* OutPassageInfluence=nullptr) const - { - const float Base=StrataDistance(Z); - float Cave=100000.0f; - float Overlay=-100000.0f; - float CaveFloorInfluence=0.0f; - float CavePassageInfluence=0.0f; - bool bCriticalAir=false; - const FBinItems* BinItems=KnownBinItems; - if(!bKnownBin) - { - const auto Bin=SpatialBins.find(PointBin(X,Y,Z)); - if(Bin!=SpatialBins.end())BinItems=&Bin->second; - } - if(BinItems) - { - for(uint32 Id:*BinItems) - { - const FPrimitive& P=Primitives[Id]; - if(P.bCarve) - { - if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] - &&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P, - static_cast(X),static_cast(Y),static_cast(Z))) - bCriticalAir=true; - if(XP.Max[0]+DetailBand - ||YP.Max[1]+DetailBand - ||ZP.Max[2]+DetailBand)continue; - float Distance=ShapeDistance(P,X,Y,Z); - float PrimitiveFloorInfluence=0.0f; - if(P.bHasFloorClip) - { - const double Floor=FloorAt(P,X,Y); - const float FloorT=(static_cast(std::abs(Z-Floor)) - -static_cast(P.FloorBlendRadius)) - /static_cast(std::max(DetailBand*2,1)); - PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT); - Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z), - static_cast(P.FloorBlendRadius)); - } - const float MetadataWidth=static_cast(std::max(P.BlendRadius,6.0)); - const float PrimitiveWeight=SmoothUnit(0.5f+(Cave-Distance)/(2.0f*MetadataWidth)); - const float Combined=SmoothMinDistance(Cave,Distance,static_cast(P.BlendRadius)); - CaveFloorInfluence=std::lerp(CaveFloorInfluence,PrimitiveFloorInfluence,PrimitiveWeight); - if(OutPassageInfluence&&(CavePassageInfluence>0.0f||P.bPassageSurface)) - CavePassageInfluence=std::lerp(CavePassageInfluence, - P.bPassageSurface?1.0f:0.0f,PrimitiveWeight); - Cave=Combined; - } - else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] - &&Z>=P.Min[2]&&Z<=P.Max[2]) - Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z)); - } - } - if(OutCriticalAir)*OutCriticalAir=bCriticalAir; - const float CarveOrOverlay=std::max(Cave,Overlay); - const ESurfaceFamily Family=Base<=CarveOrOverlay?ESurfaceFamily::Strata: - (Cave>=Overlay?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay); - constexpr float FamilyBlendWidth=6.0f; - const float CaveVsOverlay=SmoothUnit(0.5f+(Cave-Overlay)/(2.0f*FamilyBlendWidth)); - const float CarveInfluence=SmoothUnit(0.5f+(Base-CarveOrOverlay)/(2.0f*FamilyBlendWidth)); - const float CaveInfluence=CarveInfluence*CaveVsOverlay; - const float FloorInfluence=CaveInfluence*CaveFloorInfluence; - const float PassageInfluence=CaveInfluence*CavePassageInfluence; - const float RockDistance=SmoothMinDistance(Base,CarveOrOverlay,StrataCaveBlendRadius); - if(OutFamily) - *OutFamily=Family; - if(OutSurfaceSDF) - *OutSurfaceSDF=Family==ESurfaceFamily::Strata?Base: - (Family==ESurfaceFamily::Cave?Cave:Overlay); - if(OutCaveInfluence)*OutCaveInfluence=CaveInfluence; - if(OutFloorInfluence)*OutFloorInfluence=FloorInfluence; - if(OutPassageInfluence)*OutPassageInfluence=PassageInfluence; - if(OutFloorSurface)*OutFloorSurface=FloorInfluence>=0.5f; - return RockDistance; - } - - struct FEffectiveRockSample - { - float Distance=0.0f; - float SurfaceSDF=0.0f; - float CaveInfluence=0.0f; - float FloorInfluence=0.0f; - float PassageInfluence=0.0f; - ESurfaceFamily Family=ESurfaceFamily::Strata; - bool bFloorSurface=false; - bool bCriticalAir=false; - }; - - void ShapeDistanceZ8(const FPrimitive& P,int32 X,int32 Y,const int32 Z[8],int32 Count, - float OutDistance[8]) const - { - const __m256d Zero=_mm256_setzero_pd(); - const __m256d One=_mm256_set1_pd(1.0); - const __m256d SignMask=_mm256_set1_pd(-0.0); - for(int32 Base=0;Base(Z[Base+std::min(Lane,Lanes-1)]); - const __m256d VZ=_mm256_setr_pd(ZLane[0],ZLane[1],ZLane[2],ZLane[3]); - __m256d Distance; - if(P.bCapsule) - { - const double VX=static_cast(X)-P.CapsuleStartWorld[0]; - const double VY=static_cast(Y)-P.CapsuleStartWorld[1]; - const __m256d VXv=_mm256_set1_pd(VX); - const __m256d VYv=_mm256_set1_pd(VY); - const __m256d VZv=_mm256_sub_pd(VZ,_mm256_set1_pd(P.CapsuleStartWorld[2])); - __m256d VD=_mm256_mul_pd(VXv,_mm256_set1_pd(P.CapsuleAxisDelta[0])); - VD=_mm256_add_pd(VD,_mm256_mul_pd(VYv,_mm256_set1_pd(P.CapsuleAxisDelta[1]))); - VD=_mm256_add_pd(VD,_mm256_mul_pd(VZv,_mm256_set1_pd(P.CapsuleAxisDelta[2]))); - const __m256d T=P.CapsuleAxisLengthSquared>1.0e-12 - ?_mm256_max_pd(Zero,_mm256_min_pd(One, - _mm256_div_pd(VD,_mm256_set1_pd(P.CapsuleAxisLengthSquared)))) - :Zero; - const __m256d EX=_mm256_sub_pd(VXv, - _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[0]))); - const __m256d EY=_mm256_sub_pd(VYv, - _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[1]))); - const __m256d EZ=_mm256_sub_pd(VZv, - _mm256_mul_pd(T,_mm256_set1_pd(P.CapsuleAxisDelta[2]))); - __m256d DistanceSquared=_mm256_mul_pd(EX,EX); - DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EY,EY)); - DistanceSquared=_mm256_add_pd(DistanceSquared,_mm256_mul_pd(EZ,EZ)); - const __m256d Root=_mm256_sqrt_pd(DistanceSquared); - double TValues[4],RootValues[4]; - _mm256_storeu_pd(TValues,T); - _mm256_storeu_pd(RootValues,Root); - for(int32 Lane=0;Lane(RootValues[Lane] - -CapsuleRadiusAtT(P,TValues[Lane],P.CapsuleAxisLength)); - continue; - } - if(P.bRoundedBox) - { - const double DX=std::abs(static_cast(X)-P.CenterSub[0]/16.0) - -P.RoundedHalfExtent[0]; - const double DY=std::abs(static_cast(Y)-P.CenterSub[1]/16.0) - -P.RoundedHalfExtent[1]; - const double DZCenter=P.CenterSub[2]/16.0; - const __m256d DZv=_mm256_sub_pd(_mm256_andnot_pd(SignMask, - _mm256_sub_pd(VZ,_mm256_set1_pd(DZCenter))), - _mm256_set1_pd(P.RoundedHalfExtent[2])); - const __m256d DXv=_mm256_set1_pd(DX),DYv=_mm256_set1_pd(DY); - const __m256d Ox=_mm256_max_pd(DXv,Zero),Oy=_mm256_max_pd(DYv,Zero); - const __m256d Oz=_mm256_max_pd(DZv,Zero); - __m256d OutsideSquared=_mm256_mul_pd(Ox,Ox); - OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oy,Oy)); - OutsideSquared=_mm256_add_pd(OutsideSquared,_mm256_mul_pd(Oz,Oz)); - const __m256d MaxXY=_mm256_max_pd(DXv,DYv); - const __m256d MaxQ=_mm256_max_pd(MaxXY,DZv); - const __m256d Inside=_mm256_min_pd(MaxQ,Zero); - Distance=_mm256_sub_pd(_mm256_add_pd(_mm256_sqrt_pd(OutsideSquared),Inside), - _mm256_set1_pd(P.Rounding)); - } - else - { - const __m256d D[3]={ - _mm256_set1_pd(static_cast(X)-P.CenterSub[0]/16.0), - _mm256_set1_pd(static_cast(Y)-P.CenterSub[1]/16.0), - _mm256_sub_pd(VZ,_mm256_set1_pd(P.CenterSub[2]/16.0))}; - __m256d Q=_mm256_setzero_pd(); - for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J) - { - const double Coefficient=static_cast(P.Q[I][J])/QScale; - __m256d Term=_mm256_mul_pd(_mm256_set1_pd(Coefficient),D[I]); - Term=_mm256_mul_pd(Term,D[J]); - Term=_mm256_mul_pd(Term,_mm256_set1_pd(256.0)); - Q=_mm256_add_pd(Q,Term); - } - Distance=_mm256_mul_pd(_mm256_sub_pd( - _mm256_sqrt_pd(_mm256_max_pd(Q,Zero)),_mm256_set1_pd(1.0)), - _mm256_set1_pd(P.MinRadius)); - } - double Values[4]; - _mm256_storeu_pd(Values,Distance); - for(int32 Lane=0;Lane(Values[Lane]); - } - } - - void EffectiveRockDistanceZ8(int32 X,int32 Y,const int32 Z[8],int32 Count, - const FBinItems* BinItems,FEffectiveRockSample Out[8]) const - { - float Base[8],Cave[8],Overlay[8],CaveFloorInfluence[8],CavePassageInfluence[8]; - bool CriticalAir[8]{}; - for(int32 Lane=0;Lane=P.Min[0]&&X<=P.Max[0] - &&Y>=P.Min[1]&&Y<=P.Max[1] - &&ZValue>=P.Min[2]&&ZValue<=P.Max[2] - &&ShapeContains(P,X,Y,ZValue)) - CriticalAir[Lane]=true; - if(XP.Max[0]+DetailBand - ||YP.Max[1]+DetailBand - ||ZValueP.Max[2]+DetailBand) - continue; - Eligible[Lane]=true; - AnyEligible=true; - } - } - else - { - for(int32 Lane=0;Lane=P.Min[0]&&X<=P.Max[0] - &&Y>=P.Min[1]&&Y<=P.Max[1] - &&ZValue>=P.Min[2]&&ZValue<=P.Max[2]; - AnyEligible=AnyEligible||Eligible[Lane]; - } - } - if(!AnyEligible)continue; - - float Distances[8]; - ShapeDistanceZ8(P,X,Y,Z,Count,Distances); - if(P.bCarve) - { - const double Floor=P.bHasFloorClip?FloorAt(P,X,Y):0.0; - const float MetadataWidth=static_cast(std::max(P.BlendRadius,6.0)); - for(int32 Lane=0;Lane(std::abs(Z[Lane]-Floor)) - -static_cast(P.FloorBlendRadius)) - /static_cast(std::max(DetailBand*2,1)); - PrimitiveFloorInfluence=1.0f-SmoothUnit(FloorT); - Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z[Lane]), - static_cast(P.FloorBlendRadius)); - } - const float PrimitiveWeight=SmoothUnit(0.5f - +(Cave[Lane]-Distance)/(2.0f*MetadataWidth)); - const float Combined=SmoothMinDistance(Cave[Lane],Distance, - static_cast(P.BlendRadius)); - CaveFloorInfluence[Lane]=std::lerp(CaveFloorInfluence[Lane], - PrimitiveFloorInfluence,PrimitiveWeight); - if(CavePassageInfluence[Lane]>0.0f||P.bPassageSurface) - CavePassageInfluence[Lane]=std::lerp(CavePassageInfluence[Lane], - P.bPassageSurface?1.0f:0.0f,PrimitiveWeight); - Cave[Lane]=Combined; - } - } - else - { - for(int32 Lane=0;Lane=Overlay[Lane]?ESurfaceFamily::Cave:ESurfaceFamily::SolidOverlay); - constexpr float FamilyBlendWidth=6.0f; - const float CaveVsOverlay=SmoothUnit(0.5f - +(Cave[Lane]-Overlay[Lane])/(2.0f*FamilyBlendWidth)); - const float CarveInfluence=SmoothUnit(0.5f - +(Base[Lane]-CarveOrOverlay)/(2.0f*FamilyBlendWidth)); - const float CaveInfluence=CarveInfluence*CaveVsOverlay; - const float FloorInfluence=CaveInfluence*CaveFloorInfluence[Lane]; - const float PassageInfluence=CaveInfluence*CavePassageInfluence[Lane]; - Out[Lane].Distance=SmoothMinDistance(Base[Lane],CarveOrOverlay,StrataCaveBlendRadius); - Out[Lane].Family=Family; - Out[Lane].SurfaceSDF=Family==ESurfaceFamily::Strata?Base[Lane]: - (Family==ESurfaceFamily::Cave?Cave[Lane]:Overlay[Lane]); - Out[Lane].CaveInfluence=CaveInfluence; - Out[Lane].FloorInfluence=FloorInfluence; - Out[Lane].PassageInfluence=PassageInfluence; - Out[Lane].bFloorSurface=FloorInfluence>=0.5f; - Out[Lane].bCriticalAir=CriticalAir[Lane]; - } - } - - float CoarseRockDistance(double X,double Y,double Z,bool* OutCriticalAir) const - { - const float Base=StrataDistance(Z); - float Cave=100000.0f,Overlay=-100000.0f; - bool bCriticalAir=false; - struct FCoarseBinCache - { - const FStandaloneShapeWorld* Owner=nullptr; - FBinKey Key{}; - const FBinItems* Items=nullptr; - bool Valid=false; - }; - static thread_local FCoarseBinCache LastQuery; - const FBinKey QueryKey=PointBin(X,Y,Z); - if(!LastQuery.Valid||LastQuery.Owner!=this||!(LastQuery.Key==QueryKey)) - { - const auto Bin=SpatialBins.find(QueryKey); - LastQuery.Owner=this; - LastQuery.Key=QueryKey; - LastQuery.Items=Bin==SpatialBins.end()?nullptr:&Bin->second; - LastQuery.Valid=true; - } - if(LastQuery.Items) - { - for(uint32 Id:*LastQuery.Items) - { - const FPrimitive& P=Primitives[Id]; - if(P.bCarve) - { - if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] - &&Z>=P.Min[2]&&Z<=P.Max[2]&&ShapeContains(P, - static_cast(X),static_cast(Y),static_cast(Z))) - bCriticalAir=true; - 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=FloorAt(P,X,Y); - Distance=SmoothMaxDistance(Distance,static_cast(Floor-Z), - static_cast(P.FloorBlendRadius)); - } - Cave=SmoothMinDistance(Cave,Distance,static_cast(P.BlendRadius)); - } - else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1] - &&Z>=P.Min[2]&&Z<=P.Max[2]) - Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z)); - } - } - if(OutCriticalAir)*OutCriticalAir=bCriticalAir; - return SmoothMinDistance(Base,std::max(Cave,Overlay),StrataCaveBlendRadius); - } - - struct FOwnerNoiseLineContext - { - const FLayerField* Field=nullptr; - int32 GridZ0=MIN_int32; - float LowerCore=0.0f,UpperCore=0.0f; - }; - - float OwnerCaveRoughnessCore(double X,double Y,double Z, - const FStrateGenerationParams& Params) const - { - const uint32 Seed=static_cast(Owner->Seed); - const float RF=Params.RoughnessFrequency; - const float NoiseX=static_cast(X); - const float NoiseY=static_cast(Y); - const float EffectiveZ=(Params.VerticalScale>0.0f&&Params.VerticalScale!=1.0f) - ?static_cast(Z)/Params.VerticalScale:static_cast(Z); - FVector3f Main(NoiseX*RF+VoxelHash::SeedOffset(Seed,11.3f), - NoiseY*RF+VoxelHash::SeedOffset(Seed,13.7f), - EffectiveZ*RF+VoxelHash::SeedOffset(Seed,17.1f)); - FVector3f Fine(NoiseX*RF*3.0f+VoxelHash::SeedOffset(Seed,19.1f)+2000.0f, - NoiseY*RF*3.0f+VoxelHash::SeedOffset(Seed,23.7f)+2500.0f, - EffectiveZ*RF*3.0f+VoxelHash::SeedOffset(Seed,29.3f)+3000.0f); - if(Params.DomainWarpStrength>0.0f) - { - const float WF=Params.DomainWarpFrequency,WS=Params.DomainWarpStrength; - float WX=0.0f,WY=0.0f,WZ=0.0f; - VoxelNoise::Perlin3D_x3( - NoiseX*WF+VoxelHash::SeedOffset(Seed,5.2f), - NoiseY*WF+VoxelHash::SeedOffset(Seed,1.3f), - EffectiveZ*WF+VoxelHash::SeedOffset(Seed,9.7f), - NoiseX*WF+100.0f+VoxelHash::SeedOffset(Seed,7.7f), - NoiseY*WF+200.0f+VoxelHash::SeedOffset(Seed,3.1f), - EffectiveZ*WF+300.0f, - NoiseX*WF+400.0f, - NoiseY*WF+500.0f+VoxelHash::SeedOffset(Seed,11.9f), - EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(Seed,13.3f),WX,WY,WZ); - WX*=VOXEL_NOISE_SCALE*WS;WY*=VOXEL_NOISE_SCALE*WS;WZ*=VOXEL_NOISE_SCALE*WS; - Main+=FVector3f(WX,WY,WZ);Fine+=FVector3f(WX,WY,WZ); - } - float Rough=0.0f,FineValue=0.0f; - switch(Params.RoughnessNoiseType) - { - case EVoxelNoiseType::Ridged: - { - float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; - VoxelNoise::FractalPair(Main.X,Main.Y,Main.Z,3, - Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged); - Rough=MainRidged;FineValue=FineRidged; - break; - } - case EVoxelNoiseType::Mixed: - { - float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; - VoxelNoise::FractalPair(Main.X,Main.Y,Main.Z,3, - Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged); - Rough=MainFBM*0.5f+MainRidged*0.5f; - FineValue=FineFBM*0.5f+FineRidged*0.5f; - break; - } - case EVoxelNoiseType::Cellular: - Rough=VoxelNoise::Cellular3D(Main);FineValue=VoxelNoise::Cellular3D(Fine);break; - case EVoxelNoiseType::FBM: - default: - { - float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; - VoxelNoise::FractalPair(Main.X,Main.Y,Main.Z,3, - Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged); - Rough=MainFBM;FineValue=FineFBM; - break; - } - } - return (Rough+FineValue*0.4f)*VOXEL_NOISE_SCALE*Params.SurfaceRoughness; - } - - float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF, - const FLayerField& Field,float CaveInfluence, - FOwnerNoiseLineContext& LineContext, - FDetailWorkerProfile* Profile=nullptr, - int32 AnchorStep=8) const - { - const FStrateGenerationParams& Params=Field.Params; - const float Depth=Params.SurfaceRoughness*2.0f; - if(!(Depth>0.0f)||std::abs(SurfaceSDF)>=Depth||!(Params.RoughnessFrequency>0.0f))return 0.0f; - if(Profile)++Profile->OwnerRoughnessActiveCalls; - const int32 ZStep=std::max(1,AnchorStep); - const int32 GridZ0=static_cast(std::floor(Z/static_cast(ZStep)))*ZStep; - const int32 SampleZ0=std::max(GridZ0,Field.Lo); - const int32 SampleZ1=std::min(GridZ0+ZStep,Field.Hi); - float Total=0.0f; - if(ZStep==1) - Total=OwnerCaveRoughnessCore(X,Y,Z,Params); - else if(SampleZ1>SampleZ0) - { - const bool bCacheHit=LineContext.Field==&Field&&LineContext.GridZ0==GridZ0; - if(Profile) - { - if(bCacheHit)++Profile->OwnerNoiseCacheHits; - else ++Profile->OwnerNoiseCacheMisses; - } - if(!bCacheHit) - { - LineContext.Field=&Field; - LineContext.GridZ0=GridZ0; - LineContext.LowerCore=OwnerCaveRoughnessCore(X,Y,SampleZ0,Params); - LineContext.UpperCore=OwnerCaveRoughnessCore(X,Y,SampleZ1,Params); - } - const float Alpha=static_cast((Z-SampleZ0)/(SampleZ1-SampleZ0)); - Total=FMath::Lerp(LineContext.LowerCore,LineContext.UpperCore,Alpha); - } - else - Total=OwnerCaveRoughnessCore(X,Y,Z,Params); - if(SurfaceSDF<0.0f) - Total=std::lerp(Total,std::min(Total,0.0f),std::clamp(CaveInfluence,0.0f,1.0f)); - const float Fade=1.0f-std::abs(SurfaceSDF)/Depth; - return Total*Fade*Fade; - } - - float OwnerCaveRoughnessForDetail(double X,double Y,double Z,float SurfaceSDF, - const FLayerField& Field,float CaveInfluence, - float PassageInfluence, - FOwnerNoiseLineContext& LineContext, - FDetailWorkerProfile* Profile=nullptr) const - { - constexpr float DirectStart=0.40f,DirectEnd=0.60f; - constexpr int32 PassageAnchorStep=2; - const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f); - if(PassageWeight>=DirectEnd) - return OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field,CaveInfluence, - LineContext,Profile,PassageAnchorStep); - - const float Anchored=OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field, - CaveInfluence,LineContext,Profile); - if(PassageWeight<=DirectStart)return Anchored; - - const float Direct=OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field, - CaveInfluence,LineContext,Profile,PassageAnchorStep); - const float DirectWeight=SmoothUnit((PassageWeight-DirectStart) - /(DirectEnd-DirectStart)); - const float AnchoredWeight=1.0f-DirectWeight; - const float Normalization=1.0f/std::sqrt( - AnchoredWeight*AnchoredWeight+DirectWeight*DirectWeight); - return (AnchoredWeight*Anchored+DirectWeight*Direct)*Normalization; - } - - static float SurfaceRelief(float X,float Y,float Z,uint32 Seed) - { - // The low band carries the broad rock shapes and also gently varies mid-band - // amplitude. The quieter 6-voxel octave keeps fine relief without restoring - // the same tight bubble pattern on every exposed surface. - const float Macro=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,32,Seed^0xB5297A4Du); - const float Mid=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,16,Seed^0x63D83595u); - const float Fine=FStandaloneResearchTunnels::ValueNoise(X,Y,Z,6,Seed^0xA511E9B3u); - return 1.00f*Macro+(0.80f+0.25f*Macro)*(0.85f*Mid+0.18f*Fine); - } - - struct FValueNoiseLineContext - { - int32 X0=0,Y0=0,Z0=std::numeric_limits::min(); - int32 Wavelength=1; - uint32 Seed=0; - float Inv=1.0f,TX=0.0f,TY=0.0f; - std::array Corners{}; - - FValueNoiseLineContext(float X,float Y,int32 InWavelength,uint32 InSeed) - :Wavelength(InWavelength),Seed(InSeed),Inv(1.0f/static_cast(InWavelength)) - { - const float FX=X*Inv,FY=Y*Inv; - X0=FMath::FloorToInt(FX);Y0=FMath::FloorToInt(FY); - TX=FStandaloneResearchTunnels::Fade(FX-static_cast(X0)); - TY=FStandaloneResearchTunnels::Fade(FY-static_cast(Y0)); - } - - void SetPlane(int32 Z,bool bUpper) - { - const size_t I=bUpper?4u:0u; - Corners[I+0]=FStandaloneResearchTunnels::NoiseValue(X0,Y0,Z,Seed); - Corners[I+1]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0,Z,Seed); - Corners[I+2]=FStandaloneResearchTunnels::NoiseValue(X0,Y0+1,Z,Seed); - Corners[I+3]=FStandaloneResearchTunnels::NoiseValue(X0+1,Y0+1,Z,Seed); - } - - float Sample(float Z) - { - const float FZ=Z*Inv; - const int32 NewZ0=FMath::FloorToInt(FZ); - const float TZ=FStandaloneResearchTunnels::Fade(FZ-static_cast(NewZ0)); - if(NewZ0!=Z0) - { - if(NewZ0==Z0+1) - { - Corners[0]=Corners[4];Corners[2]=Corners[6]; - Corners[1]=Corners[5];Corners[3]=Corners[7]; - SetPlane(NewZ0+1,true); - } - else - { - SetPlane(NewZ0,false);SetPlane(NewZ0+1,true); - } - Z0=NewZ0; - } - const float Z00=FMath::Lerp(Corners[0],Corners[1],TX); - const float Z10=FMath::Lerp(Corners[2],Corners[3],TX); - const float Z01=FMath::Lerp(Corners[4],Corners[5],TX); - const float Z11=FMath::Lerp(Corners[6],Corners[7],TX); - return FMath::Lerp(FMath::Lerp(Z00,Z10,TY),FMath::Lerp(Z01,Z11,TY),TZ); - } - }; - - struct FDetailNoiseLineContext - { - FValueNoiseLineContext Macro,Mid,Fine; - FOwnerNoiseLineContext OwnerNoise; - FDetailNoiseLineContext(float X,float Y,uint32 Seed) - :Macro(X,Y,32,Seed^0xB5297A4Du),Mid(X,Y,16,Seed^0x63D83595u), - Fine(X,Y,6,Seed^0xA511E9B3u) - {} - - float SurfaceReliefAt(float Z,bool bMacroKnown=false,float KnownMacro=0.0f) - { - const float MacroValue=bMacroKnown?KnownMacro:Macro.Sample(Z); - const float MidValue=Mid.Sample(Z); - const float FineValue=Fine.Sample(Z); - return 1.00f*MacroValue+(0.80f+0.25f*MacroValue) - *(0.85f*MidValue+0.18f*FineValue); - } - - }; - - float DetailNoise(double X,double Y,double Z,float SurfaceSDF, - float CaveInfluence,float FloorInfluence,float PassageInfluence) 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; - } - const float Relief=SurfaceRelief(static_cast(X),static_cast(Y), - static_cast(Z),static_cast(Owner->Seed)); - const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f); - const FLayerField* Field=FindLayerField(Z); - FOwnerNoiseLineContext OwnerNoise; - float OwnerRelief=0.0f; - if(Field) - OwnerRelief=OwnerCaveRoughnessForDetail(X,Y,Z,SurfaceSDF,*Field, - CaveInfluence,PassageWeight,OwnerNoise); - const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) - +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); - return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f); - } - - float DetailNoiseOnLine(double X,double Y,double Z,float SurfaceSDF, - float CaveInfluence,float FloorInfluence,float PassageInfluence, - FDetailNoiseLineContext& NoiseLine, - FDetailWorkerProfile* Profile=nullptr, - const FLayerField* KnownField=nullptr, - bool bFieldKnown=false,bool bMacroKnown=false, - float KnownMacro=0.0f) const - { - if(DetailVariant!=EDetailVariant::Coherent) - return DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence,PassageInfluence); - using Clock=std::chrono::steady_clock; - const auto ReliefStart=Profile?Clock::now():Clock::time_point{}; - const float Relief=NoiseLine.SurfaceReliefAt(static_cast(Z), - bMacroKnown,KnownMacro); - if(Profile) - Profile->ReliefSampleNanoseconds+=static_cast( - std::chrono::duration_cast(Clock::now()-ReliefStart).count()); - const auto LookupStart=Profile?Clock::now():Clock::time_point{}; - const FLayerField* Field=bFieldKnown?KnownField:FindLayerField(Z); - if(Profile) - Profile->LayerLookupSampleNanoseconds+=static_cast( - std::chrono::duration_cast(Clock::now()-LookupStart).count()); - const auto OwnerStart=Profile?Clock::now():Clock::time_point{}; - if(Profile&&Field)++Profile->OwnerRoughnessFieldCalls; - const float PassageWeight=std::clamp(PassageInfluence,0.0f,1.0f); - float OwnerRelief=0.0f; - if(Field) - OwnerRelief=OwnerCaveRoughnessForDetail(X,Y,Z,SurfaceSDF,*Field, - CaveInfluence,PassageWeight,NoiseLine.OwnerNoise,Profile); - if(Profile) - Profile->OwnerRoughnessSampleNanoseconds+=static_cast( - std::chrono::duration_cast(Clock::now()-OwnerStart).count()); - const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) - +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); - return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f); - } - - bool EvaluateFast(double X,double Y,double Z) const - { - ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; - bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; - float PassageInfluence=0.0f; - float RockDistance=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF, - &bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir,nullptr,false, - &PassageInfluence); - 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(RockDistance)<=DetailSupremum+0.125f) - { - const float Fade=DetailBandFadeAt(X,Y,Z,RockDistance,SurfaceSDF, - CaveInfluence,FloorInfluence); - if(Fade>0.0f)RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence, - FloorInfluence,PassageInfluence)*Fade; - } - if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); - return RockDistance>=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 SurfaceRelief(static_cast(X),static_cast(Y),static_cast(Z),11); - }; - for(int32 Z=0;Z void ParallelLines(size_t Count,Fn&& Work,FParallelTiming* Timing=nullptr, - std::array* WorkerActiveSeconds=nullptr,size_t WorkBatch=1) const - { - const auto WallStart=std::chrono::steady_clock::now(); - if(WorkerCount<=1||Count<512) - { - const auto WorkerStart=std::chrono::steady_clock::now(); - for(size_t I=0;I)Work(I,0); - else Work(I); - } - if(WorkerActiveSeconds) - (*WorkerActiveSeconds)[0]=std::chrono::duration( - std::chrono::steady_clock::now()-WorkerStart).count(); - if(Timing) - { - const double WorkerSeconds=std::chrono::duration(std::chrono::steady_clock::now()-WorkerStart).count(); - Timing->WallSeconds+=std::chrono::duration(std::chrono::steady_clock::now()-WallStart).count(); - Timing->WorkerSeconds+=WorkerSeconds; - } - return; - } - WorkBatch=std::max(WorkBatch,1); - std::atomic Next{0};std::atomic WorkerNanoseconds{0}; - std::vector Threads;Threads.reserve(WorkerCount); - for(int32 W=0;W=Count)break; - const size_t Last=First+std::min(WorkBatch,Count-First); - for(size_t I=First;I)Work(I,W); - else Work(I); - } - } - const uint64 WorkerNs=static_cast(std::chrono::duration_cast( - std::chrono::steady_clock::now()-WorkerStart).count()); - if(WorkerActiveSeconds)(*WorkerActiveSeconds)[static_cast(W)]=static_cast(WorkerNs)*1.0e-9; - WorkerNanoseconds.fetch_add(WorkerNs,std::memory_order_relaxed); - }); - for(std::thread& T:Threads)T.join(); - if(Timing) - { - Timing->WallSeconds+=std::chrono::duration(std::chrono::steady_clock::now()-WallStart).count(); - Timing->WorkerSeconds+=static_cast(WorkerNanoseconds.load(std::memory_order_relaxed))*1.0e-9; - } - } - - FStandaloneOwnerWorld* Owner=nullptr; - int32 WorkerCount=6; - int32 Min[3]{},Max[3]{},ClipMin[3]{},ClipMax[3]{},Size[3]{}; - FIntVector EditBoundsMin=FIntVector(MAX_int32,MAX_int32,MAX_int32); - FIntVector EditBoundsMax=FIntVector(MIN_int32,MIN_int32,MIN_int32); - int32 ActiveSpatialBinSize=LineBinSize; - double ActiveBlendRadius=0.0; - double ActiveFloorBlendRadius=0.0; - size_t WordsPerZ=0; - EDetailVariant DetailVariant=EDetailVariant::Coherent; - bool Built=false; - bool FarBuilt=false; - std::vector LayerRanges; - std::vector LayerFields; - std::vector Primitives; - std::vector FarConnectorSegments; - std::unordered_map,FBinHash> FarConnectorSpatialBins; - std::vector EditBrushes; - std::array AxisBins; - std::unordered_map SpatialBins; - std::unordered_map CriticalAirBins; - std::vector RockWords; - std::unique_ptr SurfaceDistanceQ; - inline static std::atomic ProfileDetailBand{false}; - std::array,3> CandidateAxisWords; - std::vector CandidateWords; - FBuildStats Stats; - inline static thread_local const std::unordered_set* ForcedFarAirSamples=nullptr; -}; - -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; - }; -} +#include "../Core/src/ShapeWorldInternal.h" diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index ba8ee5a..c8f7abd 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -1,7 +1,7 @@ #include "OwnerWorldLoader.h" #include "StreamingWorld.h" -#include "StandaloneResearchTunnels.h" -#include "StandaloneShapeWorld.h" +#include "StandaloneCoreScheduler.h" +#include "../Core/src/StandaloneSnapshotAdapter.h" #include "VoxelDensityProfile.h" #ifdef TEXT @@ -313,13 +313,14 @@ namespace // Keep the owner's walkable rooms, tunnels, and strate passages intact. The research // round field composes as additional air after the owner field, so it can widen/connect // spaces but cannot remove the authored entrance-to-exit route. - FStandaloneResearchTunnels::Set(bRoundTunnels,bEnhancedWalls); World.Manager=std::make_unique(); if(!World.Manager->Initialize(World.Settings.get(),Seed)) {Error="standalone copied strate manager failed while applying viewer settings";return false;} World.Generator=std::make_unique(); World.Generator->InitializeSettings(World.Settings.get()); World.Generator->SetStrateManager(World.Manager.get()); + World.Generator->bStandaloneResearchRoundTunnels=bRoundTunnels; + World.Generator->bStandaloneResearchEnhancedWalls=bEnhancedWalls; return true; } @@ -337,9 +338,11 @@ namespace ID3D11Buffer* Constants=nullptr; ID3D11RasterizerState* SolidRasterizer=nullptr; ID3D11RasterizerState* WireRasterizer=nullptr; - std::unique_ptr World; - std::unique_ptr ShapeWorld; - std::unique_ptr PendingFullShapeWorld; + std::shared_ptr World; + VoxelForgeCore::ResolvedWorldSnapshot Snapshot; + std::unique_ptr ShapeWorld; + std::unique_ptr PendingFullShapeWorld; + FStandaloneCoreScheduler CoreScheduler; std::unique_ptr Streamer; std::thread FullShapeBuildThread; std::atomic bFullShapeBuildFinished{false}; @@ -376,13 +379,14 @@ namespace bool bShapeAuditOnly=false; bool bShapeCompare=false; bool bFarShapeStagePending=false; - FStandaloneShapeWorld::EDetailVariant ShapeDetailVariant=FStandaloneShapeWorld::EDetailVariant::Coherent; + VoxelForgeCore::DetailVariant ShapeDetailVariant=VoxelForgeCore::DetailVariant::coherent; bool bWireframe=false; bool bCaptured=false; bool bAutoCaptureWritten=false; bool bFullDetailReported=false; bool bInitialClipmapReported=false; bool bProfileAttribution=false; + bool bProfileDetailBand=false; bool bCompareEvaluators=false; bool bParityOnly=false; bool bDensityHash=false; @@ -649,9 +653,9 @@ 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(); + Snapshot={}; + World=std::make_shared(); if(!StandaloneOwnerLoader::Load(DataPath,*World,Error))return false; if(!bParityChecked&&Seed==World->Seed&&!bRoundTunnels)PrintParity(); const int32 ExportClipRadius=World->ClipRadius; @@ -718,15 +722,23 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; Min.X-=NearBuildPad;Min.Y-=NearBuildPad;Min.Z-=NearBuildPad; Max.X+=NearBuildPad;Max.Y+=NearBuildPad;Max.Z+=NearBuildPad; } - ShapeWorld=std::make_unique(); + Snapshot=VoxelForgeCore::MakeStandaloneSnapshot(World); + if(!Snapshot){Error="the standalone adapter could not make a resolved core snapshot";return false;} + ShapeWorld=std::make_unique(Snapshot); const int32 ShapeWorkers=bNeighborhoodOnly?(WorkerCount>=8?2:1):WorkerCount; - if(!ShapeWorld->Build(*World,Min,Max,ClipMin,ClipMax,ShapeWorkers, - ShapeDetailVariant,bIncludeFarWorld,Error)) + VoxelForgeCore::ShapeBuildRequest Request; + Request.minVoxel={Min.X,Min.Y,Min.Z};Request.maxVoxel={Max.X,Max.Y,Max.Z}; + Request.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z}; + Request.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z}; + Request.workerLimit=static_cast(ShapeWorkers);Request.detail=ShapeDetailVariant; + Request.includeFarWorld=bIncludeFarWorld;Request.roundTunnels=bRoundTunnels; + Request.enhancedTunnelWalls=bRoundTunnelEnhanced; + Request.profileDetailBand=bProfileDetailBand; + if(!ShapeWorld->compile(Request,CoreScheduler,Error)) {ShapeWorld.reset();return false;} - FStandaloneShapeWorldAccess::SetActive(ShapeWorld.get()); bShapePipelineActive=bNeighborhoodOnly; bNeighborhoodReadyReported=false; - bFarShapeStagePending=bShapeWorld&&!bNeighborhoodOnly&&!ShapeWorld->HasFarWorld(); + 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", 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); @@ -748,15 +760,24 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } 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(); + PendingFullShapeWorld=std::make_unique(Snapshot); 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); + VoxelForgeCore::ShapeBuildRequest Request; + Request.minVoxel={FineMin.X,FineMin.Y,FineMin.Z}; + Request.maxVoxel={FineMax.X,FineMax.Y,FineMax.Z}; + Request.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z}; + Request.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z}; + Request.workerLimit=static_cast(FullWorkers);Request.detail=ShapeDetailVariant; + Request.includeFarWorld=true;Request.roundTunnels=bRoundTunnels; + Request.enhancedTunnelWalls=bRoundTunnelEnhanced; + Request.profileDetailBand=bProfileDetailBand; + bFullShapeBuildSucceeded=PendingFullShapeWorld->compile( + Request,CoreScheduler,FullShapeBuildError); bFullShapeBuildFinished.store(true,std::memory_order_release); }); std::printf("[ShapePipelineBuildStart] neighborhood_tiles=%zu near_mesh_workers=%d full_shape_workers=%d\n", @@ -826,12 +847,20 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels); DesiredTiles=Desired.Tiles; + if(ShapeWorld) + { + const auto CoreTiles=ShapeWorld->desiredTiles({CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z}); + DesiredTiles.clear();DesiredTiles.reserve(CoreTiles.size()); + for(const auto& Key:CoreTiles) + DesiredTiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); + } for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0; for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level]; const int32 StreamWorkers=bShapePipelineActive?(WorkerCount>=8?2:1):WorkerCount; Streamer=std::make_unique(*World,StreamWorkers,false, (!bShapeWorld&&MeshCache.IsLoaded())?&MeshCache:nullptr, - bFarShapeStagePending?World->FullResClipLevels-1:-1); + bFarShapeStagePending?World->FullResClipLevels-1:-1, + false,false,false,false,ShapeWorld.get()); if(bShapePipelineActive)Streamer->SetDesiredTileFilter(InitialNeighborhoodTiles); const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex); if(bShapePipelineActive&&Update.DesiredTiles!=static_cast(InitialNeighborhoodTiles.size())) @@ -872,14 +901,15 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; void StartFarShapeGeneration() { - if(!bFarShapeStagePending||!ShapeWorld||ShapeWorld->HasFarWorld()||!Streamer)return; - ShapeWorld->BuildFarWorld(); + 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); + Streamer=std::make_unique(*World,WorkerCount,false,nullptr,-1, + false,false,false,false,ShapeWorld.get()); const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0); ++CrossingIndex; const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,ResidentKeys,CrossingIndex); @@ -929,11 +959,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; 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(*World,WorkerCount,false,nullptr,-1); + Streamer=std::make_unique(*World,WorkerCount,false,nullptr,-1, + false,false,false,false,ShapeWorld.get()); ++CrossingIndex; const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,ResidentKeys,CrossingIndex); if(Update.DesiredTiles!=static_cast(DesiredTiles.size())) @@ -1370,13 +1400,24 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } int RunTileBenchmark(FStandaloneOwnerWorld& World,V3 CameraPosition,int32 Level, - int32 Count,bool bProfile,bool bCompareEvaluators,bool bDensityHash) + int32 Count,bool bProfile,bool bCompareEvaluators,bool bDensityHash, + VoxelForgeCore::World* CoreWorld) { const FVector CameraVoxels(CameraPosition.X*4.0,CameraPosition.Y*4.0,CameraPosition.Z*4.0); const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraVoxels); std::vector Tiles;Tiles.reserve(static_cast(std::max(Count,1))); - for(const FVoxelTileKey& Tile:Desired.Tiles) - if(Tile.Level==Level&&static_cast(Tiles.size())desiredTiles({CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z}); + for(const auto& Key:CoreTiles) + if(Key.lod==Level&&static_cast(Tiles.size())(Tiles.size()) DebugDensitySamples; - FVoxelMeshData Mesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr, - BandMin,BandMax,&TileSamples,nullptr,nullptr, - bDensityHash?&DebugDensitySamples:nullptr); + uint64 TileVertices=0,TileIndices=0; + if(CoreWorld&&!bCompareEvaluators) + { + VoxelForgeCore::TileGenerationOptions Options; + Options.bandMinZVoxel=BandMin;Options.bandMaxZVoxel=BandMax; + Options.captureDensitySamples=bDensityHash; + std::string CoreError; + auto Mesh=CoreWorld->generateTile( + {{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level},Options,CoreError); + if(!Mesh) + { + std::fprintf(stderr,"[VoxelForgeCore] benchmark tile generation failed: %s\n",CoreError.c_str()); + return 8; + } + TileSamples=static_cast(Mesh->densitySampleCount); + TileVertices=Mesh->vertices.size();TileIndices=Mesh->indices.size(); + if(bDensityHash)DebugDensitySamples=std::move(Mesh->debugDensitySamples); + } + else + { + FVoxelMeshData Mesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr, + BandMin,BandMax,&TileSamples,nullptr,nullptr, + bDensityHash?&DebugDensitySamples:nullptr); + TileVertices=static_cast(Mesh.Vertices.Num()); + TileIndices=static_cast(Mesh.Triangles.Num()); + } Samples+=static_cast(FMath::Max(0,TileSamples)); - Vertices+=static_cast(Mesh.Vertices.Num()); - Indices+=static_cast(Mesh.Triangles.Num()); + Vertices+=TileVertices; + Indices+=TileIndices; if(bDensityHash) { for(const float Density:DebugDensitySamples) @@ -1607,17 +1673,15 @@ 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, + int RunOwnerShapeComparison(FStandaloneOwnerWorld& World,VoxelForgeCore::World& Shape, V3 CameraPosition,float CameraYaw,float CameraPitch, 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(); + if(!Shape.isCompiled()) + {std::fprintf(stderr,"Shape comparison requires a compiled core world.\n");return 11;} + if(LevelFilter>=World.FullResClipLevels&&!Shape.hasFarWorld())Shape.buildFarWorld(); const FVector CameraVoxels(CameraPosition.X*4.0,CameraPosition.Y*4.0,CameraPosition.Z*4.0); - Shape.AuditViewSurface(CameraPosition.X,CameraPosition.Y,CameraPosition.Z, - CameraYaw,CameraPitch); + Shape.auditViewSurface({CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z},CameraYaw,CameraPitch); { 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) @@ -1626,17 +1690,18 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; 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); + 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); + Shape.debugCriticalAirAt({X,Y,Z})?1:0,Shape.debugSolidOverlayAt({X,Y,Z})?1:0); } } - const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraVoxels); + std::vector CoreDesired; + for(const auto& Key:Shape.desiredTiles({CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z})) + CoreDesired.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); std::vector Tiles; - for(const FVoxelTileKey& Tile:Desired.Tiles) + for(const FVoxelTileKey& Tile:CoreDesired) if(LevelFilter<0?Tile.Level0&&Tiles.size()>static_cast(TileLimit)) @@ -1670,7 +1735,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; std::atomic Next{0}; std::vector Threads; - const int32 ThreadCount=std::clamp(Workers,1,16); + const int32 ThreadCount=std::clamp(Workers,1,8); Threads.reserve(ThreadCount); for(int32 Worker=0;Worker NewDensities,OldDensities; - FVoxelMeshData NewMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr, - MinChunkZ,MaxChunkZ,nullptr,nullptr,nullptr,&NewDensities); + VoxelForgeCore::TileGenerationOptions ShapeOptions; + ShapeOptions.bandMinZVoxel=MinChunkZ;ShapeOptions.bandMaxZVoxel=MaxChunkZ; + ShapeOptions.captureDensitySamples=true; + std::string CoreError; + auto NewMesh=Shape.generateTile({{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level}, + ShapeOptions,CoreError); + std::vector NewDensities=NewMesh?std::move(NewMesh->debugDensitySamples):std::vector{}; + std::vector OldDensities; FVoxelMeshData OldMesh; - { - FStandaloneShapeWorldAccess::FScopedBypass Bypass; - OldMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr, - MinChunkZ,MaxChunkZ,nullptr,nullptr,nullptr,&OldDensities); - } + 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; @@ -1708,9 +1775,12 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; 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});} + if(NewMesh) + { + R.NewSurface.reserve(NewMesh->vertices.size()/VertexStride+1); + for(size_t I=0;Ivertices.size();I+=VertexStride) + {const auto& P=NewMesh->vertices[I].position;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});} @@ -1779,9 +1849,12 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } uint64 TotalSamples=0,ChangedSigns=0; uint64 LegacyHash=14695981039346656037ull,DetailedHash=14695981039346656037ull; + const bool PreviousRoundTunnels=World.Generator->bStandaloneResearchRoundTunnels; + const bool PreviousEnhancedWalls=World.Generator->bStandaloneResearchEnhancedWalls; const auto Evaluate=[&](bool bEnhanced,std::vector& Out) { - FStandaloneResearchTunnels::Set(true,bEnhanced); + World.Generator->bStandaloneResearchRoundTunnels=true; + World.Generator->bStandaloneResearchEnhancedWalls=bEnhanced; for(size_t I=0;IbStandaloneResearchRoundTunnels=PreviousRoundTunnels; + World.Generator->bStandaloneResearchEnhancedWalls=PreviousEnhancedWalls; FDirectedSurfaceDistance LegacyToDetailed,DetailedToLegacy; std::vector AllLegacySurface,AllDetailedSurface; for(size_t TileIndex=0;TileIndex(LegacyHash),static_cast(DetailedHash), AllLegacySurface.size(),AllDetailedSurface.size(),MeanDistance,Hausdorff, static_cast(LegacyToDetailed.Unmatched+DetailedToLegacy.Unmatched)); - FStandaloneResearchTunnels::Set(true,true); return TotalSamples>0&&LegacyToDetailed.Unmatched+DetailedToLegacy.Unmatched==0?0:10; } @@ -1866,12 +1940,14 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; return Hash; } - int RunPipelinedStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels, - int32 WorkerCount,int32 MaxLevel,FStandaloneShapeWorld::EDetailVariant DetailVariant, + int RunPipelinedStreamingBenchmark(std::shared_ptr OwnerWorld, + const FVector& CameraPositionVoxels,int32 WorkerCount,int32 MaxLevel, + VoxelForgeCore::DetailVariant DetailVariant,const VoxelForgeCore::JobScheduler& Scheduler, bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors, std::chrono::steady_clock::time_point ColdStart) { using Clock=std::chrono::steady_clock; + FStandaloneOwnerWorld& World=*OwnerWorld; FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels); if(MaxLevel>=0) Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(), @@ -1902,24 +1978,30 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; if(Neighborhood.size()!=27) {std::fprintf(stderr,"[StreamPipeline] expected 27 player-neighborhood tiles, found %zu.\n",Neighborhood.size());return 7;} - FStandaloneShapeWorld Shape; + VoxelForgeCore::World Shape(VoxelForgeCore::MakeStandaloneSnapshot(OwnerWorld)); std::string BuildError; const bool bIncludeFarWorld=MaxLevel<0||MaxLevel>=World.FullResClipLevels; const auto ShapeBuildStart=Clock::now(); - if(!Shape.Build(World,FineMin,FineMax,ClipMin,ClipMax,WorkerCount, - DetailVariant,bIncludeFarWorld,BuildError)) + VoxelForgeCore::ShapeBuildRequest ShapeRequest; + ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z}; + ShapeRequest.maxVoxel={FineMax.X,FineMax.Y,FineMax.Z}; + ShapeRequest.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z}; + ShapeRequest.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z}; + ShapeRequest.workerLimit=static_cast(std::clamp(WorkerCount,1,8)); + ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld; + ShapeRequest.roundTunnels=World.Generator->bStandaloneResearchRoundTunnels; + ShapeRequest.enhancedTunnelWalls=World.Generator->bStandaloneResearchEnhancedWalls; + if(!Shape.compile(ShapeRequest,Scheduler,BuildError)) {std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2;} const auto ShapeReady=Clock::now(); const double ShapeSeconds=std::chrono::duration(ShapeReady-ShapeBuildStart).count(); - FStandaloneShapeWorldAccess::SetActive(&Shape); - FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel, - bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors); + bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape); const auto StreamStart=Clock::now(); const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels); if(Update.DesiredTiles!=static_cast(Desired.Tiles.size())) { - Streamer.Stop();FStandaloneShapeWorldAccess::SetActive(nullptr); + Streamer.Stop(); std::fprintf(stderr,"[StreamPipeline] expected %zu tiles but queued %d.\n", Desired.Tiles.size(),Update.DesiredTiles);return 7; } @@ -2005,11 +2087,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; static_cast(HashTileRows(PayloadRows)), static_cast(HashTileRows(TopologyRows))); } - FStandaloneShapeWorldAccess::SetActive(nullptr); return NeighborhoodFinished==static_cast(Neighborhood.size()) &&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing?0:6; } - int RunStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels, + int RunStreamingBenchmark(FStandaloneOwnerWorld& World,VoxelForgeCore::World* CoreWorld, + const FVector& CameraPositionVoxels, int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels, bool bEnhancedWalls,const FStandaloneMeshCache* Cache, bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups, @@ -2020,6 +2102,14 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; const FStandaloneMeshCacheMetadata& CacheMetadata) { FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels); + if(CoreWorld) + { + const auto CoreTiles=CoreWorld->desiredTiles( + {CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z}); + Desired.Tiles.clear();Desired.Tiles.reserve(CoreTiles.size()); + for(const auto& Key:CoreTiles) + Desired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); + } 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()); @@ -2035,10 +2125,9 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; std::unique_ptr 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(World,*Shape,Desired.Tiles,bFarAuditBadShell); + if(!CoreWorld||!CoreWorld->hasFarWorld()) + {std::fprintf(stderr,"Far survival audit requires a compiled core world with far-parent data.\n");return 15;} + FarAudit=std::make_unique(World,*CoreWorld,Desired.Tiles,bFarAuditBadShell); } std::array,MaxLevels+1> HashRows; std::array,MaxLevels+1> MeshHashRows; @@ -2067,7 +2156,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; const auto Start=std::chrono::steady_clock::now(); FStandaloneStreamingGenerator Streamer(World,WorkerCount, bHashDensity||!BakeCachePath.empty(),Cache,MaxLevel, - bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors); + bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,CoreWorld); const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels); const double LOD0Width=static_cast(2*World.ClipRadius+2) *CHUNK_SIZE*VOXEL_SIZE/100.0; @@ -2279,47 +2368,58 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; return 0; } - int RunEditTransactionBenchmark(FStandaloneOwnerWorld& World,FStandaloneShapeWorld& Shape, - const FVector& CameraPositionVoxels,int32 WorkerCount) + int RunEditTransactionBenchmark(FStandaloneOwnerWorld& World,VoxelForgeCore::World& Shape, + const FVector& CameraPositionVoxels,int32 WorkerCount) { using Clock=std::chrono::steady_clock; // Place the spell across the LOD0/LOD1 clipmap seam so one edit exercises every visible // level it intersects, instead of timing only the center LOD0 tiles. const double X=CameraPositionVoxels.X+3.0*CHUNK_SIZE; const double Y=CameraPositionVoxels.Y,Z=CameraPositionVoxels.Z; - std::vector Brushes(3); - Brushes[0].Shape=FStandaloneShapeWorld::FEditBrush::EShape::Sphere; - Brushes[0].Operation=FStandaloneShapeWorld::FEditBrush::EOperation::Carve; - Brushes[0].A={X,Y,Z};Brushes[0].Radius=20.0; - Brushes[1].Shape=FStandaloneShapeWorld::FEditBrush::EShape::RoundedBox; - Brushes[1].Operation=FStandaloneShapeWorld::FEditBrush::EOperation::Carve; - Brushes[1].A={X,Y,Z+12.0};Brushes[1].HalfExtent={11.0,11.0,4.0};Brushes[1].Rounding=3.0; - Brushes[2].Shape=FStandaloneShapeWorld::FEditBrush::EShape::Capsule; - Brushes[2].Operation=FStandaloneShapeWorld::FEditBrush::EOperation::AddRock; - Brushes[2].A={X-22.0,Y,Z-8.0};Brushes[2].B={X+22.0,Y,Z-8.0};Brushes[2].Radius=3.0; - const auto EditStart=Clock::now(); - Shape.SetEditTransaction(std::move(Brushes)); - FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels); - const FIntVector EditMin=Shape.GetEditBoundsMin(),EditMax=Shape.GetEditBoundsMax(); + const auto CoreDesired=Shape.desiredTiles( + {CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z}); + Desired.Tiles.clear();Desired.Tiles.reserve(CoreDesired.size()); + std::vector CoreKeys;CoreKeys.reserve(CoreDesired.size()); + for(const auto& Key:CoreDesired) + { + Desired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); + CoreKeys.push_back(Key); + } + VoxelForgeCore::EditTransaction Transaction;Transaction.sequence=1;Transaction.brushes.resize(3); + Transaction.brushes[0].shape=VoxelForgeCore::EditBrush::Shape::sphere; + Transaction.brushes[0].operation=VoxelForgeCore::EditBrush::Operation::carve; + Transaction.brushes[0].a={X,Y,Z};Transaction.brushes[0].radius=20.0; + Transaction.brushes[1].shape=VoxelForgeCore::EditBrush::Shape::roundedBox; + Transaction.brushes[1].operation=VoxelForgeCore::EditBrush::Operation::carve; + Transaction.brushes[1].a={X,Y,Z+12.0};Transaction.brushes[1].halfExtent={11.0,11.0,4.0}; + Transaction.brushes[1].rounding=3.0; + Transaction.brushes[2].shape=VoxelForgeCore::EditBrush::Shape::capsule; + Transaction.brushes[2].operation=VoxelForgeCore::EditBrush::Operation::addRock; + Transaction.brushes[2].a={X-22.0,Y,Z-8.0};Transaction.brushes[2].b={X+22.0,Y,Z-8.0}; + Transaction.brushes[2].radius=3.0; + const auto EditStart=Clock::now(); + std::string EditError; + const auto Edit=Shape.applyEditTransaction(Transaction,CoreKeys,EditError); + if(!Edit){std::fprintf(stderr,"[EditTransaction] rejected: %s\n",EditError.c_str());return 9;} std::vector Affected;Affected.reserve(128); std::array AffectedByLevel{},FinishedByLevel{}; - for(const FVoxelTileKey& Tile:Desired.Tiles) + for(const auto& LodTiles:Edit->affectedByLod) { - const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels(); - const int32 Pad=std::max(2,Tile.StepVoxels()); - if(Origin.X-Pad>EditMax.X||Origin.X+Extent+PadEditMax.Y||Origin.Y+Extent+PadEditMax.Z||Origin.Z+Extent+Pad=0&&Tile.Level<=MaxLevels)++AffectedByLevel[Tile.Level]; + for(const auto& Key:LodTiles) + { + const FVoxelTileKey Tile(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); + Affected.push_back(Tile); + if(Tile.Level>=0&&Tile.Level<=MaxLevels)++AffectedByLevel[Tile.Level]; + } } if(Affected.empty()) { std::fprintf(stderr,"[EditTransaction] failed: no desired tiles intersect the edit bounds.\n"); return 9; } - FStandaloneStreamingGenerator Streamer(World,WorkerCount,false,nullptr,-1); + FStandaloneStreamingGenerator Streamer(World,WorkerCount,false,nullptr,-1, + false,false,false,false,&Shape); Streamer.SetDesiredTileFilter(Affected); const auto Start=Clock::now(); const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels); @@ -2400,8 +2500,8 @@ int main(int Argc,char** Argv) 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; + if(Value=="smooth")App.ShapeDetailVariant=VoxelForgeCore::DetailVariant::smooth; + else if(Value=="per-voxel")App.ShapeDetailVariant=VoxelForgeCore::DetailVariant::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; @@ -2439,7 +2539,7 @@ int main(int Argc,char** Argv) else if(Arg.starts_with("--workers=")) { const int32 Parsed=std::atoi(std::string(Arg.substr(10)).c_str()); - App.WorkerCount=std::clamp(Parsed,1,16); + App.WorkerCount=std::clamp(Parsed,1,8); } else if(Arg.starts_with("--clip-radius=")) App.ClipRadius=std::clamp(std::atoi(std::string(Arg.substr(14)).c_str()),1,64); @@ -2461,9 +2561,9 @@ int main(int Argc,char** Argv) } else if(!Arg.empty()&&Arg.front()!='-')App.DataPath=std::string(Arg); } - FStandaloneShapeWorld::SetDetailProfileEnabled(bProfileDetailBand); + App.bProfileDetailBand=bProfileDetailBand; std::string Error; - App.World=std::make_unique(); + App.World=std::make_shared(); if(!StandaloneOwnerLoader::Load(App.DataPath,*App.World,Error)){std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;} App.Seed=App.World->Seed; if(App.bParityOnly) @@ -2500,10 +2600,11 @@ int main(int Argc,char** Argv) if(App.bShapeAuditOnly) { if(bPassageDetailAuditOnly) - return App.ShapeWorld->AuditSurfaceBandFamilies(true)?0:8; - const bool bSurfacePassing=App.ShapeWorld->AuditViewSurface(App.Position.X,App.Position.Y, - App.Position.Z,App.Yaw,App.Pitch); - return App.ShapeWorld->IsDetailAuditPassing()&&bSurfacePassing?0:7; + return App.ShapeWorld->auditSurfaceFamilies(true)?0:8; + const bool bSurfacePassing=App.ShapeWorld->auditViewSurface( + {App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0}, + App.Yaw,App.Pitch); + return App.ShapeWorld->auditDetailPassing()&&bSurfacePassing?0:7; } if(App.bShapeCompare) return RunOwnerShapeComparison(*App.World,*App.ShapeWorld,App.Position,App.Yaw,App.Pitch,App.WorkerCount, @@ -2513,9 +2614,9 @@ int main(int Argc,char** Argv) if(bStreamBenchmark) { if(bStreamPipeline) - return RunPipelinedStreamingBenchmark(*App.World, + 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, + App.WorkerCount,StreamBenchmarkMaxLevel,App.ShapeDetailVariant,App.CoreScheduler, bStreamHash||!BakeMeshCachePath.empty(),bSkipUvAndColors,bSkipMaterialGroups, bSkipUV,bSkipColors,ColdProcessStart); if(bEditBenchmark) @@ -2526,7 +2627,7 @@ int main(int Argc,char** Argv) 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.bShapeWorld)App.LoadMeshCache(); - return RunStreamingBenchmark(*App.World, + return RunStreamingBenchmark(*App.World,App.ShapeWorld.get(), FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0), App.WorkerCount,StreamBenchmarkMaxLevel,App.bRoundTunnels, App.bRoundTunnelEnhanced,(!App.bShapeWorld&&App.MeshCache.IsLoaded())?&App.MeshCache:nullptr, @@ -2536,7 +2637,7 @@ int main(int Argc,char** Argv) } if(BenchmarkLevel>=0) return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount, - App.bProfileAttribution,App.bCompareEvaluators,App.bDensityHash); + App.bProfileAttribution,App.bCompareEvaluators,App.bDensityHash,App.ShapeWorld.get()); App.World.reset(); WNDCLASSA Class{};Class.lpfnWndProc=WindowProc;Class.hInstance=GetModuleHandleA(nullptr);Class.lpszClassName="VoxelForgeStandaloneWorld"; diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index 860217a..14ae290 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -1,7 +1,7 @@ #pragma once #include "WorldMesher.h" -#include "StandaloneShapeWorld.h" +#include "VoxelForgeCore/VoxelForgeCore.h" #include #include @@ -153,813 +153,7 @@ inline uint64 HashStandaloneCpuTileMesh(const FStandaloneCpuTile& Tile) return Hash; } -struct FFarMeshSegmentIndex -{ - FFarMeshSegmentIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent, - int32 InCells,double InStep) - :Mesh(InMesh),Origin(InOrigin),Extent(InExtent),Cells(std::max(1,InCells)),Step(InStep), - ByCell(static_cast(Cells)*Cells*Cells), - TriangleMarks(static_cast(InMesh.Triangles.Num()/3),0) - { - for(int32 Triangle=0;Triangle+2=Mesh.Vertices.Num()||B>=Mesh.Vertices.Num()||C>=Mesh.Vertices.Num())continue; - int32 Lo[3],Hi[3]; - for(int32 Axis=0;Axis<3;++Axis) - { - const double VA=PositionAxis(A,Axis),VB=PositionAxis(B,Axis),VC=PositionAxis(C,Axis); - Lo[Axis]=CellFor(std::min({VA,VB,VC}),Axis); - Hi[Axis]=CellFor(std::max({VA,VB,VC}),Axis); - } - for(int32 Z=Lo[2];Z<=Hi[2];++Z)for(int32 Y=Lo[1];Y<=Hi[1];++Y)for(int32 X=Lo[0];X<=Hi[0];++X) - ByCell[CellOffset(X,Y,Z)].push_back(Triangle/3); - } - } - - double NearestHitDifference(const FVector& Start,const FVector& Direction,double Length, - double TargetDistance) - { - double BestDifference=std::numeric_limits::infinity(); - ++QueryStamp; - if(QueryStamp==0){std::fill(TriangleMarks.begin(),TriangleMarks.end(),0);QueryStamp=1;} - const auto TestCell=[&](int32 X,int32 Y,int32 Z) - { - if(X<0||X>=Cells||Y<0||Y>=Cells||Z<0||Z>=Cells)return; - for(const int32 Triangle:ByCell[CellOffset(X,Y,Z)]) - { - if(TriangleMarks[static_cast(Triangle)]==QueryStamp)continue; - TriangleMarks[static_cast(Triangle)]=QueryStamp; - const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],IC=Mesh.Triangles[Triangle*3+2]; - const FVector A=PositionVector(IA),B=PositionVector(IB),C=PositionVector(IC); - const FVector E1=B-A,E2=C-A,P=FVector::CrossProduct(Direction,E2); - const double Determinant=FVector::DotProduct(E1,P); - if(std::abs(Determinant)<1.0e-8)continue; - const double Inv=1.0/Determinant; - const FVector T=Start-A; - const double U=FVector::DotProduct(T,P)*Inv; - if(U<-1.0e-5||U>1.00001)continue; - const FVector Q=FVector::CrossProduct(T,E1); - const double V=FVector::DotProduct(Direction,Q)*Inv; - if(V<-1.0e-5||U+V>1.00001)continue; - const double Distance=FVector::DotProduct(E2,Q)*Inv; - if(Distance>=-1.0e-4&&Distance<=Length+1.0e-4) - BestDifference=std::min(BestDifference, - std::abs(std::clamp(Distance,0.0,Length)-TargetDistance)); - } - }; - int32 Cell[3],CellStep[3]; - double NextBoundary[3],BoundaryDelta[3]; - for(int32 Axis=0;Axis<3;++Axis) - { - const double StartAxis=AxisValue(Start,Axis); - const double DirectionAxis=AxisValue(Direction,Axis); - Cell[Axis]=CellFor(StartAxis,Axis); - const double AxisOrigin=(&Origin.X)[Axis]; - if(DirectionAxis>1.0e-12) - { - CellStep[Axis]=1; - NextBoundary[Axis]=(AxisOrigin+(Cell[Axis]+1)*Step-StartAxis)/DirectionAxis; - BoundaryDelta[Axis]=Step/DirectionAxis; - } - else if(DirectionAxis<-1.0e-12) - { - CellStep[Axis]=-1; - NextBoundary[Axis]=(AxisOrigin+Cell[Axis]*Step-StartAxis)/DirectionAxis; - BoundaryDelta[Axis]=-Step/DirectionAxis; - } - else - { - CellStep[Axis]=0; - NextBoundary[Axis]=BoundaryDelta[Axis]=std::numeric_limits::infinity(); - } - } - for(;;) - { - TestCell(Cell[0],Cell[1],Cell[2]); - const double NextT=std::min({NextBoundary[0],NextBoundary[1],NextBoundary[2]}); - if(!std::isfinite(NextT)||NextT>Length+1.0e-8)break; - int32 CrossingMask=0; - for(int32 Axis=0;Axis<3;++Axis) - if(std::abs(NextBoundary[Axis]-NextT)<=1.0e-8)CrossingMask|=1<(std::floor((Value-(&Origin.X)[Axis])/Step)),0,Cells-1);} - size_t CellOffset(int32 X,int32 Y,int32 Z)const - {return (static_cast(Z)*Cells+Y)*Cells+X;} - static double AxisValue(const FVector& P,int32 Axis) - {return Axis==0?P.X:(Axis==1?P.Y:P.Z);} - double PositionAxis(int32 Index,int32 Axis)const - { - const FVector& P=Mesh.Vertices[Index]; - return AxisValue(P,Axis)*0.04; - } - FVector PositionVector(int32 Index)const - { - const FVector& P=Mesh.Vertices[Index];return P*0.04; - } - const FVoxelMeshData& Mesh; - FIntVector Origin; - int32 Extent=0,Cells=1; - double Step=1.0; - std::vector> ByCell; - std::vector TriangleMarks; - uint32 QueryStamp=0; -}; - -inline bool ContainsFarTilePoint(const FVoxelTileKey& Key,const FVector& P) -{ - const FIntVector Origin=Key.OriginVoxels();const int32 Extent=Key.ExtentVoxels(); - return P.X>=Origin.X&&P.X<=Origin.X+Extent&&P.Y>=Origin.Y&&P.Y<=Origin.Y+Extent - &&P.Z>=Origin.Z&&P.Z<=Origin.Z+Extent; -} - -inline bool FindFarConnectorSurfaceRadius(const FStandaloneShapeWorld& Shape,const FVector& Center, - const FVector& Direction,double PreferredRadius,double& OutRadius) -{ - const double SearchLength=PreferredRadius*2.0+32.0; - const double AcceptBand=std::max(16.0,PreferredRadius*0.75); - constexpr double Stride=8.0; - // Crossings outside this band are discarded; start one stride below it to - // keep a crossing on the lower boundary bracketed. - const double SearchMin=std::max(0.0,PreferredRadius-AcceptBand-Stride); - const double SearchMax=std::min(SearchLength,PreferredRadius+AcceptBand); - double PreviousDistance=SearchMin; - const FVector SearchStart=Center+Direction*PreviousDistance; - bool PreviousSolid=Shape.DensityAt(static_cast(SearchStart.X),static_cast(SearchStart.Y), - static_cast(SearchStart.Z))<0.0f; - double BestDifference=std::numeric_limits::infinity();bool bFound=false; - for(double Distance=std::min(PreviousDistance+Stride,SearchMax);Distance<=SearchMax+1.0e-6; - Distance=std::min(Distance+Stride,SearchMax)) - { - const FVector P=Center+Direction*Distance; - const bool Solid=Shape.DensityAt(static_cast(P.X),static_cast(P.Y), - static_cast(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(M.X),static_cast(M.Y), - static_cast(M.Z))<0.0f; - if(MidSolid==PreviousSolid)Lo=Mid;else Hi=Mid; - } - const double Crossing=(Lo+Hi)*0.5,Difference=std::abs(Crossing-PreferredRadius); - if(Difference<=AcceptBand&&Difference=SearchMax-1.0e-6)break; - PreviousDistance=Distance;PreviousSolid=Solid; - } - if(!bFound||BestDifference>8.0) - { - const double LocalMin=std::max(0.0,PreferredRadius-8.0); - const double LocalMax=std::min(SearchLength,PreferredRadius+8.0); - double LocalPrevious=LocalMin; - bool LocalPreviousSolid=Shape.DensityAt(static_cast(Center.X+Direction.X*LocalPrevious), - static_cast(Center.Y+Direction.Y*LocalPrevious), - static_cast(Center.Z+Direction.Z*LocalPrevious))<0.0f; - for(double Distance=std::min(LocalMin+1.0,LocalMax);Distance<=LocalMax+1.0e-6; - Distance=std::min(Distance+1.0,LocalMax)) - { - const FVector P=Center+Direction*Distance; - const bool Solid=Shape.DensityAt(static_cast(P.X),static_cast(P.Y), - static_cast(P.Z))<0.0f; - if(Solid!=LocalPreviousSolid) - { - double Lo=LocalPrevious,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(M.X),static_cast(M.Y), - static_cast(M.Z))<0.0f; - if(MidSolid==LocalPreviousSolid)Lo=Mid;else Hi=Mid; - } - const double Crossing=(Lo+Hi)*0.5,Difference=std::abs(Crossing-PreferredRadius); - if(Difference<=AcceptBand&&Difference=LocalMax-1.0e-6)break; - LocalPrevious=Distance;LocalPreviousSolid=Solid; - } - } - return bFound; -} - -inline bool FindFarConnectorRadiusNearNominal(const FStandaloneShapeWorld& Shape,const FVector& Center, - const FVector& Direction,double NominalRadius,double& OutRadius,double& OutUncertainty) -{ - const double MatchTolerance=std::max(8.0,NominalRadius*0.35); - const double HalfWidth=std::min(16.0,MatchTolerance*0.75); - const double Lo=std::max(0.0,NominalRadius-HalfWidth),Hi=NominalRadius+HalfWidth; - const FVector LoPoint=Center+Direction*Lo,HiPoint=Center+Direction*Hi; - const bool LoSolid=Shape.DensityAt(static_cast(LoPoint.X),static_cast(LoPoint.Y), - static_cast(LoPoint.Z))<0.0f; - const bool HiSolid=Shape.DensityAt(static_cast(HiPoint.X),static_cast(HiPoint.Y), - static_cast(HiPoint.Z))<0.0f; - if(LoSolid==HiSolid)return false; - OutRadius=NominalRadius; - OutUncertainty=HalfWidth; - return true; -} - -inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FVoxelTileKey& Key, - int32 Step,int32 Cells,FVoxelMeshData& Mesh) -{ - if(Key.Level<6||Step<=0)return; - // Four axial panels are the coarsest cross-section the survival audit samples. - constexpr int32 SideCount=4; - const FIntVector TileOrigin=Key.OriginVoxels(); - const int32 TileExtent=Key.ExtentVoxels(); - FStandaloneShapeWorld::FScopedForcedFarAirSamples SourceFieldScope(nullptr); - std::unique_ptr MeshIndex; - std::vector NewVertices; - std::vector NewNormals; - std::vector NewUVs; - std::vector NewColors; - std::vector NewTriangles; - const int32 OldVertexCount=Mesh.Vertices.Num(); - const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount); - 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& Connectors=Shape.GetFarConnectorSegments(); - std::vector CandidateIndices; - Shape.FindFarConnectorCandidates( - FIntVector(TileOrigin.X-64,TileOrigin.Y-64,TileOrigin.Z-64), - FIntVector(TileOrigin.X+TileExtent+64,TileOrigin.Y+TileExtent+64,TileOrigin.Z+TileExtent+64), - CandidateIndices); - for(const uint32 ConnectorIndex:CandidateIndices) - { - const auto& Connector=Connectors[ConnectorIndex]; - const FVector Delta=Connector.End-Connector.Start; - 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}; - bool bIntersects=true; - for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex) - { - const double EndAxis=Starts[AxisIndex]+Deltas[AxisIndex]; - if(std::max(Starts[AxisIndex],EndAxis)High[AxisIndex]) - {bIntersects=false;break;} - } - if(!bIntersects)continue; - - const double Length=std::sqrt(Delta.X*Delta.X+Delta.Y*Delta.Y+Delta.Z*Delta.Z); - if(Length<=1.0e-6)continue; - const FVector Axis=Delta/Length; - FVector U=FVector::CrossProduct(Axis,FVector::UpVector); - if(U.SizeSquared()<0.01)U=FVector::CrossProduct(Axis,FVector::RightVector); - U.Normalize(); - FVector V=FVector::CrossProduct(Axis,U);V.Normalize(); - const FVector RadialDirections[4]={U,U*-1.0,V,V*-1.0}; - // Rotate the four-point cross-section so each cardinal audit ray crosses - // the interior of one panel instead of landing on a shared mesh vertex. - const int32 SideForDirection[4]={3,1,0,2}; - double TMin=0.0,TMax=1.0; - bIntersects=true; - for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex) - { - if(std::abs(Deltas[AxisIndex])<1.0e-9) - {if(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 int32 AxialSampleCount=std::max(1,static_cast(std::ceil(Length/128.0))); - std::vector MissingMasks(static_cast(AxialSampleCount),0); - std::vector> SourceRadii(static_cast(AxialSampleCount)); - for(auto& Radii:SourceRadii)Radii.fill(0.0); - bool bAnyMissing=false; - bool bHasAuditSection=false; - for(int32 Sample=0;Sample(Sample)+0.5)/AxialSampleCount; - const FVector Center=Connector.Start+Delta*T; - if(!ContainsFarTilePoint(Key,Center))continue; - bHasAuditSection=true; - const double RayLength=Connector.Radius*2.0+32.0; - const double SourceRadiusBand=std::max(16.0,static_cast(Connector.Radius)*0.75); - const double MatchTolerance=std::max(8.0,static_cast(Connector.Radius)*0.35); - const double DetectionTolerance=MatchTolerance*0.98; - for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) - { - const FVector& Direction=RadialDirections[DirectionIndex]; - if(!ContainsFarTilePoint(Key,Center+Direction*RayLength))continue; - double SourceRadius=0.0,SourceUncertainty=0.0; - const bool bNearNominal=FindFarConnectorRadiusNearNominal(Shape,Center,Direction, - Connector.Radius,SourceRadius,SourceUncertainty); - if(!bNearNominal&&!FindFarConnectorSurfaceRadius(Shape,Center,Direction, - Connector.Radius,SourceRadius))continue; - if(std::abs(SourceRadius-Connector.Radius)>SourceRadiusBand)continue; - if(!MeshIndex)MeshIndex=std::make_unique(Mesh,TileOrigin,TileExtent,Cells,Step); - double BestDifference=MeshIndex->NearestHitDifference( - Center,Direction,RayLength,SourceRadius); - // Leave margin for the audit's finer 2-voxel density march and - // float-coordinate rounding at the tolerance boundary. - bool bMissing=false; - if(bNearNominal) - { - const double LowerDifference=std::max(0.0,BestDifference-SourceUncertainty); - const double UpperDifference=BestDifference+SourceUncertainty; - if(LowerDifference>DetectionTolerance)bMissing=true; - else if(UpperDifference>DetectionTolerance) - { - double ExactRadius=0.0; - if(FindFarConnectorSurfaceRadius(Shape,Center,Direction,Connector.Radius,ExactRadius)) - { - if(std::abs(ExactRadius-Connector.Radius)>SourceRadiusBand)continue; - SourceRadius=ExactRadius; - BestDifference=MeshIndex->NearestHitDifference(Center,Direction,RayLength,SourceRadius); - bMissing=BestDifference>DetectionTolerance; - } - else bMissing=true; - } - } - else bMissing=BestDifference>DetectionTolerance; - SourceRadii[static_cast(Sample)][DirectionIndex]=SourceRadius; - if(bMissing) - { - MissingMasks[static_cast(Sample)]|=static_cast(1u< Radii{}; }; - const auto MakeRing=[&](double T,int32 FirstSample,int32 LastSample,uint8 RequiredRadiusMask) - { - FRing Ring;Ring.T=T; - const FVector Center=Connector.Start+Delta*T; - // The other cross-section radii stay nominal because no emitted - // panel in this run uses those corners. - for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) - { - if((RequiredRadiusMask&(1u<(Sample)][DirectionIndex]; - if(Known>0.0){Radius=Known;break;} - } - } - Ring.Radii[DirectionIndex]=Radius; - } - return Ring; - }; - - int32 RunStart=0; - while(RunStart(RunStart)]){++RunStart;continue;} - int32 RunEnd=RunStart; - while(RunEnd+1(RunEnd+1)])++RunEnd; - const double RunMin=std::max(TMin,static_cast(RunStart)/AxialSampleCount); - const double RunMax=std::min(TMax,static_cast(RunEnd+1)/AxialSampleCount); - if(RunMax-RunMin>1.0e-7) - { - // Probe only radii used by faces that a missing audit ray needs. - uint8 RunRadiusMask=0; - for(int32 Sample=RunStart;Sample<=RunEnd;++Sample) - { - const uint8 Mask=MissingMasks[static_cast(Sample)]; - for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) - { - if((Mask&(1u<((1u<<0)|(1u<<2));break; - case 1:RunRadiusMask|=static_cast((1u<<1)|(1u<<2));break; - case 2:RunRadiusMask|=static_cast((1u<<1)|(1u<<3));break; - default:RunRadiusMask|=static_cast((1u<<0)|(1u<<3));break; - } - } - } - std::vector Rings; - const double MaxRingStep=128.0; - double CurrentT=RunMin; - Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask)); - while(CurrentT(Sample)]; - if(!Mask)continue; - const double SampleT=(static_cast(Sample)+0.5)/AxialSampleCount; - auto RightIt=std::lower_bound(Rings.begin(),Rings.end(),SampleT, - [](const FRing& Ring,double T){return Ring.TT-SampleT)<=1.0e-7) - { - for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) - { - if((Mask&(1u<(Sample)][DirectionIndex]; - if(Exact>0.0)RightIt->Radii[DirectionIndex]=Exact; - } - continue; - } - - if(RightIt==Rings.begin()||RightIt==Rings.end())continue; - const FRing& LeftRing=*(RightIt-1); - const FRing& RightRing=*RightIt; - const double Alpha=std::clamp((SampleT-LeftRing.T)/(RightRing.T-LeftRing.T),0.0,1.0); - bool bNeedsSampleRing=false; - for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) - { - if((Mask&(1u<(Sample)][DirectionIndex]; - const double Predicted=LeftRing.Radii[DirectionIndex]*(1.0-Alpha) - +RightRing.Radii[DirectionIndex]*Alpha; - const double Tolerance=std::max(8.0,static_cast(Connector.Radius)*0.35); - if(Actual>0.0&&std::abs(Predicted-Actual)>Tolerance*0.9) - {bNeedsSampleRing=true;break;} - } - if(!bNeedsSampleRing)continue; - FRing ExactRing;ExactRing.T=SampleT; - for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex) - { - ExactRing.Radii[DirectionIndex]=LeftRing.Radii[DirectionIndex]*(1.0-Alpha) - +RightRing.Radii[DirectionIndex]*Alpha; - if((Mask&(1u<(Sample)][DirectionIndex]; - if(Exact>0.0)ExactRing.Radii[DirectionIndex]=Exact; - } - Rings.insert(RightIt,std::move(ExactRing)); - bAddedSampleRing=true; - } - } - if(Rings.size()>=2) - { - std::vector IntervalMasks(Rings.size()-1,0); - std::vector> NeededSides(Rings.size()); - for(auto& Sides:NeededSides)Sides.fill(0); - for(int32 Sample=RunStart;Sample<=RunEnd;++Sample) - { - const uint8 Mask=MissingMasks[static_cast(Sample)]; - if(!Mask)continue; - const double SampleT=(static_cast(Sample)+0.5)/AxialSampleCount; - const auto Lower=std::lower_bound(Rings.begin(),Rings.end(),SampleT, - [](const FRing& Ring,double T){return Ring.T(Lower-Rings.begin()); - if(Right0)IntervalMasks[Right-1]|=Mask; - if(Right+10&&Right> RingVertexIndex(Rings.size()); - for(auto& Indices:RingVertexIndex)Indices.fill(INDEX_NONE); - for(size_t Ring=0;Ring(NewVertices.size()); - 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(Rings[Ring].T*Length/256.0), - static_cast(Side)/SideCount); - if(bHasColors)NewColors.push_back(Color); - } - } - for(size_t Ring=0;Ring+1(1u<(NewVertices.size()); - auto InsertVertices=[&](auto& Existing,const auto& Added) - { - using TArrayType=std::remove_reference_t; - TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast(Added.size())); - for(int32 I=0;I=OldSurfaceVertices)Index+=AddedVertices; - const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3); - TArray CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast(NewTriangles.size())); - for(int32 I=0;I(InCells)*InCells) - { - for(int32 Triangle=0;Triangle+2=Mesh.Vertices.Num()||B>=Mesh.Vertices.Num()||C>=Mesh.Vertices.Num())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(std::floor((MinX-Origin.X)/Step)),0,Cells-1); - const int32 X1=std::clamp(static_cast(std::floor((MaxX-Origin.X)/Step)),0,Cells-1); - const int32 Y0=std::clamp(static_cast(std::floor((MinY-Origin.Y)/Step)),0,Cells-1); - const int32 Y1=std::clamp(static_cast(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(Y)*Cells+X].push_back(Triangle/3); - } - } - - std::vector VerticalHits(double X,double Y,double ZMin,double ZMax) const - { - const int32 CX=std::clamp(static_cast(std::floor((X-Origin.X)/Step)),0,Cells-1); - const int32 CY=std::clamp(static_cast(std::floor((Y-Origin.Y)/Step)),0,Cells-1); - const auto& Candidates=ByColumn[static_cast(CY)*Cells+CX]; - std::vector Hits;Hits.reserve(8); - for(const int32 Triangle:Candidates) - { - const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],IC=Mesh.Triangles[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); - } - std::sort(Hits.begin(),Hits.end()); - Hits.erase(std::unique(Hits.begin(),Hits.end(),[](double A,double B){return std::abs(A-B)<=0.25;}),Hits.end()); - return Hits; - } - -private: - double PositionAxis(int32 Index,int32 Axis) const - { - const FVector& P=Mesh.Vertices[Index]; - return (Axis==0?P.X:(Axis==1?P.Y:P.Z))*0.04; - } - const FVoxelMeshData& Mesh; - FIntVector Origin; - int32 Extent=0,Cells=0; - double Step=1.0; - std::vector> ByColumn; -}; - -inline std::vector FindFarStratumCrossings(const FStandaloneShapeWorld& Shape, - double X,double Y,double ZMin,double ZMax) -{ - std::vector Crossings; - if(ZMax<=ZMin)return Crossings; - double PreviousZ=ZMin; - bool PreviousSolid=Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(PreviousZ))<0.0f; - for(double Z=std::min(ZMin+1.0,ZMax);Z<=ZMax+1.0e-6;Z=std::min(Z+1.0,ZMax)) - { - const bool Solid=Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(Z))<0.0f; - if(Solid!=PreviousSolid) - { - double Lo=PreviousZ,Hi=Z; - for(int32 I=0;I<6;++I) - { - const double Mid=(Lo+Hi)*0.5; - if((Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(Mid))<0.0f)==PreviousSolid)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; -} - -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 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(Params.BoundarySealThickness)+8.0); - Boundaries.push_back({static_cast(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth}); - Boundaries.push_back({static_cast(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth}); - } - std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z TileBoundaries; - for(const FBoundary& Boundary:Boundaries) - if(Boundary.Z+Boundary.HalfWidth>=Origin.Z&&Boundary.Z-Boundary.HalfWidth<=Origin.Z+Extent) - TileBoundaries.push_back(Boundary); - if(TileBoundaries.empty())return; - - const int32 OldVertexCount=Mesh.Vertices.Num(); - const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount); - 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 int32 Cells=std::max(1,World.CoarseTileCells); - FFarMeshColumnIndex MeshIndex(Mesh,Origin,Extent,Cells,Step); - std::vector NewVertices,NewNormals; - std::vector NewUVs;std::vector NewColors; - std::vector NewTriangles; - static constexpr int32 ColumnOffsets[4]={1,5,9,13}; - const auto Density=[&](double X,double Y,double Z) - {return Shape.DensityAt(static_cast(X),static_cast(Y),static_cast(Z));}; - for(const FBoundary& Boundary:TileBoundaries) - { - const double ZMin=Boundary.Z-Boundary.HalfWidth,ZMax=Boundary.Z+Boundary.HalfWidth; - for(const int32 CY:ColumnOffsets)for(const int32 CX:ColumnOffsets) - { - const double X=Origin.X+(CX+0.371)*Step,Y=Origin.Y+(CY+0.619)*Step; - const std::vector Source=FindFarStratumCrossings(Shape,X,Y,ZMin,ZMax); - if(Source.empty())continue; - std::vector Hits=MeshIndex.VerticalHits(X,Y,ZMin,ZMax); - std::vector Used(Hits.size(),0); - const double MatchTolerance=std::max(4.0,Step*1.5); - for(const double Crossing:Source) - { - size_t Best=Hits.size();double BestDistance=std::numeric_limits::infinity(); - for(size_t I=0;I(Origin.X,X-HalfSize),X1=std::min(Origin.X+Extent,X+HalfSize); - const double Y0=std::max(Origin.Y,Y-HalfSize),Y1=std::min(Origin.Y+Extent,Y+HalfSize); - if(X1-X0<1.0||Y1-Y0<1.0)continue; - const auto SurfaceZ=[&](double PX,double PY) - { - if(std::abs(GZ)<1.0e-5)return Crossing; - const double Offset=-(GX*(PX-X)+GY*(PY-Y))/GZ; - return std::clamp(Crossing+Offset,ZMin,ZMax); - }; - const int32 Base=static_cast(NewVertices.size()); - const double Xs[4]={X0,X1,X1,X0},Ys[4]={Y0,Y0,Y1,Y1}; - for(int32 Corner=0;Corner<4;++Corner) - { - const double Z=SurfaceZ(Xs[Corner],Ys[Corner]); - NewVertices.emplace_back(Xs[Corner]*25.0,Ys[Corner]*25.0,Z*25.0); - NewNormals.push_back(Normal); - if(bHasUVs)NewUVs.emplace_back(static_cast((Xs[Corner]-Origin.X)/256.0), - static_cast((Ys[Corner]-Origin.Y)/256.0)); - if(bHasColors)NewColors.push_back(Color); - } - const bool bBelowSolid=Density(X,Y,Crossing-1.0)<0.0f; - if(bBelowSolid)NewTriangles.insert(NewTriangles.end(),{Base,Base+1,Base+3,Base+1,Base+2,Base+3}); - else NewTriangles.insert(NewTriangles.end(),{Base,Base+3,Base+1,Base+1,Base+3,Base+2}); - } - } - } - if(NewVertices.empty()||NewTriangles.empty())return; - const int32 AddedVertices=static_cast(NewVertices.size()); - auto InsertVertices=[&](auto& Existing,const auto& Added) - { - using TArrayType=std::remove_reference_t; - TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast(Added.size())); - for(int32 I=0;I=OldSurfaceVertices)Index+=AddedVertices; - const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3); - TArray CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast(NewTriangles.size())); - for(int32 I=0;IB.PriorityDistanceSquared; + if(A.PriorityRank!=B.PriorityRank)return A.PriorityRank>B.PriorityRank; if(A.Key.Level!=B.Key.Level)return A.Key.Level>B.Key.Level; if(A.Key.Coord.Z!=B.Key.Coord.Z)return A.Key.Coord.Z>B.Key.Coord.Z; if(A.Key.Coord.Y!=B.Key.Coord.Y)return A.Key.Coord.Y>B.Key.Coord.Y; @@ -1208,10 +401,11 @@ public: bool bInSkipUvAndColors=false, bool bInSkipMaterialGroups=false, bool bInSkipUV=false, - bool bInSkipColors=false) - :World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,16)),bHashDensity(bInHashDensity), + bool bInSkipColors=false, + VoxelForgeCore::World* InCoreWorld=nullptr) + :World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,8)),bHashDensity(bInHashDensity), MeshCache(InMeshCache),MaxLevelFilter(InMaxLevelFilter), - bSkipUvAndColors(bInSkipUvAndColors),bSkipMaterialGroups(bInSkipMaterialGroups), + CoreWorld(InCoreWorld),bSkipUvAndColors(bInSkipUvAndColors),bSkipMaterialGroups(bInSkipMaterialGroups), bSkipUV(bInSkipUV),bSkipColors(bInSkipColors) { VoxelGenLOD::InitializeConsoleSwitches(); @@ -1260,6 +454,13 @@ public: uint64 CrossingIndex=0) { FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraPositionVoxels); + if(CoreWorld) + { + const auto CoreTiles=CoreWorld->desiredTiles({CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z}); + NewDesired.Tiles.clear();NewDesired.Tiles.reserve(CoreTiles.size()); + for(const auto& Key:CoreTiles) + NewDesired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); + } if(MaxLevelFilter>=0) NewDesired.Tiles.erase(std::remove_if(NewDesired.Tiles.begin(),NewDesired.Tiles.end(), [&](const FVoxelTileKey& Key){return Key.Level>MaxLevelFilter;}),NewDesired.Tiles.end()); @@ -1293,9 +494,19 @@ public: Stats.CancelledByCrossing.reserve(CancelledByCrossing.size()); for(const auto& Pair:CancelledByCrossing)Stats.CancelledByCrossing.push_back(Pair); - for(FStandaloneTileJob& Job:Queue) - Job.PriorityDistanceSquared=StandaloneWorldMesher::TileDistanceSquaredChunks( - Job.Key,NewDesired.CenterChunk); + std::vector Unordered;Unordered.reserve(NewDesired.Tiles.size()); + for(const FVoxelTileKey& Key:NewDesired.Tiles) + Unordered.push_back({{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level}); + const auto Prioritized=VoxelForgeCore::World::prioritizeTiles(Unordered, + {NewDesired.CenterChunk.X,NewDesired.CenterChunk.Y,NewDesired.CenterChunk.Z}); + std::unordered_map PriorityRanks; + PriorityRanks.reserve(Prioritized.size()*2+1); + for(size_t Rank=0;RankStrateContentCutMinLevel<9&&NewDesired.bHasStrate ? NewDesired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32; @@ -1304,8 +515,7 @@ public: for(const FVoxelTileKey& Key:NewDesired.Tiles) { if(Resident.contains(Key)||Outstanding.contains(Key))continue; - Queue.push_back({Key,BandMin,BandMax,CrossingIndex, - StandaloneWorldMesher::TileDistanceSquaredChunks(Key,NewDesired.CenterChunk)}); + Queue.push_back({Key,BandMin,BandMax,CrossingIndex,PriorityRanks.at(Key)}); Outstanding.insert(Key);++Stats.NewJobs; } CurrentDesired=std::move(NewDesired); @@ -1365,20 +575,52 @@ private: const int32 Extent=Job.Key.ExtentVoxels(); const int32 Cells=LevelFullResClipLevels?CHUNK_SIZE:FMath::Max(1,World->CoarseTileCells); const int32 Step=FMath::Max(1,Extent/Cells); - const FStandaloneShapeWorld* Shape=FStandaloneShapeWorldAccess::GetActive(); int64 SampleCount=0; const bool bProfile=VoxelDensityProfile::GetMode()!=VoxelDensityProfile::EMode::Disabled; const VoxelDensityProfile::FSnapshot Before=bProfile ? VoxelDensityProfile::SnapshotCurrentThread():VoxelDensityProfile::FSnapshot{}; + if(CoreWorld) + { + VoxelForgeCore::TileGenerationOptions Options; + Options.bandMinZVoxel=Job.BandZMin;Options.bandMaxZVoxel=Job.BandZMax; + Options.hashDensity=bHashDensity;Options.skipUvAndColors=bSkipUvAndColors; + Options.skipMaterialGroups=bSkipMaterialGroups;Options.skipUv=bSkipUV;Options.skipColors=bSkipColors; + std::string Error; + auto Result=CoreWorld->generateTile( + {{Job.Key.Coord.X,Job.Key.Coord.Y,Job.Key.Coord.Z},Job.Key.Level},Options,Error); + if(!Result) + { + std::fprintf(stderr,"[VoxelForgeCore] tile generation failed: %s\n",Error.c_str()); + Out.WorkerSeconds=std::chrono::duration(FClock::now()-StartTime).count(); + Out.CompletedAt=FClock::now();return Out; + } + Out.DensitySamples=Result->densitySampleCount; + Out.MeshPayloadAllocatedBytes=Result->meshPayloadAllocatedBytes; + Out.DensityHash=Result->densityHash;Out.MeshHash=Result->meshHash; + Out.PayloadHash=Result->payloadHash;Out.TopologyHash=Result->topologyHash; + Out.bHasDensityHash=Result->hasDensityHash;Out.bHasMeshHash=Result->hasMeshHash; + Out.bHasPayloadHash=Result->hasPayloadHash;Out.bHasTopologyHash=Result->hasTopologyHash; + Out.Vertices.reserve(Result->vertices.size()); + for(const auto& Vertex:Result->vertices) + Out.Vertices.push_back({ + {static_cast(Vertex.position.x*0.01),static_cast(Vertex.position.y*0.01), + static_cast(Vertex.position.z*0.01)}, + {static_cast(Vertex.normal.x),static_cast(Vertex.normal.y), + static_cast(Vertex.normal.z)}}); + Out.Indices=std::move(Result->indices); + if(bProfile) + { + const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread(); + for(size_t I=0;I=Before.Cycles[I]?After.Cycles[I]-Before.Cycles[I]:0; + } + Out.WorkerSeconds=std::chrono::duration(FClock::now()-StartTime).count(); + Out.CompletedAt=FClock::now();return Out; + } std::vector DensityValues; FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr, Job.BandZMin,Job.BandZMax,&SampleCount,nullptr,nullptr, bHashDensity?&DensityValues:nullptr); - if(Shape&&Shape->HasFarWorld()) - { - AppendFarStratumShells(*World,*Shape,Job.Key,Step,Mesh); - AppendFarConnectorShells(*Shape,Job.Key,Step,World->CoarseTileCells,Mesh); - } if(bHashDensity) { Out.bHasDensityHash=true; @@ -1453,6 +695,7 @@ private: bool bHashDensity=false; const FStandaloneMeshCache* MeshCache=nullptr; int32 MaxLevelFilter=-1; + VoxelForgeCore::World* CoreWorld=nullptr; bool bSkipUvAndColors=false; bool bSkipMaterialGroups=false; bool bSkipUV=false; diff --git a/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp b/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp index c8266fb..7f63c64 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -6,7 +6,6 @@ #include "VoxelGenerator.h" #include "StandaloneResearchTunnels.h" -#include "StandaloneShapeWorld.h" #include "VoxelSettings.h" #include "VoxelStrateManager.h" #include "VoxelStrateDefinition.h" @@ -2850,12 +2849,6 @@ 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. @@ -4076,19 +4069,14 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ, bFinalFieldDifferent); } Result = FStandaloneResearchTunnels::Apply( - Result,WorldX,WorldY,WorldZ,static_cast(Seed)); + Result,WorldX,WorldY,WorldZ,static_cast(Seed), + bStandaloneResearchRoundTunnels,bStandaloneResearchEnhancedWalls); return Result; } float UVoxelGenerator::GetDensityAtLattice(int32 WorldX, int32 WorldY, int32 WorldZ, float* OutPreEditDensity) const { - if (const FStandaloneShapeWorld* ShapeWorld=FStandaloneShapeWorldAccess::GetActive()) - { - const float ShapeDensity=ShapeWorld->DensityAtLattice(WorldX,WorldY,WorldZ); - if (OutPreEditDensity) *OutPreEditDensity=ShapeDensity; - return ShapeDensity; - } return GetDensityAt(static_cast(WorldX),static_cast(WorldY), static_cast(WorldZ),OutPreEditDensity); } @@ -6501,19 +6489,6 @@ 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 diff --git a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index f39887d..7a9e9d1 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -563,9 +563,19 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, { VoxelDensityProfile::RecordClassifierCall(bSampleClassifier); } - const EVoxelTileClass Verdict = Generator->ClassifyTile( - BlockOrigin, Step, BlockCells, - bSampleClassifier ? &ClassifierStats : nullptr); + EVoxelTileClass Verdict; + if(ShapeWorldContext) + { + const auto ShapeVerdict=ShapeWorldContext->ClassifyBox( + BlockOrigin,Step,BlockCells); + Verdict=ShapeVerdict==EVoxelTileClass::AllSolid + ?EVoxelTileClass::AllSolid + :ShapeVerdict==EVoxelTileClass::AllAir + ?EVoxelTileClass::AllAir:EVoxelTileClass::Mixed; + } + else + Verdict=Generator->ClassifyTile(BlockOrigin,Step,BlockCells, + bSampleClassifier ? &ClassifierStats : nullptr); if (bSampleClassifier) { VoxelDensityProfile::AddSampledMeasurement( @@ -700,10 +710,13 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, { for (int32 X = 0; X <= BlockCells; ++X) { - const float Density = Generator->GetDensityAt( - BlockOrigin.X + X * Step, - BlockOrigin.Y + Y * Step, - BlockOrigin.Z + Z * Step); + const float Density = ShapeWorldContext + ?ShapeWorldContext->DensityAt( + static_cast(BlockOrigin.X+X*Step), + static_cast(BlockOrigin.Y+Y*Step), + static_cast(BlockOrigin.Z+Z*Step)) + :Generator->GetDensityAt(BlockOrigin.X+X*Step, + BlockOrigin.Y+Y*Step,BlockOrigin.Z+Z*Step); if (!VoxelMath::IsFinite(Density) || Density == 0.0f) { bAllSolid = false; @@ -1003,11 +1016,13 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, ((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY) * ReachBlocksPerAxis + ReachBlockX; float PreEditDensity = 0.0f; - DensityGrid[LocalIndex] = Generator->GetDensityAtLattice( - OriginVoxels.X + gx * Step, - OriginVoxels.Y + gy * Step, - OriginVoxels.Z + gz * Step, - bCapturePreEditGrid ? &PreEditDensity : nullptr); + const int32 SampleX=OriginVoxels.X+gx*Step; + const int32 SampleY=OriginVoxels.Y+gy*Step; + const int32 SampleZ=OriginVoxels.Z+gz*Step; + DensityGrid[LocalIndex] = ShapeWorldContext + ?ShapeWorldContext->DensityAtLattice(SampleX,SampleY,SampleZ) + :Generator->GetDensityAtLattice(SampleX,SampleY,SampleZ, + bCapturePreEditGrid ? &PreEditDensity : nullptr); if (bCapturePreEditGrid) { OutPreEditDensityGrid->Samples[LocalIndex] = PreEditDensity; @@ -1024,7 +1039,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, // lower overhead when a tile spans only one room-graph region. const int64 TileExtentVoxels = static_cast(Step) * CellsPerAxis; const bool bUseVerticalCacheTraversal = TileExtentVoxels > CHUNK_SIZE * 16; - const FStandaloneShapeWorld* ActiveShapeWorld=FStandaloneShapeWorldAccess::GetActive(); + const FStandaloneShapeWorld* ActiveShapeWorld=ShapeWorldContext; const bool bUseShapeWorldXBatch=ActiveShapeWorld!=nullptr &&!bUseSharedDensityGrid&&!bCapturePreEditGrid; if (bUseVerticalCacheTraversal) diff --git a/Standalone/Source/VoxelForge/Public/VoxelGenerator.h b/Standalone/Source/VoxelForge/Public/VoxelGenerator.h index 72d80ff..2feda1e 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Standalone/Source/VoxelForge/Public/VoxelGenerator.h @@ -118,6 +118,11 @@ public: // Les formes créatives viennent ensuite des params de strate. int32 Seed = 0; + // Standalone host options are carried on this generator instance; the density result never + // depends on a module-wide switch or which worker happens to evaluate the sample. + bool bStandaloneResearchRoundTunnels = false; + bool bStandaloneResearchEnhancedWalls = true; + // Authored radius (voxels) of the guaranteed flat-floored landing room at world XY (0,0) in // every strate — the old “spine” setting now sizes a room and never creates a vertical bore. // 0 disables the origin landing rooms. Copied from VoxelSettings. diff --git a/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h b/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h index 0cb97ab..3f65baa 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h +++ b/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h @@ -18,6 +18,8 @@ #include "VoxelGenerator.h" #include "VoxelMarchingCubesMesher.generated.h" +class FStandaloneShapeWorld; + // A run-scoped, read-only density lattice. The explorer owns the storage and installs the // pointer only while its canonical tiles are being meshed. The lattice includes the one-point // halo needed by every tile, so each world sample is generated once and copied into both adjacent @@ -101,6 +103,10 @@ public: // the canonical generator path rather than changing geometry. void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; } + // Standalone's interval world is an explicit immutable sampling context. Game builds do not + // set this pointer and continue through Generator. + void SetStandaloneShapeWorld(const FStandaloneShapeWorld* InWorld) { ShapeWorldContext = InWorld; } + private: FORCEINLINE bool ShouldAbortWork() const { @@ -109,6 +115,7 @@ private: const std::atomic* ShutdownFlag = nullptr; const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr; + const FStandaloneShapeWorld* ShapeWorldContext = nullptr; public: