wip(assembler): round S3 -- far ground from the native heightfield; LOD7/8 tiles 36-66% cheaper

Far-shell repair (81-96% of S2's LOD7/8 tile time) is skipped for certified
SurfaceWorld tiles; ground and cap come from the shared planar heightfield.
Near LOD0-3 hashes equal S2; far recipe version 1 salts LOD5+ identities.
Cold first view 6.6 -> 3.4 s (aim 1.57 s). Not a visual sign-off: distant cap
shows floating pieces, sprint streaming arrives late.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-18 21:29:52 +02:00
1 parent 6ddb834b1c
commit d0f97bf79a
26 files changed
+976 -30

No files matched your search

+31
View File
@@ -1364,6 +1364,37 @@ Validation compares complete placement transforms and canonical anchor residuals
clears an isolated populated HISM fixture under BuildHost null RHI. These CPU costs do not measure
rendering or GPU upload. See the Round 6 report for results and limits.
### 8.24 Shared native far horizon — World Assembler M3 Round 7
The shared Core tile generator reads each family declaration from the immutable assembly snapshot.
`WorldAssemblyPlanV1::strates` declares SurfaceWorld ground and cap profiles through `Heightfield2D`;
`floatingIslands` declares bounded 3D features and has no planar ground/cap datum. This keeps the
horizon rule in the shared worker path while allowing each family to provide the shape it can prove.
At LOD5 and above, an unedited SurfaceWorld tile builds its ground from the native heightfield on
a globally aligned 128-voxel grid, using the tile step below 128. The cap remains on the 128-voxel
grid so adjacent far levels share samples. Ground and cap are clipped to the tile-Z bounds and to
`cap >= ground`; shared ground vertices replace nearby upward-facing coarse MC ground triangles,
while existing ceiling seam skirts remain. At LOD7/8, a tile that intersects a native SurfaceWorld
profile but intersects no island plan skips the derived stratum/connector repair
shells: the exact native ground and cap already provide its exterior closure. Player-edited tiles
keep the real edited field and repair path at every visible LOD. Islands-only and mixed island tiles
keep the shared 3D field and far-shell path.
For the 3D connector repair path, LOD7/8 use the same 256-voxel axial station/ring spacing. LOD6 and
nearer far-shell work retain 128-voxel stations. This reduces source-field probes while keeping a
continuous repair mesh at the resolution used by the far tile. It does not alter canonical density
arithmetic, feature order, water datum, `Epoch`, `ModsLock`, task ownership, collision policy, or the
Core-off legacy route. All horizon work runs on the resolved immutable worker snapshot.
`TileGenerationStats` exposes classification, XY column, MC meshing, shell and horizon times, repair
field calls, skipped repairs and replaced ground triangles. `farHorizonVersion` is part of the tile
generation recipe and salts far mesh, payload and topology hashes at LOD5+; it does not salt density
or LOD0–LOD3 identities. Increase it when changing far geometry again. Round 7's staged owner checks
measure eight warmed keys per LOD and compare the shared LOD7/8 surface lattice against exact near-field
density roots, including a 100 cm datum-offset negative control. See
`Standalone/Research/WorldAssemblerM3/Round7/REPORT.md` for results and the visual evidence limits.
## 9. Multiplayer model (listen-server first, dedicated-friendly)
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
+13
View File
@@ -966,6 +966,19 @@ Hard, and Interleaved transitions produce the same Core probe hash and are not f
See the [round-4 coverage table and small matched captures](Standalone/UNREAL_INTEGRATION_REPORT.md)
and [probe artifacts](Standalone/Research/OwnerShapeWorld/core_strate_coverage/).
World Assembler M3 Round 7 adds a shared far-horizon path. `WorldAssemblyPlanV1::strates` is the
SurfaceWorld declaration: `World::generateTile` evaluates native ground on a globally aligned grid
with step min(tile step, 128 voxels), and cap on the shared 128-voxel grid. It clips both surfaces
to the tile and to `cap >= ground`, and replaces nearby upward coarse-MC ground faces. An unedited
SurfaceWorld tile at LOD7/8 that intersects a native surface plan and no island plan uses those
profile surfaces instead of derived far stratum/connector repairs. Tiles with edits or intersecting
FloatingIslands plans keep the canonical field and repairs. FloatingIslands keeps the shared 3D field
and repair path, with a common 256-voxel axial station/ring grid at LOD7/8; closer LODs retain
128-voxel stations. `TileGenerationStats` reports classification, column, meshing, shell and horizon
time, repair field calls, skipped repairs and replaced triangles. `farHorizonVersion` salts far
mesh/payload/topology hashes only. The BuildHost commandlet owns warmed eight-key timings and the
exact-density-root LOD7/8 agreement audit with its datum-offset negative control.
## 4. The density pipeline (most-edited hot path)
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
@@ -15147,14 +15147,244 @@ bool RunCoreOffActorTest(const FExploreArguments& Arguments, FString& OutError)
return true;
}
bool RunCoreFarHorizonAgreementAudit(VoxelForgeCore::World& World,
const std::vector<VoxelForgeCore::TileKey>& Keys,
const VoxelForgeCore::WorldAssemblyManifest& Assembly,FString& OutError)
{
constexpr std::int64_t SubvoxelsPerVoxel=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
constexpr double ToleranceCm=2.0;
constexpr double NegativeDatumOffsetCm=100.0;
const double VoxelSizeCm=static_cast<double>(World.configuration().voxelSizeMicrons)/10000.0;
using FXYKey=std::pair<std::int64_t,std::int64_t>;
struct FGroundVertices { std::map<FXYKey,std::vector<std::int64_t>> ByXY; };
const auto GatherGroundVertices=[&](const VoxelForgeCore::TileMesh& Mesh)
{
FGroundVertices Out;
constexpr std::int64_t GridPeriod=128*SubvoxelsPerVoxel;
for(const VoxelForgeCore::MeshVertex& Vertex:Mesh.vertices)
{
if(Vertex.normal.z<0.35)continue;
const auto X=static_cast<std::int64_t>(std::llround(
Vertex.position.x/VoxelSizeCm*SubvoxelsPerVoxel));
const auto Y=static_cast<std::int64_t>(std::llround(
Vertex.position.y/VoxelSizeCm*SubvoxelsPerVoxel));
if(X%GridPeriod!=0||Y%GridPeriod!=0)continue;
const auto Z=static_cast<std::int64_t>(std::llround(
Vertex.position.z/VoxelSizeCm*SubvoxelsPerVoxel));
Out.ByXY[{X,Y}].push_back(Z);
}
return Out;
};
const auto TileBounds=[](const VoxelForgeCore::TileKey& Key)
{
const std::int64_t Extent=static_cast<std::int64_t>(32)<<Key.lod;
const std::int64_t X=static_cast<std::int64_t>(Key.coordinate.x)*Extent;
const std::int64_t Y=static_cast<std::int64_t>(Key.coordinate.y)*Extent;
const std::int64_t Z=static_cast<std::int64_t>(Key.coordinate.z)*Extent;
return std::array<std::int64_t,6>{X,Y,Z,X+Extent,Y+Extent,Z+Extent};
};
struct FSample { std::int64_t X=0,Y=0,ProfileGround=0,Far7=0,Far8=0; };
std::vector<FSample> Samples;
std::string CoreError;
VoxelForgeCore::TileGenerationOptions Options;
Options.hashDensity=true;
for(const auto& Key7:Keys)
{
if(Key7.lod!=7)continue;
const auto Bounds7=TileBounds(Key7);
for(const auto& Key8:Keys)
{
if(Key8.lod!=8)continue;
const auto Bounds8=TileBounds(Key8);
bool bTouches=true;
for(int Axis=0;Axis<3;++Axis)
bTouches=bTouches&&Bounds7[Axis]<=Bounds8[Axis+3]&&Bounds8[Axis]<=Bounds7[Axis+3];
if(!bTouches)continue;
const auto Mesh7=World.generateTile(Key7,Options,CoreError);
const auto Mesh8=World.generateTile(Key8,Options,CoreError);
if(!Mesh7||!Mesh8)
{
OutError=FString::Printf(TEXT("Far-horizon agreement tile generation failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
const FGroundVertices Ground7=GatherGroundVertices(*Mesh7);
const FGroundVertices Ground8=GatherGroundVertices(*Mesh8);
std::vector<FSample> PairSamples;
for(const auto& Plan:Assembly.plan.strates)
{
const VoxelForgeCore::Heightfield2D Field(Assembly.recipe,Plan);
if(!Field.isValid())continue;
for(const auto& Pair:Ground7.ByXY)
{
const auto Other=Ground8.ByXY.find(Pair.first);
if(Other==Ground8.ByXY.end())continue;
const auto Column=Field.point(Pair.first.first,Pair.first.second,0);
const std::int64_t Ground=Column.groundHeightSubvoxels;
const std::int64_t Cap=Column.capHeightSubvoxels;
if(Ground>=Cap)continue;
const double GroundVoxel=static_cast<double>(Ground)/SubvoxelsPerVoxel;
if(GroundVoxel<Bounds7[2]||GroundVoxel>Bounds7[5]
||GroundVoxel<Bounds8[2]||GroundVoxel>Bounds8[5])continue;
const auto Nearest=[Ground](const std::vector<std::int64_t>& Values)
->std::optional<std::int64_t>
{
if(Values.empty())return std::nullopt;
return *std::min_element(Values.begin(),Values.end(),[Ground](auto A,auto B)
{return std::abs(A-Ground)<std::abs(B-Ground);});
};
const auto Z7=Nearest(Pair.second),Z8=Nearest(Other->second);
if(Z7&&Z8)PairSamples.push_back({Pair.first.first,Pair.first.second,Ground,*Z7,*Z8});
}
if(PairSamples.size()>=64)break;
}
if(PairSamples.size()>Samples.size())Samples=std::move(PairSamples);
if(Samples.size()>=64)break;
}
if(Samples.size()>=64)break;
}
bool bUsedWalkRouteGrid=false;
if(Samples.size()<64)
{
Samples.clear();
for(const auto& Plan:Assembly.plan.strates)
{
const VoxelForgeCore::Heightfield2D Field(Assembly.recipe,Plan);
if(!Field.isValid())continue;
std::int64_t RouteXSub=0,RouteYSub=0;
if(!Plan.route.empty())
{
RouteXSub=Plan.route.front().positionSubvoxels[0];
RouteYSub=Plan.route.front().positionSubvoxels[1];
}
constexpr std::int64_t GridPeriod=128*SubvoxelsPerVoxel;
const std::int64_t XSub=static_cast<std::int64_t>(std::llround(
static_cast<double>(RouteXSub)/GridPeriod))*GridPeriod;
const std::int64_t YSub=static_cast<std::int64_t>(std::llround(
static_cast<double>(RouteYSub)/GridPeriod))*GridPeriod;
const auto WalkColumn=Field.point(XSub,YSub,0);
if(WalkColumn.groundHeightSubvoxels>=WalkColumn.capHeightSubvoxels)continue;
const std::int64_t XVoxel=XSub/SubvoxelsPerVoxel,YVoxel=YSub/SubvoxelsPerVoxel;
const std::int64_t WalkGroundVoxel=WalkColumn.groundHeightSubvoxels/SubvoxelsPerVoxel;
const auto MakeKey=[](std::int64_t X,std::int64_t Y,std::int64_t Z,std::int64_t Extent,int32 LOD)
{
VoxelForgeCore::TileKey Key;
Key.coordinate={static_cast<int32>(std::floor(static_cast<double>(X)/Extent)),
static_cast<int32>(std::floor(static_cast<double>(Y)/Extent)),
static_cast<int32>(std::floor(static_cast<double>(Z)/Extent))};
Key.lod=LOD;
return Key;
};
const VoxelForgeCore::TileKey Key7=MakeKey(XVoxel,YVoxel,WalkGroundVoxel,4096,7);
const VoxelForgeCore::TileKey Key8=MakeKey(XVoxel,YVoxel,WalkGroundVoxel,8192,8);
const auto Mesh7=World.generateTile(Key7,Options,CoreError);
const auto Mesh8=World.generateTile(Key8,Options,CoreError);
if(!Mesh7||!Mesh8)
{
OutError=FString::Printf(TEXT("Walk-route far-horizon tile generation failed: %s"),
UTF8_TO_TCHAR(CoreError.c_str()));
return false;
}
const FGroundVertices Ground7=GatherGroundVertices(*Mesh7);
const FGroundVertices Ground8=GatherGroundVertices(*Mesh8);
for(const auto& Pair:Ground7.ByXY)
{
const auto Other=Ground8.ByXY.find(Pair.first);
if(Other==Ground8.ByXY.end())continue;
const auto Column=Field.point(Pair.first.first,Pair.first.second,0);
const std::int64_t Ground=Column.groundHeightSubvoxels;
if(Ground>=Column.capHeightSubvoxels)continue;
const auto Bounds7=TileBounds(Key7),Bounds8=TileBounds(Key8);
const double GroundVoxel=static_cast<double>(Ground)/SubvoxelsPerVoxel;
if(GroundVoxel<Bounds7[2]||GroundVoxel>Bounds7[5]
||GroundVoxel<Bounds8[2]||GroundVoxel>Bounds8[5])continue;
const auto Nearest=[Ground](const std::vector<std::int64_t>& Values)
->std::optional<std::int64_t>
{
if(Values.empty())return std::nullopt;
return *std::min_element(Values.begin(),Values.end(),[Ground](auto A,auto B)
{return std::abs(A-Ground)<std::abs(B-Ground);});
};
const auto Z7=Nearest(Pair.second),Z8=Nearest(Other->second);
if(Z7&&Z8)
Samples.push_back({Pair.first.first,Pair.first.second,Ground,*Z7,*Z8});
if(Samples.size()>=256)break;
}
if(Samples.size()>=64){bUsedWalkRouteGrid=true;break;}
Samples.clear();
}
}
if(Samples.size()<64)
{OutError=TEXT("Far-horizon agreement audit found fewer than 64 shared LOD7/8 surface points around the staged walk route.");return false;}
const auto FindNearRoot=[&](std::int64_t XSub,std::int64_t YSub,double GroundVoxel,double& OutVoxel)
{
const double X=static_cast<double>(XSub)/SubvoxelsPerVoxel;
const double Y=static_cast<double>(YSub)/SubvoxelsPerVoxel;
for(double Radius=4.0;Radius<=4096.0;Radius*=2.0)
{
double Lo=GroundVoxel-Radius,Hi=GroundVoxel+Radius;
if(!(World.densityAt({X,Y,Lo})<0.0f&&World.densityAt({X,Y,Hi})>=0.0f))continue;
for(int Iteration=0;Iteration<14;++Iteration)
{
const double Mid=(Lo+Hi)*0.5;
if(World.densityAt({X,Y,Mid})<0.0f)Lo=Mid;else Hi=Mid;
}
OutVoxel=(Lo+Hi)*0.5;
return true;
}
return false;
};
double MaximumFar7ErrorCm=0.0,MaximumFar8ErrorCm=0.0,MaximumCrossLodErrorCm=0.0;
double MaximumNegativeControlErrorCm=0.0;
std::size_t Compared=0;
const std::size_t SampleStride=std::max<std::size_t>(1,(Samples.size()+127)/128);
for(std::size_t Index=0;Index<Samples.size();Index+=SampleStride)
{
const auto& Sample=Samples[Index];
double NearVoxel=0.0;
if(!FindNearRoot(Sample.X,Sample.Y,
static_cast<double>(Sample.ProfileGround)/SubvoxelsPerVoxel,NearVoxel))continue;
const double Far7Cm=static_cast<double>(Sample.Far7)/SubvoxelsPerVoxel*VoxelSizeCm;
const double Far8Cm=static_cast<double>(Sample.Far8)/SubvoxelsPerVoxel*VoxelSizeCm;
const double NearCm=NearVoxel*VoxelSizeCm;
MaximumFar7ErrorCm=std::max(MaximumFar7ErrorCm,std::abs(Far7Cm-NearCm));
MaximumFar8ErrorCm=std::max(MaximumFar8ErrorCm,std::abs(Far8Cm-NearCm));
MaximumCrossLodErrorCm=std::max(MaximumCrossLodErrorCm,std::abs(Far7Cm-Far8Cm));
MaximumNegativeControlErrorCm=std::max(MaximumNegativeControlErrorCm,
std::abs((Far8Cm+NegativeDatumOffsetCm)-NearCm));
++Compared;
}
const bool bNegativeControlDetected=Compared>0&&MaximumNegativeControlErrorCm>ToleranceCm;
if(Compared<32||MaximumFar7ErrorCm>ToleranceCm||MaximumFar8ErrorCm>ToleranceCm
||MaximumCrossLodErrorCm>ToleranceCm||!bNegativeControlDetected)
{
OutError=FString::Printf(TEXT("Far-horizon agreement failed: points=%llu far7_cm=%.4f far8_cm=%.4f cross_lod_cm=%.4f tolerance_cm=%.2f negative_control_cm=%.4f detected=%d."),
static_cast<unsigned long long>(Compared),MaximumFar7ErrorCm,MaximumFar8ErrorCm,
MaximumCrossLodErrorCm,ToleranceCm,MaximumNegativeControlErrorCm,bNegativeControlDetected?1:0);
return false;
}
UE_LOG(LogTemp,Display,
TEXT("[VoxelForgeFarHorizonAgreement] status=PASS lod_pair=7/8 shared_points=%llu source=%s max_far7_near_cm=%.4f max_far8_near_cm=%.4f max_cross_lod_cm=%.4f tolerance_cm=%.2f near_source=exact_density_root negative_control=100cm_datum_offset max_error_cm=%.4f detected=1"),
static_cast<unsigned long long>(Compared),
bUsedWalkRouteGrid?TEXT("staged_walk_route_grid"):TEXT("desired_tile_overlap"),
MaximumFar7ErrorCm,MaximumFar8ErrorCm,MaximumCrossLodErrorCm,
ToleranceCm,MaximumNegativeControlErrorCm);
return true;
}
// Paired engine timings include preparation and derived shells, with canonical output hashes.
// Mesures moteur appariees : preparation et coques derivees incluses, avec hashes canoniques.
bool RunCoreSharedFeatureCostAudit(VoxelForgeCore::World& World,
const std::vector<VoxelForgeCore::TileKey>& Keys,const TCHAR* Label,FString& OutError)
const std::vector<VoxelForgeCore::TileKey>& Keys,const TCHAR* Label,
const VoxelForgeCore::WorldAssemblyManifest* Assembly,FString& OutError)
{
for (const int32 LOD : {0,1,7,8})
{
TArray<double> FullTimes,CandidateTimes,FullShellTimes,CandidateShellTimes,ListTimes;
TArray<double> UnknownBoxes,FieldCalls,ColumnMisses,StratumShellTimes,ConnectorShellTimes;
TArray<double> ClassificationTimes,ColumnComputeTimes,MeshingTimes,HorizonTimes,RepairSkipped;
TArray<double> StratumFieldCalls,ConnectorFieldCalls,ShellTriangles,GroundTriangles,GroundRemoved,CapTriangles;
for (const auto& Key : Keys)
{
if (Key.lod!=LOD || FullTimes.Num()>=8) continue;
@@ -15182,15 +15412,37 @@ bool RunCoreSharedFeatureCostAudit(VoxelForgeCore::World& World,
FullShellTimes.Add(Full->diagnostics.farShellMilliseconds);
CandidateShellTimes.Add(Candidate->diagnostics.farShellMilliseconds);
ListTimes.Add(Candidate->diagnostics.featureListMilliseconds);
UnknownBoxes.Add(static_cast<double>(Full->diagnostics.unknownBoxes));
FieldCalls.Add(static_cast<double>(Full->diagnostics.fieldCalls));
ColumnMisses.Add(static_cast<double>(Full->diagnostics.columnCacheMisses));
ClassificationTimes.Add(Full->diagnostics.classificationMilliseconds);
ColumnComputeTimes.Add(Full->diagnostics.columnComputeMilliseconds);
MeshingTimes.Add(Full->diagnostics.meshingMilliseconds);
HorizonTimes.Add(Full->diagnostics.farHorizonMilliseconds);
RepairSkipped.Add(Full->diagnostics.farShellRepairSkipped?1.0:0.0);
StratumShellTimes.Add(Full->diagnostics.farStratumShellMilliseconds);
ConnectorShellTimes.Add(Full->diagnostics.farConnectorShellMilliseconds);
StratumFieldCalls.Add(static_cast<double>(Full->diagnostics.farStratumFieldCalls));
ConnectorFieldCalls.Add(static_cast<double>(Full->diagnostics.farConnectorFieldCalls));
ShellTriangles.Add(static_cast<double>(Full->diagnostics.farShellTrianglesAdded));
GroundTriangles.Add(static_cast<double>(Full->diagnostics.horizonGroundTriangles));
GroundRemoved.Add(static_cast<double>(Full->diagnostics.horizonGroundTrianglesRemoved));
CapTriangles.Add(static_cast<double>(Full->diagnostics.horizonCapTriangles));
}
const auto P95=[](TArray<double> Values)
{ Values.Sort(); return Values.IsEmpty() ? 0.0 : Values[FMath::Clamp(
FMath::CeilToInt(Values.Num()*0.95f)-1,0,Values.Num()-1)]; };
UE_LOG(LogTemp,Display,
TEXT("[VoxelForgeSharedFeatureCost] label=%s lod=%d tiles=%d hashes_identical=1 full_p95_ms=%.5f candidate_p95_ms=%.5f list_p95_ms=%.5f full_shell_p95_ms=%.5f candidate_shell_p95_ms=%.5f"),
TEXT("[VoxelForgeSharedFeatureCost] label=%s lod=%d tiles=%d hashes_identical=1 full_p95_ms=%.5f candidate_p95_ms=%.5f list_p95_ms=%.5f classification_p95_ms=%.5f column_compute_p95_ms=%.5f meshing_p95_ms=%.5f horizon_p95_ms=%.5f full_shell_p95_ms=%.5f candidate_shell_p95_ms=%.5f repair_skipped_p95=%.0f unknown_boxes_p95=%.0f field_calls_p95=%.0f column_misses_p95=%.0f stratum_shell_p95_ms=%.5f stratum_field_calls_p95=%.0f connector_shell_p95_ms=%.5f connector_field_calls_p95=%.0f shell_triangles_p95=%.0f horizon_ground_triangles_p95=%.0f horizon_ground_removed_p95=%.0f horizon_cap_triangles_p95=%.0f"),
Label,LOD,FullTimes.Num(),P95(FullTimes),P95(CandidateTimes),P95(ListTimes),
P95(FullShellTimes),P95(CandidateShellTimes));
P95(ClassificationTimes),P95(ColumnComputeTimes),P95(MeshingTimes),P95(HorizonTimes),
P95(FullShellTimes),P95(CandidateShellTimes),P95(RepairSkipped),P95(UnknownBoxes),P95(FieldCalls),
P95(ColumnMisses),P95(StratumShellTimes),P95(StratumFieldCalls),
P95(ConnectorShellTimes),P95(ConnectorFieldCalls),P95(ShellTriangles),
P95(GroundTriangles),P95(GroundRemoved),P95(CapTriangles));
}
if(Assembly!=nullptr&&!Assembly->plan.strates.empty()
&&!RunCoreFarHorizonAgreementAudit(World,Keys,*Assembly,OutError))return false;
return true;
}
@@ -15429,7 +15681,8 @@ bool RunCoreFloatingIslandActorIntegrationTest(
return false;
}
ReferenceWorld.retainTileRegions(Desired);
if (!RunCoreSharedFeatureCostAudit(ReferenceWorld,Desired,TEXT("islands"),OutError)) return false;
if (!RunCoreSharedFeatureCostAudit(ReferenceWorld,Desired,TEXT("islands"),
&NativeAssembly,OutError)) return false;
VoxelForgeCore::TileGenerationOptions Options;
Options.hashDensity = true;
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
@@ -16838,7 +17091,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
return true;
}
if (!RunCoreSharedFeatureCostAudit(ReferenceWorld,Desired,TEXT("surface"),OutError)) return false;
if (!RunCoreSharedFeatureCostAudit(ReferenceWorld,Desired,TEXT("surface"),
Assembly,OutError)) return false;
if (FParse::Param(FCommandLine::Get(),TEXT("corefeatureauditonly")))
{
UE_LOG(LogTemp,Display,TEXT("[VoxelForgeSharedFeatureCost] pass complete=1 warmed_routes=1"));
@@ -128,6 +128,17 @@ namespace VoxelForgeCore
double meshingMilliseconds = 0.0;
double featureListMilliseconds = 0.0;
double farShellMilliseconds = 0.0;
double farStratumShellMilliseconds = 0.0;
double farConnectorShellMilliseconds = 0.0;
double farHorizonMilliseconds = 0.0;
std::uint64_t farStratumFieldCalls = 0;
std::uint64_t farConnectorFieldCalls = 0;
std::uint64_t farShellTrianglesAdded = 0;
std::uint64_t horizonGroundTriangles = 0;
std::uint64_t horizonGroundTrianglesRemoved = 0;
std::uint64_t horizonCapTriangles = 0;
std::uint32_t farHorizonVersion = 0;
bool farShellRepairSkipped = false;
bool tileWasClassified = false;
bool meshingSkipped = false;
bool featureRefinedResolution = false;
@@ -984,6 +995,11 @@ namespace VoxelForgeCore
// Validation control only; never participates in recipe/world identity.
// Controle de validation seulement, sans effet sur l'identite du monde.
bool useAssemblyFeatureQueries = true;
// Bump when the far-horizon mesh representation changes. Far tile mesh hashes are
// salted with this version; density and LOD0-3 hashes are intentionally unchanged.
// Incremente quand la representation d'horizon lointain change; les hashes de maillage
// lointain incluent cette version, sans toucher a la densite ni aux LOD0-3.
std::uint32_t farHorizonVersion = 1;
};
class VFCORE_API World
+69 -8
View File
@@ -311,7 +311,10 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
if(TMin>TMax){bIntersects=false;break;}
}
if(!bIntersects||TMax-TMin<1.0e-6)continue;
const int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/128.0)));
// LOD7 and LOD8 share a 256-voxel connector station grid; near LOD keeps 128-voxel checks.
// LOD7 et LOD8 partagent des stations aux 256 voxels; pres du joueur, garder les pas de 128.
const double ConnectorStationStep=Key.Level>=7?std::max(256.0,static_cast<double>(Step)):128.0;
const int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/ConnectorStationStep)));
std::vector<uint8> MissingMasks(static_cast<size_t>(AxialSampleCount),0);
std::vector<std::array<double,4>> SourceRadii(static_cast<size_t>(AxialSampleCount));
for(auto& Radii:SourceRadii)Radii.fill(0.0);
@@ -429,7 +432,7 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
}
}
std::vector<FRing> Rings;
const double MaxRingStep=128.0;
const double MaxRingStep=Key.Level>=7?std::max(256.0,static_cast<double>(Step)):128.0;
double CurrentT=RunMin;
Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask));
while(CurrentT<RunMax-1.0e-7)
@@ -901,6 +904,67 @@ inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld&
return Crossings;
}
inline bool FindNearestFarStratumCrossing(const FStandaloneShapeWorld& Shape,
double X,double Y,double ZMin,double ZMax,double Prediction,double& OutCrossing)
{
if(ZMax<=ZMin||!std::isfinite(Prediction))return false;
const int32 Center=std::clamp(static_cast<int32>(std::floor(Prediction)),
static_cast<int32>(std::ceil(ZMin)),static_cast<int32>(std::floor(ZMax)));
double PreviousLeft=Center,PreviousRight=Center;
bool PreviousLeftSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(PreviousLeft))<0.0f;
bool PreviousRightSolid=PreviousLeftSolid;
const int32 MaximumRadius=std::max(Center-static_cast<int32>(std::floor(ZMin)),
static_cast<int32>(std::ceil(ZMax))-Center)+1;
for(int32 Radius=1;Radius<=MaximumRadius;++Radius)
{
bool bFound=false;
double Best=0.0,BestDistance=std::numeric_limits<double>::infinity();
const auto CheckBracket=[&](double A,double B,bool ASolid)
{
double Lo=std::min(A,B),Hi=std::max(A,B);
for(int32 I=0;I<6;++I)
{
const double Mid=(Lo+Hi)*0.5;
if((Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Mid))<0.0f)==ASolid)
{
if(A<B)Lo=Mid;else Hi=Mid;
}
else
{
if(A<B)Hi=Mid;else Lo=Mid;
}
}
const double Crossing=(Lo+Hi)*0.5;
const double Difference=std::abs(Crossing-Prediction);
if(Difference<BestDistance)
{Best=Crossing;BestDistance=Difference;bFound=true;}
};
const double Left=std::max(ZMin,static_cast<double>(Center-Radius));
if(Left<PreviousLeft-1.0e-9)
{
const bool LeftSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Left))<0.0f;
if(LeftSolid!=PreviousLeftSolid)CheckBracket(Left,PreviousLeft,LeftSolid);
PreviousLeft=Left;PreviousLeftSolid=LeftSolid;
}
const double Right=std::min(ZMax,static_cast<double>(Center+Radius));
if(Right>PreviousRight+1.0e-9)
{
const bool RightSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Right))<0.0f;
if(RightSolid!=PreviousRightSolid)CheckBracket(PreviousRight,Right,PreviousRightSolid);
PreviousRight=Right;PreviousRightSolid=RightSolid;
}
if(bFound)
{OutCrossing=Best;return true;}
if(Left<=ZMin+1.0e-9&&Right>=ZMax-1.0e-9)break;
}
return false;
}
inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FStandaloneShapeWorld& Shape,
const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
{
@@ -992,12 +1056,9 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
Vertex.X=X0+(X1-X0)*static_cast<double>(PX)/PatchDivisions;
Vertex.Y=Y0+(Y1-Y0)*static_cast<double>(PY)/PatchDivisions;
const double PredictedZ=SurfaceZ(Vertex.X,Vertex.Y);
const std::vector<double> Crossings=FindFarStratumCrossings(
Shape,Vertex.X,Vertex.Y,ZMin,ZMax);
if(Crossings.empty()){bValidPatch=false;break;}
Vertex.Z=*std::min_element(Crossings.begin(),Crossings.end(),
[PredictedZ](double A,double B)
{return std::abs(A-PredictedZ)<std::abs(B-PredictedZ);});
if(!FindNearestFarStratumCrossing(Shape,Vertex.X,Vertex.Y,
ZMin,ZMax,PredictedZ,Vertex.Z))
{bValidPatch=false;break;}
const float VertexGX=Density(Vertex.X+2,Vertex.Y,Vertex.Z)
-Density(Vertex.X-2,Vertex.Y,Vertex.Z);
const float VertexGY=Density(Vertex.X,Vertex.Y+2,Vertex.Z)
+365 -17
View File
@@ -210,6 +210,13 @@ namespace VoxelForgeCore
&&std::isfinite(brush.strength)&&std::isfinite(brush.falloff)&&brush.falloff>=0.0f;
}
std::uint64_t SaltFarHorizonHash(std::uint64_t Hash,std::uint32_t Version)
{
AddWord(Hash,0x464152484f52495aull,8);
AddWord(Hash,Version,4);
return Hash;
}
FStandaloneShapeWorld::EDetailVariant ToNative(DetailVariant value)
{
switch(value)
@@ -226,6 +233,7 @@ namespace VoxelForgeCore
double YSub = 0.0;
double ZSub = 0.0;
double GroundSub = 0.0;
double CapSub = 0.0;
FVector Normal = FVector(0.0,0.0,-1.0);
FColor Color = FColor::White;
};
@@ -234,6 +242,7 @@ namespace VoxelForgeCore
{
std::uint64_t Cells = 0;
std::uint64_t Triangles = 0;
std::uint64_t RemovedTriangles = 0;
bool Refined = false;
};
@@ -252,6 +261,7 @@ namespace VoxelForgeCore
Out.YSub=A.YSub+(B.YSub-A.YSub)*T;
Out.ZSub=A.ZSub+(B.ZSub-A.ZSub)*T;
Out.GroundSub=A.GroundSub+(B.GroundSub-A.GroundSub)*T;
Out.CapSub=A.CapSub+(B.CapSub-A.CapSub)*T;
Out.Normal=NormalizeCapNormal(A.Normal+(B.Normal-A.Normal)*T);
const auto LerpByte=[T](uint8 AValue,uint8 BValue)
{return static_cast<uint8>(std::clamp(std::lround(AValue+(BValue-AValue)*T),0l,255l));};
@@ -287,11 +297,286 @@ namespace VoxelForgeCore
return OutputCount;
}
FNativeCapRefinementStats RefineLod8NativeCap(const FStandaloneOwnerWorld& Owner,
FNativeCapRefinementStats RefineFarNativeGround(const FStandaloneOwnerWorld& Owner,
const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,FVoxelMeshData& Mesh)
{
FNativeCapRefinementStats Stats;
if(Key.Level<8||!Owner.Generator)return Stats;
if(Key.Level<5||!Owner.Generator)return Stats;
const int32 GridStepVoxels=std::min(128,1<<Key.Level);
constexpr int64 Scale=WorldAssemblySubvoxelsPerVoxel;
const int64 GridStepSubvoxels=static_cast<int64>(GridStepVoxels)*Scale;
const FIntVector TileOrigin=Key.OriginVoxels();
const int32 TileExtent=Key.ExtentVoxels();
if(TileExtent<=0||TileExtent%GridStepVoxels!=0)return Stats;
const int32 Cells=TileExtent/GridStepVoxels;
const double TileMinZ=static_cast<double>(TileOrigin.Z)*Scale;
const double TileMaxZ=static_cast<double>(TileOrigin.Z+TileExtent)*Scale;
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,Mesh.Vertices.Num());
const bool bHasUVs=Mesh.UVs.Num()==Mesh.Vertices.Num();
const bool bHasColors=Mesh.Colors.Num()==Mesh.Vertices.Num();
std::vector<FVector> AddedVertices,AddedNormals;
std::vector<FVector2f> AddedUVs;
std::vector<FColor> AddedColors;
std::vector<int32> AddedTriangles;
const int32 OldCeilingTriangleCount=std::max(0,Mesh.NumCeilingTriangles);
const int32 OldCeilingStart=std::max(0,Mesh.Triangles.Num()-OldCeilingTriangleCount*3);
struct FGroundProfile
{
int32 Cells=0,GridStep=0;
double MinimumZ=0.0,MaximumZ=0.0;
std::vector<double> Ground,Cap;
};
std::vector<FGroundProfile> GroundProfiles;
for(const auto& Plan:Manifest.plan.strates)
{
const int64 StratumMinZ=static_cast<int64>(Plan.bottomChunkZ)*CHUNK_SIZE*Scale;
const int64 StratumMaxZ=(static_cast<int64>(Plan.topChunkZ)+1)*CHUNK_SIZE*Scale;
if(TileMaxZ<StratumMinZ||TileMinZ>StratumMaxZ)continue;
const VoxelForgeCore::Heightfield2D Block(Manifest.recipe,Plan);
if(!Block.isValid())continue;
struct FGridSample
{
double Ground=0.0,Cap=0.0;
FVector Normal=FVector(0.0,0.0,1.0);
FColor Color=FColor::White;
int32 VertexIndex=INDEX_NONE;
};
std::vector<FGridSample> Grid(static_cast<std::size_t>(Cells+1)*(Cells+1));
const auto GridIndex=[Cells](int32 X,int32 Y)
{return static_cast<std::size_t>(Y)*(Cells+1)+X;};
for(int32 Y=0;Y<=Cells;++Y)
for(int32 X=0;X<=Cells;++X)
{
const int64 XSub=(static_cast<int64>(TileOrigin.X)+static_cast<int64>(X)*GridStepVoxels)*Scale;
const int64 YSub=(static_cast<int64>(TileOrigin.Y)+static_cast<int64>(Y)*GridStepVoxels)*Scale;
const auto Column=Block.point(XSub,YSub,0);
auto& Sample=Grid[GridIndex(X,Y)];
Sample.Ground=static_cast<double>(Column.groundHeightSubvoxels);
Sample.Cap=static_cast<double>(Column.capHeightSubvoxels);
int32 Dominant=0,Neighbor=0;float Blend=0.0f;
Owner.Generator->GetBiomeMaterialAt(static_cast<float>(XSub)/Scale,
static_cast<float>(YSub)/Scale,static_cast<float>(Sample.Ground/Scale),
Dominant,Neighbor,Blend);
Sample.Color=FColor(static_cast<uint8>(std::clamp(Dominant,0,255)),0,
static_cast<uint8>(std::clamp(std::lround(Blend*255.0f),0l,255l)),
static_cast<uint8>(std::clamp(Neighbor,0,255)));
}
for(int32 Y=0;Y<=Cells;++Y)
for(int32 X=0;X<=Cells;++X)
{
const int32 Left=std::max(0,X-1),Right=std::min(Cells,X+1);
const int32 Down=std::max(0,Y-1),Up=std::min(Cells,Y+1);
const double DX=static_cast<double>(Right-Left)*GridStepSubvoxels;
const double DY=static_cast<double>(Up-Down)*GridStepSubvoxels;
const double SlopeX=DX>0.0?(Grid[GridIndex(Right,Y)].Ground
-Grid[GridIndex(Left,Y)].Ground)/DX:0.0;
const double SlopeY=DY>0.0?(Grid[GridIndex(X,Up)].Ground
-Grid[GridIndex(X,Down)].Ground)/DY:0.0;
auto& Sample=Grid[GridIndex(X,Y)];
Sample.Normal=NormalizeCapNormal(FVector(-SlopeX,-SlopeY,1.0));
Sample.Color.G=static_cast<uint8>(std::clamp(
std::lround((1.0-std::abs(Sample.Normal.Z))*255.0),0l,255l));
}
FGroundProfile Profile;
Profile.Cells=Cells;Profile.GridStep=GridStepVoxels;
Profile.MinimumZ=static_cast<double>(std::min(Plan.bottomChunkZ,Plan.topChunkZ))*CHUNK_SIZE;
Profile.MaximumZ=static_cast<double>(std::max(Plan.bottomChunkZ,Plan.topChunkZ)+1)*CHUNK_SIZE;
Profile.Ground.reserve(Grid.size());Profile.Cap.reserve(Grid.size());
for(const auto& Sample:Grid)
{Profile.Ground.push_back(Sample.Ground/Scale);Profile.Cap.push_back(Sample.Cap/Scale);}
GroundProfiles.push_back(std::move(Profile));
// Partager les sommets du profil entre cellules reduit le payload d'horizon.
// Share regular profile vertices across adjacent cells to bound horizon payload.
for(int32 Y=0;Y<=Cells;++Y)
for(int32 X=0;X<=Cells;++X)
{
auto& Sample=Grid[GridIndex(X,Y)];
if(Sample.Ground>=Sample.Cap||Sample.Ground<TileMinZ||Sample.Ground>TileMaxZ)continue;
const int64 XSub=(static_cast<int64>(TileOrigin.X)+static_cast<int64>(X)*GridStepVoxels)*Scale;
const int64 YSub=(static_cast<int64>(TileOrigin.Y)+static_cast<int64>(Y)*GridStepVoxels)*Scale;
const double VoxelSizeCm=static_cast<double>(Owner.CoreVoxelSizeMicrons)/10000.0;
Sample.VertexIndex=static_cast<int32>(AddedVertices.size());
AddedVertices.emplace_back(static_cast<double>(TileOrigin.X+X*GridStepVoxels)*VoxelSizeCm,
static_cast<double>(TileOrigin.Y+Y*GridStepVoxels)*VoxelSizeCm,
Sample.Ground/Scale*VoxelSizeCm);
AddedNormals.push_back(Sample.Normal);
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(XSub/Scale),static_cast<float>(YSub/Scale));
if(bHasColors)AddedColors.push_back(Sample.Color);
}
for(int32 Y=0;Y<Cells;++Y)
for(int32 X=0;X<Cells;++X)
{
const int32 CellX[4]={X,X+1,X+1,X};
const int32 CellY[4]={Y,Y,Y+1,Y+1};
std::array<int32,4> GridVertices{};
bool bCompleteCell=true;
for(int32 Corner=0;Corner<4;++Corner)
{
GridVertices[Corner]=Grid[GridIndex(CellX[Corner],CellY[Corner])].VertexIndex;
bCompleteCell=bCompleteCell&&GridVertices[Corner]!=INDEX_NONE;
}
if(bCompleteCell)
{
AddedTriangles.insert(AddedTriangles.end(),{GridVertices[0],GridVertices[1],GridVertices[2],
GridVertices[0],GridVertices[2],GridVertices[3]});
++Stats.Cells;Stats.Triangles+=2;
continue;
}
std::array<FNativeCapVertex,16> PolygonA{},PolygonB{};
for(int32 Corner=0;Corner<4;++Corner)
{
const int32 GX=CellX[Corner],GY=CellY[Corner];
const auto& Sample=Grid[GridIndex(GX,GY)];
PolygonA[Corner].XSub=static_cast<double>(TileOrigin.X+GX*GridStepVoxels)*Scale;
PolygonA[Corner].YSub=static_cast<double>(TileOrigin.Y+GY*GridStepVoxels)*Scale;
PolygonA[Corner].GroundSub=Sample.Ground;
PolygonA[Corner].CapSub=Sample.Cap;
PolygonA[Corner].ZSub=Sample.Ground;
PolygonA[Corner].Normal=Sample.Normal;
PolygonA[Corner].Color=Sample.Color;
}
int32 Count=ClipNativeCapPolygon(PolygonA.data(),4,PolygonB.data(),
[](const auto& V){return V.CapSub-V.ZSub;});
Count=ClipNativeCapPolygon(PolygonB.data(),Count,PolygonA.data(),
[TileMinZ](const auto& V){return V.ZSub-TileMinZ;});
Count=ClipNativeCapPolygon(PolygonA.data(),Count,PolygonB.data(),
[TileMaxZ](const auto& V){return TileMaxZ-V.ZSub;});
if(Count<3)continue;
double TwiceArea=0.0;
for(int32 I=0;I<Count;++I)
{
const auto& A=PolygonB[I];const auto& B=PolygonB[(I+1)%Count];
TwiceArea+=A.XSub*B.YSub-B.XSub*A.YSub;
}
if(std::abs(TwiceArea)<1.0e-5)continue;
++Stats.Cells;
const int32 Base=static_cast<int32>(AddedVertices.size());
for(int32 I=0;I<Count;++I)
{
const auto& V=PolygonB[I];
const double VoxelSizeCm=static_cast<double>(Owner.CoreVoxelSizeMicrons)/10000.0;
AddedVertices.emplace_back(V.XSub/Scale*VoxelSizeCm,
V.YSub/Scale*VoxelSizeCm,V.ZSub/Scale*VoxelSizeCm);
AddedNormals.push_back(V.Normal);
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(V.XSub/Scale),
static_cast<float>(V.YSub/Scale));
if(bHasColors)AddedColors.push_back(V.Color);
}
for(int32 Fan=1;Fan+1<Count;++Fan)
{
AddedTriangles.insert(AddedTriangles.end(),{Base,Base+Fan,Base+Fan+1});
++Stats.Triangles;
}
}
}
if(AddedTriangles.empty())return Stats;
// Retirer les faces de sol MC proches du profil exact remplace le terrain coarse
// au lieu de le superposer au nouveau maillage d'horizon.
// Remove coarse upward MC faces around the exact profile instead of overlaying them.
const double VoxelSizeCm=static_cast<double>(Owner.CoreVoxelSizeMicrons)/10000.0;
const int32 OldSurfaceTriangleCount=OldCeilingStart/3;
std::vector<uint8> RemoveOldGround(static_cast<std::size_t>(OldSurfaceTriangleCount),0);
const TArray<uint8>& ExistingMasks=Mesh.TriangleSkirtFaceMasks;
const auto InterpolateProfile=[](const std::vector<double>& Values,const FGroundProfile& Profile,
double LocalX,double LocalY)
{
const double GridX=std::clamp(LocalX/Profile.GridStep,0.0,static_cast<double>(Profile.Cells));
const double GridY=std::clamp(LocalY/Profile.GridStep,0.0,static_cast<double>(Profile.Cells));
const int32 X=std::min(Profile.Cells-1,static_cast<int32>(std::floor(GridX)));
const int32 Y=std::min(Profile.Cells-1,static_cast<int32>(std::floor(GridY)));
const double TX=GridX-X,TY=GridY-Y;
const std::size_t Width=static_cast<std::size_t>(Profile.Cells+1);
const auto At=[&](int32 PX,int32 PY){return Values[static_cast<std::size_t>(PY)*Width+PX];};
if(TX>=TY)
return At(X,Y)*(1.0-TX)+At(X+1,Y)*(TX-TY)+At(X+1,Y+1)*TY;
return At(X,Y)*(1.0-TY)+At(X+1,Y+1)*TX+At(X,Y+1)*(TY-TX);
};
for(int32 Triangle=0;Triangle<OldSurfaceTriangleCount;++Triangle)
{
if(Triangle<ExistingMasks.Num()&&ExistingMasks[Triangle]!=0)continue;
const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],IC=Mesh.Triangles[Triangle*3+2];
if(IA<0||IB<0||IC<0||IA>=OldSurfaceVertices||IB>=OldSurfaceVertices||IC>=OldSurfaceVertices)continue;
const FVector& A=Mesh.Vertices[IA];const FVector& B=Mesh.Vertices[IB];const FVector& C=Mesh.Vertices[IC];
double NormalZ=0.0;
if(Mesh.Normals.Num()==Mesh.Vertices.Num())
NormalZ=(Mesh.Normals[IA].Z+Mesh.Normals[IB].Z+Mesh.Normals[IC].Z)/3.0;
else
NormalZ=FVector::CrossProduct(B-A,C-A).GetSafeNormal().Z;
if(NormalZ<0.35)continue;
const double X=((A.X+B.X+C.X)/3.0)/VoxelSizeCm;
const double Y=((A.Y+B.Y+C.Y)/3.0)/VoxelSizeCm;
const double Z=((A.Z+B.Z+C.Z)/3.0)/VoxelSizeCm;
const double LocalX=X-TileOrigin.X,LocalY=Y-TileOrigin.Y;
if(LocalX<0.0||LocalY<0.0||LocalX>TileExtent||LocalY>TileExtent)continue;
for(const auto& Profile:GroundProfiles)
{
if(LocalX>Profile.Cells*Profile.GridStep||LocalY>Profile.Cells*Profile.GridStep)continue;
const double Ground=InterpolateProfile(Profile.Ground,Profile,LocalX,LocalY);
const double Cap=InterpolateProfile(Profile.Cap,Profile,LocalX,LocalY);
if(Cap-Ground<=1.0||Ground<TileMinZ/Scale||Ground>TileMaxZ/Scale
||Ground<Profile.MinimumZ||Ground>Profile.MaximumZ)continue;
const double ReplacementBand=std::max(32.0,2.0*Profile.GridStep);
if(Z>=Ground-ReplacementBand)
{
RemoveOldGround[static_cast<std::size_t>(Triangle)]=1;
++Stats.RemovedTriangles;
}
break;
}
}
const int32 AddedVertexCount=static_cast<int32>(AddedVertices.size());
auto InsertSurfaceVertices=[&](auto& Existing,const auto& Added)
{
using TArrayType=std::remove_reference_t<decltype(Existing)>;
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
for(const auto& Value:Added)Combined.Add(Value);
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
Existing=std::move(Combined);
};
InsertSurfaceVertices(Mesh.Vertices,AddedVertices);
InsertSurfaceVertices(Mesh.Normals,AddedNormals);
if(bHasUVs)InsertSurfaceVertices(Mesh.UVs,AddedUVs);
if(bHasColors)InsertSurfaceVertices(Mesh.Colors,AddedColors);
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertexCount;
const int32 OldTriangleCount=Mesh.Triangles.Num()/3;
TArray<uint8> OldMasks=Mesh.TriangleSkirtFaceMasks;OldMasks.SetNum(OldTriangleCount);
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(
Mesh.Triangles.Num()+static_cast<int32>(AddedTriangles.size()));
TArray<uint8> CombinedMasks;CombinedMasks.Reserve(
OldTriangleCount+static_cast<int32>(AddedTriangles.size()/3));
for(int32 Triangle=0;Triangle<OldCeilingStart/3;++Triangle)
{
if(RemoveOldGround[static_cast<std::size_t>(Triangle)])continue;
for(int32 Corner=0;Corner<3;++Corner)CombinedTriangles.Add(Mesh.Triangles[Triangle*3+Corner]);
CombinedMasks.Add(OldMasks[Triangle]);
}
for(const int32 Index:AddedTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
for(std::size_t I=0;I<AddedTriangles.size()/3;++I)CombinedMasks.Add(0);
for(int32 Triangle=OldCeilingStart/3;Triangle<OldTriangleCount;++Triangle)
{
for(int32 Corner=0;Corner<3;++Corner)CombinedTriangles.Add(Mesh.Triangles[Triangle*3+Corner]);
CombinedMasks.Add(OldMasks[Triangle]);
}
Mesh.Triangles=std::move(CombinedTriangles);
Mesh.TriangleSkirtFaceMasks=std::move(CombinedMasks);
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertexCount;
Stats.Refined=true;
return Stats;
}
FNativeCapRefinementStats RefineFarNativeCap(const FStandaloneOwnerWorld& Owner,
const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,FVoxelMeshData& Mesh)
{
FNativeCapRefinementStats Stats;
if(Key.Level<5||!Owner.Generator)return Stats;
constexpr int32 GridStepVoxels=128;
constexpr int64 Scale=WorldAssemblySubvoxelsPerVoxel;
constexpr int64 GridStepSubvoxels=GridStepVoxels*Scale;
@@ -916,6 +1201,8 @@ namespace VoxelForgeCore
&&options.mesherRecipe!=MesherRecipe::worldAssemblerHybridV1
&&options.mesherRecipe!=MesherRecipe::worldAssemblerHybridV2)
{error="tile mesher recipe version is unsupported";return std::nullopt;}
if(level>=5&&options.farHorizonVersion==0)
{error="far-horizon recipe version must be nonzero";return std::nullopt;}
TArray<FVoxelHybridSharpFeatureBox> sharpFeatures;
if(options.mesherRecipe!=MesherRecipe::marchingCubesV1&&level==0&&impl_->worldAssembly)
{
@@ -1001,6 +1288,42 @@ namespace VoxelForgeCore
const bool bHasWorldAssembly=impl_->worldAssembly!=nullptr;
const bool bNeedsFarShell=bHasWorldAssembly&&key.lod>=7
&&(tileShape.HasFarWorld()||impl_->shape->HasFarWorld());
bool bOverlapsPlayerEdit=false;
if(tileShape.HasEditTransaction())
{
const FIntVector EditMinimum=tileShape.GetEditBoundsMin();
const FIntVector EditMaximum=tileShape.GetEditBoundsMax();
bOverlapsPlayerEdit=EditMinimum.X<=origin.X+extent+step
&&EditMaximum.X>=origin.X-step
&&EditMinimum.Y<=origin.Y+extent+step
&&EditMaximum.Y>=origin.Y-step
&&EditMinimum.Z<=origin.Z+extent+step
&&EditMaximum.Z>=origin.Z-step;
}
bool bHasNativeSurfaceHorizonPlan=false;
bool bHasIslandPlanInTile=false;
if(impl_->worldAssembly)
{
const int64 TileMinimumZ=static_cast<int64>(origin.Z);
const int64 TileMaximumZ=static_cast<int64>(origin.Z)+extent;
for(const auto& Plan:impl_->worldAssembly->plan.strates)
{
const int64 PlanMinimumZ=static_cast<int64>(std::min(Plan.bottomChunkZ,Plan.topChunkZ))*CHUNK_SIZE;
const int64 PlanMaximumZ=(static_cast<int64>(std::max(Plan.bottomChunkZ,Plan.topChunkZ))+1)*CHUNK_SIZE;
bHasNativeSurfaceHorizonPlan=bHasNativeSurfaceHorizonPlan
||(TileMinimumZ<=PlanMaximumZ&&TileMaximumZ>=PlanMinimumZ);
}
for(const auto& Entry:impl_->worldAssembly->floatingIslands)
{
const int64 PlanMinimumZ=static_cast<int64>(std::min(Entry.bottomChunkZ,Entry.topChunkZ))*CHUNK_SIZE;
const int64 PlanMaximumZ=(static_cast<int64>(std::max(Entry.bottomChunkZ,Entry.topChunkZ))+1)*CHUNK_SIZE;
bHasIslandPlanInTile=bHasIslandPlanInTile
||(TileMinimumZ<=PlanMaximumZ&&TileMaximumZ>=PlanMinimumZ);
}
}
const bool bUseNativeSurfaceHorizon=key.lod>=5&&!bOverlapsPlayerEdit
&&bHasNativeSurfaceHorizonPlan&&!bHasIslandPlanInTile;
const bool bSkipFarShellRepair=key.lod>=7&&bUseNativeSurfaceHorizon;
FStandaloneShapeWorld::ResetThreadClassificationStats();
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats();
bool bNativeSurfaceTile=false;
@@ -1082,18 +1405,6 @@ namespace VoxelForgeCore
static_cast<std::int64_t>(origin.X+extent+step)*SubvoxelScale,
static_cast<std::int64_t>(origin.Y+extent+step)*SubvoxelScale,
static_cast<std::int64_t>(origin.Z+extent+step)*SubvoxelScale};
bool bOverlapsPlayerEdit=false;
if(tileShape.HasEditTransaction())
{
const FIntVector EditMinimum=tileShape.GetEditBoundsMin();
const FIntVector EditMaximum=tileShape.GetEditBoundsMax();
bOverlapsPlayerEdit=EditMinimum.X<=origin.X+extent+step
&&EditMaximum.X>=origin.X-step
&&EditMinimum.Y<=origin.Y+extent+step
&&EditMaximum.Y>=origin.Y-step
&&EditMinimum.Z<=origin.Z+extent+step
&&EditMaximum.Z>=origin.Z-step;
}
if(!bOverlapsPlayerEdit)
{
const VoxelForgeCore::Heightfield2D Heightfield(
@@ -1190,21 +1501,52 @@ namespace VoxelForgeCore
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-MeshingStart).count();
out.diagnostics.farHorizonVersion=key.lod>=5?options.farHorizonVersion:0;
const auto FarShellStart=std::chrono::steady_clock::now();
const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:*impl_->shape;
if(farShape.HasFarWorld())
out.diagnostics.farShellRepairSkipped=bSkipFarShellRepair;
if(farShape.HasFarWorld()&&!bSkipFarShellRepair)
{
const auto BeforeStratum=FStandaloneShapeWorld::GetThreadClassificationStats();
const int32 BeforeStratumTriangles=mesh.Triangles.Num()/3;
const auto StratumStart=std::chrono::steady_clock::now();
AppendFarStratumShells(world,farShape,nativeKey,step,mesh);
const auto AfterStratum=FStandaloneShapeWorld::GetThreadClassificationStats();
out.diagnostics.farStratumShellMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-StratumStart).count();
out.diagnostics.farStratumFieldCalls=
AfterStratum.NativeFloatFieldCalls-BeforeStratum.NativeFloatFieldCalls;
out.diagnostics.farShellTrianglesAdded=static_cast<std::uint64_t>(
std::max(0,mesh.Triangles.Num()/3-BeforeStratumTriangles));
const auto BeforeConnector=AfterStratum;
const int32 BeforeConnectorTriangles=mesh.Triangles.Num()/3;
const auto ConnectorStart=std::chrono::steady_clock::now();
AppendFarConnectorShells(farShape,nativeKey,step,world.CoarseTileCells,mesh);
const auto AfterConnector=FStandaloneShapeWorld::GetThreadClassificationStats();
out.diagnostics.farConnectorShellMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-ConnectorStart).count();
out.diagnostics.farConnectorFieldCalls=
AfterConnector.NativeFloatFieldCalls-BeforeConnector.NativeFloatFieldCalls;
out.diagnostics.farShellTrianglesAdded+=static_cast<std::uint64_t>(
std::max(0,mesh.Triangles.Num()/3-BeforeConnectorTriangles));
}
out.diagnostics.farShellMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-FarShellStart).count();
if(impl_->worldAssembly&&key.lod>=8)
if(impl_->worldAssembly&&key.lod>=5&&!bOverlapsPlayerEdit)
{
const auto CapStats=RefineLod8NativeCap(world,*impl_->worldAssembly,nativeKey,mesh);
const auto HorizonStart=std::chrono::steady_clock::now();
const auto GroundStats=RefineFarNativeGround(
world,*impl_->worldAssembly,nativeKey,mesh);
const auto CapStats=RefineFarNativeCap(world,*impl_->worldAssembly,nativeKey,mesh);
out.diagnostics.farHorizonMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-HorizonStart).count();
out.diagnostics.capRefined=CapStats.Refined;
out.diagnostics.capRefinedCells=CapStats.Cells;
out.diagnostics.capRefinedTriangles=CapStats.Triangles;
out.diagnostics.horizonGroundTriangles=GroundStats.Triangles;
out.diagnostics.horizonGroundTrianglesRemoved=GroundStats.RemovedTriangles;
out.diagnostics.horizonCapTriangles=CapStats.Triangles;
}
out.densitySampleCount=static_cast<std::uint64_t>(std::max<int64>(0,sampleCount));
@@ -1270,6 +1612,12 @@ namespace VoxelForgeCore
{ densityHash^=std::bit_cast<std::uint32_t>(value);densityHash*=1099511628211ull; }
out.densityHash=densityHash;out.meshHash=HashViewerMesh(mesh);
out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh);
if(key.lod>=5)
{
out.meshHash=SaltFarHorizonHash(out.meshHash,options.farHorizonVersion);
out.payloadHash=SaltFarHorizonHash(out.payloadHash,options.farHorizonVersion);
out.topologyHash=SaltFarHorizonHash(out.topologyHash,options.farHorizonVersion);
}
out.hasDensityHash=out.hasMeshHash=out.hasPayloadHash=out.hasTopologyHash=true;
}
return out;
@@ -1194,3 +1194,27 @@ and no link, commit, push or stash was created.
[Full S2 report](../../WorldAssemblerM3/Round6/REPORT.md).
— Sol
## World assembler S3 / M3 Round 7 — 2026-10-18
Far SurfaceWorld ground and cap now come from the native heightfield at LOD5+, and certified
LOD7/8 SurfaceWorld tiles skip the derived shell repair. The S2 shell stage used about 81% of
SurfaceWorld LOD7/8 tile p95 and 96% of FloatingIslands LOD8. S3 warmed p95 fell 39–66% across
those owner LOD7/8 rows. A falsifiable LOD7/8 ground audit passes 128 shared points within 0.043 cm
of exact near-field roots, and its 100 cm datum-offset control fails. LOD0–LOD3 density, mesh,
payload and topology hashes match S2 exactly; SurfaceWorld and FloatingIslands W1/W8/W16 pass.
The owner horizon is not ready for visual sign-off. Cold whole-view presentation improved from
6.622 s to 3.420 s, still above the 1.57 s target. At 60 m/s the clipmap arrived after the player
crossed the route. The approach/retreat captures expose gaps and large faces, and the new distant
cap has isolated-looking pieces. I report those as failures rather than claiming a pop-free sequence.
No new source file was added. Core, editor audit and standalone viewer compile spots are listed with
the run ledger and capture links in [WorldAssemblerM3/Round7/REPORT.md](../../WorldAssemblerM3/Round7/REPORT.md).
CODEMAP section 3 and ARCHITECTURE section 8.24 are updated. Core-off, M2 fixtures, staged 25 cm
SurfaceWorld/FloatingIslands, edits and W1/W8/W16 pass. No new UECC folder, timeout or 0xC0000142
occurred. I did not open the owner's editor, build his project or edit assets, and I did not commit,
push or stash.
— Luna
@@ -0,0 +1,171 @@
# World assembler S3 / M3 Round 7
Date: 2026-10-18. Base: S2 / 6ddb834, building on S1 / d6e6942. No commit, push or stash.
## Result
The far-ground profile is now sampled directly from the native SurfaceWorld heightfield and the
S2 far-shell cost centre is largely removed. BuildHost checks pass, far LOD7/8 agree with the near
field within 0.043 cm at shared points, and LOD0–LOD3 hashes match S2 exactly.
This is not a horizon sign-off. Cold full-view presentation is still 3.42 s against the 1.57 s aim.
At 60 m/s the clipmap completes after the player has crossed the route, and the approach captures
show missing horizon/large faces during loading. The new cap is visible at distance, but its isolated
pieces and the persistent distant cliff need visual review. The captures do not prove a clean
approach/retreat with no visible pop.
## What changed and what the counters said
The owner-walk LOD8 hypothesis was mostly right about repair geometry and wrong about classification.
In S2's warmed eight-key timings, SurfaceWorld LOD7 spent 26.378/32.739 ms (81%) in far-shell repair
and LOD8 spent 135.960/168.545 ms (81%). FloatingIslands LOD8 spent 69.930/73.038 ms (96%). These
shell timings exclude LOD8 cap refinement. The old M1 Round 4 whole-clipmap measurement had 165
unknown LOD8 tiles, 13.27 M field calls and 49.8 s summed column time versus 1.85 s meshing. That
M1 window predates S1's relief changes, the shared feature lists and the single voxel-size source;
it is historical context, not a comparable S2 baseline.
The final SurfaceWorld actor route reports the following LOD7/8 tile p95 diagnostics. The sub-stage
percentiles are separate timers and are not additive; column-cache time overlaps native horizon work.
| LOD | Full tile p95 | MC mesh | Shell repair | Native horizon | Unknown boxes | Field calls | Column misses |
|---:|---:|---:|---:|---:|---:|---:|---:|
| 7 | 19.801 ms | 4.864 ms | 0.0001 ms | 13.156 ms | 1 | 6,167 | 3,025 |
| 8 | 58.346 ms | 6.013 ms | 0.0001 ms | 47.204 ms | 1 | 11,304 | 9,679 |
The shared broad-box classifier is not the current bottleneck: the route has one unknown box per
sampled far tile and classification is only a few microseconds. S2 repair connectors dominated.
After removing that work for certified SurfaceWorld tiles, native heightfield sampling/building is
the new LOD8 cost centre. I did not change noise octave evaluation.
### Owner timing comparison
Warmed BuildHost p95s use the staged 25 cm plans and eight owner keys per LOD; the S2 values are from
Round 6's same workload. These are tile timings, not rendered-frame percentiles.
| Family | LOD | S2 p95 | S3 p95 | Change |
|---|---:|---:|---:|---:|
| SurfaceWorld | 7 | 32.739 ms | 19.801 ms | -39.5% |
| SurfaceWorld | 8 | 168.545 ms | 58.346 ms | -65.4% |
| FloatingIslands | 7 | 20.409 ms | 13.112 ms | -35.8% |
| FloatingIslands | 8 | 72.923 ms | 24.638 ms | -66.2% |
S2 and S3 standalone stream benchmarks used the same owner JSON (SHA-256
289CD75597C5510FE8751102144FDC949D9B20B8BB23EEECDA37D049A49AD711), camera, 2,553 desired tiles,
12 workers and cold generation. CPU-only full-clipmap wall time was 6.099 -> 2.975 s (51% lower).
It is a different timing scope from the rendered viewer run.
The matched cold viewer runs used the owner walk camera, 12 workers, 60 fps with VSync and 2,553
tiles. S2 reached InitialClipmapReady at 6.567 s and first whole-view presentation at 6.622 s.
S3 reached them at 3.081 s and 3.420 s. The first presentation is still 2.18x the 1.57 s aim.
At the requested sprint speed, a cold approach from y=-1800 m at 60 m/s crossed 250 centers in
41.520 s. Nearest-tile ready p95 was 83.978 ms, maximum crossing wall time 40.050 s, and the full
clipmap completed at 42.339 s; status was FAIL. All 2,553 tiles eventually became resident, so this
is a late-horizon failure rather than a permanent missing-tile count. A second visual approach at
10 m/s from y=-900 m also failed its streaming gate: 50 crossings, 47.471 s elapsed, 134.014 ms
nearest-ready p95, 41.881 s maximum crossing wall time and InitialClipmapReady at 53.752 s. A
separate 60 m/s retreat run completed at 42.660 s and failed its latency gate as well.
## Shape agreement, determinism and regressions
The commandlet agreement check uses 128 shared LOD7/8 XY samples on the staged walk route. At each
point it compares far ground vertices to a near-field density root. Tolerance is 2 cm:
| Measure | Result |
|---|---:|
| Maximum LOD7 to near-field error | 0.0427 cm |
| Maximum LOD8 to near-field error | 0.0427 cm |
| Maximum LOD7 to LOD8 error | 0.0000 cm |
| Negative control, 100 cm datum offset | 100.0427 cm; detected |
The negative control fails the 2 cm gate as required. This checks the ground profile; the cap is
sampled from the same native heightfield at its shared 128-voxel grid, but this round did not add a
separate cap-to-near-root metric. That limitation and the visual cap artifacts remain open.
A direct S2/S3 full-clipmap hash comparison used --stream-hash at the same walk camera. All four
hashes below are identical per LOD, establishing the near terrain result:
| LOD | Density | Mesh | Payload | Topology |
|---:|---|---|---|---|
| 0 | 769f2a706d585a72 | 33eeeb39fa443816 | 645621a1d968983f | c2457ed905036c9d |
| 1 | 69cc5a5c0a2c60af | b48335f5410c57d5 | d15c5480162ab8ba | feca1bd025823604 |
| 2 | 85deed5683a77202 | 53469895987aeea2 | e55f8ae7e1532033 | d5feb12a0e85c09b |
| 3 | 6518caefb3be7e7f | ec513c90b853a614 | f62fa1137ff17f82 | ccf1dcf681965816 |
The whole-clipmap density hash also matches S2: c10697a8f48380c7. S3's BuildHost SurfaceWorld
W1/W8/W16 hashes match at density 0cd2a1a4152ee4aa, mesh 96df0c8767fd98c6, payload
377dce268955d353 and topology 5165549880b1c16a. FloatingIslands W1/W8/W16 match at density
4dcfb37585d7d986, mesh 112f6f2486ac56c5, payload b2c0a75cf5b9f12a and topology dfffcf4a5e29a111.
Core-off legacy at 12.5 cm passes. M2 V1/V2 fixture repeats, borders, V2 MC identity and 125 doorway
phases pass. BuildHost's staged SurfaceWorld and FloatingIslands actor checks pass at 25 cm. Surface
edit, carve/fill, scale, reference and anchor checks pass. No 20 cm run was made.
## Visual evidence
All included images are 640x400 and under 300 KB. S2 and S3 stills are separate cold runs with the
same owner plan and 60 fps/VSync capture setup.
| View | S2 | S3 | Readout |
|---|---|---|---|
| Owner walk horizon | [before](captures/before_s2_surface.jpg) | [after](captures/after_s3_surface.jpg) | Broad ground shape is similar. |
| Cap at distance | [before](captures/before_s2_cap.jpg) | [after](captures/after_s3_cap.jpg) | S2 shows open sky; S3 shows the ceiling, with isolated-looking pieces. |
| Far view, y=-1800 m | [before](captures/before_s2_far.jpg) | [after](captures/after_s3_far.jpg) | The same tall cliff is present in both; S3 has a small detached-looking cap band. |
The 60 m/s approach and retreat captures are a failed streaming sequence, not a no-pop claim:
[approach 0.1 s](captures/approach_00.jpg), [15 s](captures/approach_02.jpg),
[32 s](captures/approach_04.jpg); [retreat 0.1 s](captures/retreat_00.jpg),
[15 s](captures/retreat_02.jpg), [32 s](captures/retreat_04.jpg). Horizon coverage is absent or
obscured during loading. The slower 10 m/s approach also shows large distant faces:
[start](captures/approach_normal_00.jpg), [15 s](captures/approach_normal_02.jpg),
[32 s](captures/approach_normal_04.jpg). Raw back-buffer PNGs and full logs remain under
Saved/BuildHost/Round7S3Evidence. VSync was on and the viewer was capped at 60 fps and closed at run
completion.
## Run ledger and limits
- BuildHost Core/UBT and standalone viewer builds pass, with at most 12 workers. One early commandlet
compile failed on C4456 variable shadowing; renaming the local fixed it. The final BuildHost source
build and link-safety audit pass.
- The first agreement audit had fewer than 64 shared points in the chosen p95 keys. A walk-route
LOD7/8 fallback was added; the 128-point audit now passes.
- The first OS screen-grab attempt failed with an invalid desktop handle. I added optional
render-loop back-buffer capture to StandaloneViewer and rebuilt; those captures work.
- One normal-speed capture launch exited before generation because Start-Process split a path at the
workspace's space. Correct quoting allowed the run to complete; its streaming status still failed.
A JPEG helper syntax error wrote no files and was corrected. A CIM memory query was denied, so
launch checks used GlobalMemoryStatusEx instead. No Unreal/viewer process overlapped another.
- The process working-set high-water observed during BuildHost actor runs was 3.1 GiB; flight viewer
runs stayed near 1.1 GiB. Free physical memory was above 4 GiB before launches. No run returned
0xC0000142, timed out or exceeded the 15 minute step limit. The BuildHost crash directory contains
only the existing 2026-09-17 UECC folder; no new UECC folder appeared.
- BuildHost emitted existing warnings for the optional NewDataAsset, certificate store and PIX
initialization. They did not fail the owner checks.
## Files and owner note
New source files: none.
Modified source/header files:
- Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
- Standalone/Core/src/VoxelForgeCore.cpp
- Standalone/Core/src/FarShellsInternal.h
- Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp
- Standalone/Source/StandaloneViewer.cpp
Likely compile spots are the Core public API and implementation, the canonical Core shim/link audit,
the editor commandlet under BuildHost UBT/UHT, and the standalone viewer target. No generated header
was edited. CODEMAP section 3 and ARCHITECTURE section 8.24 describe the change.
The far recipe version is 1. It salts far mesh, payload and topology identities from LOD5 upward;
density hashes and LOD0–LOD3 identities remain unchanged. Bump it when changing this far mesh recipe.
The only family-specific planar representation is declared by SurfaceWorld's ground/cap heightfield.
FloatingIslands has no planar datum and keeps the shared 3D connector/field path. Player-edited tiles
continue through the authoritative field and repair path at every visible LOD.
Owner note: keep Core enabled at the existing 25 cm setting; no asset or owner-project change is
needed. The tile cost and near-hash checks pass, but the horizon is not ready for sign-off: cold
presentation misses the target, sprint streaming arrives late, and the captures do not prove a
pop-free transition. Do not treat these results as a completed visual acceptance.
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

