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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
631dd57eb4
commit
557f6aac30
22 files changed
+6185
-5063
No files matched your search
+55
-45
@@ -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" }
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,235 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<MeshVertex> vertices;
|
||||
std::vector<std::uint32_t> indices;
|
||||
std::vector<float> 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<ResolvedWorldSnapshot> FromJson(
|
||||
std::string_view json, std::string& error);
|
||||
|
||||
explicit operator bool() const noexcept { return static_cast<bool>(data_); }
|
||||
const WorldConfiguration& configuration() const;
|
||||
|
||||
private:
|
||||
struct Data;
|
||||
explicit ResolvedWorldSnapshot(std::shared_ptr<const Data> data) : data_(std::move(data)) {}
|
||||
std::shared_ptr<const Data> 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<EditBrush> brushes;
|
||||
};
|
||||
|
||||
struct EditResult
|
||||
{
|
||||
std::uint64_t digest = 0;
|
||||
std::vector<std::vector<TileKey>> 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<TileKey> desiredTiles(Vec3d cameraPositionVoxels) const;
|
||||
static std::vector<TileKey> prioritizeTiles(std::span<const TileKey> tiles,
|
||||
Vec3i centerChunk);
|
||||
std::optional<TileMesh> generateTile(const TileKey& key,
|
||||
const TileGenerationOptions& options,
|
||||
std::string& error) const;
|
||||
|
||||
std::optional<EditResult> applyEditTransaction(const EditTransaction& transaction,
|
||||
std::span<const TileKey> desiredTiles,
|
||||
std::string& error);
|
||||
std::vector<FarConnectorSegment> farConnectorSegments() const;
|
||||
std::vector<std::uint32_t> 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> impl_;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,821 @@
|
||||
#pragma once
|
||||
|
||||
#include "OwnerWorldLoader.h"
|
||||
#include "StandaloneShapeWorld.h"
|
||||
#include "VoxelTypes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<size_t>(Cells)*Cells*Cells),
|
||||
TriangleMarks(static_cast<size_t>(InMesh.Triangles.Num()/3),0)
|
||||
{
|
||||
for(int32 Triangle=0;Triangle+2<Mesh.Triangles.Num();Triangle+=3)
|
||||
{
|
||||
const int32 A=Mesh.Triangles[Triangle],B=Mesh.Triangles[Triangle+1],C=Mesh.Triangles[Triangle+2];
|
||||
if(A<0||B<0||C<0||A>=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<double>::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<size_t>(Triangle)]==QueryStamp)continue;
|
||||
TriangleMarks[static_cast<size_t>(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<double>::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<<Axis;
|
||||
for(int32 Subset=1;Subset<8;++Subset)
|
||||
{
|
||||
if(Subset&~CrossingMask)continue;
|
||||
int32 X=Cell[0],Y=Cell[1],Z=Cell[2];
|
||||
if(Subset&1)X+=CellStep[0];
|
||||
if(Subset&2)Y+=CellStep[1];
|
||||
if(Subset&4)Z+=CellStep[2];
|
||||
TestCell(X,Y,Z);
|
||||
}
|
||||
for(int32 Axis=0;Axis<3;++Axis)
|
||||
if(CrossingMask&(1<<Axis))
|
||||
{Cell[Axis]+=CellStep[Axis];NextBoundary[Axis]+=BoundaryDelta[Axis];}
|
||||
}
|
||||
return BestDifference;
|
||||
}
|
||||
|
||||
private:
|
||||
int32 CellFor(double Value,int32 Axis)const
|
||||
{return std::clamp(static_cast<int32>(std::floor((Value-(&Origin.X)[Axis])/Step)),0,Cells-1);}
|
||||
size_t CellOffset(int32 X,int32 Y,int32 Z)const
|
||||
{return (static_cast<size_t>(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<std::vector<int32>> ByCell;
|
||||
std::vector<uint32> 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<float>(SearchStart.X),static_cast<float>(SearchStart.Y),
|
||||
static_cast<float>(SearchStart.Z))<0.0f;
|
||||
double BestDifference=std::numeric_limits<double>::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<float>(P.X),static_cast<float>(P.Y),
|
||||
static_cast<float>(P.Z))<0.0f;
|
||||
if(Solid!=PreviousSolid)
|
||||
{
|
||||
double Lo=PreviousDistance,Hi=Distance;
|
||||
for(int32 I=0;I<6;++I)
|
||||
{
|
||||
const double Mid=(Lo+Hi)*0.5;const FVector M=Center+Direction*Mid;
|
||||
const bool MidSolid=Shape.DensityAt(static_cast<float>(M.X),static_cast<float>(M.Y),
|
||||
static_cast<float>(M.Z))<0.0f;
|
||||
if(MidSolid==PreviousSolid)Lo=Mid;else Hi=Mid;
|
||||
}
|
||||
const double Crossing=(Lo+Hi)*0.5,Difference=std::abs(Crossing-PreferredRadius);
|
||||
if(Difference<=AcceptBand&&Difference<BestDifference)
|
||||
{OutRadius=Crossing;BestDifference=Difference;bFound=true;}
|
||||
}
|
||||
if(Distance>=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<float>(Center.X+Direction.X*LocalPrevious),
|
||||
static_cast<float>(Center.Y+Direction.Y*LocalPrevious),
|
||||
static_cast<float>(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<float>(P.X),static_cast<float>(P.Y),
|
||||
static_cast<float>(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<float>(M.X),static_cast<float>(M.Y),
|
||||
static_cast<float>(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<BestDifference)
|
||||
{OutRadius=Crossing;BestDifference=Difference;bFound=true;}
|
||||
}
|
||||
if(Distance>=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<float>(LoPoint.X),static_cast<float>(LoPoint.Y),
|
||||
static_cast<float>(LoPoint.Z))<0.0f;
|
||||
const bool HiSolid=Shape.DensityAt(static_cast<float>(HiPoint.X),static_cast<float>(HiPoint.Y),
|
||||
static_cast<float>(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<FFarMeshSegmentIndex> MeshIndex;
|
||||
std::vector<FVector> NewVertices;
|
||||
std::vector<FVector> NewNormals;
|
||||
std::vector<FVector2f> NewUVs;
|
||||
std::vector<FColor> NewColors;
|
||||
std::vector<int32> NewTriangles;
|
||||
const int32 OldVertexCount=Mesh.Vertices.Num();
|
||||
const 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<uint32> CandidateIndices;
|
||||
Shape.FindFarConnectorCandidates(
|
||||
FIntVector(TileOrigin.X-64,TileOrigin.Y-64,TileOrigin.Z-64),
|
||||
FIntVector(TileOrigin.X+TileExtent+64,TileOrigin.Y+TileExtent+64,TileOrigin.Z+TileExtent+64),
|
||||
CandidateIndices);
|
||||
for(const uint32 ConnectorIndex:CandidateIndices)
|
||||
{
|
||||
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)<Low[AxisIndex]
|
||||
||std::min(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]<Low[AxisIndex]||Starts[AxisIndex]>High[AxisIndex])bIntersects=false;continue;}
|
||||
double A=(Low[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex];
|
||||
double B=(High[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex];
|
||||
if(A>B)std::swap(A,B);
|
||||
TMin=std::max(TMin,A);TMax=std::min(TMax,B);
|
||||
if(TMin>TMax){bIntersects=false;break;}
|
||||
}
|
||||
if(!bIntersects||TMax-TMin<1.0e-6)continue;
|
||||
const int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/128.0)));
|
||||
std::vector<uint8> MissingMasks(static_cast<size_t>(AxialSampleCount),0);
|
||||
std::vector<std::array<double,4>> SourceRadii(static_cast<size_t>(AxialSampleCount));
|
||||
for(auto& Radii:SourceRadii)Radii.fill(0.0);
|
||||
bool bAnyMissing=false;
|
||||
bool bHasAuditSection=false;
|
||||
for(int32 Sample=0;Sample<AxialSampleCount;++Sample)
|
||||
{
|
||||
const double T=(static_cast<double>(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<double>(Connector.Radius)*0.75);
|
||||
const double MatchTolerance=std::max(8.0,static_cast<double>(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<FFarMeshSegmentIndex>(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<size_t>(Sample)][DirectionIndex]=SourceRadius;
|
||||
if(bMissing)
|
||||
{
|
||||
MissingMasks[static_cast<size_t>(Sample)]|=static_cast<uint8>(1u<<DirectionIndex);
|
||||
bAnyMissing=true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!bHasAuditSection||!bAnyMissing)continue;
|
||||
|
||||
struct FRing { double T=0.0;std::array<double,4> 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<<DirectionIndex))==0)
|
||||
{Ring.Radii[DirectionIndex]=Connector.Radius;continue;}
|
||||
double Radius=Connector.Radius;
|
||||
double RadiusUncertainty=0.0;
|
||||
if(!FindFarConnectorRadiusNearNominal(Shape,Center,
|
||||
RadialDirections[DirectionIndex],Connector.Radius,Radius,RadiusUncertainty)
|
||||
&&!FindFarConnectorSurfaceRadius(Shape,Center,RadialDirections[DirectionIndex],
|
||||
Connector.Radius,Radius))
|
||||
{
|
||||
for(int32 Sample=FirstSample;Sample<=LastSample;++Sample)
|
||||
{
|
||||
const double Known=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
|
||||
if(Known>0.0){Radius=Known;break;}
|
||||
}
|
||||
}
|
||||
Ring.Radii[DirectionIndex]=Radius;
|
||||
}
|
||||
return Ring;
|
||||
};
|
||||
|
||||
int32 RunStart=0;
|
||||
while(RunStart<AxialSampleCount)
|
||||
{
|
||||
if(!MissingMasks[static_cast<size_t>(RunStart)]){++RunStart;continue;}
|
||||
int32 RunEnd=RunStart;
|
||||
while(RunEnd+1<AxialSampleCount&&MissingMasks[static_cast<size_t>(RunEnd+1)])++RunEnd;
|
||||
const double RunMin=std::max(TMin,static_cast<double>(RunStart)/AxialSampleCount);
|
||||
const double RunMax=std::min(TMax,static_cast<double>(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<size_t>(Sample)];
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
switch(SideForDirection[DirectionIndex])
|
||||
{
|
||||
case 0:RunRadiusMask|=static_cast<uint8>((1u<<0)|(1u<<2));break;
|
||||
case 1:RunRadiusMask|=static_cast<uint8>((1u<<1)|(1u<<2));break;
|
||||
case 2:RunRadiusMask|=static_cast<uint8>((1u<<1)|(1u<<3));break;
|
||||
default:RunRadiusMask|=static_cast<uint8>((1u<<0)|(1u<<3));break;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<FRing> Rings;
|
||||
const double MaxRingStep=128.0;
|
||||
double CurrentT=RunMin;
|
||||
Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask));
|
||||
while(CurrentT<RunMax-1.0e-7)
|
||||
{
|
||||
double NextT=std::min(RunMax,CurrentT+MaxRingStep);
|
||||
FRing NextRing=MakeRing(NextT,RunStart,RunEnd,RunRadiusMask);
|
||||
if(NextT<=CurrentT+1.0e-8)break;
|
||||
Rings.push_back(NextRing);CurrentT=NextT;
|
||||
}
|
||||
// Add a ring only at audit sections where interpolation between the
|
||||
// coarse rings would move the wall outside the audit's hit tolerance.
|
||||
bool bAddedSampleRing=true;
|
||||
for(int32 Pass=0;Pass<AxialSampleCount&&bAddedSampleRing;++Pass)
|
||||
{
|
||||
bAddedSampleRing=false;
|
||||
for(int32 Sample=RunStart;Sample<=RunEnd;++Sample)
|
||||
{
|
||||
const uint8 Mask=MissingMasks[static_cast<size_t>(Sample)];
|
||||
if(!Mask)continue;
|
||||
const double SampleT=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
|
||||
auto RightIt=std::lower_bound(Rings.begin(),Rings.end(),SampleT,
|
||||
[](const FRing& Ring,double T){return Ring.T<T;});
|
||||
if(RightIt!=Rings.end()&&std::abs(RightIt->T-SampleT)<=1.0e-7)
|
||||
{
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
const double Exact=SourceRadii[static_cast<size_t>(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<<DirectionIndex))==0)continue;
|
||||
const double Actual=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
|
||||
const double Predicted=LeftRing.Radii[DirectionIndex]*(1.0-Alpha)
|
||||
+RightRing.Radii[DirectionIndex]*Alpha;
|
||||
const double Tolerance=std::max(8.0,static_cast<double>(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<<DirectionIndex))==0)continue;
|
||||
const double Exact=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
|
||||
if(Exact>0.0)ExactRing.Radii[DirectionIndex]=Exact;
|
||||
}
|
||||
Rings.insert(RightIt,std::move(ExactRing));
|
||||
bAddedSampleRing=true;
|
||||
}
|
||||
}
|
||||
if(Rings.size()>=2)
|
||||
{
|
||||
std::vector<uint8> IntervalMasks(Rings.size()-1,0);
|
||||
std::vector<std::array<uint8,SideCount>> NeededSides(Rings.size());
|
||||
for(auto& Sides:NeededSides)Sides.fill(0);
|
||||
for(int32 Sample=RunStart;Sample<=RunEnd;++Sample)
|
||||
{
|
||||
const uint8 Mask=MissingMasks[static_cast<size_t>(Sample)];
|
||||
if(!Mask)continue;
|
||||
const double SampleT=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
|
||||
const auto Lower=std::lower_bound(Rings.begin(),Rings.end(),SampleT,
|
||||
[](const FRing& Ring,double T){return Ring.T<T;});
|
||||
const size_t Right=static_cast<size_t>(Lower-Rings.begin());
|
||||
if(Right<Rings.size()&&std::abs(Rings[Right].T-SampleT)<=1.0e-7)
|
||||
{
|
||||
if(Right>0)IntervalMasks[Right-1]|=Mask;
|
||||
if(Right+1<Rings.size())IntervalMasks[Right]|=Mask;
|
||||
}
|
||||
else if(Right>0&&Right<Rings.size())IntervalMasks[Right-1]|=Mask;
|
||||
}
|
||||
for(size_t Ring=0;Ring<IntervalMasks.size();++Ring)
|
||||
{
|
||||
const uint8 Mask=IntervalMasks[Ring];
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
const int32 Side=SideForDirection[DirectionIndex];
|
||||
const int32 Next=(Side+1)%SideCount;
|
||||
NeededSides[Ring][Side]=NeededSides[Ring][Next]=1;
|
||||
NeededSides[Ring+1][Side]=NeededSides[Ring+1][Next]=1;
|
||||
}
|
||||
}
|
||||
std::vector<std::array<int32,SideCount>> RingVertexIndex(Rings.size());
|
||||
for(auto& Indices:RingVertexIndex)Indices.fill(INDEX_NONE);
|
||||
for(size_t Ring=0;Ring<Rings.size();++Ring)
|
||||
{
|
||||
if(std::none_of(NeededSides[Ring].begin(),NeededSides[Ring].end(),
|
||||
[](uint8 Needed){return Needed!=0;}))continue;
|
||||
const FVector Center=Connector.Start+Delta*Rings[Ring].T;
|
||||
for(int32 Side=0;Side<SideCount;++Side)
|
||||
{
|
||||
if(!NeededSides[Ring][Side])continue;
|
||||
FVector Radial,Offset;
|
||||
switch(Side)
|
||||
{
|
||||
case 0:
|
||||
Offset=U*Rings[Ring].Radii[0]+V*Rings[Ring].Radii[2];
|
||||
Radial=U+V;break;
|
||||
case 1:
|
||||
Offset=U*(-Rings[Ring].Radii[1])+V*Rings[Ring].Radii[2];
|
||||
Radial=U*-1.0+V;break;
|
||||
case 2:
|
||||
Offset=U*(-Rings[Ring].Radii[1])+V*(-Rings[Ring].Radii[3]);
|
||||
Radial=U*-1.0+V*-1.0;break;
|
||||
default:
|
||||
Offset=U*Rings[Ring].Radii[0]+V*(-Rings[Ring].Radii[3]);
|
||||
Radial=U+V*-1.0;break;
|
||||
}
|
||||
Radial.Normalize();
|
||||
const FVector P=Center+Offset;
|
||||
RingVertexIndex[Ring][Side]=static_cast<int32>(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<float>(Rings[Ring].T*Length/256.0),
|
||||
static_cast<float>(Side)/SideCount);
|
||||
if(bHasColors)NewColors.push_back(Color);
|
||||
}
|
||||
}
|
||||
for(size_t Ring=0;Ring+1<Rings.size();++Ring)
|
||||
{
|
||||
const uint8 Mask=IntervalMasks[Ring];
|
||||
if(!Mask)continue;
|
||||
uint8 FaceMask=0;
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
const int32 Side=SideForDirection[DirectionIndex];
|
||||
FaceMask|=static_cast<uint8>(1u<<Side);
|
||||
}
|
||||
for(int32 FaceSide=0;FaceSide<SideCount;++FaceSide)
|
||||
{
|
||||
if((FaceMask&(1u<<FaceSide))==0)continue;
|
||||
const int32 Next=(FaceSide+1)%SideCount;
|
||||
const int32 A=RingVertexIndex[Ring][FaceSide];
|
||||
const int32 B=RingVertexIndex[Ring][Next];
|
||||
const int32 C=RingVertexIndex[Ring+1][FaceSide];
|
||||
const int32 D=RingVertexIndex[Ring+1][Next];
|
||||
// Shared panels are emitted once even when both adjoining radial rays are missing.
|
||||
NewTriangles.insert(NewTriangles.end(),{A,C,B,B,C,D});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RunStart=RunEnd+1;
|
||||
}
|
||||
}
|
||||
if(NewVertices.empty()||NewTriangles.empty())return;
|
||||
|
||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||
{
|
||||
using TArrayType=std::remove_reference_t<decltype(Existing)>;
|
||||
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
|
||||
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
|
||||
for(const auto& Value:Added)Combined.Add(Value);
|
||||
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
|
||||
Existing=std::move(Combined);
|
||||
};
|
||||
InsertVertices(Mesh.Vertices,NewVertices);
|
||||
InsertVertices(Mesh.Normals,NewNormals);
|
||||
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
|
||||
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
|
||||
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
|
||||
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
|
||||
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
|
||||
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
|
||||
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
Mesh.Triangles=std::move(CombinedTriangles);
|
||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||
}
|
||||
|
||||
struct FFarMeshColumnIndex
|
||||
{
|
||||
FFarMeshColumnIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent,
|
||||
int32 InCells,double InStep)
|
||||
:Mesh(InMesh),Origin(InOrigin),Extent(InExtent),Cells(InCells),Step(InStep),
|
||||
ByColumn(static_cast<size_t>(InCells)*InCells)
|
||||
{
|
||||
for(int32 Triangle=0;Triangle+2<Mesh.Triangles.Num();Triangle+=3)
|
||||
{
|
||||
const int32 A=Mesh.Triangles[Triangle],B=Mesh.Triangles[Triangle+1],C=Mesh.Triangles[Triangle+2];
|
||||
if(A<0||B<0||C<0||A>=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<int32>(std::floor((MinX-Origin.X)/Step)),0,Cells-1);
|
||||
const int32 X1=std::clamp(static_cast<int32>(std::floor((MaxX-Origin.X)/Step)),0,Cells-1);
|
||||
const int32 Y0=std::clamp(static_cast<int32>(std::floor((MinY-Origin.Y)/Step)),0,Cells-1);
|
||||
const int32 Y1=std::clamp(static_cast<int32>(std::floor((MaxY-Origin.Y)/Step)),0,Cells-1);
|
||||
for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X)
|
||||
ByColumn[static_cast<size_t>(Y)*Cells+X].push_back(Triangle/3);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<double> VerticalHits(double X,double Y,double ZMin,double ZMax) const
|
||||
{
|
||||
const int32 CX=std::clamp(static_cast<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
|
||||
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
|
||||
const auto& Candidates=ByColumn[static_cast<size_t>(CY)*Cells+CX];
|
||||
std::vector<double> Hits;Hits.reserve(8);
|
||||
for(const 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<std::vector<int32>> ByColumn;
|
||||
};
|
||||
|
||||
inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld& Shape,
|
||||
double X,double Y,double ZMin,double ZMax)
|
||||
{
|
||||
std::vector<double> Crossings;
|
||||
if(ZMax<=ZMin)return Crossings;
|
||||
double PreviousZ=ZMin;
|
||||
bool PreviousSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(PreviousZ))<0.0f;
|
||||
for(double Z=std::min(ZMin+1.0,ZMax);Z<=ZMax+1.0e-6;Z=std::min(Z+1.0,ZMax))
|
||||
{
|
||||
const bool Solid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z))<0.0f;
|
||||
if(Solid!=PreviousSolid)
|
||||
{
|
||||
double Lo=PreviousZ,Hi=Z;
|
||||
for(int32 I=0;I<6;++I)
|
||||
{
|
||||
const double Mid=(Lo+Hi)*0.5;
|
||||
if((Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(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<FBoundary> Boundaries;
|
||||
for(const FStrateSlot& Slot:Layout)
|
||||
{
|
||||
const FStrateGenerationParams Params=World.Manager->GetGenerationParams(
|
||||
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
|
||||
const double HalfWidth=std::max(8.0,static_cast<double>(Params.BoundarySealThickness)+8.0);
|
||||
Boundaries.push_back({static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth});
|
||||
Boundaries.push_back({static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth});
|
||||
}
|
||||
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z<B.Z;});
|
||||
Boundaries.erase(std::unique(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
|
||||
{return std::abs(A.Z-B.Z)<0.01;}),Boundaries.end());
|
||||
const FIntVector Origin=Key.OriginVoxels();const int32 Extent=Key.ExtentVoxels();
|
||||
std::vector<FBoundary> 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<FVector> NewVertices,NewNormals;
|
||||
std::vector<FVector2f> NewUVs;std::vector<FColor> NewColors;
|
||||
std::vector<int32> NewTriangles;
|
||||
static constexpr int32 ColumnOffsets[4]={1,5,9,13};
|
||||
const auto Density=[&](double X,double Y,double Z)
|
||||
{return Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z));};
|
||||
for(const FBoundary& Boundary: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<double> Source=FindFarStratumCrossings(Shape,X,Y,ZMin,ZMax);
|
||||
if(Source.empty())continue;
|
||||
std::vector<double> Hits=MeshIndex.VerticalHits(X,Y,ZMin,ZMax);
|
||||
std::vector<uint8> 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<double>::infinity();
|
||||
for(size_t I=0;I<Hits.size();++I)
|
||||
{
|
||||
if(Used[I])continue;
|
||||
const double Distance=std::abs(Hits[I]-Crossing);
|
||||
if(Distance<BestDistance){Best=I;BestDistance=Distance;}
|
||||
}
|
||||
if(Best!=Hits.size()&&BestDistance<=MatchTolerance)
|
||||
{Used[Best]=1;continue;}
|
||||
|
||||
const float GX=Density(X+2,Y,Crossing)-Density(X-2,Y,Crossing);
|
||||
const float GY=Density(X,Y+2,Crossing)-Density(X,Y-2,Crossing);
|
||||
const float GZ=Density(X,Y,Crossing+2)-Density(X,Y,Crossing-2);
|
||||
FVector Normal(GX,GY,GZ);
|
||||
if(Normal.SizeSquared()<1.0e-8)Normal=FVector(0,0,1);
|
||||
Normal.Normalize();
|
||||
const double HalfSize=Step*2.0;
|
||||
const double X0=std::max<double>(Origin.X,X-HalfSize),X1=std::min<double>(Origin.X+Extent,X+HalfSize);
|
||||
const double Y0=std::max<double>(Origin.Y,Y-HalfSize),Y1=std::min<double>(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<int32>(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<float>((Xs[Corner]-Origin.X)/256.0),
|
||||
static_cast<float>((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<int32>(NewVertices.size());
|
||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||
{
|
||||
using TArrayType=std::remove_reference_t<decltype(Existing)>;
|
||||
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
|
||||
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
|
||||
for(const auto& Value:Added)Combined.Add(Value);
|
||||
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
|
||||
Existing=std::move(Combined);
|
||||
};
|
||||
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
|
||||
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
|
||||
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
|
||||
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
|
||||
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
|
||||
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
|
||||
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
|
||||
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
Mesh.Triangles=std::move(CombinedTriangles);Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
#pragma once
|
||||
|
||||
// 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 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<uint32>(X) * 0x85EBCA6Bu);
|
||||
H = Mix(H ^ static_cast<uint32>(Y) * 0xC2B2AE35u);
|
||||
return Mix(H ^ static_cast<uint32>(Axis) * 0x27D4EB2Fu);
|
||||
}
|
||||
|
||||
inline float Unit(uint32 X)
|
||||
{
|
||||
return static_cast<float>(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<uint32>(X)*0x68E31DA4u);
|
||||
H=Mix(H^static_cast<uint32>(Y)*0x1B56C4E9u);
|
||||
return Mix(H^static_cast<uint32>(Z)*0x9E3779B9u);
|
||||
}
|
||||
|
||||
inline float NoiseValue(int32 X,int32 Y,int32 Z,uint32 Seed)
|
||||
{
|
||||
return static_cast<float>(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<float>(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<float>(X0));
|
||||
const float TY=Fade(FY-static_cast<float>(Y0));
|
||||
const float TZ=Fade(FZ-static_cast<float>(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<float>(CX)+0.5f)*Pitch
|
||||
+ (Unit(H^0xA511E9B3u)-0.5f)*Pitch*0.18f;
|
||||
OutY = (static_cast<float>(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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "OwnerWorldLoader.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace VoxelForgeCore
|
||||
{
|
||||
// Private standalone adapter: keeps the compatibility holders behind the public value API.
|
||||
ResolvedWorldSnapshot MakeStandaloneSnapshot(std::shared_ptr<FStandaloneOwnerWorld> owner);
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
#include "StandaloneSnapshotAdapter.h"
|
||||
|
||||
#include "FarShellsInternal.h"
|
||||
#include "VoxelClipmapDesiredTiles.h"
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace VoxelForgeCore
|
||||
{
|
||||
struct ResolvedWorldSnapshot::Data
|
||||
{
|
||||
std::shared_ptr<FStandaloneOwnerWorld> 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<double>(1<<tile.lod);
|
||||
const double dx=(static_cast<double>(tile.coordinate.x)+0.5)*scale-center.x;
|
||||
const double dy=(static_cast<double>(tile.coordinate.y)+0.5)*scale-center.y;
|
||||
const double dz=(static_cast<double>(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<bytes;++i)
|
||||
{
|
||||
hash^=static_cast<std::uint8_t>(word>>(i*8));
|
||||
hash*=1099511628211ull;
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t HashPayload(const FVoxelMeshData& mesh)
|
||||
{
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
AddWord(hash,static_cast<std::uint64_t>(mesh.Vertices.Num()),8);
|
||||
for(const FVector& p:mesh.Vertices)
|
||||
{ AddWord(hash,std::bit_cast<std::uint64_t>(p.X),8); AddWord(hash,std::bit_cast<std::uint64_t>(p.Y),8); AddWord(hash,std::bit_cast<std::uint64_t>(p.Z),8); }
|
||||
AddWord(hash,static_cast<std::uint64_t>(mesh.Normals.Num()),8);
|
||||
for(const FVector& n:mesh.Normals)
|
||||
{ AddWord(hash,std::bit_cast<std::uint64_t>(n.X),8); AddWord(hash,std::bit_cast<std::uint64_t>(n.Y),8); AddWord(hash,std::bit_cast<std::uint64_t>(n.Z),8); }
|
||||
AddWord(hash,static_cast<std::uint64_t>(mesh.UVs.Num()),8);
|
||||
for(const auto& uv:mesh.UVs)
|
||||
{ AddWord(hash,std::bit_cast<std::uint64_t>(static_cast<double>(uv.X)),8);
|
||||
AddWord(hash,std::bit_cast<std::uint64_t>(static_cast<double>(uv.Y)),8); }
|
||||
AddWord(hash,static_cast<std::uint64_t>(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<std::uint64_t>(mesh.Triangles.Num()),8);
|
||||
for(const int32 index:mesh.Triangles)AddWord(hash,static_cast<std::uint32_t>(index),4);
|
||||
AddWord(hash,static_cast<std::uint32_t>(mesh.NumSurfaceVertices),4);
|
||||
AddWord(hash,static_cast<std::uint32_t>(mesh.NumSkirtTriangles),4);
|
||||
AddWord(hash,static_cast<std::uint32_t>(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<std::uint64_t>(v),8);
|
||||
return hash;
|
||||
};
|
||||
std::vector<std::array<std::uint64_t,3>> triangles;
|
||||
triangles.reserve(static_cast<std::size_t>(mesh.Triangles.Num()/3));
|
||||
for(int32 i=0;i+2<mesh.Triangles.Num();i+=3)
|
||||
triangles.push_back({vertexKey(mesh.Triangles[i]),vertexKey(mesh.Triangles[i+1]),vertexKey(mesh.Triangles[i+2])});
|
||||
std::sort(triangles.begin(),triangles.end());
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
AddWord(hash,static_cast<std::uint64_t>(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<std::uint64_t>(mesh.Vertices.Num()),8);
|
||||
for(int32 i=0;i<mesh.Vertices.Num();++i)
|
||||
{
|
||||
const FVector& p=mesh.Vertices[i]; const FVector& n=mesh.Normals[i];
|
||||
for(float value:{static_cast<float>(p.X*0.01),static_cast<float>(p.Y*0.01),static_cast<float>(p.Z*0.01),
|
||||
static_cast<float>(n.X),static_cast<float>(n.Y),static_cast<float>(n.Z)})
|
||||
AddWord(hash,std::bit_cast<std::uint32_t>(value),4);
|
||||
}
|
||||
AddWord(hash,static_cast<std::uint64_t>(mesh.Triangles.Num()),8);
|
||||
for(const int32 index:mesh.Triangles)AddWord(hash,static_cast<std::uint32_t>(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<std::uint8_t>(brush.shape),1);
|
||||
AddWord(hash,static_cast<std::uint8_t>(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<std::uint64_t>(v),8);
|
||||
AddWord(hash,std::bit_cast<std::uint32_t>(brush.strength),4);
|
||||
AddWord(hash,std::bit_cast<std::uint32_t>(brush.falloff),4);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool IsFiniteBrush(const EditBrush& brush)
|
||||
{
|
||||
const std::array<double,11> 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<FStandaloneOwnerWorld> owner)
|
||||
{
|
||||
if(!owner||!owner->Settings||!owner->Manager||!owner->Generator)return {};
|
||||
auto data=std::make_shared<ResolvedWorldSnapshot::Data>();
|
||||
data->configuration=MakeConfiguration(*owner);
|
||||
data->owner=std::move(owner);
|
||||
return ResolvedWorldSnapshot(std::move(data));
|
||||
}
|
||||
};
|
||||
|
||||
ResolvedWorldSnapshot MakeStandaloneSnapshot(std::shared_ptr<FStandaloneOwnerWorld> owner)
|
||||
{ return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner)); }
|
||||
|
||||
std::optional<ResolvedWorldSnapshot> ResolvedWorldSnapshot::FromJson(
|
||||
std::string_view json,std::string& error)
|
||||
{
|
||||
auto owner=std::make_shared<FStandaloneOwnerWorld>();
|
||||
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<FStandaloneOwnerWorld> owner;
|
||||
WorldConfiguration configuration;
|
||||
std::unique_ptr<FStandaloneShapeWorld> shape;
|
||||
std::vector<FStandaloneShapeWorld::FEditBrush> editLog;
|
||||
std::map<std::uint64_t,EditResult> transactions;
|
||||
bool hasLastSequence=false;
|
||||
std::uint64_t lastSequence=0;
|
||||
};
|
||||
|
||||
World::World(ResolvedWorldSnapshot snapshot):impl_(std::make_unique<Impl>())
|
||||
{
|
||||
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<FStandaloneShapeWorld>();
|
||||
const bool ok=shape->Build(*impl_->owner,ToNative(request.minVoxel),ToNative(request.maxVoxel),
|
||||
ToNative(request.clipMinVoxel),ToNative(request.clipMaxVoxel),
|
||||
static_cast<int32>(std::clamp<std::uint32_t>(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<float>(position.x),static_cast<float>(position.y),static_cast<float>(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<std::size_t>(1,static_cast<std::size_t>(std::ceil(length/spacing)));
|
||||
for(std::size_t i=0;i<=intervals;++i)
|
||||
{
|
||||
const double t=static_cast<double>(i)/static_cast<double>(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<TileKey> World::prioritizeTiles(std::span<const TileKey> tiles,Vec3i centerChunk)
|
||||
{
|
||||
std::vector<TileKey> 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<db;
|
||||
if(a.lod!=b.lod)return a.lod<b.lod;
|
||||
if(a.coordinate.z!=b.coordinate.z)return a.coordinate.z<b.coordinate.z;
|
||||
if(a.coordinate.y!=b.coordinate.y)return a.coordinate.y<b.coordinate.y;
|
||||
return a.coordinate.x<b.coordinate.x;
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
std::vector<TileKey> 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<FVoxelTileKey> nativeTiles;
|
||||
VoxelClipmapDesiredTiles::Build(center,minZ,maxZ,parameters,nativeTiles);
|
||||
std::vector<TileKey> tiles;tiles.reserve(static_cast<std::size_t>(nativeTiles.Num()));
|
||||
for(const FVoxelTileKey& key:nativeTiles)tiles.push_back(FromNative(key));
|
||||
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<b.lod:da<db;
|
||||
});
|
||||
return tiles;
|
||||
}
|
||||
|
||||
std::optional<TileMesh> 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<float>(world.SkirtCells);
|
||||
mesher.LODOctaveDrop=world.LODOctaveDrop;
|
||||
const int32 level=key.lod,extent=CHUNK_SIZE<<level;
|
||||
const int32 cells=level<world.FullResClipLevels?CHUNK_SIZE:FMath::Max(1,world.CoarseTileCells);
|
||||
const int32 step=FMath::Max(1,extent/cells);
|
||||
int64 sampleCount=0;std::vector<float> 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::uint64_t>(std::max<int64>(0,sampleCount));
|
||||
out.meshPayloadAllocatedBytes=static_cast<std::uint64_t>(mesh.Vertices.GetAllocatedSize())
|
||||
+static_cast<std::uint64_t>(mesh.Normals.GetAllocatedSize())
|
||||
+static_cast<std::uint64_t>(mesh.UVs.GetAllocatedSize())
|
||||
+static_cast<std::uint64_t>(mesh.Colors.GetAllocatedSize())
|
||||
+static_cast<std::uint64_t>(mesh.Triangles.GetAllocatedSize());
|
||||
out.groups.surfaceVertexCount=static_cast<std::uint32_t>(mesh.NumSurfaceVertices);
|
||||
out.groups.ceilingTriangleCount=static_cast<std::uint32_t>(mesh.NumCeilingTriangles);
|
||||
out.groups.groundTriangleCount=static_cast<std::uint32_t>(
|
||||
std::max(0,mesh.Triangles.Num()/3-mesh.NumCeilingTriangles));
|
||||
out.groups.skirtTriangleCount=static_cast<std::uint32_t>(mesh.NumSkirtTriangles);
|
||||
out.vertices.reserve(static_cast<std::size_t>(mesh.Vertices.Num()));
|
||||
for(int32 i=0;i<mesh.Vertices.Num();++i)
|
||||
{
|
||||
const FVector& p=mesh.Vertices[i];
|
||||
const FVector normal=i<mesh.Normals.Num()?mesh.Normals[i]:FVector::ZeroVector;
|
||||
const FVector2f uv=i<mesh.UVs.Num()?mesh.UVs[i]:FVector2f(0.0f,0.0f);
|
||||
const FColor color=i<mesh.Colors.Num()?mesh.Colors[i]:FColor(0,0,0,0);
|
||||
out.vertices.push_back({{p.X,p.Y,p.Z},{normal.X,normal.Y,normal.Z},{uv.X,uv.Y},
|
||||
{color.R,color.G,color.B,color.A}});
|
||||
}
|
||||
out.indices.reserve(static_cast<std::size_t>(mesh.Triangles.Num()));
|
||||
for(const int32 index:mesh.Triangles)out.indices.push_back(static_cast<std::uint32_t>(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<std::uint32_t>(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<EditResult> World::applyEditTransaction(const EditTransaction& transaction,
|
||||
std::span<const TileKey> 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<unsigned>(brush.shape)>static_cast<unsigned>(EditBrush::Shape::roundedBox)
|
||||
||static_cast<unsigned>(brush.operation)>static_cast<unsigned>(EditBrush::Operation::densityOffset))
|
||||
{error="edit brush has an unknown shape or operation";return std::nullopt;}
|
||||
}
|
||||
std::vector<FStandaloneShapeWorld::FEditBrush> native;
|
||||
native.reserve(transaction.brushes.size());
|
||||
for(const EditBrush& brush:transaction.brushes)
|
||||
{
|
||||
FStandaloneShapeWorld::FEditBrush output;
|
||||
output.Shape=static_cast<FStandaloneShapeWorld::FEditBrush::EShape>(brush.shape);
|
||||
output.Operation=static_cast<FStandaloneShapeWorld::FEditBrush::EOperation>(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::size_t>(std::max(impl_->configuration.maxLod,0)+1));
|
||||
for(const TileKey& tile:desired)
|
||||
{
|
||||
if(tile.lod<0||tile.lod>=static_cast<int32>(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+pad<min.X
|
||||
||origin.Y-pad>max.Y||origin.Y+extent+pad<min.Y
|
||||
||origin.Z-pad>max.Z||origin.Z+extent+pad<min.Z)continue;
|
||||
result.affectedByLod[static_cast<std::size_t>(tile.lod)].push_back(tile);
|
||||
}
|
||||
impl_->transactions.emplace(transaction.sequence,result);
|
||||
impl_->hasLastSequence=true;impl_->lastSequence=transaction.sequence;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<FarConnectorSegment> World::farConnectorSegments() const
|
||||
{
|
||||
std::vector<FarConnectorSegment> 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<std::uint32_t> World::farConnectorCandidates(Vec3i minVoxel,Vec3i maxVoxel) const
|
||||
{
|
||||
std::vector<std::uint32_t> 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<float>(yaw),static_cast<float>(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<std::uint32_t>(key.coordinate.x)*0x9E3779B1u;
|
||||
h^=static_cast<std::uint32_t>(key.coordinate.y)+0x9E3779B9u+(h<<6)+(h>>2);
|
||||
h^=static_cast<std::uint32_t>(key.coordinate.z)+0x9E3779B9u+(h<<6)+(h>>2);
|
||||
h^=static_cast<std::uint32_t>(key.lod)+0x9E3779B9u+(h<<6)+(h>>2);
|
||||
return h;
|
||||
}
|
||||
+17
-15
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
@@ -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<FVoxelTileKey>& DesiredTiles,bool bInDropOneShell)
|
||||
:World(InWorld),Shape(InShape),bDropOneShell(bInDropOneShell)
|
||||
{
|
||||
@@ -257,7 +259,7 @@ private:
|
||||
|
||||
void BuildConnectorSamples(const std::vector<FVoxelTileKey>& 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<Connectors.size();++I)
|
||||
{
|
||||
const auto& Connector=Connectors[I];
|
||||
const V3 A=ToV3(Connector.Start),B=ToV3(Connector.End);
|
||||
const V3 A{static_cast<float>(Connector.start.x),static_cast<float>(Connector.start.y),static_cast<float>(Connector.start.z)};
|
||||
const V3 B{static_cast<float>(Connector.end.x),static_cast<float>(Connector.end.y),static_cast<float>(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<float>(RayLength)))))
|
||||
Mask|=static_cast<uint8>(1u<<D);
|
||||
if(!Mask)continue;
|
||||
ConnectorSamples[Key].push_back({I,Center,U,V,Connector.Radius,Mask});
|
||||
ConnectorSamples[Key].push_back({I,Center,U,V,Connector.radius,Mask});
|
||||
ConnectorHasSamples[I]=1;break;
|
||||
}
|
||||
}
|
||||
@@ -324,20 +327,20 @@ private:
|
||||
std::vector<double> Crossings;
|
||||
if(ZMax<=ZMin)return Crossings;
|
||||
double PreviousZ=ZMin;
|
||||
bool PreviousSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(PreviousZ))<0.0f;
|
||||
bool PreviousSolid=Shape.densityAt({static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(PreviousZ)})<0.0f;
|
||||
for(double Z=std::min(ZMin+Stride,ZMax);Z<=ZMax+1.0e-6;Z=std::min(Z+Stride,ZMax))
|
||||
{
|
||||
const bool Solid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z))<0.0f;
|
||||
const bool Solid=Shape.densityAt({static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Z)})<0.0f;
|
||||
if(Solid!=PreviousSolid)
|
||||
{
|
||||
double Lo=PreviousZ,Hi=Z;const bool LoSolid=PreviousSolid;
|
||||
for(int32 I=0;I<6;++I)
|
||||
{
|
||||
const double Mid=(Lo+Hi)*0.5;
|
||||
const bool MidSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Mid))<0.0f;
|
||||
const bool MidSolid=Shape.densityAt({static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Mid)})<0.0f;
|
||||
if(MidSolid==LoSolid)Lo=Mid;else Hi=Mid;
|
||||
}
|
||||
Crossings.push_back((Lo+Hi)*0.5);
|
||||
@@ -451,12 +454,12 @@ private:
|
||||
{
|
||||
std::vector<double> 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<float>(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<float>(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<FBoundary> Boundaries;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
// 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<std::size_t>(workerLimit,1,
|
||||
std::min<std::size_t>(8,std::max<std::size_t>(1,count)));
|
||||
const std::size_t grain=std::max<std::size_t>(1,batchSize);
|
||||
std::atomic<std::size_t> next{0};
|
||||
std::vector<double> active(workers,0.0);
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(workers);
|
||||
for(std::size_t worker=0;worker<workers;++worker)
|
||||
threads.emplace_back([&,worker]
|
||||
{
|
||||
const auto started=std::chrono::steady_clock::now();
|
||||
for(;;)
|
||||
{
|
||||
const std::size_t first=next.fetch_add(grain,std::memory_order_relaxed);
|
||||
if(first>=count)break;
|
||||
const std::size_t end=first+std::min(grain,count-first);
|
||||
for(std::size_t index=first;index<end;++index)task(context,index,worker);
|
||||
}
|
||||
active[worker]=std::chrono::duration<double>(
|
||||
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<workers;++worker)
|
||||
{
|
||||
total+=active[worker];
|
||||
if(workerActiveSeconds&&worker<workerActiveCapacity)
|
||||
workerActiveSeconds[worker]=active[worker];
|
||||
}
|
||||
return total;
|
||||
}
|
||||
};
|
||||
@@ -1,183 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
// 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<bool> Enabled{false};
|
||||
inline std::atomic<bool> 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<uint32>(X) * 0x85EBCA6Bu);
|
||||
H = Mix(H ^ static_cast<uint32>(Y) * 0xC2B2AE35u);
|
||||
return Mix(H ^ static_cast<uint32>(Axis) * 0x27D4EB2Fu);
|
||||
}
|
||||
|
||||
inline float Unit(uint32 X)
|
||||
{
|
||||
return static_cast<float>(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<uint32>(X)*0x68E31DA4u);
|
||||
H=Mix(H^static_cast<uint32>(Y)*0x1B56C4E9u);
|
||||
return Mix(H^static_cast<uint32>(Z)*0x9E3779B9u);
|
||||
}
|
||||
|
||||
inline float NoiseValue(int32 X,int32 Y,int32 Z,uint32 Seed)
|
||||
{
|
||||
return static_cast<float>(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<float>(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<float>(X0));
|
||||
const float TY=Fade(FY-static_cast<float>(Y0));
|
||||
const float TZ=Fade(FZ-static_cast<float>(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<float>(CX)+0.5f)*Pitch
|
||||
+ (Unit(H^0xA511E9B3u)-0.5f)*Pitch*0.18f;
|
||||
OutY = (static_cast<float>(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"
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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<UVoxelStrateManager>();
|
||||
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<UVoxelGenerator>();
|
||||
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<FStandaloneOwnerWorld> World;
|
||||
std::unique_ptr<FStandaloneShapeWorld> ShapeWorld;
|
||||
std::unique_ptr<FStandaloneShapeWorld> PendingFullShapeWorld;
|
||||
std::shared_ptr<FStandaloneOwnerWorld> World;
|
||||
VoxelForgeCore::ResolvedWorldSnapshot Snapshot;
|
||||
std::unique_ptr<VoxelForgeCore::World> ShapeWorld;
|
||||
std::unique_ptr<VoxelForgeCore::World> PendingFullShapeWorld;
|
||||
FStandaloneCoreScheduler CoreScheduler;
|
||||
std::unique_ptr<FStandaloneStreamingGenerator> Streamer;
|
||||
std::thread FullShapeBuildThread;
|
||||
std::atomic<bool> 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<FStandaloneOwnerWorld>();
|
||||
Snapshot={};
|
||||
World=std::make_shared<FStandaloneOwnerWorld>();
|
||||
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<FStandaloneShapeWorld>();
|
||||
Snapshot=VoxelForgeCore::MakeStandaloneSnapshot(World);
|
||||
if(!Snapshot){Error="the standalone adapter could not make a resolved core snapshot";return false;}
|
||||
ShapeWorld=std::make_unique<VoxelForgeCore::World>(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<uint32>(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<unsigned long long>(FineTiles),Min.X,Min.Y,Min.Z,Max.X,Max.Y,Max.Z,
|
||||
ClipMin.X,ClipMin.Y,ClipMin.Z,ClipMax.X,ClipMax.Y,ClipMax.Z);
|
||||
@@ -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<FStandaloneShapeWorld>();
|
||||
PendingFullShapeWorld=std::make_unique<VoxelForgeCore::World>(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<uint32>(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<FStandaloneStreamingGenerator>(*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<int32>(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<FVoxelTileKey> ResidentKeys;ResidentKeys.reserve(ResidentTiles.size());
|
||||
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
|
||||
Streamer.reset();
|
||||
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount,false,nullptr,-1);
|
||||
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*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<FStandaloneStreamingGenerator>(*World,WorkerCount,false,nullptr,-1);
|
||||
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*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<int32>(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<FVoxelTileKey> Tiles;Tiles.reserve(static_cast<size_t>(std::max(Count,1)));
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
if(Tile.Level==Level&&static_cast<int32>(Tiles.size())<Count)Tiles.push_back(Tile);
|
||||
if(CoreWorld)
|
||||
{
|
||||
const auto CoreTiles=CoreWorld->desiredTiles({CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z});
|
||||
for(const auto& Key:CoreTiles)
|
||||
if(Key.lod==Level&&static_cast<int32>(Tiles.size())<Count)
|
||||
Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
||||
}
|
||||
else
|
||||
{
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
if(Tile.Level==Level&&static_cast<int32>(Tiles.size())<Count)Tiles.push_back(Tile);
|
||||
}
|
||||
if(Tiles.empty())
|
||||
{std::fprintf(stderr,"No LOD %d tiles are in the current desired set.\n",Level);return 4;}
|
||||
|
||||
@@ -1386,8 +1427,10 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
FParse::Value(FCommandLine::Get(),"voxel.TileCacheWindow=",TileCacheWindow);
|
||||
FParse::Value(FCommandLine::Get(),"voxel.TileCacheWindowMinLOD=",TileCacheWindowMinLOD);
|
||||
FParse::Value(FCommandLine::Get(),"voxel.RoomGraphCacheSlots=",RoomGraphCacheSlots);
|
||||
std::printf("[BenchmarkConfig] tile_cache_window=%d fused_min_lod=%d room_graph_cache_slots=%d command='%s'\n",
|
||||
TileCacheWindow,TileCacheWindowMinLOD,RoomGraphCacheSlots,FCommandLine::Get());
|
||||
std::printf("[BenchmarkConfig] tile_cache_window=%d fused_min_lod=%d room_graph_cache_slots=%d generator=%s command='%s'\n",
|
||||
TileCacheWindow,TileCacheWindowMinLOD,RoomGraphCacheSlots,
|
||||
CoreWorld&&!bCompareEvaluators?"VoxelForgeCore":"compatibility-diagnostic",
|
||||
FCommandLine::Get());
|
||||
VoxelDensityProfile::SetMode(bProfile?VoxelDensityProfile::EMode::Attribution
|
||||
:VoxelDensityProfile::EMode::Disabled,128);
|
||||
if(bProfile)VoxelDensityProfile::CalibrateTimer();
|
||||
@@ -1458,12 +1501,35 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
const int32 Step=FMath::Max(1,Tile.ExtentVoxels()/Cells);
|
||||
int64 TileSamples=0;
|
||||
std::vector<float> 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<int64>(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<uint64>(Mesh.Vertices.Num());
|
||||
TileIndices=static_cast<uint64>(Mesh.Triangles.Num());
|
||||
}
|
||||
Samples+=static_cast<uint64>(FMath::Max<int64>(0,TileSamples));
|
||||
Vertices+=static_cast<uint64>(Mesh.Vertices.Num());
|
||||
Indices+=static_cast<uint64>(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<FStandaloneShapeWorld&>(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<float>(CameraVoxels.Y+Offset[1]);
|
||||
const float Z=static_cast<float>(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<FVoxelTileKey> 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<FVoxelTileKey> Tiles;
|
||||
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||
for(const FVoxelTileKey& Tile:CoreDesired)
|
||||
if(LevelFilter<0?Tile.Level<World.FullResClipLevels:Tile.Level==LevelFilter)
|
||||
Tiles.push_back(Tile);
|
||||
if(TileLimit>0&&Tiles.size()>static_cast<size_t>(TileLimit))
|
||||
@@ -1670,7 +1735,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
|
||||
std::atomic<size_t> Next{0};
|
||||
std::vector<std::thread> 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<ThreadCount;++Worker)
|
||||
{
|
||||
@@ -1688,15 +1753,17 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
const FVoxelTileKey& Tile=Tiles[Index];
|
||||
const int32 Cells=Tile.Level<World.FullResClipLevels?CHUNK_SIZE:World.CoarseTileCells;
|
||||
const int32 Step=std::max(1,Tile.ExtentVoxels()/Cells);
|
||||
std::vector<float> 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<float> NewDensities=NewMesh?std::move(NewMesh->debugDensitySamples):std::vector<float>{};
|
||||
std::vector<float> 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<uint32>(OldDensities[I]);R.OldHash*=1099511628211ull;
|
||||
}
|
||||
constexpr size_t VertexStride=4;
|
||||
R.NewSurface.reserve(static_cast<size_t>(NewMesh.Vertices.Num())/VertexStride+1);
|
||||
for(int32 I=0;I<NewMesh.Vertices.Num();I+=static_cast<int32>(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;I<NewMesh->vertices.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<size_t>(OldMesh.Vertices.Num())/VertexStride+1);
|
||||
for(int32 I=0;I<OldMesh.Vertices.Num();I+=static_cast<int32>(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<FComparisonTile>& Out)
|
||||
{
|
||||
FStandaloneResearchTunnels::Set(true,bEnhanced);
|
||||
World.Generator->bStandaloneResearchRoundTunnels=true;
|
||||
World.Generator->bStandaloneResearchEnhancedWalls=bEnhanced;
|
||||
for(size_t I=0;I<Tiles.size();++I)
|
||||
{
|
||||
const FVoxelTileKey& Tile=Tiles[I];
|
||||
@@ -1795,6 +1868,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
};
|
||||
Evaluate(false,Legacy);
|
||||
Evaluate(true,Detailed);
|
||||
World.Generator->bStandaloneResearchRoundTunnels=PreviousRoundTunnels;
|
||||
World.Generator->bStandaloneResearchEnhancedWalls=PreviousEnhancedWalls;
|
||||
FDirectedSurfaceDistance LegacyToDetailed,DetailedToLegacy;
|
||||
std::vector<FComparePoint> AllLegacySurface,AllDetailedSurface;
|
||||
for(size_t TileIndex=0;TileIndex<Tiles.size();++TileIndex)
|
||||
@@ -1823,7 +1898,6 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
static_cast<unsigned long long>(LegacyHash),static_cast<unsigned long long>(DetailedHash),
|
||||
AllLegacySurface.size(),AllDetailedSurface.size(),MeanDistance,Hausdorff,
|
||||
static_cast<unsigned long long>(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<FStandaloneOwnerWorld> 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<uint32>(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<double>(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<int32>(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<unsigned long long>(HashTileRows(PayloadRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(TopologyRows)));
|
||||
}
|
||||
FStandaloneShapeWorldAccess::SetActive(nullptr);
|
||||
return NeighborhoodFinished==static_cast<int32>(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<FFarSurvivalAudit> FarAudit;
|
||||
if(bFarSurvivalAudit)
|
||||
{
|
||||
const FStandaloneShapeWorld* Shape=FStandaloneShapeWorldAccess::GetActive();
|
||||
if(!Shape||!Shape->HasFarWorld())
|
||||
{std::fprintf(stderr,"Far survival audit requires an active shape world with far-parent data.\n");return 15;}
|
||||
FarAudit=std::make_unique<FFarSurvivalAudit>(World,*Shape,Desired.Tiles,bFarAuditBadShell);
|
||||
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<FFarSurvivalAudit>(World,*CoreWorld,Desired.Tiles,bFarAuditBadShell);
|
||||
}
|
||||
std::array<std::vector<FHashedTile>,MaxLevels+1> HashRows;
|
||||
std::array<std::vector<FHashedTile>,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<double>(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<FStandaloneShapeWorld::FEditBrush> 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<VoxelForgeCore::TileKey> 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<FVoxelTileKey> Affected;Affected.reserve(128);
|
||||
std::array<uint64,MaxLevels+1> 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+Pad<EditMin.X
|
||||
||Origin.Y-Pad>EditMax.Y||Origin.Y+Extent+Pad<EditMin.Y
|
||||
||Origin.Z-Pad>EditMax.Z||Origin.Z+Extent+Pad<EditMin.Z)continue;
|
||||
Affected.push_back(Tile);
|
||||
if(Tile.Level>=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<int>(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<FStandaloneOwnerWorld>();
|
||||
App.World=std::make_shared<FStandaloneOwnerWorld>();
|
||||
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";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "WorldMesher.h"
|
||||
#include "StandaloneShapeWorld.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <algorithm>
|
||||
@@ -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<size_t>(Cells)*Cells*Cells),
|
||||
TriangleMarks(static_cast<size_t>(InMesh.Triangles.Num()/3),0)
|
||||
{
|
||||
for(int32 Triangle=0;Triangle+2<Mesh.Triangles.Num();Triangle+=3)
|
||||
{
|
||||
const int32 A=Mesh.Triangles[Triangle],B=Mesh.Triangles[Triangle+1],C=Mesh.Triangles[Triangle+2];
|
||||
if(A<0||B<0||C<0||A>=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<double>::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<size_t>(Triangle)]==QueryStamp)continue;
|
||||
TriangleMarks[static_cast<size_t>(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<double>::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<<Axis;
|
||||
for(int32 Subset=1;Subset<8;++Subset)
|
||||
{
|
||||
if(Subset&~CrossingMask)continue;
|
||||
int32 X=Cell[0],Y=Cell[1],Z=Cell[2];
|
||||
if(Subset&1)X+=CellStep[0];
|
||||
if(Subset&2)Y+=CellStep[1];
|
||||
if(Subset&4)Z+=CellStep[2];
|
||||
TestCell(X,Y,Z);
|
||||
}
|
||||
for(int32 Axis=0;Axis<3;++Axis)
|
||||
if(CrossingMask&(1<<Axis))
|
||||
{Cell[Axis]+=CellStep[Axis];NextBoundary[Axis]+=BoundaryDelta[Axis];}
|
||||
}
|
||||
return BestDifference;
|
||||
}
|
||||
|
||||
private:
|
||||
int32 CellFor(double Value,int32 Axis)const
|
||||
{return std::clamp(static_cast<int32>(std::floor((Value-(&Origin.X)[Axis])/Step)),0,Cells-1);}
|
||||
size_t CellOffset(int32 X,int32 Y,int32 Z)const
|
||||
{return (static_cast<size_t>(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<std::vector<int32>> ByCell;
|
||||
std::vector<uint32> 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<float>(SearchStart.X),static_cast<float>(SearchStart.Y),
|
||||
static_cast<float>(SearchStart.Z))<0.0f;
|
||||
double BestDifference=std::numeric_limits<double>::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<float>(P.X),static_cast<float>(P.Y),
|
||||
static_cast<float>(P.Z))<0.0f;
|
||||
if(Solid!=PreviousSolid)
|
||||
{
|
||||
double Lo=PreviousDistance,Hi=Distance;
|
||||
for(int32 I=0;I<6;++I)
|
||||
{
|
||||
const double Mid=(Lo+Hi)*0.5;const FVector M=Center+Direction*Mid;
|
||||
const bool MidSolid=Shape.DensityAt(static_cast<float>(M.X),static_cast<float>(M.Y),
|
||||
static_cast<float>(M.Z))<0.0f;
|
||||
if(MidSolid==PreviousSolid)Lo=Mid;else Hi=Mid;
|
||||
}
|
||||
const double Crossing=(Lo+Hi)*0.5,Difference=std::abs(Crossing-PreferredRadius);
|
||||
if(Difference<=AcceptBand&&Difference<BestDifference)
|
||||
{OutRadius=Crossing;BestDifference=Difference;bFound=true;}
|
||||
}
|
||||
if(Distance>=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<float>(Center.X+Direction.X*LocalPrevious),
|
||||
static_cast<float>(Center.Y+Direction.Y*LocalPrevious),
|
||||
static_cast<float>(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<float>(P.X),static_cast<float>(P.Y),
|
||||
static_cast<float>(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<float>(M.X),static_cast<float>(M.Y),
|
||||
static_cast<float>(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<BestDifference)
|
||||
{OutRadius=Crossing;BestDifference=Difference;bFound=true;}
|
||||
}
|
||||
if(Distance>=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<float>(LoPoint.X),static_cast<float>(LoPoint.Y),
|
||||
static_cast<float>(LoPoint.Z))<0.0f;
|
||||
const bool HiSolid=Shape.DensityAt(static_cast<float>(HiPoint.X),static_cast<float>(HiPoint.Y),
|
||||
static_cast<float>(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<FFarMeshSegmentIndex> MeshIndex;
|
||||
std::vector<FVector> NewVertices;
|
||||
std::vector<FVector> NewNormals;
|
||||
std::vector<FVector2f> NewUVs;
|
||||
std::vector<FColor> NewColors;
|
||||
std::vector<int32> NewTriangles;
|
||||
const int32 OldVertexCount=Mesh.Vertices.Num();
|
||||
const 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<uint32> CandidateIndices;
|
||||
Shape.FindFarConnectorCandidates(
|
||||
FIntVector(TileOrigin.X-64,TileOrigin.Y-64,TileOrigin.Z-64),
|
||||
FIntVector(TileOrigin.X+TileExtent+64,TileOrigin.Y+TileExtent+64,TileOrigin.Z+TileExtent+64),
|
||||
CandidateIndices);
|
||||
for(const uint32 ConnectorIndex:CandidateIndices)
|
||||
{
|
||||
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)<Low[AxisIndex]
|
||||
||std::min(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]<Low[AxisIndex]||Starts[AxisIndex]>High[AxisIndex])bIntersects=false;continue;}
|
||||
double A=(Low[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex];
|
||||
double B=(High[AxisIndex]-Starts[AxisIndex])/Deltas[AxisIndex];
|
||||
if(A>B)std::swap(A,B);
|
||||
TMin=std::max(TMin,A);TMax=std::min(TMax,B);
|
||||
if(TMin>TMax){bIntersects=false;break;}
|
||||
}
|
||||
if(!bIntersects||TMax-TMin<1.0e-6)continue;
|
||||
const int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/128.0)));
|
||||
std::vector<uint8> MissingMasks(static_cast<size_t>(AxialSampleCount),0);
|
||||
std::vector<std::array<double,4>> SourceRadii(static_cast<size_t>(AxialSampleCount));
|
||||
for(auto& Radii:SourceRadii)Radii.fill(0.0);
|
||||
bool bAnyMissing=false;
|
||||
bool bHasAuditSection=false;
|
||||
for(int32 Sample=0;Sample<AxialSampleCount;++Sample)
|
||||
{
|
||||
const double T=(static_cast<double>(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<double>(Connector.Radius)*0.75);
|
||||
const double MatchTolerance=std::max(8.0,static_cast<double>(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<FFarMeshSegmentIndex>(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<size_t>(Sample)][DirectionIndex]=SourceRadius;
|
||||
if(bMissing)
|
||||
{
|
||||
MissingMasks[static_cast<size_t>(Sample)]|=static_cast<uint8>(1u<<DirectionIndex);
|
||||
bAnyMissing=true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!bHasAuditSection||!bAnyMissing)continue;
|
||||
|
||||
struct FRing { double T=0.0;std::array<double,4> 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<<DirectionIndex))==0)
|
||||
{Ring.Radii[DirectionIndex]=Connector.Radius;continue;}
|
||||
double Radius=Connector.Radius;
|
||||
double RadiusUncertainty=0.0;
|
||||
if(!FindFarConnectorRadiusNearNominal(Shape,Center,
|
||||
RadialDirections[DirectionIndex],Connector.Radius,Radius,RadiusUncertainty)
|
||||
&&!FindFarConnectorSurfaceRadius(Shape,Center,RadialDirections[DirectionIndex],
|
||||
Connector.Radius,Radius))
|
||||
{
|
||||
for(int32 Sample=FirstSample;Sample<=LastSample;++Sample)
|
||||
{
|
||||
const double Known=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
|
||||
if(Known>0.0){Radius=Known;break;}
|
||||
}
|
||||
}
|
||||
Ring.Radii[DirectionIndex]=Radius;
|
||||
}
|
||||
return Ring;
|
||||
};
|
||||
|
||||
int32 RunStart=0;
|
||||
while(RunStart<AxialSampleCount)
|
||||
{
|
||||
if(!MissingMasks[static_cast<size_t>(RunStart)]){++RunStart;continue;}
|
||||
int32 RunEnd=RunStart;
|
||||
while(RunEnd+1<AxialSampleCount&&MissingMasks[static_cast<size_t>(RunEnd+1)])++RunEnd;
|
||||
const double RunMin=std::max(TMin,static_cast<double>(RunStart)/AxialSampleCount);
|
||||
const double RunMax=std::min(TMax,static_cast<double>(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<size_t>(Sample)];
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
switch(SideForDirection[DirectionIndex])
|
||||
{
|
||||
case 0:RunRadiusMask|=static_cast<uint8>((1u<<0)|(1u<<2));break;
|
||||
case 1:RunRadiusMask|=static_cast<uint8>((1u<<1)|(1u<<2));break;
|
||||
case 2:RunRadiusMask|=static_cast<uint8>((1u<<1)|(1u<<3));break;
|
||||
default:RunRadiusMask|=static_cast<uint8>((1u<<0)|(1u<<3));break;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<FRing> Rings;
|
||||
const double MaxRingStep=128.0;
|
||||
double CurrentT=RunMin;
|
||||
Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask));
|
||||
while(CurrentT<RunMax-1.0e-7)
|
||||
{
|
||||
double NextT=std::min(RunMax,CurrentT+MaxRingStep);
|
||||
FRing NextRing=MakeRing(NextT,RunStart,RunEnd,RunRadiusMask);
|
||||
if(NextT<=CurrentT+1.0e-8)break;
|
||||
Rings.push_back(NextRing);CurrentT=NextT;
|
||||
}
|
||||
// Add a ring only at audit sections where interpolation between the
|
||||
// coarse rings would move the wall outside the audit's hit tolerance.
|
||||
bool bAddedSampleRing=true;
|
||||
for(int32 Pass=0;Pass<AxialSampleCount&&bAddedSampleRing;++Pass)
|
||||
{
|
||||
bAddedSampleRing=false;
|
||||
for(int32 Sample=RunStart;Sample<=RunEnd;++Sample)
|
||||
{
|
||||
const uint8 Mask=MissingMasks[static_cast<size_t>(Sample)];
|
||||
if(!Mask)continue;
|
||||
const double SampleT=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
|
||||
auto RightIt=std::lower_bound(Rings.begin(),Rings.end(),SampleT,
|
||||
[](const FRing& Ring,double T){return Ring.T<T;});
|
||||
if(RightIt!=Rings.end()&&std::abs(RightIt->T-SampleT)<=1.0e-7)
|
||||
{
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
const double Exact=SourceRadii[static_cast<size_t>(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<<DirectionIndex))==0)continue;
|
||||
const double Actual=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
|
||||
const double Predicted=LeftRing.Radii[DirectionIndex]*(1.0-Alpha)
|
||||
+RightRing.Radii[DirectionIndex]*Alpha;
|
||||
const double Tolerance=std::max(8.0,static_cast<double>(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<<DirectionIndex))==0)continue;
|
||||
const double Exact=SourceRadii[static_cast<size_t>(Sample)][DirectionIndex];
|
||||
if(Exact>0.0)ExactRing.Radii[DirectionIndex]=Exact;
|
||||
}
|
||||
Rings.insert(RightIt,std::move(ExactRing));
|
||||
bAddedSampleRing=true;
|
||||
}
|
||||
}
|
||||
if(Rings.size()>=2)
|
||||
{
|
||||
std::vector<uint8> IntervalMasks(Rings.size()-1,0);
|
||||
std::vector<std::array<uint8,SideCount>> NeededSides(Rings.size());
|
||||
for(auto& Sides:NeededSides)Sides.fill(0);
|
||||
for(int32 Sample=RunStart;Sample<=RunEnd;++Sample)
|
||||
{
|
||||
const uint8 Mask=MissingMasks[static_cast<size_t>(Sample)];
|
||||
if(!Mask)continue;
|
||||
const double SampleT=(static_cast<double>(Sample)+0.5)/AxialSampleCount;
|
||||
const auto Lower=std::lower_bound(Rings.begin(),Rings.end(),SampleT,
|
||||
[](const FRing& Ring,double T){return Ring.T<T;});
|
||||
const size_t Right=static_cast<size_t>(Lower-Rings.begin());
|
||||
if(Right<Rings.size()&&std::abs(Rings[Right].T-SampleT)<=1.0e-7)
|
||||
{
|
||||
if(Right>0)IntervalMasks[Right-1]|=Mask;
|
||||
if(Right+1<Rings.size())IntervalMasks[Right]|=Mask;
|
||||
}
|
||||
else if(Right>0&&Right<Rings.size())IntervalMasks[Right-1]|=Mask;
|
||||
}
|
||||
for(size_t Ring=0;Ring<IntervalMasks.size();++Ring)
|
||||
{
|
||||
const uint8 Mask=IntervalMasks[Ring];
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
const int32 Side=SideForDirection[DirectionIndex];
|
||||
const int32 Next=(Side+1)%SideCount;
|
||||
NeededSides[Ring][Side]=NeededSides[Ring][Next]=1;
|
||||
NeededSides[Ring+1][Side]=NeededSides[Ring+1][Next]=1;
|
||||
}
|
||||
}
|
||||
std::vector<std::array<int32,SideCount>> RingVertexIndex(Rings.size());
|
||||
for(auto& Indices:RingVertexIndex)Indices.fill(INDEX_NONE);
|
||||
for(size_t Ring=0;Ring<Rings.size();++Ring)
|
||||
{
|
||||
if(std::none_of(NeededSides[Ring].begin(),NeededSides[Ring].end(),
|
||||
[](uint8 Needed){return Needed!=0;}))continue;
|
||||
const FVector Center=Connector.Start+Delta*Rings[Ring].T;
|
||||
for(int32 Side=0;Side<SideCount;++Side)
|
||||
{
|
||||
if(!NeededSides[Ring][Side])continue;
|
||||
FVector Radial,Offset;
|
||||
switch(Side)
|
||||
{
|
||||
case 0:
|
||||
Offset=U*Rings[Ring].Radii[0]+V*Rings[Ring].Radii[2];
|
||||
Radial=U+V;break;
|
||||
case 1:
|
||||
Offset=U*(-Rings[Ring].Radii[1])+V*Rings[Ring].Radii[2];
|
||||
Radial=U*-1.0+V;break;
|
||||
case 2:
|
||||
Offset=U*(-Rings[Ring].Radii[1])+V*(-Rings[Ring].Radii[3]);
|
||||
Radial=U*-1.0+V*-1.0;break;
|
||||
default:
|
||||
Offset=U*Rings[Ring].Radii[0]+V*(-Rings[Ring].Radii[3]);
|
||||
Radial=U+V*-1.0;break;
|
||||
}
|
||||
Radial.Normalize();
|
||||
const FVector P=Center+Offset;
|
||||
RingVertexIndex[Ring][Side]=static_cast<int32>(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<float>(Rings[Ring].T*Length/256.0),
|
||||
static_cast<float>(Side)/SideCount);
|
||||
if(bHasColors)NewColors.push_back(Color);
|
||||
}
|
||||
}
|
||||
for(size_t Ring=0;Ring+1<Rings.size();++Ring)
|
||||
{
|
||||
const uint8 Mask=IntervalMasks[Ring];
|
||||
if(!Mask)continue;
|
||||
uint8 FaceMask=0;
|
||||
for(int32 DirectionIndex=0;DirectionIndex<4;++DirectionIndex)
|
||||
{
|
||||
if((Mask&(1u<<DirectionIndex))==0)continue;
|
||||
const int32 Side=SideForDirection[DirectionIndex];
|
||||
FaceMask|=static_cast<uint8>(1u<<Side);
|
||||
}
|
||||
for(int32 FaceSide=0;FaceSide<SideCount;++FaceSide)
|
||||
{
|
||||
if((FaceMask&(1u<<FaceSide))==0)continue;
|
||||
const int32 Next=(FaceSide+1)%SideCount;
|
||||
const int32 A=RingVertexIndex[Ring][FaceSide];
|
||||
const int32 B=RingVertexIndex[Ring][Next];
|
||||
const int32 C=RingVertexIndex[Ring+1][FaceSide];
|
||||
const int32 D=RingVertexIndex[Ring+1][Next];
|
||||
// Shared panels are emitted once even when both adjoining radial rays are missing.
|
||||
NewTriangles.insert(NewTriangles.end(),{A,C,B,B,C,D});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RunStart=RunEnd+1;
|
||||
}
|
||||
}
|
||||
if(NewVertices.empty()||NewTriangles.empty())return;
|
||||
|
||||
const int32 AddedVertices=static_cast<int32>(NewVertices.size());
|
||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||
{
|
||||
using TArrayType=std::remove_reference_t<decltype(Existing)>;
|
||||
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
|
||||
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
|
||||
for(const auto& Value:Added)Combined.Add(Value);
|
||||
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
|
||||
Existing=std::move(Combined);
|
||||
};
|
||||
InsertVertices(Mesh.Vertices,NewVertices);
|
||||
InsertVertices(Mesh.Normals,NewNormals);
|
||||
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
|
||||
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
|
||||
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
|
||||
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
|
||||
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
|
||||
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
|
||||
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
Mesh.Triangles=std::move(CombinedTriangles);
|
||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||
}
|
||||
|
||||
struct FFarMeshColumnIndex
|
||||
{
|
||||
FFarMeshColumnIndex(const FVoxelMeshData& InMesh,const FIntVector& InOrigin,int32 InExtent,
|
||||
int32 InCells,double InStep)
|
||||
:Mesh(InMesh),Origin(InOrigin),Extent(InExtent),Cells(InCells),Step(InStep),
|
||||
ByColumn(static_cast<size_t>(InCells)*InCells)
|
||||
{
|
||||
for(int32 Triangle=0;Triangle+2<Mesh.Triangles.Num();Triangle+=3)
|
||||
{
|
||||
const int32 A=Mesh.Triangles[Triangle],B=Mesh.Triangles[Triangle+1],C=Mesh.Triangles[Triangle+2];
|
||||
if(A<0||B<0||C<0||A>=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<int32>(std::floor((MinX-Origin.X)/Step)),0,Cells-1);
|
||||
const int32 X1=std::clamp(static_cast<int32>(std::floor((MaxX-Origin.X)/Step)),0,Cells-1);
|
||||
const int32 Y0=std::clamp(static_cast<int32>(std::floor((MinY-Origin.Y)/Step)),0,Cells-1);
|
||||
const int32 Y1=std::clamp(static_cast<int32>(std::floor((MaxY-Origin.Y)/Step)),0,Cells-1);
|
||||
for(int32 Y=Y0;Y<=Y1;++Y)for(int32 X=X0;X<=X1;++X)
|
||||
ByColumn[static_cast<size_t>(Y)*Cells+X].push_back(Triangle/3);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<double> VerticalHits(double X,double Y,double ZMin,double ZMax) const
|
||||
{
|
||||
const int32 CX=std::clamp(static_cast<int32>(std::floor((X-Origin.X)/Step)),0,Cells-1);
|
||||
const int32 CY=std::clamp(static_cast<int32>(std::floor((Y-Origin.Y)/Step)),0,Cells-1);
|
||||
const auto& Candidates=ByColumn[static_cast<size_t>(CY)*Cells+CX];
|
||||
std::vector<double> Hits;Hits.reserve(8);
|
||||
for(const 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<std::vector<int32>> ByColumn;
|
||||
};
|
||||
|
||||
inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld& Shape,
|
||||
double X,double Y,double ZMin,double ZMax)
|
||||
{
|
||||
std::vector<double> Crossings;
|
||||
if(ZMax<=ZMin)return Crossings;
|
||||
double PreviousZ=ZMin;
|
||||
bool PreviousSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(PreviousZ))<0.0f;
|
||||
for(double Z=std::min(ZMin+1.0,ZMax);Z<=ZMax+1.0e-6;Z=std::min(Z+1.0,ZMax))
|
||||
{
|
||||
const bool Solid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z))<0.0f;
|
||||
if(Solid!=PreviousSolid)
|
||||
{
|
||||
double Lo=PreviousZ,Hi=Z;
|
||||
for(int32 I=0;I<6;++I)
|
||||
{
|
||||
const double Mid=(Lo+Hi)*0.5;
|
||||
if((Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(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<FBoundary> Boundaries;
|
||||
for(const FStrateSlot& Slot:Layout)
|
||||
{
|
||||
const FStrateGenerationParams Params=World.Manager->GetGenerationParams(
|
||||
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
|
||||
const double HalfWidth=std::max(8.0,static_cast<double>(Params.BoundarySealThickness)+8.0);
|
||||
Boundaries.push_back({static_cast<double>(Slot.BottomChunkZ)*CHUNK_SIZE,HalfWidth});
|
||||
Boundaries.push_back({static_cast<double>(Slot.TopChunkZ+1)*CHUNK_SIZE,HalfWidth});
|
||||
}
|
||||
std::sort(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B){return A.Z<B.Z;});
|
||||
Boundaries.erase(std::unique(Boundaries.begin(),Boundaries.end(),[](const FBoundary& A,const FBoundary& B)
|
||||
{return std::abs(A.Z-B.Z)<0.01;}),Boundaries.end());
|
||||
const FIntVector Origin=Key.OriginVoxels();const int32 Extent=Key.ExtentVoxels();
|
||||
std::vector<FBoundary> 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<FVector> NewVertices,NewNormals;
|
||||
std::vector<FVector2f> NewUVs;std::vector<FColor> NewColors;
|
||||
std::vector<int32> NewTriangles;
|
||||
static constexpr int32 ColumnOffsets[4]={1,5,9,13};
|
||||
const auto Density=[&](double X,double Y,double Z)
|
||||
{return Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),static_cast<float>(Z));};
|
||||
for(const FBoundary& Boundary: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<double> Source=FindFarStratumCrossings(Shape,X,Y,ZMin,ZMax);
|
||||
if(Source.empty())continue;
|
||||
std::vector<double> Hits=MeshIndex.VerticalHits(X,Y,ZMin,ZMax);
|
||||
std::vector<uint8> 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<double>::infinity();
|
||||
for(size_t I=0;I<Hits.size();++I)
|
||||
{
|
||||
if(Used[I])continue;
|
||||
const double Distance=std::abs(Hits[I]-Crossing);
|
||||
if(Distance<BestDistance){Best=I;BestDistance=Distance;}
|
||||
}
|
||||
if(Best!=Hits.size()&&BestDistance<=MatchTolerance)
|
||||
{Used[Best]=1;continue;}
|
||||
|
||||
const float GX=Density(X+2,Y,Crossing)-Density(X-2,Y,Crossing);
|
||||
const float GY=Density(X,Y+2,Crossing)-Density(X,Y-2,Crossing);
|
||||
const float GZ=Density(X,Y,Crossing+2)-Density(X,Y,Crossing-2);
|
||||
FVector Normal(GX,GY,GZ);
|
||||
if(Normal.SizeSquared()<1.0e-8)Normal=FVector(0,0,1);
|
||||
Normal.Normalize();
|
||||
const double HalfSize=Step*2.0;
|
||||
const double X0=std::max<double>(Origin.X,X-HalfSize),X1=std::min<double>(Origin.X+Extent,X+HalfSize);
|
||||
const double Y0=std::max<double>(Origin.Y,Y-HalfSize),Y1=std::min<double>(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<int32>(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<float>((Xs[Corner]-Origin.X)/256.0),
|
||||
static_cast<float>((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<int32>(NewVertices.size());
|
||||
auto InsertVertices=[&](auto& Existing,const auto& Added)
|
||||
{
|
||||
using TArrayType=std::remove_reference_t<decltype(Existing)>;
|
||||
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
|
||||
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
|
||||
for(const auto& Value:Added)Combined.Add(Value);
|
||||
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
|
||||
Existing=std::move(Combined);
|
||||
};
|
||||
InsertVertices(Mesh.Vertices,NewVertices);InsertVertices(Mesh.Normals,NewNormals);
|
||||
if(bHasUVs)InsertVertices(Mesh.UVs,NewUVs);
|
||||
if(bHasColors)InsertVertices(Mesh.Colors,NewColors);
|
||||
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertices;
|
||||
const int32 CeilingStart=std::max(0,Mesh.Triangles.Num()-Mesh.NumCeilingTriangles*3);
|
||||
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(Mesh.Triangles.Num()+static_cast<int32>(NewTriangles.size()));
|
||||
for(int32 I=0;I<CeilingStart;++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
for(const int32 Index:NewTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
|
||||
for(int32 I=CeilingStart;I<Mesh.Triangles.Num();++I)CombinedTriangles.Add(Mesh.Triangles[I]);
|
||||
Mesh.Triangles=std::move(CombinedTriangles);Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertices;
|
||||
}
|
||||
// Far parent repairs and shell stitching are implemented by VoxelForgeCore.
|
||||
|
||||
struct FStandaloneMeshCacheMetadata
|
||||
{
|
||||
@@ -1139,15 +333,14 @@ struct FStandaloneTileJob
|
||||
int32 BandZMin=MIN_int32;
|
||||
int32 BandZMax=MAX_int32;
|
||||
uint64 RequestedCrossing=0;
|
||||
double PriorityDistanceSquared=0.0;
|
||||
size_t PriorityRank=0;
|
||||
};
|
||||
|
||||
struct FStandaloneTileJobHeapOrder
|
||||
{
|
||||
bool operator()(const FStandaloneTileJob& A,const FStandaloneTileJob& B)const
|
||||
{
|
||||
if(A.PriorityDistanceSquared!=B.PriorityDistanceSquared)
|
||||
return A.PriorityDistanceSquared>B.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<VoxelForgeCore::TileKey> 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<FVoxelTileKey,size_t,FStandaloneTileKeyHash> PriorityRanks;
|
||||
PriorityRanks.reserve(Prioritized.size()*2+1);
|
||||
for(size_t Rank=0;Rank<Prioritized.size();++Rank)
|
||||
{
|
||||
const auto& Key=Prioritized[Rank];
|
||||
PriorityRanks.emplace(FVoxelTileKey(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod),Rank);
|
||||
}
|
||||
for(FStandaloneTileJob& Job:Queue)Job.PriorityRank=PriorityRanks.at(Job.Key);
|
||||
|
||||
const int32 BandMin=World->StrateContentCutMinLevel<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=Level<World->FullResClipLevels?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<double>(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<float>(Vertex.position.x*0.01),static_cast<float>(Vertex.position.y*0.01),
|
||||
static_cast<float>(Vertex.position.z*0.01)},
|
||||
{static_cast<float>(Vertex.normal.x),static_cast<float>(Vertex.normal.y),
|
||||
static_cast<float>(Vertex.normal.z)}});
|
||||
Out.Indices=std::move(Result->indices);
|
||||
if(bProfile)
|
||||
{
|
||||
const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread();
|
||||
for(size_t I=0;I<Out.ProfileCycles.size();++I)
|
||||
Out.ProfileCycles[I]=After.Cycles[I]>=Before.Cycles[I]?After.Cycles[I]-Before.Cycles[I]:0;
|
||||
}
|
||||
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
|
||||
Out.CompletedAt=FClock::now();return Out;
|
||||
}
|
||||
std::vector<float> 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;
|
||||
|
||||
@@ -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<uint32>(Seed));
|
||||
Result,WorldX,WorldY,WorldZ,static_cast<uint32>(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<float>(WorldX),static_cast<float>(WorldY),
|
||||
static_cast<float>(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
|
||||
|
||||
@@ -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<float>(BlockOrigin.X+X*Step),
|
||||
static_cast<float>(BlockOrigin.Y+Y*Step),
|
||||
static_cast<float>(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<int64>(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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<bool>* ShutdownFlag = nullptr;
|
||||
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
|
||||
const FStandaloneShapeWorld* ShapeWorldContext = nullptr;
|
||||
|
||||
public:
|
||||
|
||||
|
||||
Reference in new issue
Block a user