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:
Fr0zkaandClaude Opus 5.5 committed 2026-10-05 11:39:02 +02:00
1 parent 631dd57eb4
commit 557f6aac30
22 files changed
+6173 -5051

No files matched your search

+55 -45
View File
@@ -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" }
+111
View File
@@ -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.
+20
View File
@@ -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_;
};
}
+821
View File
@@ -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);
}
+484
View File
@@ -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
View File
@@ -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
+120 -60
View File
@@ -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`.
+19 -16
View File
@@ -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;
+15 -3
View File
@@ -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 -181
View File
@@ -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
+205 -104
View File
@@ -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)));
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;
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);
}
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,7 +2368,7 @@ 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,
int RunEditTransactionBenchmark(FStandaloneOwnerWorld& World,VoxelForgeCore::World& Shape,
const FVector& CameraPositionVoxels,int32 WorkerCount)
{
using Clock=std::chrono::steady_clock;
@@ -2287,39 +2376,50 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
// 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;
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";
+68 -825
View File
@@ -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,8 +563,18 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
{
VoxelDensityProfile::RecordClassifierCall(bSampleClassifier);
}
const EVoxelTileClass Verdict = Generator->ClassifyTile(
BlockOrigin, Step, BlockCells,
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)
{
@@ -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,10 +1016,12 @@ 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,
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)
{
@@ -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: