wip(assembler): M3 round 1 -- native FloatingIslands recipe in Core (standalone)
Bounds 517 boxes, 0 missed, negative control caught; capsule route 1044 centers, raised-route control fails; W1/W8/W16 stable (field bf3ce7da876431e2); 39 ms native vs 64 ms legacy. My review: islands read as thin flat pads with a radial pinch under them, not rock; next round fixes the look. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
4661a445be
commit
53a1e69af7
16 files changed
+2971
-1
No files matched your search
@@ -0,0 +1,118 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ProbePath = (Join-Path $PSScriptRoot '..\..\OwnerShapeWorld\core_strate_coverage\probes\type_6_FloatingIslands.json'),
|
||||
[string]$OutputDirectory = (Join-Path $PSScriptRoot 'results')
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ScriptProcess = Get-Process -Id $PID
|
||||
$ScriptProcess.PriorityClass = 'BelowNormal'
|
||||
$StandaloneRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
|
||||
$BuildRoot = Join-Path $StandaloneRoot 'Build\VoxelForgeCore'
|
||||
$CoreLibrary = Join-Path $BuildRoot 'VoxelForgeCore.lib'
|
||||
$AuditSource = Join-Path $PSScriptRoot 'FloatingIslandsAudit.cpp'
|
||||
$AuditExe = Join-Path $BuildRoot 'FloatingIslandsAudit.exe'
|
||||
$ProbePath = [IO.Path]::GetFullPath($ProbePath)
|
||||
$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
|
||||
$VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat'
|
||||
if (-not (Test-Path -LiteralPath $CoreLibrary -PathType Leaf)) { throw "Core library is missing: $CoreLibrary" }
|
||||
if (-not (Test-Path -LiteralPath $ProbePath -PathType Leaf)) { throw "Legacy probe is missing: $ProbePath" }
|
||||
if (-not (Test-Path -LiteralPath $VcVars -PathType Leaf)) { throw "MSVC environment script is missing: $VcVars" }
|
||||
[IO.Directory]::CreateDirectory($OutputDirectory) | Out-Null
|
||||
|
||||
$IncludeArgs = @(
|
||||
('/I"{0}"' -f (Join-Path $StandaloneRoot 'Core\include')),
|
||||
('/I"{0}"' -f (Join-Path $StandaloneRoot 'Core\src')),
|
||||
('/I"{0}"' -f (Join-Path $StandaloneRoot 'Compat')),
|
||||
('/I"{0}"' -f (Join-Path $StandaloneRoot 'Source')),
|
||||
('/I"{0}"' -f (Join-Path $StandaloneRoot 'Source\VoxelForge\Public')),
|
||||
('/I"{0}"' -f (Join-Path $StandaloneRoot 'Source\VoxelForge\Private'))
|
||||
)
|
||||
$LegacyObjectNames = @(
|
||||
'UnrealPerlin.obj','StandaloneDataAssetGlue.obj','VoxelBiomeDefinition.obj','VoxelCaveMorphology.obj',
|
||||
'VoxelDensityAblation.obj','VoxelDensityOpStack.obj','VoxelDensityProfile.obj','VoxelDiffLayer.obj',
|
||||
'VoxelGenerator.obj','VoxelHeightOpStack.obj','VoxelMarchingCubesMesher.obj','VoxelSettings.obj',
|
||||
'VoxelStrateComposer.obj','VoxelStrateManager.obj','VoxelStrateMeasure.obj','VoxelTerrainOpDefinition.obj',
|
||||
'VoxelWormField.obj','GpuFarViewerLinkStubs.obj'
|
||||
)
|
||||
foreach ($ObjectName in $LegacyObjectNames) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $BuildRoot $ObjectName) -PathType Leaf)) {
|
||||
throw "Required legacy bridge object is missing: $ObjectName; run Standalone/Build.ps1 -Target All first."
|
||||
}
|
||||
}
|
||||
|
||||
$ResponsePath = Join-Path $BuildRoot 'floating-islands-audit.rsp'
|
||||
$ResponseLines = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc','/D_CRT_SECURE_NO_WARNINGS') +
|
||||
$IncludeArgs + @(('/Fe"{0}"' -f $AuditExe),('"{0}"' -f $AuditSource)) +
|
||||
@($LegacyObjectNames | ForEach-Object { '"{0}"' -f $_ }) + @(
|
||||
('"{0}"' -f $CoreLibrary),'/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib'
|
||||
)
|
||||
[IO.File]::WriteAllLines($ResponsePath,$ResponseLines,[Text.UTF8Encoding]::new($false))
|
||||
Push-Location $BuildRoot
|
||||
try {
|
||||
$CompileCommand = 'call "{0}" >nul && cl.exe @"{1}"' -f $VcVars,$ResponsePath
|
||||
& $env:ComSpec /d /s /c $CompileCommand
|
||||
if ($LASTEXITCODE -eq -1073741502) { throw 'Audit build exited with 0xC0000142; retry once using this same wrapper.' }
|
||||
if ($LASTEXITCODE -ne 0) { throw "Floating-island audit build failed with exit code $LASTEXITCODE" }
|
||||
if (-not (Test-Path -LiteralPath $AuditExe -PathType Leaf)) { throw "Audit executable was not produced: $AuditExe" }
|
||||
} finally { Pop-Location }
|
||||
|
||||
$RunLedger = Join-Path $OutputDirectory 'run_ledger.txt'
|
||||
$RunRecords = [Collections.Generic.List[string]]::new()
|
||||
$Succeeded = $false
|
||||
for ($Attempt = 1; $Attempt -le 2 -and -not $Succeeded; ++$Attempt) {
|
||||
$StartInfo = [Diagnostics.ProcessStartInfo]::new()
|
||||
$StartInfo.FileName = $AuditExe
|
||||
$StartInfo.Arguments = '"{0}" "{1}"' -f $ProbePath.Replace('"','\"'),$OutputDirectory.Replace('"','\"')
|
||||
$StartInfo.WorkingDirectory = $BuildRoot
|
||||
$StartInfo.UseShellExecute = $false
|
||||
$StartInfo.CreateNoWindow = $true
|
||||
$StartInfo.RedirectStandardOutput = $true
|
||||
$StartInfo.RedirectStandardError = $true
|
||||
$Process = [Diagnostics.Process]::new()
|
||||
$Process.StartInfo = $StartInfo
|
||||
[void]$Process.Start()
|
||||
try { $Process.PriorityClass = 'BelowNormal' } catch { }
|
||||
$StdoutTask = $Process.StandardOutput.ReadToEndAsync()
|
||||
$StderrTask = $Process.StandardError.ReadToEndAsync()
|
||||
$Clock = [Diagnostics.Stopwatch]::StartNew()
|
||||
[int64]$PeakPrivateBytes = 0
|
||||
$LimitFailure = $null
|
||||
while (-not $Process.WaitForExit(250)) {
|
||||
$Process.Refresh()
|
||||
$PeakPrivateBytes = [Math]::Max($PeakPrivateBytes,$Process.PrivateMemorySize64)
|
||||
if ($PeakPrivateBytes -gt 8GB) {
|
||||
$LimitFailure = 'process private bytes exceeded 8 GiB'
|
||||
$Process.Kill()
|
||||
break
|
||||
}
|
||||
if ($Clock.Elapsed.TotalMinutes -ge 15.0) {
|
||||
$LimitFailure = 'audit test exceeded the 15-minute time box'
|
||||
$Process.Kill()
|
||||
break
|
||||
}
|
||||
}
|
||||
$Process.WaitForExit()
|
||||
$Clock.Stop()
|
||||
$Process.Refresh()
|
||||
$PeakPrivateBytes = [Math]::Max($PeakPrivateBytes,$Process.PrivateMemorySize64)
|
||||
$Stdout = $StdoutTask.GetAwaiter().GetResult()
|
||||
$Stderr = $StderrTask.GetAwaiter().GetResult()
|
||||
if ($Stdout) { Write-Output $Stdout.TrimEnd() }
|
||||
if ($Stderr) { [Console]::Error.WriteLine($Stderr.TrimEnd()) }
|
||||
$ExitCode = $Process.ExitCode
|
||||
$Status = if ($LimitFailure) { $LimitFailure } elseif ($ExitCode -eq 0) { 'passed' } else { "failed exit=$ExitCode" }
|
||||
$RunRecords.Add("attempt=$Attempt status=$Status elapsed_s=$([Math]::Round($Clock.Elapsed.TotalSeconds,2)) peak_private_MiB=$([Math]::Round($PeakPrivateBytes/1MB,1)) exit=$ExitCode")
|
||||
if ($LimitFailure) { break }
|
||||
if ($ExitCode -eq 0) { $Succeeded = $true; break }
|
||||
if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) {
|
||||
$RunRecords.Add('retry_reason=0xC0000142 single permitted retry')
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
[IO.File]::WriteAllLines($RunLedger,$RunRecords,[Text.UTF8Encoding]::new($false))
|
||||
Get-Content -LiteralPath $RunLedger | Write-Output
|
||||
if (-not $Succeeded) { throw "Floating-island audit run failed or exceeded a process limit. See $RunLedger" }
|
||||
Write-Output "audit_executable=$AuditExe"
|
||||
Write-Output "audit_results=$OutputDirectory"
|
||||
@@ -0,0 +1,728 @@
|
||||
#include "CoreMinimal.h"
|
||||
#include "OwnerWorldLoader.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifdef TEXT
|
||||
#undef TEXT
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#ifdef TEXT
|
||||
#undef TEXT
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
constexpr std::int64_t Q = WorldAssemblySubvoxelsPerVoxel;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Point = std::array<std::int64_t, 3>;
|
||||
|
||||
struct OracleAnchor
|
||||
{
|
||||
Point surfaceQ{};
|
||||
std::uint32_t supportScore = 0;
|
||||
};
|
||||
|
||||
struct SampleStats
|
||||
{
|
||||
std::string hash;
|
||||
double legacyMilliseconds = 0.0;
|
||||
double nativeMilliseconds = 0.0;
|
||||
std::uint64_t signMismatches = 0;
|
||||
std::uint64_t legacySolidSamples = 0;
|
||||
std::uint64_t nativeSolidSamples = 0;
|
||||
std::uint64_t legacyOnlySolidSamples = 0;
|
||||
std::uint64_t nativeOnlySolidSamples = 0;
|
||||
double legacyChecksum = 0.0;
|
||||
double nativeChecksum = 0.0;
|
||||
};
|
||||
|
||||
struct Triangle
|
||||
{
|
||||
std::array<float, 12> values{}; // Three xyz vertices followed by one flat normal.
|
||||
};
|
||||
|
||||
struct MeshResult
|
||||
{
|
||||
std::uint64_t gridSamples = 0;
|
||||
std::uint64_t triangles = 0;
|
||||
std::uint64_t bytes = 0;
|
||||
};
|
||||
|
||||
struct ExistingHashes
|
||||
{
|
||||
std::string round1Manifest;
|
||||
std::string surfaceWorldRound4Manifest;
|
||||
};
|
||||
|
||||
std::uint64_t Mix(std::uint64_t value)
|
||||
{
|
||||
value += 0x9e3779b97f4a7c15ull;
|
||||
value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ull;
|
||||
value = (value ^ (value >> 27)) * 0x94d049bb133111ebull;
|
||||
return value ^ (value >> 31);
|
||||
}
|
||||
|
||||
std::uint64_t Next(std::uint64_t& state)
|
||||
{
|
||||
state += 0x9e3779b97f4a7c15ull;
|
||||
return Mix(state);
|
||||
}
|
||||
|
||||
std::int64_t VoxQ(double value)
|
||||
{ return static_cast<std::int64_t>(std::llround(value * static_cast<double>(Q))); }
|
||||
|
||||
std::string Hex64(std::uint64_t value)
|
||||
{
|
||||
std::ostringstream output;
|
||||
output << std::hex << std::setfill('0') << std::setw(16) << value;
|
||||
return output.str();
|
||||
}
|
||||
|
||||
std::string HashValues(const std::vector<std::int64_t>& values)
|
||||
{
|
||||
std::uint64_t hash = 14695981039346656037ull;
|
||||
for (const auto value : values)
|
||||
for (int byte = 0; byte < 8; ++byte)
|
||||
{
|
||||
hash ^= static_cast<std::uint8_t>(static_cast<std::uint64_t>(value) >> (byte * 8));
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return Hex64(hash);
|
||||
}
|
||||
|
||||
ExistingHashes VerifyExistingManifests(const std::filesystem::path& standaloneRoot)
|
||||
{
|
||||
const auto read = [](const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input) throw std::runtime_error("cannot read existing manifest: " + path.string());
|
||||
return std::string(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
|
||||
};
|
||||
ExistingHashes hashes;
|
||||
std::string error;
|
||||
const auto round1 = WorldAssemblyManifest::FromJson(read(
|
||||
standaloneRoot / "Research/WorldAssemblerM1/round1-manifest.json"), error);
|
||||
if (!round1 || !round1->validate(error)
|
||||
|| Hex64(round1->contentHash) != "c87ae279b0e26f53")
|
||||
throw std::runtime_error("existing M1 manifest hash changed or failed validation: " + error);
|
||||
hashes.round1Manifest = Hex64(round1->contentHash);
|
||||
error.clear();
|
||||
const auto surface = WorldAssemblyManifest::FromJson(read(
|
||||
standaloneRoot / "Research/WorldAssemblerM1/round4/world_assembly.json"), error);
|
||||
if (!surface || !surface->validate(error)
|
||||
|| Hex64(surface->contentHash) != "8933693d9560ba9f")
|
||||
throw std::runtime_error("existing SurfaceWorld round-4 hash changed or failed validation: " + error);
|
||||
hashes.surfaceWorldRound4Manifest = Hex64(surface->contentHash);
|
||||
return hashes;
|
||||
}
|
||||
|
||||
float LegacyDensity(const FStandaloneOwnerWorld& owner, const Point& q)
|
||||
{
|
||||
return owner.Generator->GetDensityAt(
|
||||
static_cast<float>(q[0] / static_cast<double>(Q)),
|
||||
static_cast<float>(q[1] / static_cast<double>(Q)),
|
||||
static_cast<float>(q[2] / static_cast<double>(Q)));
|
||||
}
|
||||
|
||||
OracleAnchor FindLegacyAnchor(const FStandaloneOwnerWorld& owner)
|
||||
{
|
||||
OracleAnchor best;
|
||||
const float highZ = -560.0f;
|
||||
const float lowZ = -2480.0f;
|
||||
constexpr float ScanStep = 8.0f;
|
||||
for (int y = -448; y <= 448; y += 16)
|
||||
for (int x = -448; x <= 448; x += 16)
|
||||
{
|
||||
float airZ = highZ;
|
||||
float above = owner.Generator->GetDensityAt(static_cast<float>(x),
|
||||
static_cast<float>(y), airZ);
|
||||
if (above < 0.0f) continue;
|
||||
for (float solidZ = highZ - ScanStep; solidZ >= lowZ; solidZ -= ScanStep)
|
||||
{
|
||||
const float density = owner.Generator->GetDensityAt(static_cast<float>(x),
|
||||
static_cast<float>(y), solidZ);
|
||||
if (density < 0.0f)
|
||||
{
|
||||
float outside = airZ, inside = solidZ;
|
||||
for (int iteration = 0; iteration < 12; ++iteration)
|
||||
{
|
||||
const float middle = (outside + inside) * 0.5f;
|
||||
if (owner.Generator->GetDensityAt(static_cast<float>(x),
|
||||
static_cast<float>(y), middle) < 0.0f) inside = middle;
|
||||
else outside = middle;
|
||||
}
|
||||
const std::int64_t topQ = VoxQ(outside);
|
||||
std::uint32_t score = 0;
|
||||
for (int dy = -16; dy <= 16; dy += 8)
|
||||
for (int dx = -16; dx <= 16; dx += 8)
|
||||
if (owner.Generator->GetDensityAt(static_cast<float>(x + dx),
|
||||
static_cast<float>(y + dy),
|
||||
static_cast<float>((topQ - 4 * Q) / static_cast<double>(Q))) < 0.0f)
|
||||
++score;
|
||||
if (score > best.supportScore
|
||||
|| (score == best.supportScore
|
||||
&& std::abs(x) + std::abs(y)
|
||||
< std::abs(best.surfaceQ[0] / Q) + std::abs(best.surfaceQ[1] / Q)))
|
||||
{
|
||||
best.surfaceQ = {static_cast<std::int64_t>(x) * Q,
|
||||
static_cast<std::int64_t>(y) * Q, topQ};
|
||||
best.supportScore = score;
|
||||
}
|
||||
break; // The first air-to-solid crossing is the island top.
|
||||
}
|
||||
airZ = solidZ;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
std::vector<Point> MakeSamples(const FloatingIslandInfluenceBounds& bounds,
|
||||
std::size_t count, std::uint64_t seed)
|
||||
{
|
||||
std::vector<Point> points(count);
|
||||
std::uint64_t state = seed;
|
||||
for (auto& point : points)
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
{
|
||||
const std::uint64_t span = static_cast<std::uint64_t>(
|
||||
bounds.maximumSubvoxels[axis] - bounds.minimumSubvoxels[axis] + 1);
|
||||
point[axis] = bounds.minimumSubvoxels[axis]
|
||||
+ static_cast<std::int64_t>(Next(state) % std::max<std::uint64_t>(span, 1));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
SampleStats HashWithWorkers(const FloatingIslandFieldV1& field,
|
||||
const FStandaloneOwnerWorld& owner, const std::vector<Point>& points,
|
||||
std::uint32_t workers, bool measure)
|
||||
{
|
||||
std::vector<std::int64_t> values(points.size());
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(workers);
|
||||
const auto evaluate = [&](std::uint32_t worker)
|
||||
{
|
||||
for (std::size_t i = worker; i < points.size(); i += workers)
|
||||
values[i] = field.densitySubvoxels(points[i][0], points[i][1], points[i][2]);
|
||||
};
|
||||
for (std::uint32_t worker = 0; worker < workers; ++worker)
|
||||
threads.emplace_back(evaluate, worker);
|
||||
for (auto& thread : threads) thread.join();
|
||||
|
||||
SampleStats stats;
|
||||
stats.hash = HashValues(values);
|
||||
if (!measure) return stats;
|
||||
std::vector<float> legacyValues(points.size());
|
||||
const auto beginLegacy = Clock::now();
|
||||
double legacyChecksum = 0.0;
|
||||
for (std::size_t i = 0; i < points.size(); ++i)
|
||||
{
|
||||
const float value = LegacyDensity(owner, points[i]);
|
||||
legacyValues[i] = value;
|
||||
legacyChecksum += value;
|
||||
}
|
||||
const auto endLegacy = Clock::now();
|
||||
const auto beginNative = Clock::now();
|
||||
double nativeChecksum = 0.0;
|
||||
for (const Point& point : points)
|
||||
nativeChecksum += static_cast<double>(field.densitySubvoxels(
|
||||
point[0], point[1], point[2])) / static_cast<double>(Q);
|
||||
const auto endNative = Clock::now();
|
||||
for (std::size_t i = 0; i < values.size(); ++i)
|
||||
{
|
||||
const bool legacySolid = legacyValues[i] < 0.0f;
|
||||
const bool nativeSolid = values[i] < 0;
|
||||
if (legacySolid) ++stats.legacySolidSamples;
|
||||
if (nativeSolid) ++stats.nativeSolidSamples;
|
||||
if (legacySolid && !nativeSolid) ++stats.legacyOnlySolidSamples;
|
||||
if (nativeSolid && !legacySolid) ++stats.nativeOnlySolidSamples;
|
||||
if (legacySolid != nativeSolid) ++stats.signMismatches;
|
||||
}
|
||||
stats.legacyMilliseconds = std::chrono::duration<double, std::milli>(endLegacy - beginLegacy).count();
|
||||
stats.nativeMilliseconds = std::chrono::duration<double, std::milli>(endNative - beginNative).count();
|
||||
stats.legacyChecksum = legacyChecksum;
|
||||
stats.nativeChecksum = nativeChecksum;
|
||||
return stats;
|
||||
}
|
||||
|
||||
using D3 = std::array<double, 3>;
|
||||
D3 Add(D3 a, D3 b) { return {a[0] + b[0], a[1] + b[1], a[2] + b[2]}; }
|
||||
D3 Sub(D3 a, D3 b) { return {a[0] - b[0], a[1] - b[1], a[2] - b[2]}; }
|
||||
D3 Mul(D3 a, double s) { return {a[0] * s, a[1] * s, a[2] * s}; }
|
||||
double Dot(D3 a, D3 b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
|
||||
D3 Cross(D3 a, D3 b)
|
||||
{ return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]}; }
|
||||
double Length(D3 a) { return std::sqrt(Dot(a, a)); }
|
||||
D3 Normalize(D3 a)
|
||||
{
|
||||
const double length = Length(a);
|
||||
return length > 1e-12 ? Mul(a, 1.0 / length) : D3{0.0, 0.0, 1.0};
|
||||
}
|
||||
|
||||
double SampleNative(const FloatingIslandFieldV1& field, const Point& q)
|
||||
{ return static_cast<double>(field.densitySubvoxels(q[0], q[1], q[2])) / static_cast<double>(Q); }
|
||||
|
||||
double SampleLegacy(const FStandaloneOwnerWorld& owner, const Point& q)
|
||||
{ return static_cast<double>(LegacyDensity(owner, q)); }
|
||||
|
||||
double TetGradient(const std::array<D3, 4>& points, const std::array<double, 4>& values, D3& output)
|
||||
{
|
||||
const D3 a = Sub(points[1], points[0]);
|
||||
const D3 b = Sub(points[2], points[0]);
|
||||
const D3 c = Sub(points[3], points[0]);
|
||||
const D3 bc = Cross(b, c), ca = Cross(c, a), ab = Cross(a, b);
|
||||
const double determinant = Dot(a, bc);
|
||||
if (std::abs(determinant) < 1e-12) { output = {0.0, 0.0, 1.0}; return 0.0; }
|
||||
output = Mul(Add(Add(Mul(bc, values[1] - values[0]),
|
||||
Mul(ca, values[2] - values[0])), Mul(ab, values[3] - values[0])), 1.0 / determinant);
|
||||
return Length(output);
|
||||
}
|
||||
|
||||
MeshResult ExtractMesh(const std::filesystem::path& outputPath,
|
||||
Point minimumQ, Point maximumQ, std::int64_t stepQ,
|
||||
const std::function<double(const Point&)>& density)
|
||||
{
|
||||
const std::size_t nx = static_cast<std::size_t>((maximumQ[0] - minimumQ[0]) / stepQ + 1);
|
||||
const std::size_t ny = static_cast<std::size_t>((maximumQ[1] - minimumQ[1]) / stepQ + 1);
|
||||
const std::size_t nz = static_cast<std::size_t>((maximumQ[2] - minimumQ[2]) / stepQ + 1);
|
||||
const std::size_t gridCount = nx * ny * nz;
|
||||
if (gridCount > 4'000'000) throw std::runtime_error("mesh grid exceeds the 4M-sample cap");
|
||||
std::vector<float> grid(gridCount);
|
||||
const auto gridIndex = [=](std::size_t x, std::size_t y, std::size_t z)
|
||||
{ return (z * ny + y) * nx + x; };
|
||||
for (std::size_t z = 0; z < nz; ++z)
|
||||
for (std::size_t y = 0; y < ny; ++y)
|
||||
for (std::size_t x = 0; x < nx; ++x)
|
||||
{
|
||||
const Point q{minimumQ[0] + static_cast<std::int64_t>(x) * stepQ,
|
||||
minimumQ[1] + static_cast<std::int64_t>(y) * stepQ,
|
||||
minimumQ[2] + static_cast<std::int64_t>(z) * stepQ};
|
||||
grid[gridIndex(x, y, z)] = static_cast<float>(density(q));
|
||||
}
|
||||
|
||||
constexpr std::array<std::array<int, 3>, 8> Corner = {{{0,0,0},{1,0,0},{1,1,0},{0,1,0},
|
||||
{0,0,1},{1,0,1},{1,1,1},{0,1,1}}};
|
||||
constexpr std::array<std::array<int, 4>, 6> Tets = {{{0,5,1,6},{0,1,2,6},{0,2,3,6},
|
||||
{0,3,7,6},{0,7,4,6},{0,4,5,6}}};
|
||||
constexpr std::array<std::array<int, 2>, 6> TetEdges = {{{0,1},{0,2},{0,3},{1,2},{1,3},{2,3}}};
|
||||
std::vector<Triangle> triangles;
|
||||
triangles.reserve(8192);
|
||||
for (std::size_t z = 0; z + 1 < nz; ++z)
|
||||
for (std::size_t y = 0; y + 1 < ny; ++y)
|
||||
for (std::size_t x = 0; x + 1 < nx; ++x)
|
||||
{
|
||||
std::array<D3, 8> cubePoints{};
|
||||
std::array<double, 8> cubeValues{};
|
||||
double low = std::numeric_limits<double>::infinity();
|
||||
double high = -std::numeric_limits<double>::infinity();
|
||||
for (std::size_t corner = 0; corner < Corner.size(); ++corner)
|
||||
{
|
||||
const auto& offset = Corner[corner];
|
||||
cubePoints[corner] = {
|
||||
static_cast<double>(minimumQ[0] + static_cast<std::int64_t>(x + offset[0]) * stepQ) / Q,
|
||||
static_cast<double>(minimumQ[1] + static_cast<std::int64_t>(y + offset[1]) * stepQ) / Q,
|
||||
static_cast<double>(minimumQ[2] + static_cast<std::int64_t>(z + offset[2]) * stepQ) / Q};
|
||||
cubeValues[corner] = grid[gridIndex(x + offset[0], y + offset[1], z + offset[2])];
|
||||
low = std::min(low, cubeValues[corner]);
|
||||
high = std::max(high, cubeValues[corner]);
|
||||
}
|
||||
if (low >= 0.0 || high < 0.0) continue;
|
||||
|
||||
for (const auto& tet : Tets)
|
||||
{
|
||||
std::array<D3, 4> points{};
|
||||
std::array<double, 4> values{};
|
||||
for (std::size_t i = 0; i < 4; ++i)
|
||||
{ points[i] = cubePoints[tet[i]]; values[i] = cubeValues[tet[i]]; }
|
||||
std::array<D3, 4> polygon{};
|
||||
std::size_t polygonCount = 0;
|
||||
for (const auto& edge : TetEdges)
|
||||
{
|
||||
const int ia = edge[0], ib = edge[1];
|
||||
if ((values[ia] < 0.0) == (values[ib] < 0.0)) continue;
|
||||
const double t = values[ia] / (values[ia] - values[ib]);
|
||||
const D3 point = Add(points[ia], Mul(Sub(points[ib], points[ia]), t));
|
||||
bool duplicate = false;
|
||||
for (std::size_t i = 0; i < polygonCount; ++i)
|
||||
if (Length(Sub(polygon[i], point)) < 1e-7) { duplicate = true; break; }
|
||||
if (!duplicate && polygonCount < polygon.size()) polygon[polygonCount++] = point;
|
||||
}
|
||||
if (polygonCount < 3) continue;
|
||||
D3 normal{};
|
||||
TetGradient(points, values, normal);
|
||||
normal = Normalize(normal);
|
||||
D3 center{};
|
||||
for (std::size_t i = 0; i < polygonCount; ++i) center = Add(center, polygon[i]);
|
||||
center = Mul(center, 1.0 / polygonCount);
|
||||
const D3 axis = std::abs(normal[2]) < 0.85 ? D3{0,0,1} : D3{0,1,0};
|
||||
const D3 right = Normalize(Cross(axis, normal));
|
||||
const D3 up = Normalize(Cross(normal, right));
|
||||
std::array<std::pair<double, D3>, 4> ordered{};
|
||||
for (std::size_t i = 0; i < polygonCount; ++i)
|
||||
{
|
||||
const D3 delta = Sub(polygon[i], center);
|
||||
ordered[i] = {std::atan2(Dot(delta, up), Dot(delta, right)), polygon[i]};
|
||||
}
|
||||
std::sort(ordered.begin(), ordered.begin() + polygonCount,
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
for (std::size_t i = 1; i + 1 < polygonCount; ++i)
|
||||
{
|
||||
D3 a = ordered[0].second, b = ordered[i].second, c = ordered[i + 1].second;
|
||||
if (Dot(Cross(Sub(b, a), Sub(c, a)), normal) < 0.0) std::swap(b, c);
|
||||
Triangle triangle;
|
||||
for (std::size_t axisIndex = 0; axisIndex < 3; ++axisIndex)
|
||||
{
|
||||
triangle.values[axisIndex] = static_cast<float>(a[axisIndex]);
|
||||
triangle.values[3 + axisIndex] = static_cast<float>(b[axisIndex]);
|
||||
triangle.values[6 + axisIndex] = static_cast<float>(c[axisIndex]);
|
||||
triangle.values[9 + axisIndex] = static_cast<float>(normal[axisIndex]);
|
||||
}
|
||||
triangles.push_back(triangle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream output(outputPath, std::ios::binary | std::ios::trunc);
|
||||
if (!output) throw std::runtime_error("cannot create mesh capture: " + outputPath.string());
|
||||
const std::uint32_t count = static_cast<std::uint32_t>(triangles.size());
|
||||
output.write("VFMI", 4);
|
||||
output.write(reinterpret_cast<const char*>(&count), sizeof(count));
|
||||
for (const auto& triangle : triangles)
|
||||
output.write(reinterpret_cast<const char*>(triangle.values.data()),
|
||||
static_cast<std::streamsize>(sizeof(triangle.values)));
|
||||
output.close();
|
||||
MeshResult result;
|
||||
result.gridSamples = gridCount;
|
||||
result.triangles = triangles.size();
|
||||
result.bytes = std::filesystem::file_size(outputPath);
|
||||
return result;
|
||||
}
|
||||
|
||||
void WriteJson(const std::filesystem::path& path, const FloatingIslandManifestV1& manifest,
|
||||
const FloatingIslandBoundsAuditResult& bounds, const FloatingIslandRouteAuditResult& route,
|
||||
const FloatingIslandRouteAuditResult& raised, const SampleStats& sample,
|
||||
const OracleAnchor& anchor, const std::array<MeshResult, 4>& meshes,
|
||||
const std::array<std::string, 3>& fieldHashes, const ExistingHashes& oldHashes)
|
||||
{
|
||||
std::ofstream out(path, std::ios::trunc);
|
||||
if (!out) throw std::runtime_error("cannot create audit result JSON");
|
||||
out << std::setprecision(12);
|
||||
out << "{\n \"recipe_hash\":\"" << Hex64(manifest.recipe.contentHash)
|
||||
<< "\",\n \"plan_hash\":\"" << Hex64(manifest.plan.contentHash)
|
||||
<< "\",\n \"manifest_hash\":\"" << Hex64(manifest.contentHash)
|
||||
<< "\",\n \"unchanged_existing_hashes\":{\"m1_round1_manifest\":\""
|
||||
<< oldHashes.round1Manifest << "\",\"surfaceworld_round4_manifest\":\""
|
||||
<< oldHashes.surfaceWorldRound4Manifest << "\"}"
|
||||
<< ",\n \"plan_islands\":" << manifest.plan.islands.size()
|
||||
<< ",\n \"plan_connectors\":" << manifest.plan.clusterEdges.size()
|
||||
<< ",\n \"route_points\":" << manifest.plan.route.size()
|
||||
<< ",\n \"legacy_anchor_voxels\":[" << anchor.surfaceQ[0] / static_cast<double>(Q)
|
||||
<< ',' << anchor.surfaceQ[1] / static_cast<double>(Q) << ','
|
||||
<< anchor.surfaceQ[2] / static_cast<double>(Q) << "],\n \"legacy_anchor_plateau_samples\":"
|
||||
<< anchor.supportScore << ",\n \"recipe_parameters\":{\"seed\":" << manifest.recipe.seed
|
||||
<< ",\"density_q16\":" << manifest.recipe.islandDensityQ16
|
||||
<< ",\"spacing_voxels\":" << manifest.recipe.spacingSubvoxels / static_cast<double>(Q)
|
||||
<< ",\"radius_voxels\":[" << manifest.recipe.minimumRadiusSubvoxels / static_cast<double>(Q)
|
||||
<< ',' << manifest.recipe.maximumRadiusSubvoxels / static_cast<double>(Q)
|
||||
<< "],\"thickness_ratio_q16\":" << manifest.recipe.thicknessRatioQ16
|
||||
<< ",\"vertical_jitter_q16\":" << manifest.recipe.verticalJitterQ16
|
||||
<< ",\"top_flatten_q16\":" << manifest.recipe.topFlattenQ16
|
||||
<< ",\"contour_warp_voxels\":" << manifest.recipe.contourWarpSubvoxels / static_cast<double>(Q)
|
||||
<< ",\"roughness_voxels\":" << manifest.recipe.roughnessSubvoxels / static_cast<double>(Q)
|
||||
<< ",\"blend_voxels\":" << manifest.recipe.blendRadiusSubvoxels / static_cast<double>(Q) << "},\n \"stable_island_ids\":[";
|
||||
for (std::size_t i = 0; i < manifest.plan.islands.size(); ++i)
|
||||
{ if (i) out << ','; out << '\"' << Hex64(manifest.plan.islands[i].stableId) << '\"'; }
|
||||
out << "],\n \"stable_connector_ids\":[";
|
||||
for (std::size_t i = 0; i < manifest.plan.clusterEdges.size(); ++i)
|
||||
{ if (i) out << ','; out << '\"' << Hex64(manifest.plan.clusterEdges[i].stableId) << '\"'; }
|
||||
out << "],\n \"bounds\":{\"passed\":" << (bounds.passed ? "true" : "false")
|
||||
<< ",\"negative_control_detected\":" << (bounds.negativeControlDetected ? "true" : "false")
|
||||
<< ",\"boxes\":" << bounds.boxesTested << ",\"samples\":" << bounds.pointSamples
|
||||
<< ",\"unknown\":" << bounds.unknownBounds << ",\"missed\":" << bounds.missedBounds
|
||||
<< ",\"island_boxes\":" << bounds.islandBoundsTested
|
||||
<< ",\"connector_boxes\":" << bounds.connectorBoundsTested
|
||||
<< ",\"void_boxes_proved_air\":" << bounds.voidBoxesProvedAir << "},\n \"route\":{\"passed\":"
|
||||
<< (route.passed ? "true" : "false") << ",\"segments\":" << route.segmentsTested
|
||||
<< ",\"centers\":" << route.centerSamples << ",\"capsule_samples\":"
|
||||
<< route.capsuleFieldSamples << ",\"blocked\":" << route.blockedCapsuleSamples
|
||||
<< ",\"support_samples\":" << route.supportProbeSamples << ",\"unsupported\":"
|
||||
<< route.unsupportedCenterSamples << ",\"grade_failures\":" << route.gradeFailures
|
||||
<< ",\"minimum_clearance_subvoxels\":" << route.minimumClearanceSubvoxels
|
||||
<< "},\n \"raised_route_negative_control\":{\"detected\":"
|
||||
<< ((!raised.passed && (raised.unsupportedCenterSamples || raised.blockedCapsuleSamples)) ? "true" : "false")
|
||||
<< ",\"passed\":" << (raised.passed ? "true" : "false")
|
||||
<< ",\"unsupported\":" << raised.unsupportedCenterSamples
|
||||
<< ",\"blocked\":" << raised.blockedCapsuleSamples << "},\n \"determinism\":{\"W1\":\""
|
||||
<< fieldHashes[0] << "\",\"W8\":\"" << fieldHashes[1] << "\",\"W16\":\""
|
||||
<< fieldHashes[2] << "\",\"plan_repeat\":true,\"hash_samples\":32768},\n \"timing\":{\"same_queries\":32768,\"legacy_ms\":"
|
||||
<< sample.legacyMilliseconds << ",\"native_ms\":" << sample.nativeMilliseconds
|
||||
<< ",\"native_over_legacy\":" << (sample.nativeMilliseconds / std::max(sample.legacyMilliseconds, 1e-9))
|
||||
<< ",\"sign_mismatches\":" << sample.signMismatches << ",\"sign_mismatch_percent\":"
|
||||
<< (100.0 * sample.signMismatches / 32768.0)
|
||||
<< ",\"legacy_solid_samples\":" << sample.legacySolidSamples
|
||||
<< ",\"native_solid_samples\":" << sample.nativeSolidSamples
|
||||
<< ",\"legacy_only_solid_samples\":" << sample.legacyOnlySolidSamples
|
||||
<< ",\"native_only_solid_samples\":" << sample.nativeOnlySolidSamples << "},\n \"mesh\":[";
|
||||
constexpr std::array<const char*, 4> names{"cluster_legacy", "cluster_native", "detail_legacy", "detail_native"};
|
||||
for (std::size_t i = 0; i < meshes.size(); ++i)
|
||||
{
|
||||
if (i) out << ',';
|
||||
out << "{\"name\":\"" << names[i] << "\",\"grid_samples\":" << meshes[i].gridSamples
|
||||
<< ",\"triangles\":" << meshes[i].triangles << ",\"bytes\":" << meshes[i].bytes << '}';
|
||||
}
|
||||
out << "],\n \"route_points_voxels\":[";
|
||||
for (std::size_t i = 0; i < manifest.plan.route.size(); ++i)
|
||||
{
|
||||
if (i) out << ',';
|
||||
const auto& point = manifest.plan.route[i];
|
||||
out << '[' << point.positionSubvoxels[0] / static_cast<double>(Q) << ','
|
||||
<< point.positionSubvoxels[1] / static_cast<double>(Q) << ','
|
||||
<< point.positionSubvoxels[2] / static_cast<double>(Q) << ']';
|
||||
}
|
||||
out << "]\n}\n";
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
SetPriorityClass(GetCurrentProcess(), BELOW_NORMAL_PRIORITY_CLASS);
|
||||
SetProcessAffinityMask(GetCurrentProcess(), static_cast<DWORD_PTR>(0xFFFF));
|
||||
#endif
|
||||
try
|
||||
{
|
||||
if (argc != 3)
|
||||
{ std::cerr << "usage: FloatingIslandsAudit <legacy-probe.json> <output-dir>\n"; return 2; }
|
||||
const std::filesystem::path probePath = argv[1];
|
||||
const std::filesystem::path outputDir = argv[2];
|
||||
std::filesystem::create_directories(outputDir);
|
||||
const auto standaloneRoot = probePath.parent_path().parent_path().parent_path()
|
||||
.parent_path().parent_path();
|
||||
const ExistingHashes oldHashes = VerifyExistingManifests(standaloneRoot);
|
||||
|
||||
FStandaloneOwnerWorld owner;
|
||||
std::string error;
|
||||
if (!StandaloneOwnerLoader::Load(probePath.string(), owner, error))
|
||||
throw std::runtime_error("legacy probe load failed: " + error);
|
||||
const OracleAnchor anchor = FindLegacyAnchor(owner);
|
||||
if (anchor.supportScore < 9)
|
||||
throw std::runtime_error("legacy scan found no broad FloatingIslands top in the frozen strate range");
|
||||
|
||||
FloatingIslandRecipeV1 recipe;
|
||||
recipe.seed = 0;
|
||||
recipe.layoutOriginXSubvoxels = anchor.surfaceQ[0];
|
||||
recipe.layoutOriginYSubvoxels = anchor.surfaceQ[1];
|
||||
recipe.layoutCenterTopZSubvoxels = anchor.surfaceQ[2];
|
||||
recipe.layoutMinimumZSubvoxels = anchor.surfaceQ[2] - 96 * Q;
|
||||
recipe.layoutMaximumZSubvoxels = anchor.surfaceQ[2] + 96 * Q;
|
||||
recipe.primaryIslandCount = 5;
|
||||
recipe.islandDensityQ16 = 32768;
|
||||
recipe.spacingSubvoxels = 112 * Q;
|
||||
recipe.minimumRadiusSubvoxels = 24 * Q;
|
||||
recipe.maximumRadiusSubvoxels = 48 * Q;
|
||||
recipe.thicknessRatioQ16 = 39322;
|
||||
recipe.verticalJitterQ16 = 39322;
|
||||
recipe.topFlattenQ16 = 39322;
|
||||
recipe.taperEndMinimumQ16 = 29491;
|
||||
recipe.taperEndMaximumQ16 = 45875;
|
||||
recipe.blendRadiusSubvoxels = 6 * Q;
|
||||
recipe.contourWarpSubvoxels = VoxQ((24.0 + 48.0) * 0.35 * 0.5);
|
||||
recipe.roughnessSubvoxels = 3 * Q;
|
||||
recipe.boundarySealSubvoxels = 4 * Q;
|
||||
recipe.bridgeHalfWidthSubvoxels = 4 * Q;
|
||||
recipe.bridgeThicknessSubvoxels = 3 * Q;
|
||||
|
||||
const auto beginPlanning = Clock::now();
|
||||
auto planned = PlanFloatingIslandCluster(recipe, error);
|
||||
const auto endPlanning = Clock::now();
|
||||
if (!planned) throw std::runtime_error("native island planner failed: " + error);
|
||||
const std::string canonical = planned->ToJson();
|
||||
for (int repeat = 0; repeat < 2; ++repeat)
|
||||
{
|
||||
std::string repeatError;
|
||||
const auto repeated = PlanFloatingIslandCluster(recipe, repeatError);
|
||||
if (!repeated || repeated->contentHash != planned->contentHash
|
||||
|| repeated->plan.contentHash != planned->plan.contentHash
|
||||
|| repeated->ToJson() != canonical)
|
||||
throw std::runtime_error("repeated native plan hash or canonical JSON changed");
|
||||
}
|
||||
const auto parsed = FloatingIslandManifestV1::FromJson(canonical, error);
|
||||
if (!parsed || parsed->ToJson() != canonical || !parsed->validate(error))
|
||||
throw std::runtime_error("native manifest did not validate after round-trip: " + error);
|
||||
|
||||
FloatingIslandFieldV1 field(planned->recipe, planned->plan);
|
||||
if (!field.isValid()) throw std::runtime_error("native field rejected the planned island graph");
|
||||
const auto bounds = AuditFloatingIslandBounds(field, 512);
|
||||
const auto route = AuditFloatingIslandRoute(field);
|
||||
const auto raisedRoute = AuditFloatingIslandRoute(field, 8 * Q);
|
||||
const auto influence = field.influenceBounds();
|
||||
const auto points = MakeSamples(influence, 32768, 0x4d33524f554e4431ull);
|
||||
const auto w1 = HashWithWorkers(field, owner, points, 1, false);
|
||||
const auto w8 = HashWithWorkers(field, owner, points, 8, false);
|
||||
const auto w16 = HashWithWorkers(field, owner, points, 16, true);
|
||||
const std::array<std::string, 3> hashes{w1.hash, w8.hash, w16.hash};
|
||||
if (w1.hash != w8.hash || w1.hash != w16.hash)
|
||||
throw std::runtime_error("native field sample hash differs between W1/W8/W16");
|
||||
if (!bounds.passed) throw std::runtime_error("native island bounds audit or negative control failed");
|
||||
std::cout << "route_diagnostics pass=" << route.passed << " segments=" << route.segmentsTested
|
||||
<< " centers=" << route.centerSamples << " capsule_samples=" << route.capsuleFieldSamples
|
||||
<< " blocked=" << route.blockedCapsuleSamples << " support_samples=" << route.supportProbeSamples
|
||||
<< " unsupported=" << route.unsupportedCenterSamples << " grade_failures=" << route.gradeFailures
|
||||
<< " minimum_clearance_q=" << route.minimumClearanceSubvoxels
|
||||
<< " first_blocked_segment=" << route.firstBlockedSegment
|
||||
<< " first_unsupported_segment=" << route.firstUnsupportedSegment
|
||||
<< " first_grade_segment=" << route.firstGradeFailureSegment
|
||||
<< " unsupported_center_voxels=" << route.firstUnsupportedCenterSubvoxels[0] / static_cast<double>(Q)
|
||||
<< ',' << route.firstUnsupportedCenterSubvoxels[1] / static_cast<double>(Q)
|
||||
<< ',' << route.firstUnsupportedCenterSubvoxels[2] / static_cast<double>(Q) << '\n';
|
||||
if (!route.passed)
|
||||
{
|
||||
const auto& routePoints = planned->plan.route;
|
||||
const double maxGrade = static_cast<double>(planned->recipe.routeConstraints.maximumGradeQ16) / 65536.0;
|
||||
for (std::size_t i = 0; i + 1 < routePoints.size(); ++i)
|
||||
{
|
||||
const auto& a = routePoints[i].positionSubvoxels;
|
||||
const auto& b = routePoints[i + 1].positionSubvoxels;
|
||||
const double horizontal = std::hypot(static_cast<double>(b[0] - a[0]),
|
||||
static_cast<double>(b[1] - a[1]));
|
||||
const double grade = horizontal > 0.0 ? std::abs(static_cast<double>(b[2] - a[2])) / horizontal : 1e9;
|
||||
if (grade > maxGrade + 1e-7)
|
||||
std::cout << "grade_segment=" << i << " ratio=" << grade << " limit=" << maxGrade
|
||||
<< " from=" << a[0] / static_cast<double>(Q) << ',' << a[1] / static_cast<double>(Q)
|
||||
<< ',' << a[2] / static_cast<double>(Q) << " to=" << b[0] / static_cast<double>(Q)
|
||||
<< ',' << b[1] / static_cast<double>(Q) << ',' << b[2] / static_cast<double>(Q) << '\n';
|
||||
}
|
||||
for (std::size_t i = 0; i < planned->plan.islands.size(); ++i)
|
||||
{
|
||||
const auto& island = planned->plan.islands[i];
|
||||
std::cout << "island=" << i << " center=" << island.centerSubvoxels[0] / static_cast<double>(Q)
|
||||
<< ',' << island.centerSubvoxels[1] / static_cast<double>(Q) << ','
|
||||
<< island.topZSubvoxels / static_cast<double>(Q) << " radius="
|
||||
<< island.radiusSubvoxels / static_cast<double>(Q) << '\n';
|
||||
}
|
||||
const auto& constraints = planned->recipe.routeConstraints;
|
||||
const auto& unsupported = route.firstUnsupportedCenterSubvoxels;
|
||||
const double foot = unsupported[2] / static_cast<double>(Q)
|
||||
- constraints.capsuleHeightSubvoxels / (2.0 * Q);
|
||||
const double probeRadius = constraints.supportProbeRadiusSubvoxels / static_cast<double>(Q);
|
||||
const std::array<std::array<double, 2>, 5> supportPoints{{
|
||||
{0.0,0.0},{probeRadius,0.0},{-probeRadius,0.0},{0.0,probeRadius},{0.0,-probeRadius}}};
|
||||
for (std::size_t probeIndex = 0; probeIndex < supportPoints.size(); ++probeIndex)
|
||||
{
|
||||
double firstSolidDepth = -1.0;
|
||||
for (double depth = (constraints.minimumClearanceSubvoxels + 0.5 * Q) / static_cast<double>(Q);
|
||||
depth <= (constraints.minimumClearanceSubvoxels + constraints.supportSearchDepthSubvoxels)
|
||||
/ static_cast<double>(Q) + 0.001; depth += 0.5)
|
||||
{
|
||||
const auto density = field.densitySubvoxels(
|
||||
std::llround(unsupported[0] + supportPoints[probeIndex][0] * Q),
|
||||
std::llround(unsupported[1] + supportPoints[probeIndex][1] * Q),
|
||||
std::llround((foot - depth) * Q));
|
||||
if (density < 0) { firstSolidDepth = depth; break; }
|
||||
}
|
||||
std::cout << "support_probe=" << probeIndex << " first_solid_depth_vox=" << firstSolidDepth << '\n';
|
||||
}
|
||||
for (std::size_t i = 0; i < planned->plan.clusterEdges.size(); ++i)
|
||||
{
|
||||
const auto& edge = planned->plan.clusterEdges[i];
|
||||
std::cout << "connector=" << i << " start=" << edge.startSubvoxels[0] / static_cast<double>(Q)
|
||||
<< ',' << edge.startSubvoxels[1] / static_cast<double>(Q) << ','
|
||||
<< edge.startSubvoxels[2] / static_cast<double>(Q) << " end="
|
||||
<< edge.endSubvoxels[0] / static_cast<double>(Q) << ','
|
||||
<< edge.endSubvoxels[1] / static_cast<double>(Q) << ','
|
||||
<< edge.endSubvoxels[2] / static_cast<double>(Q) << '\n';
|
||||
}
|
||||
}
|
||||
if (!route.passed) throw std::runtime_error("native route capsule sweep failed");
|
||||
if (raisedRoute.passed || (raisedRoute.unsupportedCenterSamples == 0
|
||||
&& raisedRoute.blockedCapsuleSamples == 0))
|
||||
throw std::runtime_error("raised-route negative control did not detect unsupported traversal");
|
||||
|
||||
const std::int64_t ox = anchor.surfaceQ[0], oy = anchor.surfaceQ[1], oz = anchor.surfaceQ[2];
|
||||
std::array<MeshResult, 4> meshes{};
|
||||
const Point clusterMin{ox - 320 * Q, oy - 208 * Q, oz - 80 * Q};
|
||||
const Point clusterMax{ox + 320 * Q, oy + 208 * Q, oz + 16 * Q};
|
||||
const Point detailMin{ox - 64 * Q, oy - 64 * Q, oz - 48 * Q};
|
||||
const Point detailMax{ox + 64 * Q, oy + 64 * Q, oz + 10 * Q};
|
||||
meshes[0] = ExtractMesh(outputDir / "cluster_legacy.vfmesh", clusterMin, clusterMax, 4 * Q,
|
||||
[&](const Point& q) { return SampleLegacy(owner, q); });
|
||||
meshes[1] = ExtractMesh(outputDir / "cluster_native.vfmesh", clusterMin, clusterMax, 4 * Q,
|
||||
[&](const Point& q) { return SampleNative(field, q); });
|
||||
meshes[2] = ExtractMesh(outputDir / "detail_legacy.vfmesh", detailMin, detailMax, 2 * Q,
|
||||
[&](const Point& q) { return SampleLegacy(owner, q); });
|
||||
meshes[3] = ExtractMesh(outputDir / "detail_native.vfmesh", detailMin, detailMax, 2 * Q,
|
||||
[&](const Point& q) { return SampleNative(field, q); });
|
||||
if (meshes[0].triangles == 0 || meshes[1].triangles == 0
|
||||
|| meshes[2].triangles == 0 || meshes[3].triangles == 0)
|
||||
throw std::runtime_error("matched legacy/native mesh extraction produced an empty field");
|
||||
|
||||
WriteJson(outputDir / "audit.json", *planned, bounds, route, raisedRoute,
|
||||
w16, anchor, meshes, hashes, oldHashes);
|
||||
std::cout << std::setprecision(4)
|
||||
<< "legacy_anchor_voxels=" << anchor.surfaceQ[0] / static_cast<double>(Q) << ','
|
||||
<< anchor.surfaceQ[1] / static_cast<double>(Q) << ','
|
||||
<< anchor.surfaceQ[2] / static_cast<double>(Q)
|
||||
<< " plateau_samples=" << anchor.supportScore << '\n'
|
||||
<< "recipe_hash=" << Hex64(planned->recipe.contentHash)
|
||||
<< " plan_hash=" << Hex64(planned->plan.contentHash)
|
||||
<< " manifest_hash=" << Hex64(planned->contentHash)
|
||||
<< " prior_m1_hash=" << oldHashes.round1Manifest
|
||||
<< " prior_surfaceworld_hash=" << oldHashes.surfaceWorldRound4Manifest
|
||||
<< " islands=" << planned->plan.islands.size()
|
||||
<< " connectors=" << planned->plan.clusterEdges.size()
|
||||
<< " route_points=" << planned->plan.route.size()
|
||||
<< " planning_ms=" << std::chrono::duration<double, std::milli>(endPlanning - beginPlanning).count() << '\n'
|
||||
<< "bounds_pass=" << bounds.passed << " boxes=" << bounds.boxesTested
|
||||
<< " point_samples=" << bounds.pointSamples << " unknown=" << bounds.unknownBounds
|
||||
<< " missed=" << bounds.missedBounds << " void_air=" << bounds.voidBoxesProvedAir
|
||||
<< " negative_control=" << bounds.negativeControlDetected << '\n'
|
||||
<< "route_pass=" << route.passed << " segments=" << route.segmentsTested
|
||||
<< " centers=" << route.centerSamples << " capsule_samples=" << route.capsuleFieldSamples
|
||||
<< " blocked=" << route.blockedCapsuleSamples << " support_probes=" << route.supportProbeSamples
|
||||
<< " unsupported=" << route.unsupportedCenterSamples << " grade_failures=" << route.gradeFailures
|
||||
<< " clearance_q=" << route.minimumClearanceSubvoxels << '\n'
|
||||
<< "raised_route_detected=" << (!raisedRoute.passed && raisedRoute.unsupportedCenterSamples > 0)
|
||||
<< " unsupported=" << raisedRoute.unsupportedCenterSamples
|
||||
<< " blocked=" << raisedRoute.blockedCapsuleSamples << '\n'
|
||||
<< "field_hashes W1=" << w1.hash << " W8=" << w8.hash << " W16=" << w16.hash
|
||||
<< " legacy_ms=" << w16.legacyMilliseconds << " native_ms=" << w16.nativeMilliseconds
|
||||
<< " mismatch_percent=" << (100.0 * w16.signMismatches / 32768.0)
|
||||
<< " legacy_solid=" << w16.legacySolidSamples << " native_solid=" << w16.nativeSolidSamples
|
||||
<< " legacy_only=" << w16.legacyOnlySolidSamples << " native_only=" << w16.nativeOnlySolidSamples << '\n'
|
||||
<< "mesh_samples=" << meshes[0].gridSamples << ',' << meshes[1].gridSamples
|
||||
<< ',' << meshes[2].gridSamples << ',' << meshes[3].gridSamples
|
||||
<< " triangles=" << meshes[0].triangles << ',' << meshes[1].triangles
|
||||
<< ',' << meshes[2].triangles << ',' << meshes[3].triangles << '\n'
|
||||
<< "audit_json=" << (outputDir / "audit.json").string() << '\n';
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
std::cerr << "FLOATING_ISLAND_AUDIT_FAIL: " << exception.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
# M3 Round 1 — FloatingIslands standalone Core proof
|
||||
|
||||
**Result:** the new finite island field builds in Core and the standalone legacy bridge. The recipe,
|
||||
bounds audit, worker-count field hashes and supported route pass for the frozen owner probe. The native
|
||||
result is intentionally denser and more routeable than the stochastic legacy slice; this is a measured
|
||||
visual difference, not a parity claim.
|
||||
|
||||
## Native recipe and deliberate differences
|
||||
|
||||
Added `FloatingIslandRecipeV1`, `FloatingIslandManifestV1`, `FloatingIslandFieldV1` and the planner to
|
||||
Core. The island field is a reusable feature family on the `island` channel; it is not a branch inside
|
||||
the owner FloatingIslands generator. The standalone manifest carries recipe and plan hashes, seeded
|
||||
anchors, stable IDs, a connected cluster graph, finite influence boxes, bridge parameters and route
|
||||
points. Coordinates and density use the existing fixed scale of 1/256 voxel; one Core voxel remains
|
||||
25 cm and negative density is solid.
|
||||
|
||||
The visual oracle was loaded from
|
||||
[`type_6_FloatingIslands.json`](../../OwnerShapeWorld/core_strate_coverage/probes/type_6_FloatingIslands.json)
|
||||
and queried through the legacy `GetDensityAt` evaluator. Its settings are seed 0, spacing 112 voxels,
|
||||
density 0.5, radii 24–48 voxels, thickness ratio 0.6, vertical jitter 0.6, top flatten 0.6,
|
||||
roughness 3 voxels, blend radius 6 voxels and boundary seal 4 voxels. The native recipe uses those
|
||||
shape ranges and aligns its middle island to a detected legacy top at `(48, 16, -1904.035)` voxels.
|
||||
|
||||
The native shape uses a flat-ish top, an explicit finite underside plane, tapered radial support and
|
||||
a bounded 12.6016-voxel three-harmonic lobe warp. This intentionally replaces the legacy query-space
|
||||
3D fBM warp with a stable bounded contour function. The legacy source has no explicit lower plane in
|
||||
its island max-SDF; the native bottom closes the taper instead of carrying the legacy centerline
|
||||
degeneracy below the nominal island bottom. Native surface roughness remains bounded to the recipe's
|
||||
3-voxel envelope.
|
||||
|
||||
The planner guarantees five primary route islands. `islandDensityQ16` selects up to two optional side
|
||||
sites; this seed selected one, producing six islands and five sharp-tag-ready connectors. This differs
|
||||
from the legacy stochastic density roll, which decides whether every grid site exists. The planner
|
||||
keeps the route-connected islands in a narrower vertical band than the legacy strate: adjacent height
|
||||
changes are clamped to the capsule grade contract, with a 5% grade reserve, and the serialized route
|
||||
is fitted above its sampled capsule footprint. Connectors land 0.62 radii into the island tops, use a
|
||||
4-voxel half-width and 3-voxel thickness, and extend the field through the landings. This is why the
|
||||
local native slice contains substantially more rock than the old slice. The fixed-season water block
|
||||
is not emitted here; player edits remain unrestricted, and this generation audit applies the route
|
||||
law only to system-generated geometry.
|
||||
|
||||
## Bounds and route audits
|
||||
|
||||
Island AABBs include radius, maximum lobe displacement, taper Z, roughness and smoothing reach.
|
||||
Connector AABBs include the swept bridge width, endpoint span, thickness, roughness and smoothing
|
||||
reach. A box intersecting any feature influence box returns unknown; a box outside every feature box
|
||||
gets the certified all-air interval. The audit sampled each local field box and the void beside the
|
||||
connectors.
|
||||
|
||||
| Audit | Result |
|
||||
|---|---:|
|
||||
| Island / connector influence boxes | 6 / 5 |
|
||||
| Bounds boxes / point samples | 517 / 13,932 |
|
||||
| Intersecting boxes left unknown | 108 |
|
||||
| Missed bounds | 0 |
|
||||
| Void boxes proven air | 4 |
|
||||
| Shrunken-island-bound negative control | Detected |
|
||||
| Bound audit | **Pass** |
|
||||
|
||||
The bounds negative control removes the lobe and blend reach from a candidate island box, then finds
|
||||
negative density outside that false box while excluding every other feature's valid influence box.
|
||||
|
||||
The final-field route has 117 waypoints and 116 segments. Its analytic capsule sweep samples route
|
||||
centers no farther apart than 0.5 voxel, 17 capsule cross-section positions and five support probes
|
||||
per center; at least three support probes must find rock within the recorded search depth.
|
||||
|
||||
| Route result | Value |
|
||||
|---|---:|
|
||||
| Swept centers / capsule field samples | 1,044 / 283,968 |
|
||||
| Support probes | 6,344 |
|
||||
| Capsule samples inside rock | 0 |
|
||||
| Unsupported centers | 0 |
|
||||
| Grade failures | 0 |
|
||||
| Minimum sampled density | 40 subvoxels (0.15625 voxel) |
|
||||
| Route sweep | **Pass** |
|
||||
|
||||
The negative control raises the whole route by 8 voxels. It is rejected at all 1,044 swept centers for
|
||||
lack of support (zero capsule collisions), so the support test detects the failure mode independently
|
||||
of capsule intersection.
|
||||
|
||||
## Determinism and existing hashes
|
||||
|
||||
Planner calls repeated three times with identical canonical JSON and hashes. The 32,768-point native
|
||||
field stream hashes identically at W1, W8 and W16. Stable IDs for this plan are:
|
||||
|
||||
| Feature | Stable IDs |
|
||||
|---|---|
|
||||
| Islands | `e6e461e18b3a9c87`, `2bc1820baba047f6`, `adc259cbe2a395e9`, `62a5f29ab581c2b0`, `0412f78b71b39ceb`, `7855fb022cc561c9` |
|
||||
| Connectors | `eaa5dd310e846520`, `034023599b466c51`, `08e698cf39fd6c22`, `3794e6d8e5033e8b`, `4962fa885545fb26` |
|
||||
|
||||
The standalone check reparsed and validated the existing manifests without changing their content
|
||||
hashes: M1 round-1 `c87ae279b0e26f53` and SurfaceWorld round-4 `8933693d9560ba9f`. The existing Core
|
||||
field evaluators and M2 mesher implementation were not edited; the full M2 actor/mesher hash matrix
|
||||
was not replayed in this standalone round.
|
||||
|
||||
| New island content | Hash |
|
||||
|---|---|
|
||||
| Recipe | `111825edfebea17f` |
|
||||
| Plan | `65675554b7aa947e` |
|
||||
| Manifest | `03a4685554edce1a` |
|
||||
| Field samples W1 / W8 / W16 | `bf3ce7da876431e2` each |
|
||||
|
||||
## Matched captures and legacy timing
|
||||
|
||||
Five CPU-rendered capture pairs use the same detected island anchor, orthographic camera and scale.
|
||||
The cluster and close-up fields were sampled at 4-voxel and 2-voxel grids respectively. The
|
||||
tile/LOD view overlays 32-voxel tile and 64-voxel LOD reference planes on the canonical field mesh;
|
||||
it is a review view, not a production mesher seam-closure test. No viewer or GPU was launched.
|
||||
|
||||
[`contact_sheet.jpg`](results/captures/contact_sheet.jpg) — 640×1,290 px, 68,601 bytes. Individual
|
||||
matched pairs are in [`captures/`](results/captures/); each is 640×258 px and 12,798–18,890 bytes.
|
||||
They show the overview, underside, lobed edge/cliff, tile/LOD references and planned route. The legacy
|
||||
side of the route pair is unmarked because the legacy generator has no planner route.
|
||||
|
||||
The same 32,768 quantized query positions were timed serially against the direct legacy owner evaluator
|
||||
and `FloatingIslandFieldV1::densitySubvoxels`, excluding plan generation, mesh extraction, rendering
|
||||
and Unreal scheduling:
|
||||
|
||||
| Evaluator | Time |
|
||||
|---|---:|
|
||||
| Legacy owner bridge (`GetDensityAt`) | 63.62 ms |
|
||||
| Native island field | 39.31 ms |
|
||||
| Native / legacy | 0.618× (about 1.62× faster) |
|
||||
|
||||
This is a point-query comparison for the island field, not a full-world or tile-meshing benchmark. In
|
||||
the same sparse sample, 90 legacy samples and 885 native samples were solid: 14 were legacy-only and
|
||||
809 native-only. The overall sign mismatch is 823/32,768 (2.512%), mostly because air dominates the
|
||||
sample volume. The side-by-side captures and these occupancy counts show that percentage must not be
|
||||
read as visual or occupancy parity; the routeable native cluster is denser in this narrow vertical
|
||||
slice than the stochastic owner field.
|
||||
|
||||
## Run ledger
|
||||
|
||||
All builds and audit runs stayed below the 15-minute stop limit. No process crashed and no
|
||||
`0xC0000142` retry occurred. The audit wrapper samples its test process; the final process peaked at
|
||||
45.8 MiB private memory and the highest observed failed-audit process was 159.3 MiB. The existing
|
||||
MSVC setup prints a French `vswhere.exe`-not-found warning before successfully finding `vcvars64.bat`;
|
||||
the standalone build also emits existing float-conversion warnings in `VoxelStrateManager.cpp`.
|
||||
|
||||
| Failed run | Failure | Correction / outcome |
|
||||
|---|---|---|
|
||||
| Initial `Target All` compile | New island methods referenced interval/detail types declared later; one `Q` use was outside its namespace. | Forward declarations and qualified fixed-scale constant; subsequent Core/All builds passed. |
|
||||
| Second `Target All` compile | One remaining unqualified `Q` in `FloatingIslandFieldV1::isValid`. | Qualified it; subsequent builds passed. |
|
||||
| First audit-wrapper invocation | Default probe path resolved one directory too high; audit executable did not start. | Corrected the probe path. |
|
||||
| First audit-harness compile | Windows `min`/`max` and `TEXT` macros broke standard-library expressions. | Set `NOMINMAX` and removed the conflicting compatibility macro around `Windows.h`. |
|
||||
| Route attempt 1 | 445 blocked capsule samples; 7 grade failures. | Fit route height to the full capsule footprint and start grade fitting. |
|
||||
| Route attempt 2 | 0 blocked; 19 unsupported centers; 3 grade failures. | Added smaller route intervals and a grade reserve. |
|
||||
| Route attempt 3 | 0 blocked; 23 unsupported centers; 10 grade failures. | Added precise first-failure diagnostics; set a 5% grade reserve. |
|
||||
| Route attempt 4 | 0 blocked; 38 unsupported centers; grade passed. | Extended bridge landings into the island tops. |
|
||||
| Route attempt 5 | 17 blocked samples; support and grade passed. | Raised the recorded capsule clearance from 0.25 to 0.5 voxel. |
|
||||
|
||||
The final `Target All` build and final standalone audit passed. The final run is retained in
|
||||
[`run_ledger.txt`](results/run_ledger.txt); audit metrics, plan data and query breakdown are in
|
||||
[`audit.json`](results/audit.json).
|
||||
|
||||
## Source inventory and round boundary
|
||||
|
||||
New source files:
|
||||
|
||||
1. `Standalone/Core/src/FloatingIslandsInternal.h` — Core field, serializer, planner, bounds and route audits.
|
||||
2. `Standalone/Research/WorldAssemblerM3/Round1/FloatingIslandsAudit.cpp` — legacy comparison and standalone audit driver.
|
||||
3. `Standalone/Research/WorldAssemblerM3/Round1/BuildFloatingIslandsAudit.ps1` — BelowNormal build/run wrapper with time and test-process memory caps.
|
||||
4. `Standalone/Research/WorldAssemblerM3/Round1/RenderFloatingIslandCaptures.py` — CPU-only small matched capture renderer.
|
||||
|
||||
Changed existing source/docs: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`,
|
||||
`Standalone/Core/src/VoxelForgeCore.cpp` (include only), `Standalone/Core/API.md`, and this report.
|
||||
`Source/` and `Standalone/Source/` were not edited. No owner project files, editor, `.uasset`, commit,
|
||||
push or stash were touched.
|
||||
|
||||
This round ends at standalone Core proof. `FloatingIslandManifestV1` is not yet embedded in
|
||||
`WorldAssemblyManifest` or the Unreal adapter. The reference-plane capture does not close production
|
||||
tile/LOD seams, the route is not cooked into Unreal collision, and the feature emits no seasonal
|
||||
water body. These belong to later M3 integration/look rounds; player-edit policy remains the owner's
|
||||
unrestricted choice.
|
||||
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small CPU-only matched views of the legacy and native FloatingIslands fields."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import math
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
RESULTS = ROOT / "results"
|
||||
CAPTURES = RESULTS / "captures"
|
||||
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||
WIDTH, HEIGHT = 320, 226
|
||||
BACKGROUND = (236, 240, 242)
|
||||
LEGACY_COLOR = (133, 127, 105)
|
||||
NATIVE_COLOR = (78, 135, 151)
|
||||
ROUTE_COLOR = (223, 72, 45)
|
||||
TILE_COLOR = (178, 60, 52)
|
||||
LOD_COLOR = (211, 136, 45)
|
||||
Q = 256.0
|
||||
|
||||
|
||||
def lower_priority() -> None:
|
||||
if hasattr(ctypes, "windll"):
|
||||
ctypes.windll.kernel32.SetPriorityClass(ctypes.windll.kernel32.GetCurrentProcess(), 0x00004000)
|
||||
|
||||
|
||||
def read_mesh(path: Path):
|
||||
raw = path.read_bytes()
|
||||
if len(raw) < 8 or raw[:4] != b"VFMI":
|
||||
raise RuntimeError(f"invalid field mesh: {path}")
|
||||
count = struct.unpack_from("<I", raw, 4)[0]
|
||||
stride = struct.calcsize("<12f")
|
||||
if len(raw) != 8 + count * stride:
|
||||
raise RuntimeError(f"truncated field mesh: {path}")
|
||||
triangles = [struct.unpack_from("<12f", raw, 8 + i * stride) for i in range(count)]
|
||||
vertices = [tri[0:3] for tri in triangles]
|
||||
vertices.extend(tri[3:6] for tri in triangles)
|
||||
vertices.extend(tri[6:9] for tri in triangles)
|
||||
return triangles, vertices
|
||||
|
||||
|
||||
def normalize(vector):
|
||||
size = math.sqrt(sum(value * value for value in vector))
|
||||
return tuple(value / size for value in vector) if size > 1.0e-12 else (0.0, 0.0, 1.0)
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
return sum(x * y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def cross(a, b):
|
||||
return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0])
|
||||
|
||||
|
||||
def project(points, target, view_dir, scale=None):
|
||||
direction = normalize(view_dir)
|
||||
forward = tuple(-value for value in direction)
|
||||
right = normalize(cross(forward, (0.0, 0.0, 1.0)))
|
||||
up = normalize(cross(right, forward))
|
||||
camera_points = []
|
||||
for point in points:
|
||||
delta = tuple(point[i] - target[i] for i in range(3))
|
||||
camera_points.append((dot(delta, right), dot(delta, up), dot(delta, forward)))
|
||||
if scale is None:
|
||||
span_x = max((p[0] for p in camera_points), default=1.0) - min((p[0] for p in camera_points), default=0.0)
|
||||
span_y = max((p[1] for p in camera_points), default=1.0) - min((p[1] for p in camera_points), default=0.0)
|
||||
scale = 0.86 * min((WIDTH - 30) / max(span_x, 1.0e-6), (HEIGHT - 30) / max(span_y, 1.0e-6))
|
||||
pixels = [
|
||||
(WIDTH * 0.5 + point[0] * scale, 31 + (HEIGHT - 31) * 0.5 - point[1] * scale, point[2])
|
||||
for point in camera_points
|
||||
]
|
||||
return pixels, scale, right, up, forward
|
||||
|
||||
|
||||
def common_scale(mesh_pair, target, view_dir):
|
||||
vertices = mesh_pair[0][1] + mesh_pair[1][1]
|
||||
_, scale, _, _, _ = project(vertices, target, view_dir)
|
||||
return scale
|
||||
|
||||
|
||||
def projected_line(a, b, target, basis, scale):
|
||||
right, up, forward = basis
|
||||
points = []
|
||||
for point in (a, b):
|
||||
delta = tuple(point[i] - target[i] for i in range(3))
|
||||
points.append((WIDTH * 0.5 + dot(delta, right) * scale,
|
||||
31 + (HEIGHT - 31) * 0.5 - dot(delta, up) * scale))
|
||||
return points
|
||||
|
||||
|
||||
def render(mesh, mesh_pair, method, target, view_dir, title, subtitle, *, route=None, guide=None):
|
||||
triangles, vertices = mesh
|
||||
scale = common_scale(mesh_pair, target, view_dir)
|
||||
_, _, right, up, forward = project((), target, view_dir, scale)
|
||||
background = Image.new("RGB", (WIDTH, HEIGHT + 32), BACKGROUND)
|
||||
draw = ImageDraw.Draw(background)
|
||||
draw.rectangle((0, 0, WIDTH - 1, 31), fill=(34, 46, 54))
|
||||
draw.text((8, 4), title, fill=(250, 252, 253))
|
||||
draw.text((8, 17), subtitle, fill=(190, 207, 215))
|
||||
pixels = background.load()
|
||||
zbuffer = [float("inf")] * (WIDTH * (HEIGHT - 31))
|
||||
camera_fill = normalize(view_dir)
|
||||
light = normalize((-0.25 + 1.2 * camera_fill[0],
|
||||
-0.35 + 1.2 * camera_fill[1],
|
||||
0.45 + 1.2 * camera_fill[2]))
|
||||
base = LEGACY_COLOR if method == "legacy" else NATIVE_COLOR
|
||||
for tri in triangles:
|
||||
points = []
|
||||
for offset in (0, 3, 6):
|
||||
index = {0: 0, 3: 1, 6: 2}[offset]
|
||||
vertex = tri[offset:offset + 3]
|
||||
delta = tuple(vertex[i] - target[i] for i in range(3))
|
||||
points.append((WIDTH * 0.5 + dot(delta, right) * scale,
|
||||
31 + (HEIGHT - 31) * 0.5 - dot(delta, up) * scale,
|
||||
dot(delta, forward)))
|
||||
x0 = max(0, int(math.floor(min(p[0] for p in points))))
|
||||
x1 = min(WIDTH - 1, int(math.ceil(max(p[0] for p in points))))
|
||||
y0 = max(31, int(math.floor(min(p[1] for p in points))))
|
||||
y1 = min(HEIGHT, int(math.ceil(max(p[1] for p in points))))
|
||||
if x0 > x1 or y0 > y1:
|
||||
continue
|
||||
(ax, ay, az), (bx, by, bz), (cx, cy, cz) = points
|
||||
denominator = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
|
||||
if abs(denominator) < 1.0e-10:
|
||||
continue
|
||||
normal = normalize(tri[9:12])
|
||||
shade = 0.50 + 0.50 * max(0.0, dot(normal, light))
|
||||
color = tuple(max(0, min(255, int(channel * shade))) for channel in base)
|
||||
for y in range(y0, y1 + 1):
|
||||
row = (y - 31) * WIDTH
|
||||
for x in range(x0, x1 + 1):
|
||||
sx, sy = x + 0.5, y + 0.5
|
||||
u = ((by - cy) * (sx - cx) + (cx - bx) * (sy - cy)) / denominator
|
||||
v = ((cy - ay) * (sx - cx) + (ax - cx) * (sy - cy)) / denominator
|
||||
w = 1.0 - u - v
|
||||
if u < -1e-8 or v < -1e-8 or w < -1e-8:
|
||||
continue
|
||||
depth = u * az + v * bz + w * cz
|
||||
index = row + x
|
||||
if depth < zbuffer[index]:
|
||||
zbuffer[index] = depth
|
||||
pixels[x, y] = color
|
||||
|
||||
basis = (right, up, forward)
|
||||
if guide:
|
||||
origin = guide["origin_voxels"]
|
||||
top = guide["top_voxels"]
|
||||
for x_delta, color, label in ((32.0, TILE_COLOR, "32v tile"), (64.0, LOD_COLOR, "64v LOD1")):
|
||||
line = projected_line((origin[0] + x_delta, origin[1] - 190.0, top - 65.0),
|
||||
(origin[0] + x_delta, origin[1] + 190.0, top + 14.0),
|
||||
target, basis, scale)
|
||||
draw.line(line, fill=color, width=2)
|
||||
draw.text((max(2, min(WIDTH - 55, int(line[1][0]) + 2)),
|
||||
max(33, min(HEIGHT - 11, int(line[1][1]) - 12))), label, fill=color)
|
||||
if route:
|
||||
points = []
|
||||
for point in route:
|
||||
delta = tuple(point[i] - target[i] for i in range(3))
|
||||
points.append((WIDTH * 0.5 + dot(delta, right) * scale,
|
||||
31 + (HEIGHT - 31) * 0.5 - dot(delta, up) * scale))
|
||||
if len(points) >= 2:
|
||||
draw.line(points, fill=(250, 248, 239), width=5, joint="curve")
|
||||
draw.line(points, fill=ROUTE_COLOR, width=3, joint="curve")
|
||||
for index in (0, len(points) - 1):
|
||||
x, y = points[index]
|
||||
draw.ellipse((x - 4, y - 4, x + 4, y + 4), fill=(250, 248, 239), outline=ROUTE_COLOR, width=2)
|
||||
return background
|
||||
|
||||
|
||||
def titled_pair(left, right, category):
|
||||
pair = Image.new("RGB", (WIDTH * 2, HEIGHT + 32), (220, 226, 229))
|
||||
pair.paste(left, (0, 0))
|
||||
pair.paste(right, (WIDTH, 0))
|
||||
draw = ImageDraw.Draw(pair)
|
||||
draw.rectangle((0, 0, WIDTH * 2 - 1, HEIGHT + 31), outline=(72, 84, 91), width=1)
|
||||
draw.text((8, HEIGHT + 14), f"{category} | same target, camera and scale", fill=(45, 55, 61))
|
||||
return pair
|
||||
|
||||
|
||||
def main():
|
||||
lower_priority()
|
||||
data = json.loads((RESULTS / "audit.json").read_text(encoding="utf-8"))
|
||||
anchor = data["legacy_anchor_voxels"]
|
||||
route = data["route_points_voxels"]
|
||||
guide = {"origin_voxels": anchor[:2], "top_voxels": anchor[2]}
|
||||
cluster = {
|
||||
"legacy": read_mesh(RESULTS / "cluster_legacy.vfmesh"),
|
||||
"native": read_mesh(RESULTS / "cluster_native.vfmesh"),
|
||||
}
|
||||
detail = {
|
||||
"legacy": read_mesh(RESULTS / "detail_legacy.vfmesh"),
|
||||
"native": read_mesh(RESULTS / "detail_native.vfmesh"),
|
||||
}
|
||||
a = anchor
|
||||
scenes = (
|
||||
("overview", "Cluster overview", cluster, (a[0], a[1], a[2] - 28.0), (8.0, -10.0, 7.0), None, None),
|
||||
("underside", "Island underside", detail, (a[0], a[1], a[2] - 25.0), (8.0, -10.0, -16.0), None, None),
|
||||
("edge_cliff", "Lobed island edge / cliff", detail, (a[0], a[1] + 35.0, a[2] - 8.0), (0.0, 12.0, 2.0), None, None),
|
||||
("tile_lod_seam", "Tile / LOD reference planes", cluster, (a[0], a[1], a[2] - 28.0), (8.0, -10.0, 7.0), None, guide),
|
||||
("route", "Planned route over final field", cluster, (a[0], a[1], a[2] - 26.0), (8.0, -10.0, 7.0), route, None),
|
||||
)
|
||||
pairs = []
|
||||
for key, title, meshes, target, camera, path, grid in scenes:
|
||||
left = render(meshes["legacy"], (meshes["legacy"], meshes["native"]), "legacy",
|
||||
target, camera, title, "Legacy bridge | exact owner field", route=None, guide=grid)
|
||||
route_caption = "Core native | planner route" if path else "Core native | matched field view"
|
||||
right = render(meshes["native"], (meshes["legacy"], meshes["native"]), "native",
|
||||
target, camera, title, route_caption, route=path, guide=grid)
|
||||
pair = titled_pair(left, right, title)
|
||||
out = CAPTURES / f"{key}_matched.jpg"
|
||||
pair.save(out, quality=82, optimize=True)
|
||||
pairs.append(pair)
|
||||
print(f"capture={out.name} bytes={out.stat().st_size} size={pair.size[0]}x{pair.size[1]}")
|
||||
|
||||
sheet = Image.new("RGB", (WIDTH * 2, (HEIGHT + 32) * len(pairs)), (218, 225, 228))
|
||||
for row, pair in enumerate(pairs):
|
||||
sheet.paste(pair, (0, row * (HEIGHT + 32)))
|
||||
sheet_path = CAPTURES / "contact_sheet.jpg"
|
||||
sheet.save(sheet_path, quality=76, optimize=True)
|
||||
oversized = [(path.name, path.stat().st_size) for path in sorted(CAPTURES.glob("*.jpg"))
|
||||
if path.stat().st_size >= 300_000]
|
||||
for path in sorted(CAPTURES.glob("*.jpg")):
|
||||
print(f"capture_size={path.name} bytes={path.stat().st_size}")
|
||||
if oversized:
|
||||
raise RuntimeError(f"captures exceed the 300 KB limit: {oversized}")
|
||||
print(f"contact_sheet={sheet_path.name} bytes={sheet_path.stat().st_size} views=5 matched_pairs=5")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"recipe_hash":"111825edfebea17f",
|
||||
"plan_hash":"65675554b7aa947e",
|
||||
"manifest_hash":"03a4685554edce1a",
|
||||
"unchanged_existing_hashes":{"m1_round1_manifest":"c87ae279b0e26f53","surfaceworld_round4_manifest":"8933693d9560ba9f"},
|
||||
"plan_islands":6,
|
||||
"plan_connectors":5,
|
||||
"route_points":117,
|
||||
"legacy_anchor_voxels":[48,16,-1904.03515625],
|
||||
"legacy_anchor_plateau_samples":25,
|
||||
"recipe_parameters":{"seed":0,"density_q16":32768,"spacing_voxels":112,"radius_voxels":[24,48],"thickness_ratio_q16":39322,"vertical_jitter_q16":39322,"top_flatten_q16":39322,"contour_warp_voxels":12.6015625,"roughness_voxels":3,"blend_voxels":6},
|
||||
"stable_island_ids":["e6e461e18b3a9c87","2bc1820baba047f6","adc259cbe2a395e9","62a5f29ab581c2b0","0412f78b71b39ceb","7855fb022cc561c9"],
|
||||
"stable_connector_ids":["eaa5dd310e846520","034023599b466c51","08e698cf39fd6c22","3794e6d8e5033e8b","4962fa885545fb26"],
|
||||
"bounds":{"passed":true,"negative_control_detected":true,"boxes":517,"samples":13932,"unknown":108,"missed":0,"island_boxes":6,"connector_boxes":5,"void_boxes_proved_air":4},
|
||||
"route":{"passed":true,"segments":116,"centers":1044,"capsule_samples":283968,"blocked":0,"support_samples":6344,"unsupported":0,"grade_failures":0,"minimum_clearance_subvoxels":40},
|
||||
"raised_route_negative_control":{"detected":true,"passed":false,"unsupported":1044,"blocked":0},
|
||||
"determinism":{"W1":"bf3ce7da876431e2","W8":"bf3ce7da876431e2","W16":"bf3ce7da876431e2","plan_repeat":true,"hash_samples":32768},
|
||||
"timing":{"same_queries":32768,"legacy_ms":63.6167,"native_ms":39.3082,"native_over_legacy":0.617891214099,"sign_mismatches":823,"sign_mismatch_percent":2.51159667969,"legacy_solid_samples":90,"native_solid_samples":885,"legacy_only_solid_samples":14,"native_only_solid_samples":809},
|
||||
"mesh":[{"name":"cluster_legacy","grid_samples":422625,"triangles":4316,"bytes":207176},{"name":"cluster_native","grid_samples":422625,"triangles":35068,"bytes":1683272},{"name":"detail_legacy","grid_samples":126750,"triangles":15972,"bytes":766664},{"name":"detail_native","grid_samples":126750,"triangles":30044,"bytes":1442120}],
|
||||
"route_points_voxels":[[-176,10.43359375,-1904.30859375],[-172.13671875,10.41015625,-1904.40625],[-168.27734375,10.38671875,-1904.70703125],[-164.4140625,10.36328125,-1905.23828125],[-160.55078125,10.33984375,-1904.484375],[-156.69140625,10.31640625,-1904.625],[-152.828125,10.29296875,-1904.73828125],[-148.96484375,10.26953125,-1904.71875],[-145.1015625,10.24609375,-1904.609375],[-141.2421875,10.22265625,-1904.3203125],[-137.37890625,10.19921875,-1904.1796875],[-133.515625,10.17578125,-1903.96875],[-129.65625,10.15234375,-1903.71484375],[-125.79296875,10.12890625,-1903.4609375],[-121.9296875,10.10546875,-1903.2109375],[-118.0703125,10.078125,-1902.95703125],[-114.20703125,10.0546875,-1902.703125],[-110.34375,10.03125,-1902.44921875],[-106.484375,10.0078125,-1902.19921875],[-102.62109375,9.984375,-1901.9453125],[-98.7578125,9.9609375,-1901.69140625],[-94.8984375,9.9375,-1901.4375],[-91.03515625,9.9140625,-1900.71484375],[-87.171875,9.890625,-1899.73046875],[-83.30859375,9.8671875,-1898.74609375],[-79.44921875,9.84375,-1897.765625],[-75.5859375,9.8203125,-1896.78125],[-71.72265625,9.796875,-1896.0390625],[-67.86328125,9.7734375,-1895.84375],[-64,9.75,-1895.9453125],[-60.13671875,9.96484375,-1896.41015625],[-56.27734375,10.1796875,-1897.2265625],[-52.4140625,10.3984375,-1898.2109375],[-48.55078125,10.61328125,-1898.98046875],[-44.69140625,10.828125,-1899.39453125],[-40.828125,11.04296875,-1900.06640625],[-36.96484375,11.2578125,-1900.56640625],[-33.1015625,11.47265625,-1900.84765625],[-29.2421875,11.69140625,-1901.04296875],[-25.37890625,11.90625,-1901.1328125],[-21.515625,12.12109375,-1901.2265625],[-17.65625,12.3359375,-1901.3203125],[-13.79296875,12.55078125,-1901.41015625],[-9.9296875,12.765625,-1901.50390625],[-6.0703125,12.984375,-1901.59375],[-2.20703125,13.19921875,-1901.6875],[1.65625,13.4140625,-1901.78125],[5.515625,13.62890625,-1901.87109375],[9.37890625,13.84375,-1901.96484375],[13.2421875,14.05859375,-1902.05859375],[17.1015625,14.27734375,-1902.00390625],[20.96484375,14.4921875,-1901.01953125],[24.828125,14.70703125,-1900.03515625],[28.69140625,14.921875,-1899.30078125],[32.55078125,15.13671875,-1898.91015625],[36.4140625,15.3515625,-1898.82421875],[40.27734375,15.5703125,-1898.9375],[44.13671875,15.78515625,-1899.29296875],[48,16,-1899.83203125],[51.86328125,16.06640625,-1900.5234375],[55.72265625,16.1328125,-1901.33984375],[59.5859375,16.19921875,-1902.2265625],[63.44921875,16.26171875,-1903.13671875],[67.30859375,16.328125,-1903.8828125],[71.171875,16.39453125,-1902.8984375],[75.03515625,16.4609375,-1902.83984375],[78.8984375,16.52734375,-1902.6796875],[82.7578125,16.59375,-1902.23046875],[86.62109375,16.66015625,-1901.98828125],[90.484375,16.72265625,-1901.52734375],[94.34375,16.7890625,-1901.0546875],[98.20703125,16.85546875,-1900.58203125],[102.0703125,16.921875,-1900.11328125],[105.9296875,16.98828125,-1899.640625],[109.79296875,17.0546875,-1899.171875],[113.65625,17.12109375,-1898.69921875],[117.515625,17.1875,-1898.23046875],[121.37890625,17.25,-1897.7578125],[125.2421875,17.31640625,-1897.28515625],[129.1015625,17.3828125,-1896.81640625],[132.96484375,17.44921875,-1896.34375],[136.828125,17.515625,-1896.67578125],[140.69140625,17.58203125,-1896.12890625],[144.55078125,17.6484375,-1895.6875],[148.4140625,17.7109375,-1895.375],[152.27734375,17.77734375,-1895.08984375],[156.13671875,17.84375,-1894.8046875],[160,17.91015625,-1894.4453125],[163.86328125,17.640625,-1894],[167.72265625,17.3671875,-1893.47265625],[171.5859375,17.09765625,-1892.9921875],[175.44921875,16.82421875,-1892.71875],[179.30859375,16.5546875,-1892.65625],[183.171875,16.28125,-1892.6875],[187.03515625,16.01171875,-1893.26171875],[190.8984375,15.73828125,-1894.19140625],[194.7578125,15.46875,-1895.17578125],[198.62109375,15.1953125,-1896.16015625],[202.484375,14.92578125,-1897.1484375],[206.34375,14.65234375,-1898.1328125],[210.20703125,14.3828125,-1899.1171875],[214.0703125,14.109375,-1900.10546875],[217.9296875,13.83984375,-1901.08984375],[221.79296875,13.56640625,-1902.07421875],[225.65625,13.296875,-1902.78125],[229.515625,13.0234375,-1903.4140625],[233.37890625,12.75390625,-1904.046875],[237.2421875,12.48046875,-1904.68359375],[241.1015625,12.2109375,-1905.31640625],[244.96484375,11.9375,-1905.94921875],[248.828125,11.66796875,-1905.16796875],[252.69140625,11.39453125,-1904.4296875],[256.55078125,11.125,-1904.078125],[260.4140625,10.8515625,-1903.9296875],[264.27734375,10.58203125,-1903.92578125],[268.13671875,10.30859375,-1903.97265625],[272,10.0390625,-1903.96484375]]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Reference in new issue
Block a user