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
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
@@ -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;}
|
||||
|
||||