+28
View File
@@ -1688,6 +1688,8 @@ namespace
int32 FlightObservedCrossings=0;
uint64 FlightStartCrossingIndex=0;
float FlightSpeedMps=16.0f;
std::string FlightCaptureSequencePrefix;
int32 FlightCaptureSequenceIndex=0;
bool bFlightStarted=false;
bool bFlightFinished=false;
bool bFlightClosePosted=false;
@@ -5505,6 +5507,29 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
void CaptureRenderedFrame()
{
// Capturer le vol dans la boucle de rendu conserve les transitions LOD.
// Capture the flight inside the render loop to retain LOD transition frames.
if(!FlightCaptureSequencePrefix.empty()&&bFlightStarted)
{
static constexpr std::array<double,5> CaptureSeconds={0.1,5.0,15.0,25.0,32.0};
const double FlightElapsed=std::chrono::duration<double>(
std::chrono::steady_clock::now()-FlightStartTime).count();
if(FlightCaptureSequenceIndex<static_cast<int32>(CaptureSeconds.size())
&&FlightElapsed>=CaptureSeconds[FlightCaptureSequenceIndex])
{
char Suffix[16]{};
std::snprintf(Suffix,sizeof(Suffix),"_%02d.png",FlightCaptureSequenceIndex);
const std::string Path=FlightCaptureSequencePrefix+Suffix;
if(SaveBackBufferPng(Path))
std::printf("[FlightSequenceCapture] index=%d elapsed_s=%.3f path=%s\n",
FlightCaptureSequenceIndex,FlightElapsed,Path.c_str());
else
std::fprintf(stderr,"[FlightSequenceCapture] index=%d failed path=%s\n",
FlightCaptureSequenceIndex,Path.c_str());
++FlightCaptureSequenceIndex;
std::fflush(stdout);
}
}
const bool bReadyToCapture=bCaptureAfterClipmap?bInitialClipmapReported:IsFullDetailUploaded();
if(CapturePath.empty()||bAutoCaptureWritten||!bReadyToCapture)return;
bAutoCaptureWritten=SaveBackBufferPng(CapturePath);
@@ -8964,6 +8989,9 @@ int main(int Argc,char** Argv)
{
const std::string_view Arg=Argv[I];
if(Arg.starts_with("--capture="))App.CapturePath=std::string(Arg.substr(10));
else if(Arg.starts_with("--capture-flight-sequence-prefix="))
App.FlightCaptureSequencePrefix=std::string(
Arg.substr(std::string_view("--capture-flight-sequence-prefix=").size()));
else if(Arg=="--no-vsync")App.bDisableVSync=true;
else if(Arg.starts_with("--frame-limit="))
{FrameLimitHz=std::clamp(std::atoi(std::string(Arg.substr(14)).c_str()),1,60);bFrameLimitSpecified=true;}