Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7022d02e6e | ||
|
|
4041d4bb4b | ||
|
|
aeb60bbda3 | ||
|
|
01a50946cb | ||
|
|
4111b967bd | ||
|
|
103fa75e1c | ||
|
|
c743f2abc7 | ||
|
|
5e7e0eb1c1 | ||
|
|
bae1144ee4 | ||
|
|
5c519f5206 | ||
|
|
161565936c | ||
|
|
707bc61553 | ||
|
|
6e52fb48b2 | ||
|
|
c2b8d39573 | ||
|
|
89504ecc97 | ||
|
|
f403af1fc1 | ||
|
|
5e875ae504 | ||
|
|
36d954cf2f | ||
|
|
16554cd426 | ||
|
|
5ec09d1ff1 |
No files matched your search
@@ -423,6 +423,7 @@ void RecordApplyFrame(const FApplyFrameSample& Sample)
|
|||||||
"\"active_tasks\":%d,\"running_workers\":%d,\"task_cap\":%d,"
|
"\"active_tasks\":%d,\"running_workers\":%d,\"task_cap\":%d,"
|
||||||
"\"pending_tiles\":%d,\"pending_results_estimate\":%d,"
|
"\"pending_tiles\":%d,\"pending_results_estimate\":%d,"
|
||||||
"\"results_drained\":%d,\"apply_results\":%d,\"meshes_applied\":%d,\"max_applies\":%d,"
|
"\"results_drained\":%d,\"apply_results\":%d,\"meshes_applied\":%d,\"max_applies\":%d,"
|
||||||
|
"\"supplemental_far_applies\":%d,"
|
||||||
"\"apply_budget_ms\":%s,\"count_limit_reached\":%d,\"time_limit_reached\":%d,"
|
"\"apply_budget_ms\":%s,\"count_limit_reached\":%d,\"time_limit_reached\":%d,"
|
||||||
"\"pending_by_lod\":%s,\"apply_results_by_lod\":%s,"
|
"\"pending_by_lod\":%s,\"apply_results_by_lod\":%s,"
|
||||||
"\"meshes_by_lod\":%s,\"apply_ms_by_lod\":%s"),
|
"\"meshes_by_lod\":%s,\"apply_ms_by_lod\":%s"),
|
||||||
@@ -432,7 +433,8 @@ void RecordApplyFrame(const FApplyFrameSample& Sample)
|
|||||||
Sample.RunningWorkerTasks, Sample.TaskCap, Sample.PendingTiles,
|
Sample.RunningWorkerTasks, Sample.TaskCap, Sample.PendingTiles,
|
||||||
Sample.PendingResultsEstimate, Sample.ResultsDrained, Sample.ApplyResultsByLevelTotal,
|
Sample.PendingResultsEstimate, Sample.ResultsDrained, Sample.ApplyResultsByLevelTotal,
|
||||||
Sample.MeshesApplied,
|
Sample.MeshesApplied,
|
||||||
Sample.MaxApplies, *Number(Sample.ApplyBudgetMilliseconds),
|
Sample.MaxApplies, Sample.SupplementalFarApplies,
|
||||||
|
*Number(Sample.ApplyBudgetMilliseconds),
|
||||||
Sample.bCountLimitReached ? 1 : 0, Sample.bTimeLimitReached ? 1 : 0,
|
Sample.bCountLimitReached ? 1 : 0, Sample.bTimeLimitReached ? 1 : 0,
|
||||||
*PendingByLevel, *ApplyResultsByLevel, *MeshesByLevel, *ApplyMillisecondsByLevel));
|
*PendingByLevel, *ApplyResultsByLevel, *MeshesByLevel, *ApplyMillisecondsByLevel));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,6 +268,12 @@ namespace
|
|||||||
GVoxelForgeStreamingResultBacklogCap,
|
GVoxelForgeStreamingResultBacklogCap,
|
||||||
TEXT("Maximum queued results plus admitted tile tasks; 0 disables the safety cap. This is separate from the worker-slot cap."));
|
TEXT("Maximum queued results plus admitted tile tasks; 0 disables the safety cap. This is separate from the worker-slot cap."));
|
||||||
|
|
||||||
|
int32 GVoxelForgeStreamingFarApplySupplement = 1;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingFarApplySupplement(
|
||||||
|
TEXT("voxel.StreamingFarApplySupplement"),
|
||||||
|
GVoxelForgeStreamingFarApplySupplement,
|
||||||
|
TEXT("After the normal visible-apply count is reached, allow one far mesh apply when half of the frame time budget remains; 0 disables the supplemental far lane."));
|
||||||
|
|
||||||
int32 GVoxelForgeStreamingReadyQueueDepth = 64;
|
int32 GVoxelForgeStreamingReadyQueueDepth = 64;
|
||||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingReadyQueueDepth(
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingReadyQueueDepth(
|
||||||
TEXT("voxel.StreamingReadyQueueDepth"),
|
TEXT("voxel.StreamingReadyQueueDepth"),
|
||||||
@@ -2223,6 +2229,12 @@ void AVoxelWorld::BeginPlay()
|
|||||||
}
|
}
|
||||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingStartupApplyBoost="),
|
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingStartupApplyBoost="),
|
||||||
GVoxelForgeStreamingStartupApplyBoost);
|
GVoxelForgeStreamingStartupApplyBoost);
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingFarApplySupplement="),
|
||||||
|
GVoxelForgeStreamingFarApplySupplement))
|
||||||
|
{
|
||||||
|
GVoxelForgeStreamingFarApplySupplement =
|
||||||
|
GVoxelForgeStreamingFarApplySupplement != 0 ? 1 : 0;
|
||||||
|
}
|
||||||
// Headless BuildHost does not reliably apply arbitrary console-variable command-line tokens
|
// Headless BuildHost does not reliably apply arbitrary console-variable command-line tokens
|
||||||
// before BeginPlay; parse the test switches explicitly so A/B runs exercise the requested path.
|
// before BeginPlay; parse the test switches explicitly so A/B runs exercise the requested path.
|
||||||
// BuildHost sans interface n'applique pas toujours les CVar arbitraires avant BeginPlay ;
|
// BuildHost sans interface n'applique pas toujours les CVar arbitraires avant BeginPlay ;
|
||||||
@@ -2737,6 +2749,7 @@ void AVoxelWorld::BeginPlay()
|
|||||||
"\"streaming_collision_lookahead\":%d,\"core_region_concurrent_admission\":%d,"
|
"\"streaming_collision_lookahead\":%d,\"core_region_concurrent_admission\":%d,"
|
||||||
"\"collision_gate\":%d,"
|
"\"collision_gate\":%d,"
|
||||||
"\"streaming_result_backlog_cap\":%d,\"streaming_startup_apply_boost\":%d,"
|
"\"streaming_result_backlog_cap\":%d,\"streaming_startup_apply_boost\":%d,"
|
||||||
|
"\"streaming_far_apply_supplement\":%d,"
|
||||||
"\"streaming_background_worker_reserve\":%d,\"core_parallel_for_background_priority\":%d,"
|
"\"streaming_background_worker_reserve\":%d,\"core_parallel_for_background_priority\":%d,"
|
||||||
"\"streaming_tile_background_low\":%d,\"streaming_tile_background_normal\":%d,"
|
"\"streaming_tile_background_low\":%d,\"streaming_tile_background_normal\":%d,"
|
||||||
"\"core_column_cache_direct_mapped\":%d,"
|
"\"core_column_cache_direct_mapped\":%d,"
|
||||||
@@ -2758,6 +2771,7 @@ void AVoxelWorld::BeginPlay()
|
|||||||
GVoxelForgeStreamingCollisionLookahead, GVoxelForgeCoreRegionConcurrentAdmission,
|
GVoxelForgeStreamingCollisionLookahead, GVoxelForgeCoreRegionConcurrentAdmission,
|
||||||
GVoxelForgeCollisionGate,
|
GVoxelForgeCollisionGate,
|
||||||
GVoxelForgeStreamingResultBacklogCap, GVoxelForgeStreamingStartupApplyBoost,
|
GVoxelForgeStreamingResultBacklogCap, GVoxelForgeStreamingStartupApplyBoost,
|
||||||
|
GVoxelForgeStreamingFarApplySupplement,
|
||||||
GVoxelForgeStreamingBackgroundWorkerReserve,
|
GVoxelForgeStreamingBackgroundWorkerReserve,
|
||||||
GVoxelForgeCoreParallelForBackgroundPriority,
|
GVoxelForgeCoreParallelForBackgroundPriority,
|
||||||
GVoxelForgeStreamingTileBackgroundLow,
|
GVoxelForgeStreamingTileBackgroundLow,
|
||||||
@@ -9881,6 +9895,7 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
const double ApplyBudgetSeconds =
|
const double ApplyBudgetSeconds =
|
||||||
static_cast<double>(ApplyBudgetMilliseconds) * 0.001;
|
static_cast<double>(ApplyBudgetMilliseconds) * 0.001;
|
||||||
int32 MeshesApplied = 0;
|
int32 MeshesApplied = 0;
|
||||||
|
int32 SupplementalFarApplies = 0;
|
||||||
int32 ResultsDrained = 0;
|
int32 ResultsDrained = 0;
|
||||||
int32 ApplyResultsByLevelTotal = 0;
|
int32 ApplyResultsByLevelTotal = 0;
|
||||||
double ApplySeconds = 0.0;
|
double ApplySeconds = 0.0;
|
||||||
@@ -9888,6 +9903,8 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
TStaticArray<int32, 9> MeshesAppliedByLevel{};
|
TStaticArray<int32, 9> MeshesAppliedByLevel{};
|
||||||
TStaticArray<double, 9> ApplySecondsByLevel{};
|
TStaticArray<double, 9> ApplySecondsByLevel{};
|
||||||
bool bTimeBudgetHit = false;
|
bool bTimeBudgetHit = false;
|
||||||
|
bool bForceSupplementalFarDequeue = false;
|
||||||
|
bool bDequeuedSupplementalFar = false;
|
||||||
const auto RecordApplyFrame = [&]()
|
const auto RecordApplyFrame = [&]()
|
||||||
{
|
{
|
||||||
if (!VoxelForgeStartupTrace::IsActive()) return;
|
if (!VoxelForgeStartupTrace::IsActive()) return;
|
||||||
@@ -9911,6 +9928,7 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
Sample.ResultsDrained = ResultsDrained;
|
Sample.ResultsDrained = ResultsDrained;
|
||||||
Sample.ApplyResultsByLevelTotal = ApplyResultsByLevelTotal;
|
Sample.ApplyResultsByLevelTotal = ApplyResultsByLevelTotal;
|
||||||
Sample.MeshesApplied = MeshesApplied;
|
Sample.MeshesApplied = MeshesApplied;
|
||||||
|
Sample.SupplementalFarApplies = SupplementalFarApplies;
|
||||||
Sample.MaxApplies = MaxApplies;
|
Sample.MaxApplies = MaxApplies;
|
||||||
Sample.ApplyBudgetMilliseconds = ApplyBudgetMilliseconds;
|
Sample.ApplyBudgetMilliseconds = ApplyBudgetMilliseconds;
|
||||||
Sample.bCountLimitReached = MeshesApplied >= MaxApplies;
|
Sample.bCountLimitReached = MeshesApplied >= MaxApplies;
|
||||||
@@ -9939,6 +9957,7 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
FChunkResult DequeuedChunk;
|
FChunkResult DequeuedChunk;
|
||||||
const auto DequeueNextResult = [&]() -> bool
|
const auto DequeueNextResult = [&]() -> bool
|
||||||
{
|
{
|
||||||
|
bDequeuedSupplementalFar = false;
|
||||||
if (bStep1UploadCorpusReleased
|
if (bStep1UploadCorpusReleased
|
||||||
&& Step1HeldApplyCursor < Step1HeldApplyResults.Num())
|
&& Step1HeldApplyCursor < Step1HeldApplyResults.Num())
|
||||||
{
|
{
|
||||||
@@ -9954,8 +9973,19 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
// Mirror the admission order. A horizon result that completed earlier must not consume
|
// Mirror the admission order. A horizon result that completed earlier must not consume
|
||||||
// the game-thread apply budget ahead of collision support, an edit, or ordinary LOD0.
|
// the game-thread apply budget ahead of collision support, an edit, or ordinary LOD0.
|
||||||
if (CriticalProcessQueue.Dequeue(DequeuedChunk)
|
if (CriticalProcessQueue.Dequeue(DequeuedChunk)
|
||||||
|| EditedProcessQueue.Dequeue(DequeuedChunk)
|
|| EditedProcessQueue.Dequeue(DequeuedChunk))
|
||||||
|| NearProcessQueue.Dequeue(DequeuedChunk)
|
{
|
||||||
|
RecordStreamingResultDequeued(DequeuedChunk);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (bForceSupplementalFarDequeue)
|
||||||
|
{
|
||||||
|
if (!ProcessQueue.Dequeue(DequeuedChunk)) return false;
|
||||||
|
bDequeuedSupplementalFar = true;
|
||||||
|
RecordStreamingResultDequeued(DequeuedChunk);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (NearProcessQueue.Dequeue(DequeuedChunk)
|
||||||
|| ProcessQueue.Dequeue(DequeuedChunk))
|
|| ProcessQueue.Dequeue(DequeuedChunk))
|
||||||
{
|
{
|
||||||
RecordStreamingResultDequeued(DequeuedChunk);
|
RecordStreamingResultDequeued(DequeuedChunk);
|
||||||
@@ -10104,6 +10134,10 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
if (bMeshApplied)
|
if (bMeshApplied)
|
||||||
{
|
{
|
||||||
++MeshesApplied;
|
++MeshesApplied;
|
||||||
|
if (bDequeuedSupplementalFar && ResultLevel >= 3)
|
||||||
|
{
|
||||||
|
++SupplementalFarApplies;
|
||||||
|
}
|
||||||
if (ResultLevel >= 0 && ResultLevel < 9)
|
if (ResultLevel >= 0 && ResultLevel < 9)
|
||||||
{
|
{
|
||||||
++MeshesAppliedByLevel[ResultLevel];
|
++MeshesAppliedByLevel[ResultLevel];
|
||||||
@@ -10122,6 +10156,19 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
}
|
}
|
||||||
if (MeshesApplied >= MaxApplies)
|
if (MeshesApplied >= MaxApplies)
|
||||||
{
|
{
|
||||||
|
const double DrainElapsedSeconds = FPlatformTime::ToSeconds64(
|
||||||
|
FPlatformTime::Cycles64() - DrainStartCycles);
|
||||||
|
const bool bCanSupplementFar = MeshesApplied == MaxApplies
|
||||||
|
&& SupplementalFarApplies == 0
|
||||||
|
&& !bUseStartupApplyBoost
|
||||||
|
&& GVoxelForgeStreamingFarApplySupplement != 0
|
||||||
|
&& ApplyBudgetSeconds > 0.0
|
||||||
|
&& DrainElapsedSeconds < ApplyBudgetSeconds * 0.5;
|
||||||
|
if (bCanSupplementFar)
|
||||||
|
{
|
||||||
|
bForceSupplementalFarDequeue = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15360,6 +15407,8 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
const int32 NumIndices = Result.NumTriangles * 3;
|
const int32 NumIndices = Result.NumTriangles * 3;
|
||||||
const int32 FirstCapIndex = FMath::Clamp(
|
const int32 FirstCapIndex = FMath::Clamp(
|
||||||
NumIndices - Result.NumCeilingTriangles * 3, 0, NumIndices);
|
NumIndices - Result.NumCeilingTriangles * 3, 0, NumIndices);
|
||||||
|
TFuture<ERealtimeMeshProxyUpdateStatus> ProxyPublicationFuture;
|
||||||
|
bool bHasProxyPublicationFuture = false;
|
||||||
{
|
{
|
||||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_RMC_CreateSections);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_RMC_CreateSections);
|
||||||
if (bHasGroundTris)
|
if (bHasGroundTris)
|
||||||
@@ -15369,9 +15418,10 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
const FRealtimeMeshStreamRange GroundRange =
|
const FRealtimeMeshStreamRange GroundRange =
|
||||||
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
|
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
|
||||||
0, Result.NumVertices, 0, FirstCapIndex);
|
0, Result.NumVertices, 0, FirstCapIndex);
|
||||||
RTMesh->CreateSection(
|
ProxyPublicationFuture = RTMesh->CreateSection(
|
||||||
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 0),
|
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 0),
|
||||||
GroundConfig, GroundRange, /*bShouldCreateCollision*/ bWantsCollision);
|
GroundConfig, GroundRange, /*bShouldCreateCollision*/ bWantsCollision);
|
||||||
|
bHasProxyPublicationFuture = true;
|
||||||
}
|
}
|
||||||
if (bHasCeilingTris)
|
if (bHasCeilingTris)
|
||||||
{
|
{
|
||||||
@@ -15380,11 +15430,48 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
const FRealtimeMeshStreamRange CapRange =
|
const FRealtimeMeshStreamRange CapRange =
|
||||||
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
|
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
|
||||||
0, Result.NumVertices, FirstCapIndex, NumIndices);
|
0, Result.NumVertices, FirstCapIndex, NumIndices);
|
||||||
RTMesh->CreateSection(
|
ProxyPublicationFuture = RTMesh->CreateSection(
|
||||||
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 1),
|
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 1),
|
||||||
CapConfig, CapRange, /*bShouldCreateCollision*/ bWantsCollision);
|
CapConfig, CapRange, /*bShouldCreateCollision*/ bWantsCollision);
|
||||||
|
bHasProxyPublicationFuture = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const uint64 ProxyPublicationSubmitCycles = FPlatformTime::Cycles64();
|
||||||
|
if (bTraceRmcTiming && bHasProxyPublicationFuture)
|
||||||
|
{
|
||||||
|
const TWeakObjectPtr<AVoxelWorld> WeakWorld(this);
|
||||||
|
const TWeakObjectPtr<URealtimeMeshComponent> WeakComponent(MeshComp);
|
||||||
|
const FVoxelTileKey PublishedTile = Tile;
|
||||||
|
const uint64 RequestOrdinal = Result.RequestOrdinal;
|
||||||
|
const uint64 ApplyStartedCycles = ApplyStartCycles;
|
||||||
|
ProxyPublicationFuture.Next(
|
||||||
|
[WeakWorld, WeakComponent, PublishedTile, RequestOrdinal,
|
||||||
|
ApplyStartedCycles, ProxyPublicationSubmitCycles](
|
||||||
|
ERealtimeMeshProxyUpdateStatus Status)
|
||||||
|
{
|
||||||
|
if (!VoxelForgeStartupTrace::IsActive()) return;
|
||||||
|
const uint64 PublishedCycles = FPlatformTime::Cycles64();
|
||||||
|
const URealtimeMeshComponent* Component = WeakComponent.Get();
|
||||||
|
VoxelForgeStartupTrace::RecordEvent(TEXT("rmc_proxy_published"), FString::Printf(
|
||||||
|
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"request_ordinal\":%llu,"
|
||||||
|
"\"proxy_update_status\":%d,\"apply_to_proxy_ms\":%.3f,"
|
||||||
|
"\"section_submit_to_proxy_ms\":%.3f,\"component_visible\":%d,"
|
||||||
|
"\"world_valid\":%d"),
|
||||||
|
PublishedTile.Coord.X, PublishedTile.Coord.Y, PublishedTile.Coord.Z,
|
||||||
|
PublishedTile.Level, static_cast<unsigned long long>(RequestOrdinal),
|
||||||
|
static_cast<int32>(Status),
|
||||||
|
ApplyStartedCycles != 0 && PublishedCycles >= ApplyStartedCycles
|
||||||
|
? FPlatformTime::ToSeconds64(PublishedCycles - ApplyStartedCycles) * 1000.0
|
||||||
|
: 0.0,
|
||||||
|
ProxyPublicationSubmitCycles != 0
|
||||||
|
&& PublishedCycles >= ProxyPublicationSubmitCycles
|
||||||
|
? FPlatformTime::ToSeconds64(
|
||||||
|
PublishedCycles - ProxyPublicationSubmitCycles) * 1000.0
|
||||||
|
: 0.0,
|
||||||
|
Component && Component->IsVisible() ? 1 : 0,
|
||||||
|
WeakWorld.IsValid() ? 1 : 0));
|
||||||
|
});
|
||||||
|
}
|
||||||
if (bTraceRmcTiming)
|
if (bTraceRmcTiming)
|
||||||
{
|
{
|
||||||
TraceSectionConfigMilliseconds = FPlatformTime::ToSeconds64(
|
TraceSectionConfigMilliseconds = FPlatformTime::ToSeconds64(
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ namespace VoxelForgeStartupTrace
|
|||||||
int32 ResultsDrained = 0;
|
int32 ResultsDrained = 0;
|
||||||
int32 ApplyResultsByLevelTotal = 0;
|
int32 ApplyResultsByLevelTotal = 0;
|
||||||
int32 MeshesApplied = 0;
|
int32 MeshesApplied = 0;
|
||||||
|
int32 SupplementalFarApplies = 0;
|
||||||
int32 MaxApplies = 0;
|
int32 MaxApplies = 0;
|
||||||
double ApplyBudgetMilliseconds = 0.0;
|
double ApplyBudgetMilliseconds = 0.0;
|
||||||
bool bCountLimitReached = false;
|
bool bCountLimitReached = false;
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <limits>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace VoxelForgeCore::PassageSpline
|
||||||
|
{
|
||||||
|
using FPoint = std::array<double,4>;
|
||||||
|
inline constexpr std::size_t ArcTableSteps = 32;
|
||||||
|
inline constexpr double UpperMouthFloorBlendRadiusMetres = 3.5;
|
||||||
|
|
||||||
|
struct FSegment
|
||||||
|
{
|
||||||
|
FPoint a{},b{};
|
||||||
|
double alongMetres=0.0,lengthMetres=0.0;
|
||||||
|
std::size_t controlSpan=0;
|
||||||
|
double tA=0.0,tB=1.0;
|
||||||
|
double plannedFloorA=0.0,plannedFloorB=0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline double KnotSpan(const FPoint& a,const FPoint& b,double unitsPerMetre)
|
||||||
|
{
|
||||||
|
const double dx=(b[0]-a[0])/unitsPerMetre;
|
||||||
|
const double dy=(b[1]-a[1])/unitsPerMetre;
|
||||||
|
return std::max(1.0e-6,std::sqrt(std::sqrt(dx*dx+dy*dy)));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FPoint EvaluateRaw(std::span<const FPoint> controls,std::size_t span,
|
||||||
|
double t,double unitsPerMetre)
|
||||||
|
{
|
||||||
|
const auto& p1=controls[span];
|
||||||
|
const auto& p2=controls[span+1];
|
||||||
|
const auto& p0=controls[span==0?0:span-1];
|
||||||
|
const auto& p3=controls[std::min(span+2,controls.size()-1)];
|
||||||
|
const double dt0=KnotSpan(p0,p1,unitsPerMetre);
|
||||||
|
const double dt1=KnotSpan(p1,p2,unitsPerMetre);
|
||||||
|
const double dt2=KnotSpan(p2,p3,unitsPerMetre);
|
||||||
|
const double t2=t*t,t3=t2*t;
|
||||||
|
FPoint result{};
|
||||||
|
for(const std::size_t axis:{0u,1u,3u})
|
||||||
|
{
|
||||||
|
const double m1=dt1*((p1[axis]-p0[axis])/dt0
|
||||||
|
-(p2[axis]-p0[axis])/(dt0+dt1)
|
||||||
|
+(p2[axis]-p1[axis])/dt1);
|
||||||
|
const double m2=dt1*((p2[axis]-p1[axis])/dt1
|
||||||
|
-(p3[axis]-p1[axis])/(dt1+dt2)
|
||||||
|
+(p3[axis]-p2[axis])/dt2);
|
||||||
|
result[axis]=(2.0*t3-3.0*t2+1.0)*p1[axis]
|
||||||
|
+(-2.0*t3+3.0*t2)*p2[axis]
|
||||||
|
+(t3-2.0*t2+t)*m1+(t3-t2)*m2;
|
||||||
|
}
|
||||||
|
result[2]=std::lerp(p1[2],p2[2],t);
|
||||||
|
result[3]=std::clamp(result[3],std::min(p1[3],p2[3]),
|
||||||
|
std::max(p1[3],p2[3]));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool ClosestRouteHeightAtXY(std::span<const std::array<double,3>> route,
|
||||||
|
double x,double y,double& height,double& distanceSquared)
|
||||||
|
{
|
||||||
|
if(route.empty())return false;
|
||||||
|
distanceSquared=std::numeric_limits<double>::infinity();
|
||||||
|
const auto consider=[&](const std::array<double,3>& point)
|
||||||
|
{
|
||||||
|
const double dx=x-point[0],dy=y-point[1];
|
||||||
|
const double candidate=dx*dx+dy*dy;
|
||||||
|
if(candidate<distanceSquared)
|
||||||
|
{distanceSquared=candidate;height=point[2];}
|
||||||
|
};
|
||||||
|
if(route.size()==1)
|
||||||
|
{
|
||||||
|
consider(route.front());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for(std::size_t i=1;i<route.size();++i)
|
||||||
|
{
|
||||||
|
const auto& a=route[i-1];
|
||||||
|
const auto& b=route[i];
|
||||||
|
const double dx=b[0]-a[0],dy=b[1]-a[1];
|
||||||
|
const double lengthSquared=dx*dx+dy*dy;
|
||||||
|
const double t=lengthSquared>1.0e-12
|
||||||
|
?std::clamp(((x-a[0])*dx+(y-a[1])*dy)/lengthSquared,0.0,1.0):0.0;
|
||||||
|
const std::array<double,3> point={a[0]+t*dx,a[1]+t*dy,
|
||||||
|
std::lerp(a[2],b[2],t)};
|
||||||
|
consider(point);
|
||||||
|
}
|
||||||
|
return std::isfinite(distanceSquared);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline double BlendUpperMouthFloorAtXY(
|
||||||
|
std::span<const std::array<double,3>> receivingRoute,double x,double y,
|
||||||
|
double plannedFloor,double mouthX,double mouthY,double mouthFloor,
|
||||||
|
double tangentX,double tangentY,
|
||||||
|
double blendRadiusMetres,double playerCenterAboveFloorMetres,
|
||||||
|
double unitsPerMetre=1.0)
|
||||||
|
{
|
||||||
|
if(receivingRoute.empty()||!(blendRadiusMetres>0.0)
|
||||||
|
||!(unitsPerMetre>0.0))return plannedFloor;
|
||||||
|
const double dx=(x-mouthX)/unitsPerMetre;
|
||||||
|
const double dy=(y-mouthY)/unitsPerMetre;
|
||||||
|
if(dx*tangentX+dy*tangentY>=0.0)return plannedFloor;
|
||||||
|
const double dz=(plannedFloor-mouthFloor)/unitsPerMetre;
|
||||||
|
const double distance=std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||||
|
double routeHeight=0.0,routeDistanceSquared=0.0;
|
||||||
|
if(distance>blendRadiusMetres
|
||||||
|
||!ClosestRouteHeightAtXY(receivingRoute,x,y,routeHeight,
|
||||||
|
routeDistanceSquared)
|
||||||
|
||routeDistanceSquared>std::pow(2.0*blendRadiusMetres*unitsPerMetre,2.0))
|
||||||
|
return plannedFloor;
|
||||||
|
const double t=std::clamp(distance/blendRadiusMetres,0.0,1.0);
|
||||||
|
const double blend=t*t*(3.0-2.0*t);
|
||||||
|
const double receivingFloor=routeHeight
|
||||||
|
-playerCenterAboveFloorMetres*unitsPerMetre;
|
||||||
|
return std::lerp(plannedFloor,receivingFloor,blend);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<FSegment> BuildSegments(std::span<const FPoint> controls,
|
||||||
|
std::size_t spanCount,double spacingMetres,double unitsPerMetre=1.0,
|
||||||
|
std::span<const double> plannedFloorProfile={},
|
||||||
|
double maximumChordErrorMetres=0.0)
|
||||||
|
{
|
||||||
|
std::vector<FSegment> result;
|
||||||
|
if(controls.size()<2||!(spacingMetres>0.0)||!(unitsPerMetre>0.0))return result;
|
||||||
|
spanCount=std::min(spanCount,controls.size()-1);
|
||||||
|
double alongMetres=0.0;
|
||||||
|
for(std::size_t span=0;span<spanCount;++span)
|
||||||
|
{
|
||||||
|
std::array<double,ArcTableSteps+1> horizontalArc{};
|
||||||
|
auto previous=EvaluateRaw(controls,span,0.0,unitsPerMetre);
|
||||||
|
for(std::size_t step=1;step<=ArcTableSteps;++step)
|
||||||
|
{
|
||||||
|
const auto current=EvaluateRaw(controls,span,
|
||||||
|
static_cast<double>(step)/ArcTableSteps,unitsPerMetre);
|
||||||
|
const double dx=(current[0]-previous[0])/unitsPerMetre;
|
||||||
|
const double dy=(current[1]-previous[1])/unitsPerMetre;
|
||||||
|
horizontalArc[step]=horizontalArc[step-1]+std::sqrt(dx*dx+dy*dy);
|
||||||
|
previous=current;
|
||||||
|
}
|
||||||
|
const double spanLength=horizontalArc.back();
|
||||||
|
const std::size_t baseSegmentCount=std::max<std::size_t>(1,
|
||||||
|
static_cast<std::size_t>(std::ceil(spanLength/spacingMetres)));
|
||||||
|
std::vector<FPoint> points;
|
||||||
|
std::vector<double> parameters;
|
||||||
|
points.reserve(baseSegmentCount+1);
|
||||||
|
parameters.reserve(baseSegmentCount+1);
|
||||||
|
const auto parameterAtArc=[&](double targetArc)
|
||||||
|
{
|
||||||
|
const auto upper=std::lower_bound(horizontalArc.begin()+1,
|
||||||
|
horizontalArc.end(),targetArc);
|
||||||
|
const std::size_t upperIndex=static_cast<std::size_t>(
|
||||||
|
upper-horizontalArc.begin());
|
||||||
|
const std::size_t lowerIndex=upperIndex-1;
|
||||||
|
const double arcSpan=horizontalArc[upperIndex]-horizontalArc[lowerIndex];
|
||||||
|
const double arcT=arcSpan>1.0e-12
|
||||||
|
?std::clamp((targetArc-horizontalArc[lowerIndex])/arcSpan,0.0,1.0):0.0;
|
||||||
|
return (static_cast<double>(lowerIndex)+arcT)/ArcTableSteps;
|
||||||
|
};
|
||||||
|
parameters.push_back(0.0);
|
||||||
|
const double chordError=std::max(0.0,maximumChordErrorMetres);
|
||||||
|
const auto appendAdaptive=[&](auto&& self,double tA,double tB,
|
||||||
|
const FPoint& a,const FPoint& b,unsigned depth)->void
|
||||||
|
{
|
||||||
|
constexpr std::array<double,3> CheckFractions={0.25,0.5,0.75};
|
||||||
|
double maximumError=0.0,polylineLength=0.0;
|
||||||
|
FPoint previous=a;
|
||||||
|
for(const double fraction:CheckFractions)
|
||||||
|
{
|
||||||
|
const FPoint current=EvaluateRaw(controls,span,
|
||||||
|
std::lerp(tA,tB,fraction),unitsPerMetre);
|
||||||
|
const double chordX=std::lerp(a[0],b[0],fraction);
|
||||||
|
const double chordY=std::lerp(a[1],b[1],fraction);
|
||||||
|
const double dx=(current[0]-chordX)/unitsPerMetre;
|
||||||
|
const double dy=(current[1]-chordY)/unitsPerMetre;
|
||||||
|
const double dr=(current[3]-std::lerp(a[3],b[3],fraction))
|
||||||
|
/unitsPerMetre;
|
||||||
|
maximumError=std::max(maximumError,
|
||||||
|
std::sqrt(dx*dx+dy*dy+dr*dr));
|
||||||
|
const double segmentX=(current[0]-previous[0])/unitsPerMetre;
|
||||||
|
const double segmentY=(current[1]-previous[1])/unitsPerMetre;
|
||||||
|
polylineLength+=std::sqrt(segmentX*segmentX+segmentY*segmentY);
|
||||||
|
previous=current;
|
||||||
|
}
|
||||||
|
const double finalX=(b[0]-previous[0])/unitsPerMetre;
|
||||||
|
const double finalY=(b[1]-previous[1])/unitsPerMetre;
|
||||||
|
polylineLength+=std::sqrt(finalX*finalX+finalY*finalY);
|
||||||
|
const bool split=(polylineLength>spacingMetres*(1.0+1.0e-9)
|
||||||
|
||(chordError>0.0&&maximumError>chordError))&&depth<12;
|
||||||
|
if(split)
|
||||||
|
{
|
||||||
|
const double tMid=(tA+tB)*0.5;
|
||||||
|
const FPoint middle=EvaluateRaw(controls,span,tMid,unitsPerMetre);
|
||||||
|
self(self,tA,tMid,a,middle,depth+1);
|
||||||
|
self(self,tMid,tB,middle,b,depth+1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parameters.push_back(tB);
|
||||||
|
};
|
||||||
|
for(std::size_t segment=1;segment<=baseSegmentCount;++segment)
|
||||||
|
{
|
||||||
|
const double targetA=spanLength*static_cast<double>(segment-1)
|
||||||
|
/baseSegmentCount;
|
||||||
|
const double targetB=spanLength*static_cast<double>(segment)
|
||||||
|
/baseSegmentCount;
|
||||||
|
const double tA=parameterAtArc(targetA);
|
||||||
|
const double tB=parameterAtArc(targetB);
|
||||||
|
const FPoint a=EvaluateRaw(controls,span,tA,unitsPerMetre);
|
||||||
|
const FPoint b=EvaluateRaw(controls,span,tB,unitsPerMetre);
|
||||||
|
appendAdaptive(appendAdaptive,tA,tB,a,b,0);
|
||||||
|
}
|
||||||
|
points.reserve(parameters.size());
|
||||||
|
for(const double t:parameters)
|
||||||
|
points.push_back(EvaluateRaw(controls,span,t,unitsPerMetre));
|
||||||
|
std::vector<double> sampledHorizontalArc(points.size(),0.0);
|
||||||
|
for(std::size_t i=1;i<points.size();++i)
|
||||||
|
{
|
||||||
|
const double dx=(points[i][0]-points[i-1][0])/unitsPerMetre;
|
||||||
|
const double dy=(points[i][1]-points[i-1][1])/unitsPerMetre;
|
||||||
|
sampledHorizontalArc[i]=sampledHorizontalArc[i-1]
|
||||||
|
+std::sqrt(dx*dx+dy*dy);
|
||||||
|
}
|
||||||
|
const double sampledSpanLength=sampledHorizontalArc.back();
|
||||||
|
for(std::size_t i=0;i<points.size();++i)
|
||||||
|
{
|
||||||
|
const double progress=sampledSpanLength>1.0e-12
|
||||||
|
?sampledHorizontalArc[i]/sampledSpanLength:parameters[i];
|
||||||
|
points[i][2]=std::lerp(controls[span][2],controls[span+1][2],progress);
|
||||||
|
}
|
||||||
|
for(std::size_t i=1;i<points.size();++i)
|
||||||
|
{
|
||||||
|
const double dx=(points[i][0]-points[i-1][0])/unitsPerMetre;
|
||||||
|
const double dy=(points[i][1]-points[i-1][1])/unitsPerMetre;
|
||||||
|
const double dz=(points[i][2]-points[i-1][2])/unitsPerMetre;
|
||||||
|
const double length=std::sqrt(dx*dx+dy*dy+dz*dz);
|
||||||
|
const auto floorAt=[&](std::size_t index)
|
||||||
|
{
|
||||||
|
if(plannedFloorProfile.size()!=controls.size())
|
||||||
|
return points[index][2];
|
||||||
|
const double progress=sampledSpanLength>1.0e-12
|
||||||
|
?sampledHorizontalArc[index]/sampledSpanLength:parameters[index];
|
||||||
|
return std::lerp(plannedFloorProfile[span],
|
||||||
|
plannedFloorProfile[span+1],progress);
|
||||||
|
};
|
||||||
|
result.push_back({points[i-1],points[i],alongMetres,length,span,
|
||||||
|
parameters[i-1],parameters[i],floorAt(i-1),floorAt(i)});
|
||||||
|
alongMetres+=length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "VoxelForgeCore/Linux77Profiler.h"
|
#include "VoxelForgeCore/Linux77Profiler.h"
|
||||||
|
#include "VoxelForgeCore/PassageSpline.h"
|
||||||
|
|
||||||
// UBT supplies this export/import macro for the runtime module. The standalone target has no
|
// UBT supplies this export/import macro for the runtime module. The standalone target has no
|
||||||
// module boundary, so leave it empty there. UBT fournit la macro; le viewer autonome reste neutre.
|
// module boundary, so leave it empty there. UBT fournit la macro; le viewer autonome reste neutre.
|
||||||
@@ -146,17 +147,35 @@ namespace VoxelForgeCore
|
|||||||
// by the opt-in passage composer profile, except the mesh composition itself.
|
// by the opt-in passage composer profile, except the mesh composition itself.
|
||||||
std::uint64_t passageSweepRouteSegmentCandidates = 0;
|
std::uint64_t passageSweepRouteSegmentCandidates = 0;
|
||||||
std::uint64_t passageSweepSegmentSamplePairs = 0;
|
std::uint64_t passageSweepSegmentSamplePairs = 0;
|
||||||
|
std::uint64_t passageSweepPrepareNanoseconds = 0;
|
||||||
std::uint64_t passageSweepOutsideInfluencePairs = 0;
|
std::uint64_t passageSweepOutsideInfluencePairs = 0;
|
||||||
|
std::uint64_t passageSweepShellSupportSkips = 0;
|
||||||
std::uint64_t passageSweepMinimumUpdates = 0;
|
std::uint64_t passageSweepMinimumUpdates = 0;
|
||||||
std::uint64_t passageSweepBaseInfluenceSamples = 0;
|
std::uint64_t passageSweepBaseInfluenceSamples = 0;
|
||||||
std::uint64_t passageSweepDetailShellSamples = 0;
|
std::uint64_t passageSweepDetailShellSamples = 0;
|
||||||
std::uint64_t passageSweepGridSamples = 0;
|
std::uint64_t passageSweepGridSamples = 0;
|
||||||
std::uint64_t passageSweepNoInfluenceGridSamples = 0;
|
std::uint64_t passageSweepNoInfluenceGridSamples = 0;
|
||||||
|
std::uint64_t passageSweepTouchedSamples = 0;
|
||||||
|
std::uint64_t passageSweepStampedOutsideSamples = 0;
|
||||||
|
std::uint64_t passageSweepCacheValidSamples = 0;
|
||||||
|
std::uint64_t passageSweepCacheValidUniqueSamples = 0;
|
||||||
|
std::uint64_t passageSweepUniqueTouchedSamples = 0;
|
||||||
|
std::uint64_t passageSweepBodyBandSamples = 0;
|
||||||
|
std::uint64_t passageSweepShellBandSamples = 0;
|
||||||
|
std::uint64_t passageSweepUniqueBandSamples = 0;
|
||||||
|
std::uint64_t passageSweepOverlapPairs = 0;
|
||||||
|
std::uint64_t passageSweepContributingPairs = 0;
|
||||||
|
std::uint64_t passageSweepCrossPassageOverlaps = 0;
|
||||||
|
std::uint64_t passageSweepFloorProfileCalls = 0;
|
||||||
|
std::uint64_t passageSweepAllocatedBytes = 0;
|
||||||
|
std::uint64_t passageSweepPrepareScratchBytes = 0;
|
||||||
std::uint64_t passageSweepLookups = 0;
|
std::uint64_t passageSweepLookups = 0;
|
||||||
std::uint64_t passageSweepCacheHits = 0;
|
std::uint64_t passageSweepCacheHits = 0;
|
||||||
std::uint64_t passageSweepBaseInfluenceHits = 0;
|
std::uint64_t passageSweepBaseInfluenceHits = 0;
|
||||||
std::uint64_t passageSweepOutOfInfluenceHits = 0;
|
std::uint64_t passageSweepOutOfInfluenceHits = 0;
|
||||||
std::uint64_t passageSweepMissingSamples = 0;
|
std::uint64_t passageSweepMissingSamples = 0;
|
||||||
|
std::uint64_t passageSweepOffGridMisses = 0;
|
||||||
|
std::uint64_t passageSweepUnpreparedMisses = 0;
|
||||||
std::array<std::uint64_t,12> passageSweepMissingLookupsByCaller{};
|
std::array<std::uint64_t,12> passageSweepMissingLookupsByCaller{};
|
||||||
std::array<std::uint64_t,12> passageSweepExactRouteFallbacksByCaller{};
|
std::array<std::uint64_t,12> passageSweepExactRouteFallbacksByCaller{};
|
||||||
std::array<std::uint64_t,12> passageSweepFeatureBoundSkipsByCaller{};
|
std::array<std::uint64_t,12> passageSweepFeatureBoundSkipsByCaller{};
|
||||||
@@ -468,8 +487,14 @@ namespace VoxelForgeCore
|
|||||||
// Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges.
|
// Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges.
|
||||||
// Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB.
|
// Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB.
|
||||||
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
|
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
|
||||||
// Version 217 replaces passage supports with flat-floor hollow-shell geometry.
|
// Version 218 sweeps passages around a dense smooth spline and limits exposed shells to a trough.
|
||||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 217;
|
// Version 219 smooths passage XY while mapping each planned height chord over its sampled horizontal arc.
|
||||||
|
// Version 220 adaptively samples passage curves and rounds open-air wall crests.
|
||||||
|
// Version 221 ranks passage segments before floor shaping and preserves their signed exterior distance.
|
||||||
|
// Version 223 projects the floor onto one continuous arc-length profile and groups
|
||||||
|
// dense passage samples into coarse tile candidates. Version 224 keeps the same
|
||||||
|
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
|
||||||
|
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 225;
|
||||||
|
|
||||||
enum class FieldChannel : std::uint32_t
|
enum class FieldChannel : std::uint32_t
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# VoxelForge — read-only design review: stamp tunnels along the curve instead of searching the curve per sample
|
||||||
|
|
||||||
|
You are reviewing a design idea against real code. **Do not edit, create or delete any file, and do not build or run
|
||||||
|
anything.** Read code and reason. Be thorough; mark each claim as [CODE] (with file:line), [MEASURED] (from a report),
|
||||||
|
or [INFERENCE].
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
VoxelForge Core (`Standalone/Core`) generates a density field: negative = solid, positive = air, 25 cm voxels, 32³ tiles,
|
||||||
|
LOD0–8 clipmap, marching cubes. Same seed must give the same world on every machine and worker count; byte-identity with
|
||||||
|
older versions is NOT required.
|
||||||
|
|
||||||
|
Inter-strate tunnels ("passages") on branch `wip/l111-halfpipe-spline` (HEAD `707bc61`) are a smooth spline swept tube:
|
||||||
|
- a D-shaped walkable floor with a continuous floor height h(s), max grade 15°;
|
||||||
|
- a rounded crest;
|
||||||
|
- an open-air half-pipe where there is no rock;
|
||||||
|
- a thin solid shell ("plain shell rule": interior always air, a thin shell always solid);
|
||||||
|
- smooth-min where tunnels meet;
|
||||||
|
- mouths that ramp into the ground.
|
||||||
|
|
||||||
|
Key code: `Standalone/Core/src/ShapeWorldInternal.h` (`FScopedPassageTileQueries`, `prepareSweptTubeBase`, around
|
||||||
|
lines 374 and 11436), `Standalone/Core/src/VoxelForgeCore.cpp:~2747`. Reports: `Standalone/Research/WorldAssemblerM3/Linux109`
|
||||||
|
… `Linux119` (REPORT.md each).
|
||||||
|
|
||||||
|
Cost today [MEASURED, L117–L119]: the smooth tunnel is 13–22% slower than the coarse L109 tunnel at LOD0–2 readiness.
|
||||||
|
About 95% of the extra cost is in passage-feature tiles. The causes: sweep pairs (sample × curve segment), exact
|
||||||
|
fallbacks that re-scan segments for off-grid queries, segment visits, per-tile prepare, and smooth-min.
|
||||||
|
|
||||||
|
## The idea to review (owner + orchestrator)
|
||||||
|
|
||||||
|
Today each sample "pulls": it searches for the nearest curve segment and computes a distance. Proposed: "push"/stamp.
|
||||||
|
For each tile, walk the curve in rings (frames: position, forward, up, as with an Unreal spline mesh) and write the
|
||||||
|
tube's signed distance into only the lattice samples near each ring. Only a narrow band of about one voxel around the
|
||||||
|
wall needs an exact value; deep rock only needs "rock" and the tunnel interior only needs "air". Off-grid queries
|
||||||
|
(normals/gradients at seams, gameplay point queries) would read the stamped lattice, interpolating, instead of
|
||||||
|
re-searching the curve. Also: accept curve accuracy at one voxel (25 cm) rather than 2.5–10 cm, and build the curve
|
||||||
|
frames once per route rather than per tile.
|
||||||
|
|
||||||
|
## Questions
|
||||||
|
|
||||||
|
1. Is the cost model right? From the code, what does a passage-feature tile do per sample today, and what would stamping
|
||||||
|
replace? Estimate the work ratio (samples touched × ops) for a typical LOD0 tile and for LOD1–2.
|
||||||
|
2. What breaks or gets harder, concretely in this code?
|
||||||
|
- box verdicts/tile skipping (`ClassifyTile`-style proofs that a tile is all solid/air);
|
||||||
|
- seams between tiles: both neighbours must stamp identical values at shared samples, deterministically;
|
||||||
|
- LOD levels (coarser lattices);
|
||||||
|
- off-grid `DensityAtUnclamped` queries and gradients;
|
||||||
|
- min/smooth-min with the rest of the field and with other tunnels;
|
||||||
|
- the floor h(s), half-pipe, shell, mouths;
|
||||||
|
- walk audits;
|
||||||
|
- thread safety.
|
||||||
|
3. Is "write into the lattice" compatible with how density is evaluated (lattice fill vs point evaluation)? If the
|
||||||
|
pipeline is pull-only, what is the smallest change that gets the push benefit? For example, a per-tile sparse
|
||||||
|
band list built by walking rings, then lookups during fill.
|
||||||
|
4. Is there a simpler or cheaper alternative we're missing? For example, an analytic local frame per sample from the
|
||||||
|
spline parameter, or a coarse per-tile segment window that is already almost free.
|
||||||
|
5. Recommendation: the design you would build, in steps, with the evidence bar (what measurement would prove or kill
|
||||||
|
it). Keep it short.
|
||||||
|
|
||||||
|
Write your answer as plain Markdown, at most ~1,200 words.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
[INFERENCE] **Build a tighter passage cache first.** Ring stamping is promising, but the code already pushes segment contributions into a tile lattice. The main opportunity is reducing coverage, overlap, and off-grid fallback work—not reversing a purely pull-based architecture.
|
||||||
|
|
||||||
|
References below: `H` = `Standalone/Core/src/ShapeWorldInternal.h`; `C` = `Standalone/Core/src/VoxelForgeCore.cpp`; `M` = `Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp`. Report references mean `Standalone/Research/WorldAssemblerM3/LinuxNNN/REPORT.md`.
|
||||||
|
|
||||||
|
**1. Cost model**
|
||||||
|
|
||||||
|
[CODE] `prepareSweptTubeBase` allocates per-passage `(cells+3)³` sample records, including a one-step halo. It walks candidate bounding boxes, lattice samples, then fine segments; individual segment bounds reject some pairs. Accepted pairs compute projection, deterministic square root, radius, floor profile, and potentially shell distance. A final pass flattens the selected tube and caches relief (`H:11436`, `H:11489`, `H:11521`, `H:11659`).
|
||||||
|
|
||||||
|
[CODE] During density evaluation, aligned samples retrieve that cache, then still perform relief weighting, floor shaping, mouth fades, shell composition, and host-field composition. Misses invoke two hierarchy searches: nearest body/floor and nearby shells—not necessarily a linear whole-route scan (`H:1247`, `H:1346`, `H:1427`, `H:1435`).
|
||||||
|
|
||||||
|
[MEASURED] L119 reports 29.40M preparation pairs and 24.46M segment visits across its four LOD0–2 profiles. Preparation fell 59.8% versus L118, while fallbacks increased 55–58%. L117 attributes about 95% of the tile-worker regression to passage-feature tiles. Thus the diagnosis is sound, but those counters do not establish equivalent costs per operation.
|
||||||
|
|
||||||
|
[INFERENCE] For an illustrative straight crossing, assume exposed surface perimeter \(P=16\) m, tile crossing length \(L=32\Delta\), and exact band ±one lattice step. Unique band samples are approximately \(2PL/\Delta^2\):
|
||||||
|
|
||||||
|
| [INFERENCE] Normal tile | LOD0 | LOD1 | LOD2 |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| Spacing Δ | 0.25 m | 0.5 m | 1 m |
|
||||||
|
| Band samples | 4,096 | 2,048 | 1,024 |
|
||||||
|
| Fraction of 35³ prepared grid | 9.6% | 4.8% | 2.4% |
|
||||||
|
|
||||||
|
[INFERENCE] These are geometric estimates, not measured “typical tiles”; clipping, floor/outer-shell surfaces, bends, and crossings change them. If today fraction \(f\) of the grid receives \(k\) segment evaluations, and stamping averages \(m\) contributions per band sample, the expensive-work ratio is approximately `(table fraction × m)/(f × k)`, multiplied by relative operation cost. For `f=0.5, k=2, m=2`, that is 19%, 9.6%, 4.8%. Allocation, classification, host evaluation, and composition remain. Dense rings can make \(m\) large and erase this advantage.
|
||||||
|
|
||||||
|
**2. Concrete difficulties**
|
||||||
|
|
||||||
|
- [CODE] **Skipping:** passage-bound overlap currently vetoes a uniform proof and returns `Mixed` (`H:5684`). [INFERENCE] Retain conservative bounds, expanded for approximation and reconstruction support. “No band samples written” proves neither solid nor air: a thin shell can lie between samples.
|
||||||
|
|
||||||
|
- [CODE] **Seams:** cache coordinates derive from integer world coordinates; nearest ties use segment index (`H:11448`, `H:11564`, `H:11640`). [INFERENCE] Generate rings from route-global parameters, never tile-local stepping. Include every contributor affecting shared samples and halos; use stable ordering and arithmetic. Independently clipping ring generation at tile boundaries risks disagreement.
|
||||||
|
|
||||||
|
- [INFERENCE] **LOD:** distinguish 25 cm curve error from lattice spacing. A fixed 25 cm band is insufficient around 1 m cells; cover interpolation corners and gradient support. A 0.75 m shell can disappear at coarse resolution. Independently reconstructed LOD fields also disagree off-grid; define one canonical point-query field.
|
||||||
|
|
||||||
|
- [CODE] **Off-grid:** `sweptSample` rejects non-lattice coordinates; public point queries call `DensityAtUnclamped` without requiring a tile cache (`H:11721`; `C:1875`). Gradients normally use density-grid values, with unclamped fallback (`M:1972`). [INFERENCE] Interpolation needs all eight valid corners, including outside loaded tiles. It changes gradients and zero crossings; interpolating only passage channels also differs from interpolating the fully composed field.
|
||||||
|
|
||||||
|
- [CODE] **Composition:** passage bodies and shells use separate hard minima; final composition includes host/native-floor exceptions (`H:1496–1519`). The floor corner uses smooth-max, implemented through smooth-min (`H:3493`, `H:6940`). [INFERENCE] Do not assume the measured smooth-min cost is tunnel-to-tunnel joining. Preserve separate void/shell channels and composition order. Arbitrary “rock/air” magnitudes can move interpolated crossings and smooth blends.
|
||||||
|
|
||||||
|
- [CODE] **Shape:** floor selection is independent of minimum round distance; the code explicitly warns that minimizing flattened segment distances makes downhill floors jump (`H:1328`). Floor evaluation uses horizontal projection, authored `h(s)`, and XY mouth blending (`H:3448`). Shells have rounded crests and five-metre mouth fades (`H:1461`, `H:3503`). [INFERENCE] A conventional spline-normal D-section changes this geometry. Stamped isolated rings can scallop; rasterize bounded slabs between frames while preserving gravity-relative floors, shell unions, and interior priority.
|
||||||
|
|
||||||
|
- [MEASURED] **Walkability:** L119’s maximum sampled floor-root offset was 7.884 mm; L113 documented voxel-aligned floor ribs. [INFERENCE] A 25 cm curve tolerance does not authorize 25 cm floor error or guarantee ≤15° grade. Audit reconstructed density and meshes, including mouths, overlaps, support, and headroom—not only planner `h(s)`.
|
||||||
|
|
||||||
|
- [CODE] **Threads:** copied worker scopes share prepared data through a shared pointer; active scope is thread-local (`H:353`, `H:11411`; `C:2854`). [INFERENCE] Finish stamping before publication. Parallel ring writes require deterministic reductions; mutable shared lazy caches introduce synchronization and lifetime problems.
|
||||||
|
|
||||||
|
**3. Smallest useful change**
|
||||||
|
|
||||||
|
[CODE] Preparation already precedes `GenerateMesh`, whose fill calls lattice evaluators (`C:2747`, `C:2892`; `M:1488`). Route segments are already prepared globally with 10 cm chord tolerance and an 8 m spacing cap (`H:4601`).
|
||||||
|
|
||||||
|
[INFERENCE] Replace preparation’s broad pair enumeration with conservative slab-to-grid spans or small spatial bins. Keep indexed lookups during pull-based fill. Store body/floor ownership and shell contributions separately; use explicit outside/interior/band states. A dense index plus compact records may beat a sparse hash map. Additional frames can be route-global, but this does not eliminate existing per-tile rasterization.
|
||||||
|
|
||||||
|
**4. Alternatives and recommendation**
|
||||||
|
|
||||||
|
[MEASURED] L115 already reduced coarse candidates 74.1% without reducing fine pairs. L118’s restriction of fallbacks to prepared spans changed output; L116’s 20.8% pair reduction did not recover readiness.
|
||||||
|
|
||||||
|
[INFERENCE] An analytic local frame still needs a parameter lookup, especially near folds. Prefer globally indexed conservative segment bins, shared by preparation and exact point queries.
|
||||||
|
|
||||||
|
[INFERENCE] Build in three steps: instrument unique samples, overlap, misses by caller, memory, and per-LOD time; implement tighter bins/slabs while preserving geometry; then separately trial 25 cm curve tolerance and canonical interpolation. Accept only reproducible near-readiness gains against L119/L109 plus unchanged walk/seam requirements and cross-compiler/worker determinism. Kill the approach if saved distance work becomes rasterization, memory, or reconstruction cost.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Linux111 report
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
Version 218 resamples passage controls with a centripetal Catmull–Rom curve at about 1 m spacing (310 control spans became 2,790 prepared segments). Open-air shells use a 0.75 m floor slab and side walls up to 1.0 m above the floor; there is no roof. Passage interior remains higher priority than shell. The second design constrained the floor route to a 14.5° grade ceiling.
|
||||||
|
|
||||||
|
The per-tile candidate lists were not measured. The 310→2,790 figures are prepared route segment counts, not tile candidate list sizes.
|
||||||
|
|
||||||
|
## Speed
|
||||||
|
|
||||||
|
L109 medians below are the candidate column from the saved five-pair timing matrix. L111 timing was not run after the audits failed.
|
||||||
|
|
||||||
|
| Stratum | L109 W8 near-ready (s) | L109 W12 near-ready (s) | L111 W8/W12 |
|
||||||
|
|---|---:|---:|---|
|
||||||
|
| SurfaceWorld | 0.737 | 0.761 | Not run |
|
||||||
|
| FloatingIslands | 0.911 | 0.863 | Not run |
|
||||||
|
| TunnelNetwork | 0.956 | 0.895 | Not run |
|
||||||
|
| VerticalShafts | 0.789 | 0.738 | Not run |
|
||||||
|
|
||||||
|
## Audits
|
||||||
|
|
||||||
|
- Clang standalone build completed. The second design removed all grade failures. Passage 0–1 still had 2 unsupported walk samples; passages 1–2 and 2–3 passed. The combined resolved-passage audit failed.
|
||||||
|
- Mouth-profile audits passed 3/3. Naturalness and ripple audits passed 2/3.
|
||||||
|
- The half-pipe shell audit passed 2/3; passage 0–1 reported 11,544 floor and 6,945 wall violations. A targeted trace showed at least one reported floor sample was 0.101 m inside the passage after relief, where interior correctly overrides the shell. The audit's smooth-route overlap model still disagreed at that point, so the remaining shell counts are not conclusive.
|
||||||
|
- Fast tier, Clang/GCC W1/W8/W12 hash matrix, readiness timing, and Unreal Linux build were not run. Canonical v218 hashes were not generated.
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
|
||||||
|
No Linux111 capture images were generated. Mouths, 25/50/75% stations, open-air sections, and programmatically verified 4 m outside/interior close-ups remain outstanding.
|
||||||
|
|
||||||
|
The candidate audit found these open-air sections:
|
||||||
|
|
||||||
|
- passage 0–1: 6.408–16.782 m; 19.500–24.439 m; 1,044.046–1,498.708 m
|
||||||
|
- passage 1–2: 81.654–85.646 m; 104.092–104.829 m
|
||||||
|
- passage 2–3: 7.701–26.008 m; 372.697–377.363 m
|
||||||
|
|
||||||
|
## Failed attempts
|
||||||
|
|
||||||
|
1. Dense spline plus half-pipe: walk grades exceeded 15° and passage 0–1 failed shell, naturalness, and ripple checks.
|
||||||
|
2. Dense spline plus a grade-limited floor: grade failures cleared, but passage 0–1 retained 2 unsupported walk samples and its shell/naturalness/ripple audits still failed. Work stopped after this second design as requested.
|
||||||
|
|
||||||
|
Logs: `Saved/Linux/l111/build-clang.log` and `Saved/Linux/l111/audit-owner.log`.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Linux 112 — horizontal spline, planned floor profile
|
||||||
|
|
||||||
|
## Shape and audits
|
||||||
|
|
||||||
|
The passage now follows a dense centripetal spline in XY. Height stays piecewise linear on the planner's horizontal arc-length profile; radius and floor depth blend smoothly. Open-air sections have a floor slab and 1 m side walls, with no roof, and passage interiors take priority over shells. Version is v219; source stamp is `c31c8baca8f8308dca62edc6d286c8e84a8214ede7ca444fdaf337ae743ccee1`.
|
||||||
|
|
||||||
|
The audit samples the same generated centreline. No walkability thresholds were loosened: the grade limit remains 15°, headroom remains 0.30 m for Caves and 2.20 m for Lattice, and the shell, mouth, naturalness, and ripple gates remain in place. The owner route has zero blocked, unsupported, or grade-failure samples. The native Caves route passes at 15.000° maximum grade and 0.300 m minimum headroom; native Lattice passes with zero support failures at 10.028° maximum grade and 2.200 m headroom. All three passage interfaces pass shell, mouth, naturalness, and ripple audits. The FI blocked control at passage/1-2 remains detected (16,380 blocked capsule samples); all three blocked controls were detected.
|
||||||
|
|
||||||
|
The native Caves field owns its corridor so the transition shell cannot clip it; the passage interior still opens within its own stratum. Lattice's authored support below the floor remains solid. These preserve the same walkability and support requirements.
|
||||||
|
|
||||||
|
## Captures
|
||||||
|
|
||||||
|
[Capture index](captures/INDEX.md): 110 images verified (60 L109 before, 50 L112 after), covering mouths, 25/50/75% stations, and every open-air section. This includes 32 verified 4 m close-ups (18 before, 14 after) of the outside and interior walls. For the close-ups, the nominal route target snaps to a nearby mesh triangle; the camera remains 4.00 m from the mesh within 0.02 m, with wall-normal alignment at least 0.25.
|
||||||
|
|
||||||
|
## Readiness
|
||||||
|
|
||||||
|
Five interleaved L80 pairs per cell, all four strata, W8 and W12. Values are median seconds, L109 binary → L112 binary; near is LOD0–2 ready and whole is whole-view ready.
|
||||||
|
|
||||||
|
| Stratum | W8 near | W8 whole | W12 near | W12 whole |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| SW | 0.779 → 0.822 | 1.837 → 1.826 | 0.802 → 0.845 | 1.768 → 1.835 |
|
||||||
|
| FI | 0.972 → 1.063 | 1.935 → 2.022 | 0.924 → 0.992 | 1.792 → 1.884 |
|
||||||
|
| TN | 1.021 → 1.092 | 1.869 → 1.970 | 0.931 → 1.023 | 1.758 → 1.866 |
|
||||||
|
| VS | 0.832 → 0.921 | 1.764 → 1.827 | 0.783 → 0.861 | 1.666 → 1.741 |
|
||||||
|
|
||||||
|
L109's prior report recorded W12 near-ready at 0.863 s for FI and 0.895 s for TN. In this matrix the L109 binary remeasured at 0.924 s and 0.931 s; L112 measured 0.992 s and 1.023 s. That is +7.4% and +9.9% against the matched rerun, or +15.0% and +14.3% against the prior report values.
|
||||||
|
|
||||||
|
## Candidate lists
|
||||||
|
|
||||||
|
Feature and route-segment lists have the same counts. Each cell shows L109 → L112; totals are candidates across profiled tiles.
|
||||||
|
|
||||||
|
| Stratum | Profiled tiles | Total candidates | p95 per tile | Maximum per tile |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| SW | 1,763 → 1,742 | 583 → 4,216 | 3 → 18 | 21 → 185 |
|
||||||
|
| FI | 1,590 → 1,573 | 1,036 → 7,068 | 4 → 22 | 47 → 420 |
|
||||||
|
| TN | 1,492 → 1,480 | 869 → 4,784 | 4 → 22 | 23 → 116 |
|
||||||
|
| VS | 1,660 → 1,660 | 644 → 3,133 | 4 → 18 | 16 → 82 |
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Fast tier: PASS, 264 checks.
|
||||||
|
- Clang/GCC W1/W8/W12 determinism: all 18 runs passed; hashes match within each configuration.
|
||||||
|
- Linux VFHostEditor build: succeeded with `-NoUBA -MaxParallelActions=4` after timing.
|
||||||
|
- No commit was created.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare per-tile passage candidate-list sizes for L109 and L112."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l112/candidate-lists"
|
||||||
|
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
BINARIES = {
|
||||||
|
"L109": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
|
||||||
|
"L112": ROOT / "Saved/Linux/l112/bin-clang/VoxelForgeCoreHeadless-l112-clang",
|
||||||
|
}
|
||||||
|
CAMERAS = {
|
||||||
|
0: (8.0, 16.0, -372.2626953125),
|
||||||
|
1: (25.8369140625, 3.19921875, -737.0),
|
||||||
|
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||||
|
3: (-20.0625, -5.068359375, -900.353515625),
|
||||||
|
}
|
||||||
|
LIST = re.compile(r"^\[PassageComposerTile\].*?features=(\d+) route_candidates=(\d+)", re.M)
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: list[int], fraction: float) -> int:
|
||||||
|
ordered = sorted(values)
|
||||||
|
if not ordered:
|
||||||
|
return 0
|
||||||
|
return ordered[min(len(ordered) - 1,
|
||||||
|
max(0, math.ceil(fraction * len(ordered)) - 1))]
|
||||||
|
|
||||||
|
|
||||||
|
def run(strate: int, version: str) -> dict:
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
log = OUT / f"{version}-s{strate}.log"
|
||||||
|
camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1740s", str(BINARIES[version]), str(DATA),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--workers=12", "--clip-radius=3", "--max-clip-level=8", "--max-level=8",
|
||||||
|
f"--camera={camera}", "--profile-passage-composer", "--stream-scheduler-policy=3",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||||
|
output = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
features = [int(row[0]) for row in LIST.findall(output)]
|
||||||
|
routes = [int(row[1]) for row in LIST.findall(output)]
|
||||||
|
if result.returncode or not features:
|
||||||
|
raise SystemExit(f"candidate-list profile missing: {log}; exit={result.returncode}")
|
||||||
|
row = {
|
||||||
|
"strate": strate, "version": version, "profiled_tiles": len(features),
|
||||||
|
"feature_candidates": {"sum": sum(features), "min": min(features),
|
||||||
|
"median": statistics.median(features), "p95": percentile(features, .95),
|
||||||
|
"max": max(features)},
|
||||||
|
"route_segment_candidates": {"sum": sum(routes), "min": min(routes),
|
||||||
|
"median": statistics.median(routes), "p95": percentile(routes, .95),
|
||||||
|
"max": max(routes)},
|
||||||
|
"elapsed_s": round(time.monotonic() - started, 3),
|
||||||
|
"log": str(log.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
print("[L112CandidateLists] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if any(not path.is_file() for path in (*BINARIES.values(), DATA)):
|
||||||
|
raise SystemExit("missing L109/L112 candidate-list input")
|
||||||
|
rows = [run(strate, version) for strate in range(4)
|
||||||
|
for version in ("L109", "L112")]
|
||||||
|
(OUT / "summary.json").write_text(json.dumps(rows, indent=2) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,839 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Capture L109 and L112 routes, every open-air section, and verified 4 m wall views."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
from array import array
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
RESEARCH = Path(__file__).resolve().parent
|
||||||
|
CAPTURES = RESEARCH / "captures"
|
||||||
|
SAVED = ROOT / "Saved/Linux/l112/captures"
|
||||||
|
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
BINARIES = {
|
||||||
|
"before": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
|
||||||
|
"after": ROOT / "Saved/Linux/l112/bin-clang/VoxelForgeCoreHeadless-l112-clang",
|
||||||
|
}
|
||||||
|
LAYOUTS = {
|
||||||
|
"before": ROOT / "Saved/Linux/l109/audit-layout-final-stamp.json",
|
||||||
|
"after": ROOT / "Saved/Linux/l112/audit-layout-candidate.json",
|
||||||
|
}
|
||||||
|
AUDIT_LOGS = {
|
||||||
|
"before": ROOT / "Saved/Linux/l109/audit-owner4-final-stamp.log",
|
||||||
|
"after": ROOT / "Saved/Linux/l112/audit-owner-clang.log",
|
||||||
|
}
|
||||||
|
RENDERERS = {
|
||||||
|
"before": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeOffscreenCapture-l109-clang",
|
||||||
|
"after": ROOT / "Saved/Linux/l112/bin-clang/VoxelForgeOffscreenCapture-l112-clang",
|
||||||
|
}
|
||||||
|
SECTION = re.compile(
|
||||||
|
r"^\[PassageOpenAirSection\] id=(\S+) section=(\d+) start_m=([\d.]+) "
|
||||||
|
r"end_m=([\d.]+) samples=(\d+) midpoint_xyz_m=\(([^)]*)\) "
|
||||||
|
r"midpoint_radius_m=([\d.]+)", re.M)
|
||||||
|
DRAWN = re.compile(r"drawn_triangles=(\d+)")
|
||||||
|
QUALITY_METRICS = re.compile(r"stddev_luma=([\d.]+).*coverage_luma_gt12=([\d.]+)")
|
||||||
|
|
||||||
|
|
||||||
|
def vector(text: str) -> tuple[float, float, float]:
|
||||||
|
return tuple(float(value) for value in text.split(","))
|
||||||
|
|
||||||
|
|
||||||
|
def add(a, b):
|
||||||
|
return tuple(x + y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def sub(a, b):
|
||||||
|
return tuple(x - y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def mul(a, scale):
|
||||||
|
return tuple(x * scale for x in a)
|
||||||
|
|
||||||
|
|
||||||
|
def norm(a):
|
||||||
|
length = math.sqrt(sum(x * x for x in a))
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
return (0.0, 1.0, 0.0)
|
||||||
|
return tuple(value / length for value in a)
|
||||||
|
|
||||||
|
|
||||||
|
def angles(camera, target):
|
||||||
|
delta = sub(target, camera)
|
||||||
|
horizontal = math.hypot(delta[0], delta[1])
|
||||||
|
return (math.degrees(math.atan2(delta[0], delta[1])),
|
||||||
|
math.degrees(math.atan2(delta[2], horizontal)))
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(values) -> str:
|
||||||
|
return ",".join(f"{value:.6f}" for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def mesh_triangle_count(mesh: Path) -> int:
|
||||||
|
"""Read only the VFMSH headers and count indexed triangles."""
|
||||||
|
with mesh.open("rb") as stream:
|
||||||
|
magic = stream.read(8)
|
||||||
|
materials = magic == b"VFMSH002"
|
||||||
|
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||||
|
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
|
||||||
|
version, _voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
if version != (2 if materials else 1):
|
||||||
|
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
|
||||||
|
stream.seek(40, 1)
|
||||||
|
triangles = 0
|
||||||
|
for _ in range(tile_count):
|
||||||
|
stream.seek(16, 1)
|
||||||
|
surface_count, _skirts, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
triangles += index_count // 3
|
||||||
|
stream.seek(vertex_count * 24 + (10 * vertex_count if materials else 0)
|
||||||
|
+ index_count * 4 + mask_count, 1)
|
||||||
|
if stream.read(1):
|
||||||
|
raise RuntimeError(f"trailing data in mesh scene: {mesh}")
|
||||||
|
return triangles
|
||||||
|
|
||||||
|
|
||||||
|
def dot(a, b):
|
||||||
|
return sum(x * y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def closest_point_triangle(point, a, b, c):
|
||||||
|
def closest_segment(start, end, first_weight, second_weight):
|
||||||
|
edge = sub(end, start)
|
||||||
|
length_squared = dot(edge, edge)
|
||||||
|
t = (min(1.0, max(0.0, dot(sub(point, start), edge) / length_squared))
|
||||||
|
if length_squared > 1.0e-16 else 0.0)
|
||||||
|
position = add(start, mul(edge, t))
|
||||||
|
weights = [0.0, 0.0, 0.0]
|
||||||
|
weights[first_weight], weights[second_weight] = 1.0 - t, t
|
||||||
|
return position, tuple(weights)
|
||||||
|
|
||||||
|
ab, ac = sub(b, a), sub(c, a)
|
||||||
|
cross = ((ab[1] * ac[2] - ab[2] * ac[1]),
|
||||||
|
(ab[2] * ac[0] - ab[0] * ac[2]),
|
||||||
|
(ab[0] * ac[1] - ab[1] * ac[0]))
|
||||||
|
if dot(cross, cross) <= 1.0e-16:
|
||||||
|
candidates = [closest_segment(a, b, 0, 1),
|
||||||
|
closest_segment(b, c, 1, 2),
|
||||||
|
closest_segment(c, a, 2, 0)]
|
||||||
|
return min(candidates, key=lambda candidate:
|
||||||
|
dot(sub(candidate[0], point), sub(candidate[0], point)))
|
||||||
|
ap = sub(point, a)
|
||||||
|
d1, d2 = dot(ab, ap), dot(ac, ap)
|
||||||
|
if d1 <= 0.0 and d2 <= 0.0:
|
||||||
|
return a, (1.0, 0.0, 0.0)
|
||||||
|
bp = sub(point, b)
|
||||||
|
d3, d4 = dot(ab, bp), dot(ac, bp)
|
||||||
|
if d3 >= 0.0 and d4 <= d3:
|
||||||
|
return b, (0.0, 1.0, 0.0)
|
||||||
|
vc = d1 * d4 - d3 * d2
|
||||||
|
if vc <= 0.0 and d1 >= 0.0 and d3 <= 0.0:
|
||||||
|
denominator = d1 - d3
|
||||||
|
if denominator <= 1.0e-16:
|
||||||
|
return a, (1.0, 0.0, 0.0)
|
||||||
|
v = d1 / denominator
|
||||||
|
return add(a, mul(ab, v)), (1.0 - v, v, 0.0)
|
||||||
|
cp = sub(point, c)
|
||||||
|
d5, d6 = dot(ab, cp), dot(ac, cp)
|
||||||
|
if d6 >= 0.0 and d5 <= d6:
|
||||||
|
return c, (0.0, 0.0, 1.0)
|
||||||
|
vb = d5 * d2 - d1 * d6
|
||||||
|
if vb <= 0.0 and d2 >= 0.0 and d6 <= 0.0:
|
||||||
|
denominator = d2 - d6
|
||||||
|
if denominator <= 1.0e-16:
|
||||||
|
return a, (1.0, 0.0, 0.0)
|
||||||
|
w = d2 / denominator
|
||||||
|
return add(a, mul(ac, w)), (1.0 - w, 0.0, w)
|
||||||
|
va = d3 * d6 - d5 * d4
|
||||||
|
if va <= 0.0 and d4 - d3 >= 0.0 and d5 - d6 >= 0.0:
|
||||||
|
denominator = (d4 - d3) + (d5 - d6)
|
||||||
|
if denominator <= 1.0e-16:
|
||||||
|
return b, (0.0, 1.0, 0.0)
|
||||||
|
w = (d4 - d3) / denominator
|
||||||
|
edge = sub(c, b)
|
||||||
|
return add(b, mul(edge, w)), (0.0, 1.0 - w, w)
|
||||||
|
face_denominator = va + vb + vc
|
||||||
|
if face_denominator <= 1.0e-16:
|
||||||
|
candidates = [closest_segment(a, b, 0, 1),
|
||||||
|
closest_segment(b, c, 1, 2),
|
||||||
|
closest_segment(c, a, 2, 0)]
|
||||||
|
return min(candidates, key=lambda candidate:
|
||||||
|
dot(sub(candidate[0], point), sub(candidate[0], point)))
|
||||||
|
denominator = 1.0 / face_denominator
|
||||||
|
v, w = vb * denominator, vc * denominator
|
||||||
|
weights = (1.0 - v - w, v, w)
|
||||||
|
closest = tuple(weights[0] * a[axis] + weights[1] * b[axis]
|
||||||
|
+ weights[2] * c[axis] for axis in range(3))
|
||||||
|
return closest, weights
|
||||||
|
|
||||||
|
|
||||||
|
def nearest_surface_anchor(mesh: Path, point, margin_m=200.0, *,
|
||||||
|
preferred_normal=None, minimum_alignment=-1.0) -> dict:
|
||||||
|
"""Return the nearest point on a VFMSH surface triangle and its normal."""
|
||||||
|
best_distance_sq = float("inf")
|
||||||
|
best_position = best_normal = None
|
||||||
|
with mesh.open("rb") as stream:
|
||||||
|
magic = stream.read(8)
|
||||||
|
materials = magic == b"VFMSH002"
|
||||||
|
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||||
|
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
|
||||||
|
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||||
|
if version != (2 if materials else 1):
|
||||||
|
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
|
||||||
|
stream.seek(40, 1)
|
||||||
|
for _ in range(tile_count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, _skirts, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
extent = (32 << key[3]) * voxel_um / 1_000_000.0
|
||||||
|
origin = (key[0] * extent, key[1] * extent, key[2] * extent)
|
||||||
|
if any(point[axis] < origin[axis] - margin_m or
|
||||||
|
point[axis] > origin[axis] + extent + margin_m for axis in range(3)):
|
||||||
|
stream.seek(vertex_count * 24 + (10 * vertex_count if materials else 0)
|
||||||
|
+ index_count * 4 + mask_count, 1)
|
||||||
|
continue
|
||||||
|
positions = array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
if materials:
|
||||||
|
stream.seek(vertex_count * 10, 1)
|
||||||
|
indices = array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
stream.seek(mask_count, 1)
|
||||||
|
if struct.pack("=I", 1) != struct.pack("<I", 1):
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
def vertex(index):
|
||||||
|
offset = index * 3
|
||||||
|
return tuple(float(positions[offset + axis]) for axis in range(3))
|
||||||
|
def vertex_normal(index):
|
||||||
|
offset = index * 3
|
||||||
|
return tuple(float(normals[offset + axis]) for axis in range(3))
|
||||||
|
for index in range(0, index_count - 2, 3):
|
||||||
|
triangle = (indices[index], indices[index + 1], indices[index + 2])
|
||||||
|
if any(vertex_index >= surface_count or vertex_index >= vertex_count
|
||||||
|
for vertex_index in triangle):
|
||||||
|
continue
|
||||||
|
a, b, c = (vertex(vertex_index) for vertex_index in triangle)
|
||||||
|
position, weights = closest_point_triangle(point, a, b, c)
|
||||||
|
delta = sub(position, point)
|
||||||
|
distance_sq = dot(delta, delta)
|
||||||
|
if distance_sq >= best_distance_sq:
|
||||||
|
continue
|
||||||
|
raw_normals = [vertex_normal(vertex_index) for vertex_index in triangle]
|
||||||
|
raw_normal = tuple(sum(weights[corner] * raw_normals[corner][axis]
|
||||||
|
for corner in range(3)) for axis in range(3))
|
||||||
|
if dot(raw_normal, raw_normal) < 1.0e-10:
|
||||||
|
raw_normal = (
|
||||||
|
(b[1] - a[1]) * (c[2] - a[2]) - (b[2] - a[2]) * (c[1] - a[1]),
|
||||||
|
(b[2] - a[2]) * (c[0] - a[0]) - (b[0] - a[0]) * (c[2] - a[2]),
|
||||||
|
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]))
|
||||||
|
if dot(raw_normal, raw_normal) < 1.0e-10:
|
||||||
|
continue
|
||||||
|
candidate_normal = norm(raw_normal)
|
||||||
|
if preferred_normal is not None:
|
||||||
|
alignment = dot(candidate_normal, preferred_normal)
|
||||||
|
if abs(alignment) < minimum_alignment:
|
||||||
|
continue
|
||||||
|
if alignment < 0.0:
|
||||||
|
candidate_normal = mul(candidate_normal, -1.0)
|
||||||
|
best_distance_sq = distance_sq
|
||||||
|
best_position = position
|
||||||
|
best_normal = candidate_normal
|
||||||
|
if best_position is None:
|
||||||
|
raise RuntimeError(f"no nearby surface vertices in mesh scene: {mesh}")
|
||||||
|
return {"position_m": best_position, "normal": best_normal,
|
||||||
|
"distance_m": math.sqrt(best_distance_sq)}
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize(value: str) -> str:
|
||||||
|
return re.sub(r"[^A-Za-z0-9_-]+", "-", value).strip("-").replace("/", "-")
|
||||||
|
|
||||||
|
|
||||||
|
def route_height_at_xy(route, x, y):
|
||||||
|
if not route:
|
||||||
|
return None
|
||||||
|
if len(route) == 1:
|
||||||
|
point = route[0]
|
||||||
|
return point[2], (x - point[0]) ** 2 + (y - point[1]) ** 2
|
||||||
|
best = (float("inf"), None)
|
||||||
|
for a, b in zip(route, route[1:]):
|
||||||
|
dx, dy = b[0] - a[0], b[1] - a[1]
|
||||||
|
length_sq = dx * dx + dy * dy
|
||||||
|
t = min(1.0, max(0.0, ((x - a[0]) * dx + (y - a[1]) * dy) / length_sq)) \
|
||||||
|
if length_sq > 1.0e-12 else 0.0
|
||||||
|
px, py = a[0] + t * dx, a[1] + t * dy
|
||||||
|
distance_sq = (x - px) ** 2 + (y - py) ** 2
|
||||||
|
if distance_sq < best[0]:
|
||||||
|
best = (distance_sq, a[2] + (b[2] - a[2]) * t)
|
||||||
|
return best[1], best[0]
|
||||||
|
|
||||||
|
|
||||||
|
def upper_mouth_floor_depth(layout: dict, passage: dict, controls) -> float:
|
||||||
|
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||||
|
authored = max(0.0, controls[0][0][2] - passage["upper_mouth_q"][2] * scale + 1.13)
|
||||||
|
stratum = next((row for row in layout["strates"]
|
||||||
|
if row["strate_index"] == passage["upper_strate_index"]), None)
|
||||||
|
if stratum is None or stratum["generator_kind"] != "FloatingIslands":
|
||||||
|
return authored
|
||||||
|
route = [tuple(value * scale for value in anchor["values"][:3])
|
||||||
|
for anchor in stratum.get("anchors", [])
|
||||||
|
if anchor.get("kind") == "route_point" and len(anchor.get("values", [])) >= 3]
|
||||||
|
projected = route_height_at_xy(route, controls[0][0][0], controls[0][0][1])
|
||||||
|
if projected is None or projected[1] > (controls[0][1] + 2.0) ** 2:
|
||||||
|
return authored
|
||||||
|
route_depth = max(0.0, controls[0][0][2] - projected[0] + 1.13)
|
||||||
|
return max(authored, route_depth)
|
||||||
|
|
||||||
|
|
||||||
|
def route_controls(layout: dict, passage: dict, *, adjust_upper_mouth=False):
|
||||||
|
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||||
|
controls = [(tuple(value * scale for value in row["position_q"]),
|
||||||
|
row["radius_q"] * scale) for row in passage["control_points"]]
|
||||||
|
lower_q = passage["lower_mouth_q"]
|
||||||
|
player_center_above_floor_q = round(
|
||||||
|
1.13 * 256.0 * 1_000_000.0 / layout["core_voxel_size_um"])
|
||||||
|
lower_index = next((index for index, row in enumerate(passage["control_points"])
|
||||||
|
if row["position_q"][0] == lower_q[0]
|
||||||
|
and row["position_q"][1] == lower_q[1]
|
||||||
|
and row["position_q"][2] - row["radius_q"]
|
||||||
|
+ player_center_above_floor_q == lower_q[2]), None)
|
||||||
|
if lower_index is None:
|
||||||
|
raise RuntimeError(f"{passage['stable_id']}: lower mouth control is unbound")
|
||||||
|
if lower_index < 1:
|
||||||
|
raise RuntimeError(f"{passage['stable_id']}: incomplete route controls")
|
||||||
|
if adjust_upper_mouth:
|
||||||
|
controls[0] = (controls[0][0], max(controls[0][1],
|
||||||
|
upper_mouth_floor_depth(layout, passage, controls)))
|
||||||
|
return controls, lower_index
|
||||||
|
|
||||||
|
|
||||||
|
def route_for(layout: dict, passage: dict) -> list[tuple[tuple[float, float, float], float]]:
|
||||||
|
controls, lower_index = route_controls(layout, passage)
|
||||||
|
return controls[:lower_index + 1]
|
||||||
|
|
||||||
|
|
||||||
|
def spline_knot_span(a, b):
|
||||||
|
return max(1.0e-6, math.sqrt(math.sqrt((b[0] - a[0]) ** 2
|
||||||
|
+ (b[1] - a[1]) ** 2)))
|
||||||
|
|
||||||
|
|
||||||
|
def spline_point(controls, span, t):
|
||||||
|
as_point = lambda index: (*controls[index][0], controls[index][1])
|
||||||
|
p0, p1, p2, p3 = (as_point(max(0, span - 1)), as_point(span),
|
||||||
|
as_point(span + 1), as_point(min(span + 2, len(controls) - 1)))
|
||||||
|
dt0, dt1, dt2 = (spline_knot_span(p0, p1), spline_knot_span(p1, p2),
|
||||||
|
spline_knot_span(p2, p3))
|
||||||
|
t2, t3 = t * t, t * t * t
|
||||||
|
result = [0.0] * 4
|
||||||
|
for axis in (0, 1, 3):
|
||||||
|
m1 = dt1 * ((p1[axis] - p0[axis]) / dt0
|
||||||
|
- (p2[axis] - p0[axis]) / (dt0 + dt1)
|
||||||
|
+ (p2[axis] - p1[axis]) / dt1)
|
||||||
|
m2 = dt1 * ((p2[axis] - p1[axis]) / dt1
|
||||||
|
- (p3[axis] - p1[axis]) / (dt1 + dt2)
|
||||||
|
+ (p3[axis] - p2[axis]) / dt2)
|
||||||
|
result[axis] = ((2 * t3 - 3 * t2 + 1) * p1[axis]
|
||||||
|
+ (-2 * t3 + 3 * t2) * p2[axis]
|
||||||
|
+ (t3 - 2 * t2 + t) * m1 + (t3 - t2) * m2)
|
||||||
|
result[2] = p1[2] + (p2[2] - p1[2]) * t
|
||||||
|
result[3] = min(max(result[3], min(p1[3], p2[3])), max(p1[3], p2[3]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def smooth_route(layout: dict, passage: dict):
|
||||||
|
"""Mirror PassageSpline::BuildSegments from the runtime shared sampler."""
|
||||||
|
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||||
|
planned_floor_profile = [
|
||||||
|
(row["position_q"][2] - row["radius_q"]) * scale
|
||||||
|
for row in passage["control_points"]]
|
||||||
|
controls, route_end = route_controls(layout, passage, adjust_upper_mouth=True)
|
||||||
|
result = []
|
||||||
|
along = 0.0
|
||||||
|
arc_steps = 32
|
||||||
|
for span in range(route_end):
|
||||||
|
arc = [0.0]
|
||||||
|
previous = spline_point(controls, span, 0.0)
|
||||||
|
for step in range(1, arc_steps + 1):
|
||||||
|
current = spline_point(controls, span, step / arc_steps)
|
||||||
|
arc.append(arc[-1] + math.hypot(current[0] - previous[0],
|
||||||
|
current[1] - previous[1]))
|
||||||
|
previous = current
|
||||||
|
segment_count = max(1, math.ceil(arc[-1]))
|
||||||
|
points = [spline_point(controls, span, 0.0)]
|
||||||
|
parameters = [0.0]
|
||||||
|
for segment in range(1, segment_count + 1):
|
||||||
|
target = min(arc[-1], arc[-1] * segment / segment_count)
|
||||||
|
upper = next(index for index in range(1, len(arc))
|
||||||
|
if arc[index] >= target)
|
||||||
|
lower = upper - 1
|
||||||
|
width = arc[upper] - arc[lower]
|
||||||
|
fraction = min(1.0, max(0.0, (target - arc[lower]) / width)) if width > 1e-12 else 0.0
|
||||||
|
t = (lower + fraction) / arc_steps
|
||||||
|
points.append(spline_point(controls, span, t))
|
||||||
|
parameters.append(t)
|
||||||
|
sampled_arc = [0.0]
|
||||||
|
for a, b in zip(points, points[1:]):
|
||||||
|
sampled_arc.append(sampled_arc[-1] + math.hypot(b[0] - a[0], b[1] - a[1]))
|
||||||
|
horizontal_length = sampled_arc[-1]
|
||||||
|
for index, point in enumerate(points):
|
||||||
|
progress = sampled_arc[index] / horizontal_length if horizontal_length > 1e-12 else parameters[index]
|
||||||
|
point[2] = controls[span][0][2] + (controls[span + 1][0][2]
|
||||||
|
- controls[span][0][2]) * progress
|
||||||
|
point.append(planned_floor_profile[span]
|
||||||
|
+ (planned_floor_profile[span + 1]
|
||||||
|
- planned_floor_profile[span]) * progress)
|
||||||
|
if not result:
|
||||||
|
result.append((tuple(points[0][:3]), points[0][3], 0.0, points[0][4]))
|
||||||
|
for a, b in zip(points, points[1:]):
|
||||||
|
length = math.dist(a[:3], b[:3])
|
||||||
|
along += length
|
||||||
|
result.append((tuple(b[:3]), b[3], along, b[4]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def derived_smooth_route(route, passage, layout):
|
||||||
|
total = route[-1][2]
|
||||||
|
interior_radii = [radius for _, radius, distance, _ in route
|
||||||
|
if distance >= 10.0 and total - distance >= 10.0]
|
||||||
|
body_depth = max(0.5, min(interior_radii or [radius for _, radius, _, _ in route]) - 0.23)
|
||||||
|
result = []
|
||||||
|
for point, radius, distance, planned_floor in route:
|
||||||
|
local_radius_depth = max(0.0, radius - 0.23)
|
||||||
|
tube_depth = min(body_depth, local_radius_depth)
|
||||||
|
center = (point[0], point[1], planned_floor + tube_depth)
|
||||||
|
result.append((center, radius, distance, planned_floor))
|
||||||
|
return result, body_depth
|
||||||
|
|
||||||
|
|
||||||
|
def route_lengths(route):
|
||||||
|
lengths = [((b[2] - a[2]) if len(a) > 2 and len(b) > 2
|
||||||
|
else math.dist(a[0], b[0])) for a, b in zip(route, route[1:])]
|
||||||
|
prefix = [0.0]
|
||||||
|
for length in lengths:
|
||||||
|
prefix.append(prefix[-1] + length)
|
||||||
|
return lengths, prefix
|
||||||
|
|
||||||
|
|
||||||
|
def at_distance(route, lengths, prefix, distance):
|
||||||
|
distance = max(0.0, min(distance, prefix[-1]))
|
||||||
|
for index, length in enumerate(lengths):
|
||||||
|
if distance <= prefix[index + 1] or index + 1 == len(lengths):
|
||||||
|
t = 0.0 if length <= 1.0e-9 else (distance - prefix[index]) / length
|
||||||
|
a, b = route[index], route[index + 1]
|
||||||
|
return (tuple(a[0][axis] + (b[0][axis] - a[0][axis]) * t
|
||||||
|
for axis in range(3)),
|
||||||
|
a[1] + (b[1] - a[1]) * t)
|
||||||
|
return route[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def tangent_at(route, lengths, prefix, distance):
|
||||||
|
before = at_distance(route, lengths, prefix, max(0.0, distance - 0.25))[0]
|
||||||
|
after = at_distance(route, lengths, prefix, min(prefix[-1], distance + 0.25))[0]
|
||||||
|
return norm(sub(after, before))
|
||||||
|
|
||||||
|
|
||||||
|
def derived_route(route, passage, layout):
|
||||||
|
"""Apply L109's fixed-depth floor offset to raw layout controls."""
|
||||||
|
lengths, prefix = route_lengths(route)
|
||||||
|
total = prefix[-1]
|
||||||
|
body_radii = [radius for index, (_, radius) in enumerate(route)
|
||||||
|
if prefix[index] >= 10.0 and total - prefix[index] >= 10.0]
|
||||||
|
body_depth = max(0.5, min(body_radii or [radius for _, radius in route]) - 0.23)
|
||||||
|
scale = layout["core_voxel_size_um"] / 256.0 / 1_000_000.0
|
||||||
|
upper_z = passage["upper_mouth_q"][2] * scale
|
||||||
|
lower_z = passage["lower_mouth_q"][2] * scale
|
||||||
|
upper_target = max(0.0, route[0][0][2] - upper_z + 1.13)
|
||||||
|
lower_target = max(0.0, route[-1][0][2] - lower_z + 1.13)
|
||||||
|
transformed = []
|
||||||
|
for index, (point, radius) in enumerate(route):
|
||||||
|
local_radius_depth = max(0.0, radius - 0.23)
|
||||||
|
mouth_distance = max(0.0, min(prefix[index], total - prefix[index]))
|
||||||
|
u = max(0.0, min(1.0, mouth_distance / 5.0))
|
||||||
|
ramp = u * u * (3.0 - 2.0 * u)
|
||||||
|
target = upper_target if prefix[index] <= total * 0.5 else lower_target
|
||||||
|
mouth_depth = max(0.0, min(radius, target))
|
||||||
|
route_depth = mouth_depth + (local_radius_depth - mouth_depth) * ramp
|
||||||
|
tube_depth = min(body_depth, local_radius_depth)
|
||||||
|
transformed.append(((point[0], point[1], point[2] - route_depth + tube_depth),
|
||||||
|
radius))
|
||||||
|
return transformed, body_depth
|
||||||
|
|
||||||
|
|
||||||
|
def make_views(layout: dict, audit_sections: list[dict], *, use_derived_shape: bool,
|
||||||
|
use_spline: bool,
|
||||||
|
include_open_air: bool = True) -> list[dict]:
|
||||||
|
views = []
|
||||||
|
passages = sorted(layout["passages"], key=lambda item: item["stable_id"])
|
||||||
|
for passage in passages:
|
||||||
|
raw_route = smooth_route(layout, passage) if use_spline else route_for(layout, passage)
|
||||||
|
if use_derived_shape:
|
||||||
|
route, body_depth = (derived_smooth_route(raw_route, passage, layout)
|
||||||
|
if use_spline else derived_route(raw_route, passage, layout))
|
||||||
|
else:
|
||||||
|
route = raw_route
|
||||||
|
body_depth = max(point[1] for point in route)
|
||||||
|
def floor_depth(radius: float) -> float:
|
||||||
|
if not use_derived_shape:
|
||||||
|
return radius
|
||||||
|
return min(body_depth, max(0.0, radius - 0.23))
|
||||||
|
lengths, prefix = route_lengths(route)
|
||||||
|
total = prefix[-1]
|
||||||
|
passage_id = passage["stable_id"]
|
||||||
|
safe_id = sanitize(passage_id)
|
||||||
|
|
||||||
|
for end_name, distance, outward_sign in (
|
||||||
|
("upper-mouth", 0.0, -1.0), ("lower-mouth", total, 1.0)):
|
||||||
|
center, radius = at_distance(route, lengths, prefix, distance)
|
||||||
|
tangent = tangent_at(route, lengths, prefix, distance)
|
||||||
|
outward = mul(tangent, outward_sign)
|
||||||
|
floor_z = center[2] - floor_depth(radius)
|
||||||
|
eye = (center[0] + outward[0] * 4.5,
|
||||||
|
center[1] + outward[1] * 4.5,
|
||||||
|
floor_z + 1.55)
|
||||||
|
if end_name == "lower-mouth":
|
||||||
|
eye = add(eye, add(mul(outward, 5.0), (0.0, 0.0, 2.0)))
|
||||||
|
target = (center[0] - outward[0] * 2.0,
|
||||||
|
center[1] - outward[1] * 2.0,
|
||||||
|
floor_z + 1.45)
|
||||||
|
yaw, pitch = angles(eye, target)
|
||||||
|
views.append({"name": f"{safe_id}-{end_name}", "passage": passage_id,
|
||||||
|
"kind": "mouth", "station": end_name,
|
||||||
|
"camera": eye, "target": target, "yaw": yaw, "pitch": pitch,
|
||||||
|
"focus": center, "center_z": center[2]})
|
||||||
|
|
||||||
|
for fraction in (0.25, 0.50, 0.75):
|
||||||
|
distance = total * fraction
|
||||||
|
center, radius = at_distance(route, lengths, prefix, distance)
|
||||||
|
tangent = tangent_at(route, lengths, prefix, distance)
|
||||||
|
depth = floor_depth(radius)
|
||||||
|
floor_z = center[2] - depth
|
||||||
|
horizontal = norm((-tangent[1], tangent[0], 0.0))
|
||||||
|
eye = (center[0] + horizontal[0] * 0.75,
|
||||||
|
center[1] + horizontal[1] * 0.75, floor_z + 1.60)
|
||||||
|
target_center, target_radius = at_distance(route, lengths, prefix,
|
||||||
|
min(total, distance + 14.0))
|
||||||
|
target_depth = floor_depth(target_radius)
|
||||||
|
target = (target_center[0] + horizontal[0] * 2.35,
|
||||||
|
target_center[1] + horizontal[1] * 2.35,
|
||||||
|
target_center[2] - target_depth + 1.80)
|
||||||
|
yaw, pitch = angles(eye, target)
|
||||||
|
views.append({"name": f"{safe_id}-station-{int(fraction * 100)}",
|
||||||
|
"passage": passage_id, "kind": "station",
|
||||||
|
"station": f"{int(fraction * 100)}%", "camera": eye,
|
||||||
|
"target": target, "yaw": yaw, "pitch": pitch,
|
||||||
|
"focus": center, "center_z": center[2],
|
||||||
|
"route_distance_m": distance,
|
||||||
|
"route_length_m": total,
|
||||||
|
"requested_route_distance_m": distance})
|
||||||
|
|
||||||
|
if not include_open_air:
|
||||||
|
continue
|
||||||
|
for section in (item for item in audit_sections if item["passage"] == passage_id):
|
||||||
|
distance = 0.5 * (section["start_m"] + section["end_m"])
|
||||||
|
center, radius = (section["midpoint"], section["radius_m"])
|
||||||
|
tangent = tangent_at(route, lengths, prefix, distance)
|
||||||
|
side = norm((-tangent[1], tangent[0], 0.0))
|
||||||
|
depth = floor_depth(radius)
|
||||||
|
section_id = section["section"]
|
||||||
|
interior_eye = (center[0], center[1], center[2] - depth + 1.25)
|
||||||
|
interior_target = add(interior_eye, mul(tangent, 7.0))
|
||||||
|
side_eye = add(center, add(mul(side, radius + 5.0), (0.0, 0.0, 1.2)))
|
||||||
|
oblique_eye = add(center, add(mul(side, -(radius + 3.5)), (0.0, 0.0, 4.0)))
|
||||||
|
targets = (add(center, (0.0, 0.0, 0.1)),
|
||||||
|
add(center, mul(tangent, 2.0)), interior_target)
|
||||||
|
for view_index, (eye, target) in enumerate(zip(
|
||||||
|
(side_eye, oblique_eye, interior_eye), targets), start=1):
|
||||||
|
yaw, pitch = angles(eye, target)
|
||||||
|
views.append({"name": f"{safe_id}-openair-{section_id}-view-{view_index}",
|
||||||
|
"passage": passage_id, "kind": "open-air",
|
||||||
|
"station": f"section-{section_id}/view-{view_index}",
|
||||||
|
"camera": eye, "target": target, "yaw": yaw,
|
||||||
|
"pitch": pitch, "focus": center,
|
||||||
|
"center_z": center[2], "open_air_section": section_id,
|
||||||
|
"route_distance_m": distance,
|
||||||
|
"requested_route_distance_m": distance,
|
||||||
|
"route_length_m": total})
|
||||||
|
floor_z = center[2] - depth
|
||||||
|
outside_target = add(center, add(mul(side, radius), (0.0, 0.0, -depth + 0.5)))
|
||||||
|
outside_eye = add(outside_target, mul(side, 4.0))
|
||||||
|
interior_radius = math.sqrt(max(0.0, radius * radius
|
||||||
|
- max(0.0, depth - 0.5) ** 2))
|
||||||
|
inside_target = add(center, add(mul(side, interior_radius),
|
||||||
|
(0.0, 0.0, -depth + 0.5)))
|
||||||
|
inside_eye = add(inside_target,
|
||||||
|
add(mul(side, -1.0), mul(tangent, math.sqrt(15.0))))
|
||||||
|
for closeup, eye, target in (("outside", outside_eye, outside_target),
|
||||||
|
("interior-wall", inside_eye, inside_target)):
|
||||||
|
yaw, pitch = angles(eye, target)
|
||||||
|
views.append({"name": f"{safe_id}-openair-{section_id}-4m-{closeup}",
|
||||||
|
"passage": passage_id, "kind": "open-air-close",
|
||||||
|
"station": f"section-{section_id}/4m-{closeup}",
|
||||||
|
"camera": eye, "target": target, "yaw": yaw,
|
||||||
|
"pitch": pitch, "focus": center, "center_z": center[2],
|
||||||
|
"open_air_section": section_id, "closeup_side": closeup,
|
||||||
|
"closeup_preferred_normal": side if closeup == "outside"
|
||||||
|
else mul(side, -1.0),
|
||||||
|
"camera_target_distance_m": math.dist(eye, target),
|
||||||
|
"route_distance_m": distance,
|
||||||
|
"requested_route_distance_m": distance,
|
||||||
|
"route_length_m": total})
|
||||||
|
return views
|
||||||
|
|
||||||
|
|
||||||
|
def run_process(name: str, command: list[str], log: Path, *, allow_failure=False) -> str:
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1740s", *map(str, command)]
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||||
|
if result.returncode and not allow_failure:
|
||||||
|
raise RuntimeError(f"{name}: exit={result.returncode} log={log}")
|
||||||
|
print(f"[L112CaptureProcess] name={name} elapsed_s={time.monotonic()-started:.2f} "
|
||||||
|
f"exit={result.returncode} log={log.relative_to(ROOT)}", flush=True)
|
||||||
|
return log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def capture(variant: str, view: dict, layout: dict) -> dict:
|
||||||
|
variant_saved = SAVED / variant
|
||||||
|
variant_saved.mkdir(parents=True, exist_ok=True)
|
||||||
|
variant_capture = CAPTURES / variant
|
||||||
|
variant_capture.mkdir(parents=True, exist_ok=True)
|
||||||
|
scene = variant_saved / f"{view['name']}.vfmesh"
|
||||||
|
image = variant_capture / f"{view['name']}.png"
|
||||||
|
candidates = [(view["camera"], view["target"], view["focus"],
|
||||||
|
view.get("route_distance_m"), 0.0, "requested")]
|
||||||
|
if variant == "before" and view["kind"] not in ("mouth", "open-air-close"):
|
||||||
|
passage = next(item for item in layout["passages"]
|
||||||
|
if item["stable_id"] == view["passage"])
|
||||||
|
route = route_for(layout, passage)
|
||||||
|
lengths, prefix = route_lengths(route)
|
||||||
|
total = prefix[-1]
|
||||||
|
eye_offset = sub(view["camera"], view["focus"])
|
||||||
|
target_offset = sub(view["target"], view["focus"])
|
||||||
|
requested = view["route_distance_m"]
|
||||||
|
for delta in (5, -5, 10, -10, 20, -20, 35, -35, 50, -50, 75, -75,
|
||||||
|
100, -100, 150, -150, 200, -200):
|
||||||
|
distance = max(0.0, min(total, requested + delta))
|
||||||
|
if abs(distance - requested) < 0.01:
|
||||||
|
continue
|
||||||
|
center, _radius = at_distance(route, lengths, prefix, distance)
|
||||||
|
candidates.append((add(center, eye_offset), add(center, target_offset), center,
|
||||||
|
distance, distance - requested, "route-offset"))
|
||||||
|
|
||||||
|
attempt_errors = []
|
||||||
|
for attempt_index, (probe_camera, probe_target, probe_focus, route_distance,
|
||||||
|
route_adjustment, attempt_kind) in enumerate(candidates):
|
||||||
|
suffix = "" if attempt_index == 0 else f"-attempt-{attempt_index:02d}"
|
||||||
|
headless_log = variant_saved / f"{view['name']}{suffix}-mesh.log"
|
||||||
|
render_log = variant_saved / f"{view['name']}{suffix}-render.log"
|
||||||
|
quality_log = variant_saved / f"{view['name']}{suffix}-quality.log"
|
||||||
|
yaw, pitch = angles(probe_camera, probe_target)
|
||||||
|
camera = (*probe_camera, yaw, pitch)
|
||||||
|
export_command = [
|
||||||
|
str(BINARIES[variant]), str(DATA), "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--workers=12",
|
||||||
|
"--clip-radius=3", "--max-clip-level=0", "--max-level=0", "--force-generate",
|
||||||
|
"--recipe=surface-world", f"--camera={fmt(camera)}",
|
||||||
|
"--render-distance-chunks=1", f"--mesh-export={scene}",
|
||||||
|
]
|
||||||
|
export_output = run_process(f"{variant}-{view['name']}{suffix}-mesh", export_command,
|
||||||
|
headless_log)
|
||||||
|
if "status=PASS" not in export_output or not scene.is_file():
|
||||||
|
raise RuntimeError(f"{variant}-{view['name']}: missing successful scene export")
|
||||||
|
source_triangles = mesh_triangle_count(scene)
|
||||||
|
if source_triangles <= 0:
|
||||||
|
attempt_errors.append(f"{attempt_kind}@{route_distance}: empty mesh")
|
||||||
|
if variant == "after" or view["kind"] == "mouth":
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
actual_camera, actual_target = probe_camera, probe_target
|
||||||
|
actual_focus = probe_focus
|
||||||
|
surface_distance = None
|
||||||
|
closeup_surface_distance = None
|
||||||
|
closeup_anchor_offset = None
|
||||||
|
closeup_normal_alignment = None
|
||||||
|
closeup_distance = None
|
||||||
|
if view["kind"] == "open-air-close":
|
||||||
|
try:
|
||||||
|
wall_anchor = nearest_surface_anchor(scene, probe_target, margin_m=5.0,
|
||||||
|
preferred_normal=view["closeup_preferred_normal"],
|
||||||
|
minimum_alignment=0.25)
|
||||||
|
closeup_surface_distance = wall_anchor["distance_m"]
|
||||||
|
closeup_anchor_offset = closeup_surface_distance
|
||||||
|
closeup_normal_alignment = dot(wall_anchor["normal"],
|
||||||
|
view["closeup_preferred_normal"])
|
||||||
|
if closeup_surface_distance > 5.0:
|
||||||
|
raise RuntimeError(f"{variant}-{view['name']}: nearest wall surface is "
|
||||||
|
f"{closeup_surface_distance:.3f} m from the route-local target")
|
||||||
|
actual_target = wall_anchor["position_m"]
|
||||||
|
actual_camera = add(actual_target, mul(wall_anchor["normal"], 4.0))
|
||||||
|
closeup_distance = math.dist(actual_camera, actual_target)
|
||||||
|
if abs(closeup_distance - 4.0) > 0.02:
|
||||||
|
raise RuntimeError(f"{variant}-{view['name']}: camera-to-wall distance is "
|
||||||
|
f"{closeup_distance:.3f} m, expected 4 m")
|
||||||
|
except RuntimeError as error:
|
||||||
|
raise RuntimeError(f"{variant}-{view['name']}: could not verify the 4 m wall "
|
||||||
|
f"view: {error}") from error
|
||||||
|
if attempt_kind == "route-offset":
|
||||||
|
anchor = nearest_surface_anchor(scene, probe_focus)
|
||||||
|
surface_distance = anchor["distance_m"]
|
||||||
|
actual_camera = add(anchor["position_m"], mul(anchor["normal"], 12.0))
|
||||||
|
actual_target = anchor["position_m"]
|
||||||
|
actual_focus = anchor["position_m"]
|
||||||
|
actual_yaw, actual_pitch = angles(actual_camera, actual_target)
|
||||||
|
render_command = [
|
||||||
|
str(RENDERERS[variant]), str(scene), str(image), "--size=1920x1080",
|
||||||
|
f"--camera-m={fmt(actual_camera)}", f"--yaw-deg={actual_yaw:.6f}",
|
||||||
|
f"--pitch-deg={actual_pitch:.6f}", "--shade=readable", "--headlamp=on",
|
||||||
|
"--height-tint=on", f"--height-center-m={actual_focus[2]:.6f}",
|
||||||
|
"--height-range-m=32",
|
||||||
|
]
|
||||||
|
render_output = run_process(f"{variant}-{view['name']}{suffix}-render", render_command,
|
||||||
|
render_log)
|
||||||
|
drawn = DRAWN.search(render_output)
|
||||||
|
if drawn is None or int(drawn.group(1)) <= 0 or not image.is_file():
|
||||||
|
attempt_errors.append(f"{attempt_kind}@{route_distance}: no drawn geometry")
|
||||||
|
if variant == "after" or view["kind"] == "mouth":
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
quality = run_process(f"{variant}-{view['name']}{suffix}-quality", [
|
||||||
|
"python3", "-B", str(ROOT / "Standalone/Tools/verify/capture_quality.py"),
|
||||||
|
str(image)], quality_log, allow_failure=True)
|
||||||
|
quality_status = "pass"
|
||||||
|
if "status=PASS" not in quality:
|
||||||
|
metrics = QUALITY_METRICS.search(quality)
|
||||||
|
coverage = float(metrics.group(2)) if metrics else 0.0
|
||||||
|
if coverage >= 0.001:
|
||||||
|
quality_status = "low_contrast_geometry_visible"
|
||||||
|
else:
|
||||||
|
attempt_errors.append(f"{attempt_kind}@{route_distance}: no visible pixel coverage")
|
||||||
|
if variant == "after" or view["kind"] == "mouth":
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
row = {**view, "variant": variant, "capture": str(image.relative_to(RESEARCH)),
|
||||||
|
"scene": str(scene.relative_to(ROOT)), "drawn_triangles": int(drawn.group(1)),
|
||||||
|
"source_triangles": source_triangles, "verified": True,
|
||||||
|
"pixel_quality_status": quality_status,
|
||||||
|
"route_distance_actual_m": route_distance,
|
||||||
|
"route_station_adjustment_m": route_adjustment,
|
||||||
|
"closeup_camera_target_distance_m": closeup_distance,
|
||||||
|
"closeup_target_surface_distance_m": closeup_surface_distance,
|
||||||
|
"closeup_predicted_target_offset_m": closeup_anchor_offset,
|
||||||
|
"closeup_wall_normal_alignment": closeup_normal_alignment,
|
||||||
|
"fallback_kind": attempt_kind if attempt_kind != "requested" else None,
|
||||||
|
"fallback_surface_distance_m": surface_distance,
|
||||||
|
"camera_actual_m": actual_camera, "target_actual_m": actual_target,
|
||||||
|
"logs": [str(headless_log.relative_to(ROOT)),
|
||||||
|
str(render_log.relative_to(ROOT)),
|
||||||
|
str(quality_log.relative_to(ROOT))]}
|
||||||
|
print("[L112Capture] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
raise RuntimeError(f"{variant}-{view['name']}: no verified image; "
|
||||||
|
+ "; ".join(attempt_errors))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if any(not path.is_file() for path in (*BINARIES.values(), *RENDERERS.values(),
|
||||||
|
*LAYOUTS.values(), *AUDIT_LOGS.values())):
|
||||||
|
raise SystemExit("missing L109 capture binary, layout, or audit log")
|
||||||
|
all_rows = []
|
||||||
|
variants = ("before", "after")
|
||||||
|
if "--after-only" in sys.argv[1:]:
|
||||||
|
prior_log = SAVED.parent / "captures-before-first.log"
|
||||||
|
if not prior_log.is_file():
|
||||||
|
raise SystemExit(f"missing reusable L109 capture rows: {prior_log}")
|
||||||
|
for line in prior_log.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||||
|
if line.startswith("[L112Capture] ") or line.startswith("[L109Capture] "):
|
||||||
|
all_rows.append(json.loads(line.split(" ", 1)[1]))
|
||||||
|
if not all_rows or any(row["variant"] != "before" for row in all_rows):
|
||||||
|
raise SystemExit("saved L109 capture rows are incomplete")
|
||||||
|
variants = ("after",)
|
||||||
|
sections_by_variant = {}
|
||||||
|
for variant in ("before", "after"):
|
||||||
|
sections = []
|
||||||
|
for passage, section, start, end, samples, midpoint, radius in SECTION.findall(
|
||||||
|
AUDIT_LOGS[variant].read_text(encoding="utf-8", errors="replace")):
|
||||||
|
sections.append({"passage": passage, "section": int(section),
|
||||||
|
"start_m": float(start), "end_m": float(end),
|
||||||
|
"samples": int(samples), "midpoint": vector(midpoint),
|
||||||
|
"radius_m": float(radius)})
|
||||||
|
if not sections:
|
||||||
|
raise SystemExit(f"no open-air sections found in the {variant} passage audit")
|
||||||
|
sections_by_variant[variant] = sections
|
||||||
|
if variant not in variants:
|
||||||
|
continue
|
||||||
|
if variant == "after" and variants == ("after",):
|
||||||
|
expected_before = 15 + 5 * len(sections_by_variant["before"])
|
||||||
|
if len(all_rows) != expected_before or any(
|
||||||
|
row.get("variant") != "before" or not row.get("verified")
|
||||||
|
for row in all_rows):
|
||||||
|
raise RuntimeError("saved L109 capture rows do not cover every required view")
|
||||||
|
layout = json.loads(LAYOUTS[variant].read_text(encoding="utf-8"))
|
||||||
|
include_open_air = True
|
||||||
|
views = make_views(layout, sections, use_derived_shape=True,
|
||||||
|
use_spline=variant == "after",
|
||||||
|
include_open_air=include_open_air)
|
||||||
|
expected = 5 * len(layout["passages"])
|
||||||
|
if include_open_air:
|
||||||
|
expected += 5 * len(sections)
|
||||||
|
if len(views) != expected:
|
||||||
|
raise RuntimeError(f"unexpected camera count: {len(views)} != {expected}")
|
||||||
|
print(f"[L112CapturePlan] variant={variant} passage_views={5*len(layout['passages'])} "
|
||||||
|
f"open_air_sections={len(sections) if include_open_air else 0} "
|
||||||
|
f"open_air_views={5*len(sections) if include_open_air else 0} "
|
||||||
|
f"total={len(views)}", flush=True)
|
||||||
|
for view in views:
|
||||||
|
all_rows.append(capture(variant, view, layout))
|
||||||
|
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||||
|
(CAPTURES / "capture-index.json").write_text(
|
||||||
|
json.dumps({"open_air_sections_by_variant": sections_by_variant,
|
||||||
|
"captures": all_rows}, indent=2) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
lines = ["# L112 passage captures", "",
|
||||||
|
"Each PNG was rendered from its own exported mesh. The renderer reported visible "
|
||||||
|
"triangles, and each decoded image met the pixel coverage floor. "
|
||||||
|
"`low_contrast_geometry_visible` marks images that met this floor below the "
|
||||||
|
"preferred contrast target.", ""]
|
||||||
|
for variant in ("before", "after"):
|
||||||
|
lines.extend([f"## {variant.title()}", ""])
|
||||||
|
for row in all_rows:
|
||||||
|
if row["variant"] != variant:
|
||||||
|
continue
|
||||||
|
flag = " **OPEN AIR**" if row["kind"] == "open-air" else ""
|
||||||
|
quality_note = (f"; {row['pixel_quality_status']}"
|
||||||
|
if row["pixel_quality_status"] != "pass" else "")
|
||||||
|
lines.append(f"- [{row['station']}]({variant}/{Path(row['capture']).name}) — "
|
||||||
|
f"{row['passage']}{flag}; {row['drawn_triangles']} visible triangles"
|
||||||
|
f"{quality_note}")
|
||||||
|
lines.append("")
|
||||||
|
(CAPTURES / "INDEX.md").write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
failures = [row for row in all_rows if not row["verified"]]
|
||||||
|
closeups = [row for row in all_rows if row["kind"] == "open-air-close"]
|
||||||
|
closeups_ok = (len(closeups) == 2 * sum(1 for row in all_rows
|
||||||
|
if row["kind"] == "open-air" and row["station"].endswith("view-1"))
|
||||||
|
and all(row["verified"] and abs(row["closeup_camera_target_distance_m"] - 4.0) <= 0.02
|
||||||
|
and row["closeup_target_surface_distance_m"] <= 5.0
|
||||||
|
and row["closeup_wall_normal_alignment"] >= 0.25
|
||||||
|
and row["drawn_triangles"] > 0
|
||||||
|
and row["pixel_quality_status"] in
|
||||||
|
("pass", "low_contrast_geometry_visible") for row in closeups))
|
||||||
|
print(f"[L112CaptureSummary] captures={len(all_rows)} verified={len(all_rows)-len(failures)} "
|
||||||
|
f"failed={len(failures)} 4m_closeups={len(closeups)} closeups_verified="
|
||||||
|
f"{str(closeups_ok).lower()}", flush=True)
|
||||||
|
return 1 if failures or not closeups_ok else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check Clang/GCC hash identity at W1/W8/W12 and pin v219 owner hashes."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l112/hashes"
|
||||||
|
DATASETS = {
|
||||||
|
"owner4": ROOT / "Standalone/data/owner_world.json",
|
||||||
|
"owner2": ROOT / "Standalone/data/regression/owner_world_2strate_l61.json",
|
||||||
|
}
|
||||||
|
BINARIES = {
|
||||||
|
"clang": ROOT / "Saved/Linux/l112/bin-clang/VoxelForgeCoreHeadless-l112-clang",
|
||||||
|
"gcc": ROOT / "Saved/Linux/l112/bin-gcc/VoxelForgeCoreHeadless-l112-gcc",
|
||||||
|
}
|
||||||
|
HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=(\S+)$", re.M)
|
||||||
|
|
||||||
|
|
||||||
|
def run(dataset: str, recipe: str, workers: int, compiler: str) -> dict:
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
label = f"{dataset}-{recipe}-w{workers}-{compiler}"
|
||||||
|
log = OUT / f"{label}.log"
|
||||||
|
layout = OUT / f"{label}.layout.json"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1740s", str(BINARIES[compiler]), str(DATASETS[dataset]),
|
||||||
|
f"--workers={workers}", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=8", f"--recipe={recipe}",
|
||||||
|
"--stream-hash", f"--resolved-layout-output={layout}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||||
|
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||||
|
output = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
hashes = HASH.findall(output)
|
||||||
|
if result.returncode or len(hashes) != 12:
|
||||||
|
raise SystemExit(f"hash run failed or incomplete: {label}; exit={result.returncode} "
|
||||||
|
f"hashes={len(hashes)} log={log}")
|
||||||
|
row = {"dataset": dataset, "recipe": recipe, "workers": workers,
|
||||||
|
"compiler": compiler, "exit_code": result.returncode,
|
||||||
|
"hashes": hashes, "source_stamp": (STAMP.search(output).group(1)
|
||||||
|
if STAMP.search(output) else None),
|
||||||
|
"elapsed_s": round(time.monotonic() - started, 3),
|
||||||
|
"log": str(log.relative_to(ROOT))}
|
||||||
|
print("[L112HashRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if any(not path.is_file() for path in (*DATASETS.values(), *BINARIES.values())):
|
||||||
|
raise SystemExit("missing L112 hash input")
|
||||||
|
rows = []
|
||||||
|
recipes = {"owner4": ("surface-world",),
|
||||||
|
"owner2": ("surface-world", "floating-islands")}
|
||||||
|
for dataset, dataset_recipes in recipes.items():
|
||||||
|
for recipe in dataset_recipes:
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
rows.append(run(dataset, recipe, workers, compiler))
|
||||||
|
(OUT / "runs-partial.json").write_text(
|
||||||
|
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||||
|
groups = {}
|
||||||
|
for dataset, dataset_recipes in recipes.items():
|
||||||
|
for recipe in dataset_recipes:
|
||||||
|
key = f"{dataset}/{recipe}"
|
||||||
|
group = [row for row in rows if row["dataset"] == dataset
|
||||||
|
and row["recipe"] == recipe]
|
||||||
|
reference = group[0]["hashes"]
|
||||||
|
groups[key] = {"all_worker_compiler_hashes_match": all(
|
||||||
|
row["hashes"] == reference for row in group),
|
||||||
|
"source_stamps": sorted({row["source_stamp"] for row in group}),
|
||||||
|
"runs": [{"workers": row["workers"], "compiler": row["compiler"],
|
||||||
|
"log": row["log"]} for row in group]}
|
||||||
|
if not groups[key]["all_worker_compiler_hashes_match"]:
|
||||||
|
raise SystemExit(f"determinism mismatch: {key}")
|
||||||
|
if dataset == "owner2":
|
||||||
|
canonical = next(row for row in group if row["workers"] == 12
|
||||||
|
and row["compiler"] == "clang")
|
||||||
|
recipe_name = "floating-islands" if recipe == "floating-islands" else "surface-world"
|
||||||
|
style = 1 if recipe_name == "surface-world" else 2
|
||||||
|
baseline = (ROOT / "Standalone/Tools/verify/baselines/owner-hash/canonical"
|
||||||
|
/ f"owner-hash-{recipe_name}-style{style}-v219.baseline")
|
||||||
|
baseline.write_text("\n".join(canonical["hashes"]) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
summary = {"runs": rows, "groups": groups,
|
||||||
|
"owner4_all_four_strata": "owner4/surface-world",
|
||||||
|
"owner2_canonical_pins": ["owner2/surface-world",
|
||||||
|
"owner2/floating-islands"]}
|
||||||
|
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Five interleaved L80 readiness pairs comparing L109 and L112 at W8 and W12."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l112/timing"
|
||||||
|
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
BINARIES = {
|
||||||
|
"base": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
|
||||||
|
"candidate": ROOT / "Saved/Linux/l112/bin-clang/VoxelForgeCoreHeadless-l112-clang",
|
||||||
|
}
|
||||||
|
CAMERAS = {
|
||||||
|
0: (8.0, 16.0, -372.2626953125),
|
||||||
|
1: (25.8369140625, 3.19921875, -737.0),
|
||||||
|
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||||
|
3: (-20.0625, -5.068359375, -900.353515625),
|
||||||
|
}
|
||||||
|
PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
|
||||||
|
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||||
|
NEAR = re.compile(
|
||||||
|
r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$",
|
||||||
|
re.M,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run(strate: int, workers: int, variant: str, phase: str, replicate: int) -> dict:
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
label = f"s{strate}-w{workers}-{variant}-{phase}{replicate}"
|
||||||
|
log = OUT / f"{label}.log"
|
||||||
|
camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1740s", str(BINARIES[variant]), str(DATA),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", "--max-level=8",
|
||||||
|
f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3",
|
||||||
|
]
|
||||||
|
start = time.monotonic()
|
||||||
|
with log.open("w", encoding="utf-8") as output:
|
||||||
|
result = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||||
|
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||||
|
content = log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
pipeline_match = PIPELINE.search(content)
|
||||||
|
near_match = NEAR.search(content)
|
||||||
|
if result.returncode or pipeline_match is None or near_match is None:
|
||||||
|
raise SystemExit(f"readiness run failed or trace missing: {log}; exit={result.returncode}")
|
||||||
|
pipeline = dict(FIELDS.findall(pipeline_match.group(1)))
|
||||||
|
row = {
|
||||||
|
"strate": strate, "workers": workers, "variant": variant,
|
||||||
|
"phase": phase, "replicate": replicate, "exit_code": result.returncode,
|
||||||
|
"wall_s": round(time.monotonic() - start, 3),
|
||||||
|
"lod02_ready_s": float(near_match.group(3)),
|
||||||
|
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
|
||||||
|
"whole_tiles": int(pipeline["whole_tiles"].split("/")[0]),
|
||||||
|
"log": str(log.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
print("[L112TimingRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if not DATA.is_file() or any(not binary.is_file() for binary in BINARIES.values()):
|
||||||
|
raise SystemExit("missing L109/L112 data or timing binary")
|
||||||
|
rows = []
|
||||||
|
for strate in range(4):
|
||||||
|
for workers in (8, 12):
|
||||||
|
run(strate, workers, "base", "warmup", 0)
|
||||||
|
run(strate, workers, "candidate", "warmup", 0)
|
||||||
|
for replicate in range(1, 6):
|
||||||
|
order = ("base", "candidate") if replicate % 2 else ("candidate", "base")
|
||||||
|
for variant in order:
|
||||||
|
rows.append(run(strate, workers, variant, "measured", replicate))
|
||||||
|
(OUT / "runs-partial.json").write_text(
|
||||||
|
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||||
|
summary = []
|
||||||
|
for strate in range(4):
|
||||||
|
for workers in (8, 12):
|
||||||
|
item = {"strate": strate, "workers": workers, "pairs": 5}
|
||||||
|
for variant in ("base", "candidate"):
|
||||||
|
group = [row for row in rows if row["strate"] == strate
|
||||||
|
and row["workers"] == workers and row["variant"] == variant]
|
||||||
|
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||||
|
values = [row[key] for row in group]
|
||||||
|
item[f"{variant}_{key}"] = values
|
||||||
|
item[f"{variant}_{key}_median"] = statistics.median(values)
|
||||||
|
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||||
|
base = item[f"base_{key}_median"]
|
||||||
|
candidate = item[f"candidate_{key}_median"]
|
||||||
|
item[f"{key}_delta_pct"] = round(100 * (candidate / base - 1), 2)
|
||||||
|
summary.append(item)
|
||||||
|
(OUT / "summary.json").write_text(
|
||||||
|
json.dumps({"runs": rows, "summary": summary}, indent=2) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# L112 passage captures
|
||||||
|
|
||||||
|
Each PNG was rendered from its own exported mesh. The renderer reported visible triangles, and each decoded image met the pixel coverage floor. `low_contrast_geometry_visible` marks images that met this floor below the preferred contrast target.
|
||||||
|
|
||||||
|
## Before
|
||||||
|
|
||||||
|
- [upper-mouth](before/passage-0-1-0-upper-mouth.png) — passage/0-1/0; 253282 visible triangles
|
||||||
|
- [lower-mouth](before/passage-0-1-0-lower-mouth.png) — passage/0-1/0; 559326 visible triangles
|
||||||
|
- [25%](before/passage-0-1-0-station-25.png) — passage/0-1/0; 145630 visible triangles
|
||||||
|
- [50%](before/passage-0-1-0-station-50.png) — passage/0-1/0; 430386 visible triangles
|
||||||
|
- [75%](before/passage-0-1-0-station-75.png) — passage/0-1/0; 294374 visible triangles
|
||||||
|
- [section-1/view-1](before/passage-0-1-0-openair-1-view-1.png) — passage/0-1/0 **OPEN AIR**; 289074 visible triangles
|
||||||
|
- [section-1/view-2](before/passage-0-1-0-openair-1-view-2.png) — passage/0-1/0 **OPEN AIR**; 298057 visible triangles
|
||||||
|
- [section-1/view-3](before/passage-0-1-0-openair-1-view-3.png) — passage/0-1/0 **OPEN AIR**; 322309 visible triangles
|
||||||
|
- [section-1/4m-outside](before/passage-0-1-0-openair-1-4m-outside.png) — passage/0-1/0; 289074 visible triangles
|
||||||
|
- [section-1/4m-interior-wall](before/passage-0-1-0-openair-1-4m-interior-wall.png) — passage/0-1/0; 322309 visible triangles
|
||||||
|
- [section-2/view-1](before/passage-0-1-0-openair-2-view-1.png) — passage/0-1/0 **OPEN AIR**; 283642 visible triangles
|
||||||
|
- [section-2/view-2](before/passage-0-1-0-openair-2-view-2.png) — passage/0-1/0 **OPEN AIR**; 316771 visible triangles
|
||||||
|
- [section-2/view-3](before/passage-0-1-0-openair-2-view-3.png) — passage/0-1/0 **OPEN AIR**; 322309 visible triangles
|
||||||
|
- [section-2/4m-outside](before/passage-0-1-0-openair-2-4m-outside.png) — passage/0-1/0; 283642 visible triangles
|
||||||
|
- [section-2/4m-interior-wall](before/passage-0-1-0-openair-2-4m-interior-wall.png) — passage/0-1/0; 363071 visible triangles
|
||||||
|
- [section-3/view-1](before/passage-0-1-0-openair-3-view-1.png) — passage/0-1/0 **OPEN AIR**; 262299 visible triangles
|
||||||
|
- [section-3/view-2](before/passage-0-1-0-openair-3-view-2.png) — passage/0-1/0 **OPEN AIR**; 310919 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-3/view-3](before/passage-0-1-0-openair-3-view-3.png) — passage/0-1/0 **OPEN AIR**; 363071 visible triangles
|
||||||
|
- [section-3/4m-outside](before/passage-0-1-0-openair-3-4m-outside.png) — passage/0-1/0; 298863 visible triangles
|
||||||
|
- [section-3/4m-interior-wall](before/passage-0-1-0-openair-3-4m-interior-wall.png) — passage/0-1/0; 340799 visible triangles
|
||||||
|
- [section-4/view-1](before/passage-0-1-0-openair-4-view-1.png) — passage/0-1/0 **OPEN AIR**; 378007 visible triangles
|
||||||
|
- [section-4/view-2](before/passage-0-1-0-openair-4-view-2.png) — passage/0-1/0 **OPEN AIR**; 471132 visible triangles
|
||||||
|
- [section-4/view-3](before/passage-0-1-0-openair-4-view-3.png) — passage/0-1/0 **OPEN AIR**; 382187 visible triangles
|
||||||
|
- [section-4/4m-outside](before/passage-0-1-0-openair-4-4m-outside.png) — passage/0-1/0; 309174 visible triangles
|
||||||
|
- [section-4/4m-interior-wall](before/passage-0-1-0-openair-4-4m-interior-wall.png) — passage/0-1/0; 382187 visible triangles
|
||||||
|
- [upper-mouth](before/passage-1-2-0-upper-mouth.png) — passage/1-2/0; 570417 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [lower-mouth](before/passage-1-2-0-lower-mouth.png) — passage/1-2/0; 511318 visible triangles
|
||||||
|
- [25%](before/passage-1-2-0-station-25.png) — passage/1-2/0; 661032 visible triangles
|
||||||
|
- [50%](before/passage-1-2-0-station-50.png) — passage/1-2/0; 602996 visible triangles
|
||||||
|
- [75%](before/passage-1-2-0-station-75.png) — passage/1-2/0; 615520 visible triangles
|
||||||
|
- [section-1/view-1](before/passage-1-2-0-openair-1-view-1.png) — passage/1-2/0 **OPEN AIR**; 629052 visible triangles
|
||||||
|
- [section-1/view-2](before/passage-1-2-0-openair-1-view-2.png) — passage/1-2/0 **OPEN AIR**; 588414 visible triangles
|
||||||
|
- [section-1/view-3](before/passage-1-2-0-openair-1-view-3.png) — passage/1-2/0 **OPEN AIR**; 616234 visible triangles
|
||||||
|
- [section-1/4m-outside](before/passage-1-2-0-openair-1-4m-outside.png) — passage/1-2/0; 629052 visible triangles
|
||||||
|
- [section-1/4m-interior-wall](before/passage-1-2-0-openair-1-4m-interior-wall.png) — passage/1-2/0; 573086 visible triangles
|
||||||
|
- [upper-mouth](before/passage-2-3-0-upper-mouth.png) — passage/2-3/0; 59562 visible triangles
|
||||||
|
- [lower-mouth](before/passage-2-3-0-lower-mouth.png) — passage/2-3/0; 294522 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [25%](before/passage-2-3-0-station-25.png) — passage/2-3/0; 55484 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [50%](before/passage-2-3-0-station-50.png) — passage/2-3/0; 457754 visible triangles
|
||||||
|
- [75%](before/passage-2-3-0-station-75.png) — passage/2-3/0; 475706 visible triangles
|
||||||
|
- [section-1/view-1](before/passage-2-3-0-openair-1-view-1.png) — passage/2-3/0 **OPEN AIR**; 104612 visible triangles
|
||||||
|
- [section-1/view-2](before/passage-2-3-0-openair-1-view-2.png) — passage/2-3/0 **OPEN AIR**; 81078 visible triangles
|
||||||
|
- [section-1/view-3](before/passage-2-3-0-openair-1-view-3.png) — passage/2-3/0 **OPEN AIR**; 94962 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-1/4m-outside](before/passage-2-3-0-openair-1-4m-outside.png) — passage/2-3/0; 104612 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-1/4m-interior-wall](before/passage-2-3-0-openair-1-4m-interior-wall.png) — passage/2-3/0; 99334 visible triangles
|
||||||
|
- [section-2/view-1](before/passage-2-3-0-openair-2-view-1.png) — passage/2-3/0 **OPEN AIR**; 475688 visible triangles
|
||||||
|
- [section-2/view-2](before/passage-2-3-0-openair-2-view-2.png) — passage/2-3/0 **OPEN AIR**; 479134 visible triangles
|
||||||
|
- [section-2/view-3](before/passage-2-3-0-openair-2-view-3.png) — passage/2-3/0 **OPEN AIR**; 479134 visible triangles
|
||||||
|
- [section-2/4m-outside](before/passage-2-3-0-openair-2-4m-outside.png) — passage/2-3/0; 475688 visible triangles
|
||||||
|
- [section-2/4m-interior-wall](before/passage-2-3-0-openair-2-4m-interior-wall.png) — passage/2-3/0; 486364 visible triangles
|
||||||
|
- [section-3/view-1](before/passage-2-3-0-openair-3-view-1.png) — passage/2-3/0 **OPEN AIR**; 485216 visible triangles
|
||||||
|
- [section-3/view-2](before/passage-2-3-0-openair-3-view-2.png) — passage/2-3/0 **OPEN AIR**; 479198 visible triangles
|
||||||
|
- [section-3/view-3](before/passage-2-3-0-openair-3-view-3.png) — passage/2-3/0 **OPEN AIR**; 490436 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-3/4m-outside](before/passage-2-3-0-openair-3-4m-outside.png) — passage/2-3/0; 485216 visible triangles
|
||||||
|
- [section-3/4m-interior-wall](before/passage-2-3-0-openair-3-4m-interior-wall.png) — passage/2-3/0; 490436 visible triangles
|
||||||
|
- [section-4/view-1](before/passage-2-3-0-openair-4-view-1.png) — passage/2-3/0 **OPEN AIR**; 475298 visible triangles
|
||||||
|
- [section-4/view-2](before/passage-2-3-0-openair-4-view-2.png) — passage/2-3/0 **OPEN AIR**; 495856 visible triangles
|
||||||
|
- [section-4/view-3](before/passage-2-3-0-openair-4-view-3.png) — passage/2-3/0 **OPEN AIR**; 496236 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-4/4m-outside](before/passage-2-3-0-openair-4-4m-outside.png) — passage/2-3/0; 475298 visible triangles
|
||||||
|
- [section-4/4m-interior-wall](before/passage-2-3-0-openair-4-4m-interior-wall.png) — passage/2-3/0; 475316 visible triangles
|
||||||
|
|
||||||
|
## After
|
||||||
|
|
||||||
|
- [upper-mouth](after/passage-0-1-0-upper-mouth.png) — passage/0-1/0; 244874 visible triangles
|
||||||
|
- [lower-mouth](after/passage-0-1-0-lower-mouth.png) — passage/0-1/0; 551596 visible triangles
|
||||||
|
- [25%](after/passage-0-1-0-station-25.png) — passage/0-1/0; 135136 visible triangles
|
||||||
|
- [50%](after/passage-0-1-0-station-50.png) — passage/0-1/0; 316508 visible triangles
|
||||||
|
- [75%](after/passage-0-1-0-station-75.png) — passage/0-1/0; 257040 visible triangles
|
||||||
|
- [section-1/view-1](after/passage-0-1-0-openair-1-view-1.png) — passage/0-1/0 **OPEN AIR**; 278629 visible triangles
|
||||||
|
- [section-1/view-2](after/passage-0-1-0-openair-1-view-2.png) — passage/0-1/0 **OPEN AIR**; 287396 visible triangles
|
||||||
|
- [section-1/view-3](after/passage-0-1-0-openair-1-view-3.png) — passage/0-1/0 **OPEN AIR**; 311780 visible triangles
|
||||||
|
- [section-1/4m-outside](after/passage-0-1-0-openair-1-4m-outside.png) — passage/0-1/0; 278629 visible triangles
|
||||||
|
- [section-1/4m-interior-wall](after/passage-0-1-0-openair-1-4m-interior-wall.png) — passage/0-1/0; 311780 visible triangles
|
||||||
|
- [section-2/view-1](after/passage-0-1-0-openair-2-view-1.png) — passage/0-1/0 **OPEN AIR**; 278629 visible triangles
|
||||||
|
- [section-2/view-2](after/passage-0-1-0-openair-2-view-2.png) — passage/0-1/0 **OPEN AIR**; 311780 visible triangles
|
||||||
|
- [section-2/view-3](after/passage-0-1-0-openair-2-view-3.png) — passage/0-1/0 **OPEN AIR**; 311780 visible triangles
|
||||||
|
- [section-2/4m-outside](after/passage-0-1-0-openair-2-4m-outside.png) — passage/0-1/0; 278629 visible triangles
|
||||||
|
- [section-2/4m-interior-wall](after/passage-0-1-0-openair-2-4m-interior-wall.png) — passage/0-1/0; 350192 visible triangles
|
||||||
|
- [section-3/view-1](after/passage-0-1-0-openair-3-view-1.png) — passage/0-1/0 **OPEN AIR**; 210501 visible triangles
|
||||||
|
- [section-3/view-2](after/passage-0-1-0-openair-3-view-2.png) — passage/0-1/0 **OPEN AIR**; 189354 visible triangles
|
||||||
|
- [section-3/view-3](after/passage-0-1-0-openair-3-view-3.png) — passage/0-1/0 **OPEN AIR**; 189354 visible triangles
|
||||||
|
- [section-3/4m-outside](after/passage-0-1-0-openair-3-4m-outside.png) — passage/0-1/0; 156696 visible triangles
|
||||||
|
- [section-3/4m-interior-wall](after/passage-0-1-0-openair-3-4m-interior-wall.png) — passage/0-1/0; 156658 visible triangles
|
||||||
|
- [upper-mouth](after/passage-1-2-0-upper-mouth.png) — passage/1-2/0; 595932 visible triangles
|
||||||
|
- [lower-mouth](after/passage-1-2-0-lower-mouth.png) — passage/1-2/0; 546268 visible triangles
|
||||||
|
- [25%](after/passage-1-2-0-station-25.png) — passage/1-2/0; 655074 visible triangles
|
||||||
|
- [50%](after/passage-1-2-0-station-50.png) — passage/1-2/0; 604662 visible triangles
|
||||||
|
- [75%](after/passage-1-2-0-station-75.png) — passage/1-2/0; 612135 visible triangles
|
||||||
|
- [section-1/view-1](after/passage-1-2-0-openair-1-view-1.png) — passage/1-2/0 **OPEN AIR**; 623352 visible triangles
|
||||||
|
- [section-1/view-2](after/passage-1-2-0-openair-1-view-2.png) — passage/1-2/0 **OPEN AIR**; 576164 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-1/view-3](after/passage-1-2-0-openair-1-view-3.png) — passage/1-2/0 **OPEN AIR**; 570605 visible triangles
|
||||||
|
- [section-1/4m-outside](after/passage-1-2-0-openair-1-4m-outside.png) — passage/1-2/0; 623352 visible triangles
|
||||||
|
- [section-1/4m-interior-wall](after/passage-1-2-0-openair-1-4m-interior-wall.png) — passage/1-2/0; 612135 visible triangles
|
||||||
|
- [section-2/view-1](after/passage-1-2-0-openair-2-view-1.png) — passage/1-2/0 **OPEN AIR**; 628316 visible triangles
|
||||||
|
- [section-2/view-2](after/passage-1-2-0-openair-2-view-2.png) — passage/1-2/0 **OPEN AIR**; 585440 visible triangles
|
||||||
|
- [section-2/view-3](after/passage-1-2-0-openair-2-view-3.png) — passage/1-2/0 **OPEN AIR**; 613722 visible triangles
|
||||||
|
- [section-2/4m-outside](after/passage-1-2-0-openair-2-4m-outside.png) — passage/1-2/0; 585802 visible triangles
|
||||||
|
- [section-2/4m-interior-wall](after/passage-1-2-0-openair-2-4m-interior-wall.png) — passage/1-2/0; 574472 visible triangles
|
||||||
|
- [upper-mouth](after/passage-2-3-0-upper-mouth.png) — passage/2-3/0; 59568 visible triangles
|
||||||
|
- [lower-mouth](after/passage-2-3-0-lower-mouth.png) — passage/2-3/0; 294004 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [25%](after/passage-2-3-0-station-25.png) — passage/2-3/0; 55514 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [50%](after/passage-2-3-0-station-50.png) — passage/2-3/0; 458230 visible triangles
|
||||||
|
- [75%](after/passage-2-3-0-station-75.png) — passage/2-3/0; 475838 visible triangles
|
||||||
|
- [section-1/view-1](after/passage-2-3-0-openair-1-view-1.png) — passage/2-3/0 **OPEN AIR**; 104612 visible triangles
|
||||||
|
- [section-1/view-2](after/passage-2-3-0-openair-1-view-2.png) — passage/2-3/0 **OPEN AIR**; 76426 visible triangles
|
||||||
|
- [section-1/view-3](after/passage-2-3-0-openair-1-view-3.png) — passage/2-3/0 **OPEN AIR**; 94962 visible triangles
|
||||||
|
- [section-1/4m-outside](after/passage-2-3-0-openair-1-4m-outside.png) — passage/2-3/0; 104612 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-1/4m-interior-wall](after/passage-2-3-0-openair-1-4m-interior-wall.png) — passage/2-3/0; 94962 visible triangles; low_contrast_geometry_visible
|
||||||
|
- [section-2/view-1](after/passage-2-3-0-openair-2-view-1.png) — passage/2-3/0 **OPEN AIR**; 476214 visible triangles
|
||||||
|
- [section-2/view-2](after/passage-2-3-0-openair-2-view-2.png) — passage/2-3/0 **OPEN AIR**; 479366 visible triangles
|
||||||
|
- [section-2/view-3](after/passage-2-3-0-openair-2-view-3.png) — passage/2-3/0 **OPEN AIR**; 479366 visible triangles
|
||||||
|
- [section-2/4m-outside](after/passage-2-3-0-openair-2-4m-outside.png) — passage/2-3/0; 476214 visible triangles
|
||||||
|
- [section-2/4m-interior-wall](after/passage-2-3-0-openair-2-4m-interior-wall.png) — passage/2-3/0; 486990 visible triangles
|
||||||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 356 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 374 KiB |
|
After Width: | Height: | Size: 426 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 461 KiB |
|
After Width: | Height: | Size: 475 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 507 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 289 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 538 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 224 KiB |