refactor(sheets): remove the far sheet ring from the runtime

The owner dropped the far sheet ring. Remove it from the streaming path:
- UVoxelMarchingCubesMesher::GenerateSheetMesh (declaration + definition);
- sheet-tile branches in LoadTile / GenerateTileResult / SyncRemeshTile and
  the bSheetTile / SheetChunkZ / Hole* parameters;
- the SheetHole* XY-hole state and its per-crossing re-queue block;
- VF_MaxMarchingCubesLevel (equal to the clamped MaxClipLevel without sheets);
- the sheet-only band arming and the 'sheet' field of the desired-level
  startup-trace events (no tool reads it).

AVoxelWorld now always passes bFarSheetRing=false to the tile selector, and
the descendant index covers levels up to the MaxClipLevel clamp (8) instead
of the old sheet maximum (12); no tile above 8 can exist any more.

Kept on purpose: UVoxelSettings::bFarSheetRing / FarSheetSpanLevels
(UPROPERTYs, now marked unused), the voxel.TestFarSheetRing switch and the
far_sheet_ring / sheet_tile fields the test harness compares (sheet_tile is
always false), and the selector's sheet parameters in
VoxelClipmapDesiredTiles.h, which two automation tests still exercise.
This commit is contained in:
Claude committed 2026-09-28 13:50:55 +00:00
1 parent 1de44af7cc
commit 4753416d4a
7 files changed
+40 -417

No files matched your search

@@ -9,6 +9,8 @@ namespace VoxelClipmapDesiredTiles
int32 ClipRadius = 3; int32 ClipRadius = 3;
int32 MaxClipLevel = 4; int32 MaxClipLevel = 4;
int32 RenderDistanceChunks = 0; int32 RenderDistanceChunks = 0;
// Far sheet ring selection. AVoxelWorld always passes false (the runtime no longer meshes
// sheets); it stays here because the clipmap automation tests still exercise it.
bool bFarSheetRing = true; bool bFarSheetRing = true;
int32 FarSheetSpanLevels = 2; int32 FarSheetSpanLevels = 2;
}; };
@@ -1126,218 +1126,3 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
return MeshData; return MeshData;
} }
//=============================================================================
// F18 — FEUILLE DE CHAMP LOINTAIN (voir le doc de la déclaration dans le .h)
//=============================================================================
// Deux grilles déplacées (sol + cap) au lieu d'un marching cubes 3D. Conventions IDENTIQUES à
// GenerateMesh : positions monde cm, UVs planaires (voxels), masques F6 par GetBiomeMaterialAt,
// jupes double-face extrudées le long de −N par seau, indices sol‖cap + NumCeilingTriangles.
// Winding : le chemin MC émet (0,2,1) des tables Bourke ⇒ une surface orientée +Z s'écrit
// (x,y) → (x,y+1) → (x+1,y) ; le cap (orienté −Z) inverse.
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateSheetMesh(FIntVector OriginVoxels, int32 StepXY,
int32 CellsXY, int32 StrateChunkZ,
int32 HoleMinXVox, int32 HoleMinYVox,
int32 HoleMaxXVox, int32 HoleMaxYVox)
{
FVoxelMeshData MeshData;
if (!Generator) return MeshData;
StepXY = FMath::Max(1, StepXY);
CellsXY = FMath::Clamp(CellsXY, 2, 512);
// TROU XY — cellule [x0, x0+Step]×[y0, y0+Step] entièrement dans le rectangle (Max exclusif)
// ⇒ sautée : la zone est couverte par les coquilles MC (voir VoxelWorld.h SheetHole*).
auto CellInHole = [&](int32 cx, int32 cy) -> bool
{
const int32 X0 = OriginVoxels.X + cx * StepXY;
const int32 Y0 = OriginVoxels.Y + cy * StepXY;
return X0 >= HoleMinXVox && X0 + StepXY <= HoleMaxXVox
&& Y0 >= HoleMinYVox && Y0 + StepXY <= HoleMaxYVox;
};
const int32 GridDim = CellsXY + 1; // points de grille par axe
const int32 MDim = GridDim + 2; // + anneau de marge ±1 (normales continues entre feuilles)
// Early-out : la validité SurfaceWorld ne dépend que de ChunkZ (les strates sont des couches
// horizontales) — une sonde suffit. La sentinelle par colonne plus bas reste par prudence.
{
float T0 = 0.0f, C0 = 0.0f;
if (!Generator->GetSurfaceHeightAt((float)OriginVoxels.X, (float)OriginVoxels.Y,
StrateChunkZ, T0, C0))
{
return MeshData; // pas une strate SurfaceWorld → feuille vide
}
}
// Colonnes (TerrainZ, CeilSurf) en Z-voxel monde absolu, marge incluse. X==FLT_MAX ⇒ invalide.
static thread_local TArray<FVector2f> SheetCols;
SheetCols.SetNumUninitialized(MDim * MDim);
for (int32 gy = -1; gy <= GridDim; ++gy)
{
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 gx = -1; gx <= GridDim; ++gx)
{
float Tz = 0.0f, Cz = 0.0f;
const bool bOk = Generator->GetSurfaceHeightAt(
(float)(OriginVoxels.X + gx * StepXY), (float)(OriginVoxels.Y + gy * StepXY),
StrateChunkZ, Tz, Cz);
SheetCols[(gy + 1) * MDim + (gx + 1)] = bOk ? FVector2f(Tz, Cz)
: FVector2f(FLT_MAX, -FLT_MAX);
}
}
auto Col = [&](int32 gx, int32 gy) -> const FVector2f&
{
return SheetCols[(gy + 1) * MDim + (gx + 1)];
};
// Index de vertex par point de grille (−1 = pas créé), sol et cap séparés — le partage de
// vertex est structurel (grille régulière), pas besoin de map de déduplication.
static thread_local TArray<int32> GroundIdx;
static thread_local TArray<int32> CapIdx;
GroundIdx.Init(-1, GridDim * GridDim);
CapIdx.Init(-1, GridDim * GridDim);
auto MakeVert = [&](int32 gx, int32 gy, bool bCap) -> int32
{
int32& Slot = (bCap ? CapIdx : GroundIdx)[gy * GridDim + gx];
if (Slot >= 0) return Slot;
const FVector2f C = Col(gx, gy);
const float H = bCap ? C.Y : C.X;
// Pente par différences centrales sur la grille de hauteurs (voxels/voxels — l'échelle cm
// se simplifie). Voisin invalide ⇒ composante plate (ne devrait pas arriver : cf. early-out).
auto HAt = [&](int32 x, int32 y) -> float
{
const FVector2f& N = Col(x, y);
return (N.X == FLT_MAX) ? H : (bCap ? N.Y : N.X);
};
const float Sx = (HAt(gx + 1, gy) - HAt(gx - 1, gy)) / (2.0f * (float)StepXY);
const float Sy = (HAt(gx, gy + 1) - HAt(gx, gy - 1)) / (2.0f * (float)StepXY);
// Sol : air au-dessus ⇒ normale vers le haut. Cap : l'air (l'intérieur de la strate) est
// EN-DESSOUS ⇒ normale vers le bas (−∇(z − C) = (Cx, Cy, −1)).
FVector Normal = bCap ? FVector(Sx, Sy, -1.0f) : FVector(-Sx, -Sy, 1.0f);
if (!Normal.Normalize())
{
Normal = FVector(0.0f, 0.0f, bCap ? -1.0f : 1.0f);
}
const float Xv = (float)(OriginVoxels.X + gx * StepXY);
const float Yv = (float)(OriginVoxels.Y + gy * StepXY);
const FVector WorldPos(Xv * VOXEL_SIZE, Yv * VOXEL_SIZE, H * VOXEL_SIZE);
Slot = MeshData.Vertices.Num();
MeshData.Vertices.Add(WorldPos);
MeshData.Normals.Add(Normal);
MeshData.UVs.Add(FVector2D(Xv, Yv)); // == WorldPos.XY / VOXEL_SIZE (convention MC)
// Mêmes masques F6 que GenerateMesh (palette biome dominant/voisin, pente, fondu).
int32 PalD = 0, PalN = 0; float BlendW = 0.0f;
Generator->GetBiomeMaterialAt(Xv, Yv, H, PalD, PalN, BlendW);
const uint8 Rr = (uint8)FMath::Clamp(PalD, 0, 255);
const uint8 Aa = (uint8)FMath::Clamp(PalN, 0, 255);
const uint8 Gg = (uint8)FMath::Clamp(FMath::RoundToInt((1.0f - FMath::Abs((float)Normal.Z)) * 255.0f), 0, 255);
const uint8 Bb = (uint8)FMath::Clamp(FMath::RoundToInt(BlendW * 255.0f), 0, 255);
MeshData.Colors.Add(FColor(Rr, Gg, Bb, Aa));
return Slot;
};
static thread_local TArray<int32> SheetGroundTris;
static thread_local TArray<int32> SheetCapTris;
SheetGroundTris.Reset();
SheetCapTris.Reset();
for (int32 cy = 0; cy < CellsXY; ++cy)
{
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 cx = 0; cx < CellsXY; ++cx)
{
if (CellInHole(cx, cy)) continue; // couverte par les coquilles MC proches
if (Col(cx, cy).X == FLT_MAX || Col(cx + 1, cy).X == FLT_MAX ||
Col(cx, cy + 1).X == FLT_MAX || Col(cx + 1, cy + 1).X == FLT_MAX)
{
continue;
}
// SOL (polygroup 0) — orienté +Z.
const int32 g00 = MakeVert(cx, cy, false);
const int32 g10 = MakeVert(cx + 1, cy, false);
const int32 g01 = MakeVert(cx, cy + 1, false);
const int32 g11 = MakeVert(cx + 1, cy + 1, false);
SheetGroundTris.Add(g00); SheetGroundTris.Add(g01); SheetGroundTris.Add(g10);
SheetGroundTris.Add(g10); SheetGroundTris.Add(g01); SheetGroundTris.Add(g11);
// CAP (polygroup 1) — orienté −Z ⇒ winding inversé.
const int32 c00 = MakeVert(cx, cy, true);
const int32 c10 = MakeVert(cx + 1, cy, true);
const int32 c01 = MakeVert(cx, cy + 1, true);
const int32 c11 = MakeVert(cx + 1, cy + 1, true);
SheetCapTris.Add(c00); SheetCapTris.Add(c10); SheetCapTris.Add(c01);
SheetCapTris.Add(c10); SheetCapTris.Add(c11); SheetCapTris.Add(c01);
}
}
// JUPES périmètre — mêmes règles que le chemin MC : extrusion le long de −N (vers le solide :
// bas pour le sol, HAUT pour le cap), double face, émises PAR SEAU (héritent le polygroup).
// Entre feuilles voisines les coins coïncident (mêmes échantillons monde) → pas de fissure ;
// la jupe couvre surtout la couture feuille ↔ anneau MC et les décalages de niveau.
if (bGenerateSkirts && (SheetGroundTris.Num() + SheetCapTris.Num()) > 0)
{
const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)StepXY * VOXEL_SIZE;
auto EmitEdgeSkirt = [&](int32 ax, int32 ay, int32 bx, int32 by, bool bCap, TArray<int32>& Tris)
{
if (Col(ax, ay).X == FLT_MAX || Col(bx, by).X == FLT_MAX) return;
const int32 iA = MakeVert(ax, ay, bCap);
const int32 iB = MakeVert(bx, by, bCap);
// COPIES par valeur — les Add ci-dessous réallouent Vertices/Normals/Colors.
const FVector PA = MeshData.Vertices[iA], PB = MeshData.Vertices[iB];
const FVector NA = MeshData.Normals[iA], NB = MeshData.Normals[iB];
const FColor CA = MeshData.Colors[iA], CB = MeshData.Colors[iB];
const int32 iA2 = MeshData.Vertices.Num();
MeshData.Vertices.Add(PA - NA * SkirtDepth);
MeshData.Normals.Add(NA);
MeshData.UVs.Add(FVector2D(PA.X / VOXEL_SIZE, PA.Y / VOXEL_SIZE));
MeshData.Colors.Add(CA);
const int32 iB2 = MeshData.Vertices.Num();
MeshData.Vertices.Add(PB - NB * SkirtDepth);
MeshData.Normals.Add(NB);
MeshData.UVs.Add(FVector2D(PB.X / VOXEL_SIZE, PB.Y / VOXEL_SIZE));
MeshData.Colors.Add(CB);
Tris.Add(iA); Tris.Add(iB); Tris.Add(iB2);
Tris.Add(iA); Tris.Add(iB2); Tris.Add(iA2);
Tris.Add(iA); Tris.Add(iB2); Tris.Add(iB);
Tris.Add(iA); Tris.Add(iA2); Tris.Add(iB2);
};
// Une jupe n'est émise que si la cellule de bord adjacente a réellement été maillée
// (pas dans le trou XY) — sinon mur flottant sans surface.
for (int32 c = 0; c < CellsXY; ++c)
{
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 Pass = 0; Pass < 2; ++Pass)
{
const bool bCap = (Pass == 1);
TArray<int32>& Tris = bCap ? SheetCapTris : SheetGroundTris;
if (!CellInHole(c, 0))
EmitEdgeSkirt(c, 0, c + 1, 0, bCap, Tris); // bord Y-min
if (!CellInHole(c, CellsXY - 1))
EmitEdgeSkirt(c, CellsXY, c + 1, CellsXY, bCap, Tris); // bord Y-max
if (!CellInHole(0, c))
EmitEdgeSkirt(0, c, 0, c + 1, bCap, Tris); // bord X-min
if (!CellInHole(CellsXY - 1, c))
EmitEdgeSkirt(CellsXY, c, CellsXY, c + 1, bCap, Tris); // bord X-max
}
}
}
// Concatène sol PUIS cap : un run d'indices contigu par polygroup (contrat F17/BuildTileStreamSet).
MeshData.Triangles.Reserve(SheetGroundTris.Num() + SheetCapTris.Num());
MeshData.Triangles.Append(SheetGroundTris);
MeshData.Triangles.Append(SheetCapTris);
MeshData.NumCeilingTriangles = SheetCapTris.Num() / 3;
return MeshData;
}
+30 -143
View File
@@ -59,7 +59,6 @@
#include "Serialization/JsonWriter.h" #include "Serialization/JsonWriter.h"
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0) #include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
static int32 VF_MaxMarchingCubesLevel(const UVoxelSettings* Settings);
static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile, static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile,
const FVector& BoundsMin, const FVector& BoundsMax); const FVector& BoundsMin, const FVector& BoundsMax);
static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B); static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B);
@@ -2232,7 +2231,7 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels, void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi, int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
bool bSheetTile, const FVoxelMeshData& MeshData) const FVoxelMeshData& MeshData)
{ {
if (!bTileHashDumpEnabled) if (!bTileHashDumpEnabled)
{ {
@@ -2246,7 +2245,6 @@ void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& Or
Record.Cells = Cells; Record.Cells = Cells;
Record.BandChunkLo = BandChunkLo; Record.BandChunkLo = BandChunkLo;
Record.BandChunkHi = BandChunkHi; Record.BandChunkHi = BandChunkHi;
Record.bSheetTile = bSheetTile;
Record.bEmpty = MeshData.IsEmpty(); Record.bEmpty = MeshData.IsEmpty();
Record.NumTriangles = MeshData.Triangles.Num() / 3; Record.NumTriangles = MeshData.Triangles.Num() / 3;
Record.GeometryHash = VoxelForgeGeometryHash::Compute(MeshData); Record.GeometryHash = VoxelForgeGeometryHash::Compute(MeshData);
@@ -2315,7 +2313,8 @@ void AVoxelWorld::WriteTileHashDump()
Writer->WriteValue(TEXT("cells"), Record.Cells); Writer->WriteValue(TEXT("cells"), Record.Cells);
Writer->WriteValue(TEXT("band_chunk_lo"), Record.BandChunkLo); Writer->WriteValue(TEXT("band_chunk_lo"), Record.BandChunkLo);
Writer->WriteValue(TEXT("band_chunk_hi"), Record.BandChunkHi); Writer->WriteValue(TEXT("band_chunk_hi"), Record.BandChunkHi);
Writer->WriteValue(TEXT("sheet_tile"), Record.bSheetTile); // The parity harness still compares this field; the runtime has no sheet tiles.
Writer->WriteValue(TEXT("sheet_tile"), false);
Writer->WriteValue(TEXT("empty"), Record.bEmpty); Writer->WriteValue(TEXT("empty"), Record.bEmpty);
Writer->WriteValue(TEXT("triangle_count"), Record.NumTriangles); Writer->WriteValue(TEXT("triangle_count"), Record.NumTriangles);
Writer->WriteValue(TEXT("geometry_hash"), Record.GeometryHash); Writer->WriteValue(TEXT("geometry_hash"), Record.GeometryHash);
@@ -4577,12 +4576,11 @@ void AVoxelWorld::Tick(float DeltaTime)
const double SpanZ = static_cast<double>(Max.Z - Min.Z + 1) * TileExtentMetres; const double SpanZ = static_cast<double>(Max.Z - Min.Z + 1) * TileExtentMetres;
VoxelForgeStartupTrace::RecordEvent(TEXT("steady_desired_level"), FString::Printf( VoxelForgeStartupTrace::RecordEvent(TEXT("steady_desired_level"), FString::Printf(
TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d]," TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d],"
"\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s],\"sheet\":%d"), "\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s]"),
Level, SteadyLevelCounts.FindChecked(Level), Level, SteadyLevelCounts.FindChecked(Level),
Min.X, Min.Y, Min.Z, Max.X, Max.Y, Max.Z, Min.X, Min.Y, Min.Z, Max.X, Max.Y, Max.Z,
*FString::SanitizeFloat(TileExtentMetres), *FString::SanitizeFloat(TileExtentMetres),
*FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ), *FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ)));
Settings && Level > VF_MaxMarchingCubesLevel(Settings) ? 1 : 0));
VoxelForgeStartupTrace::RecordEvent(TEXT("steady_loaded_level"), FString::Printf( VoxelForgeStartupTrace::RecordEvent(TEXT("steady_loaded_level"), FString::Printf(
TEXT("\"level\":%d,\"tiles\":%d,\"vertices\":%llu,\"triangles\":%llu,\"geometry_bytes\":%llu"), TEXT("\"level\":%d,\"tiles\":%d,\"vertices\":%llu,\"triangles\":%llu,\"geometry_bytes\":%llu"),
Level, LoadedLevelCounts.FindRef(Level), Level, LoadedLevelCounts.FindRef(Level),
@@ -6217,8 +6215,7 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
Result.GenerationStartCycles = GenerateStartCycles; Result.GenerationStartCycles = GenerateStartCycles;
GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false, GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi, BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
/*bSheetTile*/ false, /*SheetChunkZ*/ 0, Result, nullptr);
/*Hole*/ 0, 0, 0, 0, Result, nullptr); // hole unused (not a sheet tile)
Result.GenerationEndCycles = GenerateStartCycles != 0 Result.GenerationEndCycles = GenerateStartCycles != 0
? FPlatformTime::Cycles64() : 0; ? FPlatformTime::Cycles64() : 0;
@@ -6471,30 +6468,18 @@ static FORCEINLINE FIntVector VF_FloorDiv(const FIntVector& V, int32 D)
return FIntVector(VF_FloorDiv(V.X, D), VF_FloorDiv(V.Y, D), VF_FloorDiv(V.Z, D)); return FIntVector(VF_FloorDiv(V.X, D), VF_FloorDiv(V.Y, D), VF_FloorDiv(V.Z, D));
} }
// F18 — the outer shell is either the MC render-distance level or the larger sheet ring. When // The outer shell is the MaxClipLevel ring stretched to RenderDistanceChunks. Shared by desired
// the sheet span skips levels, VF_MaxMarchingCubesLevel fills them with MC bridge shells. // selection and range culling so both use the same outer horizon. The selector's far sheet ring
// Shared by desired selection and range culling so both use the same outer horizon. // is not part of the runtime, so it is always switched off here.
static FORCEINLINE void VF_OuterShell(const UVoxelSettings* Settings, int32 R, int32 MaxLevel, static FORCEINLINE void VF_OuterShell(const UVoxelSettings* Settings, int32 R, int32 MaxLevel,
int32& OutLevel, int32& OutRadius) int32& OutLevel, int32& OutRadius)
{ {
VoxelClipmapDesiredTiles::FParameters Parameters; VoxelClipmapDesiredTiles::FParameters Parameters;
Parameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0; Parameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0;
Parameters.bFarSheetRing = Settings && Settings->bFarSheetRing; Parameters.bFarSheetRing = false;
Parameters.FarSheetSpanLevels = Settings ? Settings->FarSheetSpanLevels : 2;
VoxelClipmapDesiredTiles::OuterShell(Parameters, R, MaxLevel, OutLevel, OutRadius); VoxelClipmapDesiredTiles::OuterShell(Parameters, R, MaxLevel, OutLevel, OutRadius);
} }
// When an expanded sheet ring skips LOD levels, keep an MC bridge at every intervening level.
static int32 VF_MaxMarchingCubesLevel(const UVoxelSettings* Settings)
{
const int32 Radius = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
int32 OuterLevel = MaxLevel, OuterRadius = Radius;
VF_OuterShell(Settings, Radius, MaxLevel, OuterLevel, OuterRadius);
(void)OuterRadius;
return OuterLevel > MaxLevel ? OuterLevel - 1 : MaxLevel;
}
void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition, void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition,
APawn* PlayerPawn, const FVector& PlayerHeading, APawn* PlayerPawn, const FVector& PlayerHeading,
TArray<FVoxelTileKey>& OutLeavers) TArray<FVoxelTileKey>& OutLeavers)
@@ -6512,8 +6497,7 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3; const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4; const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
// Render-distance tiles expand horizontally. The sheet ring retains its configured span; // Render-distance tiles expand horizontally.
// intermediate levels are emitted as marching-cubes bridge shells below it.
// IsTileInClipRange shares the outer-shell calculation so culling sees this same horizon. // IsTileInClipRange shares the outer-shell calculation so culling sees this same horizon.
int32 StrateTopZ = 0, StrateBottomZ = 0; int32 StrateTopZ = 0, StrateBottomZ = 0;
bool bHasStrate = false; bool bHasStrate = false;
@@ -6537,8 +6521,7 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
TileParameters.ClipRadius = R; TileParameters.ClipRadius = R;
TileParameters.MaxClipLevel = MaxLevel; TileParameters.MaxClipLevel = MaxLevel;
TileParameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0; TileParameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0;
TileParameters.bFarSheetRing = Settings && Settings->bFarSheetRing; TileParameters.bFarSheetRing = false;
TileParameters.FarSheetSpanLevels = Settings ? Settings->FarSheetSpanLevels : 2;
VoxelClipmapDesiredTiles::Build(Center, ZLo, ZHi, TileParameters, DesiredSorted); VoxelClipmapDesiredTiles::Build(Center, ZLo, ZHi, TileParameters, DesiredSorted);
for (const FVoxelTileKey& Key : DesiredSorted) for (const FVoxelTileKey& Key : DesiredSorted)
{ {
@@ -6681,10 +6664,10 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
DesiredTransitionMaxLevel = FMath::Max(DesiredTransitionMaxLevel, Pair.Key.Level); DesiredTransitionMaxLevel = FMath::Max(DesiredTransitionMaxLevel, Pair.Key.Level);
} }
// A worker from the previous desired epoch can finish late with a component at a level no // A worker from the previous desired epoch can finish late with a component at a level no
// longer present in TileComponents when this index is built. Index through the supported // longer present in TileComponents when this index is built. Index through the highest
// maximum sheet level (MaxClipLevel 8 + FarSheetSpanLevels 4) so that such a retired tile still // possible tile level (MaxClipLevel is clamped to 8) so that such a retired tile still
// finds all of its desired descendants when the result is reconciled. // finds all of its desired descendants when the result is reconciled.
constexpr int32 MaxSupportedTileLevel = 12; constexpr int32 MaxSupportedTileLevel = 8;
const int32 DescendantIndexMaxLevel = FMath::Max( const int32 DescendantIndexMaxLevel = FMath::Max(
DesiredTransitionMaxLevel, MaxSupportedTileLevel); DesiredTransitionMaxLevel, MaxSupportedTileLevel);
for (const FVoxelTileKey& Key : DesiredSorted) for (const FVoxelTileKey& Key : DesiredSorted)
@@ -6727,7 +6710,7 @@ void AVoxelWorld::CancelObsoleteTileWork()
} }
// Fold every registered anchor's small level-0 box into the current desired set (§9.3). Runs inside // Fold every registered anchor's small level-0 box into the current desired set (§9.3). Runs inside
// BuildDesiredTiles after the player clipmap + sheet ring, keyed on the same DesiredStamp so the leaver // BuildDesiredTiles after the player clipmap, keyed on the same DesiredStamp so the leaver
// sweep + delta cull handle anchor tiles leaving. Level-0 only (collision lives on level-0 tiles); empty // sweep + delta cull handle anchor tiles leaving. Level-0 only (collision lives on level-0 tiles); empty
// tiles in the box are ~free (the trivial-tile reject skips gen). Dedup vs the player clipmap by stamp. // tiles in the box are ~free (the trivial-tile reject skips gen). Dedup vs the player clipmap by stamp.
// §9.4: a tile the player clipmap did NOT stamp (bNew) that only a CollisionOnly anchor wants goes into // §9.4: a tile the player clipmap did NOT stamp (bNew) that only a CollisionOnly anchor wants goes into
@@ -6839,7 +6822,7 @@ void AVoxelWorld::UnregisterStreamingAnchor(AActor* Actor)
bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector& Center) const bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector& Center) const
{ {
// In range = the tile's centre falls within the OUTERMOST shell (VF_OuterShell — the // In range = the tile's centre falls within the OUTERMOST shell (VF_OuterShell — the
// render-distance/sheet-ring level+radius, same as BuildDesiredTiles). A loaded-but-not- // render-distance level+radius, same as BuildDesiredTiles). A loaded-but-not-
// desired tile in range is mid-LOD-transition (wait for its replacement); one out of range // desired tile in range is mid-LOD-transition (wait for its replacement); one out of range
// has left the view entirely (cull immediately). // has left the view entirely (cull immediately).
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3; const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
@@ -7021,13 +7004,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
const double SpanZ = static_cast<double>(Summary.Max.Z - Summary.Min.Z + 1) * TileExtentMetres; const double SpanZ = static_cast<double>(Summary.Max.Z - Summary.Min.Z + 1) * TileExtentMetres;
VoxelForgeStartupTrace::RecordEvent(TEXT("desired_level"), FString::Printf( VoxelForgeStartupTrace::RecordEvent(TEXT("desired_level"), FString::Printf(
TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d]," TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d],"
"\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s],\"sheet\":%d"), "\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s]"),
Summary.Level, Summary.Count, Summary.Level, Summary.Count,
Summary.Min.X, Summary.Min.Y, Summary.Min.Z, Summary.Min.X, Summary.Min.Y, Summary.Min.Z,
Summary.Max.X, Summary.Max.Y, Summary.Max.Z, Summary.Max.X, Summary.Max.Y, Summary.Max.Z,
*FString::SanitizeFloat(TileExtentMetres), *FString::SanitizeFloat(TileExtentMetres),
*FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ), *FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY),
Settings && Summary.Level > VF_MaxMarchingCubesLevel(Settings) ? 1 : 0)); *FString::SanitizeFloat(SpanZ)));
} }
bStartupTraceDesiredRecorded = true; bStartupTraceDesiredRecorded = true;
} }
@@ -7044,13 +7027,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
// either way; everything else re-gens in place via BandRemeshQueue (no visual pop). // either way; everything else re-gens in place via BandRemeshQueue (no visual pop).
{ {
const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9; const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9;
// F18 — l'anneau feuille dépend aussi de la bande (sa strate de référence) : on l'arme
// dès que les feuilles sont actives, même si la coupe de contenu MC est désactivée.
const bool bSheetsWantBand = Settings && Settings->bFarSheetRing
&& Settings->RenderDistanceChunks > 0;
const int32 TopMC = VF_MaxMarchingCubesLevel(Settings);
int32 NewLo = MIN_int32, NewHi = MAX_int32; int32 NewLo = MIN_int32, NewHi = MAX_int32;
if ((CutMin <= 8 || bSheetsWantBand) && StrateManager) if (CutMin <= 8 && StrateManager)
{ {
int32 StrTopZ = 0, StrBotZ = 0; int32 StrTopZ = 0, StrBotZ = 0;
if (StrateManager->GetStrateChunkZBounds(CenterChunk.Z, StrTopZ, StrBotZ)) if (StrateManager->GetStrateChunkZBounds(CenterChunk.Z, StrTopZ, StrBotZ))
@@ -7069,8 +7047,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
NewLo, NewHi, MeshBandChunkLo, MeshBandChunkHi, CutMin, CenterChunk.Z); NewLo, NewHi, MeshBandChunkLo, MeshBandChunkHi, CutMin, CenterChunk.Z);
for (const FVoxelTileKey& T : LoadedTiles) for (const FVoxelTileKey& T : LoadedTiles)
{ {
// Sheets (above the last MC bridge level) always depend on the strate band. if (T.Level < CutMin) continue;
if (T.Level < CutMin && T.Level <= TopMC) continue;
const int32 CLo = T.Coord.Z << T.Level; const int32 CLo = T.Coord.Z << T.Level;
const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1; const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1;
const bool bInOld = CLo >= MeshBandChunkLo && CHi <= MeshBandChunkHi; const bool bInOld = CLo >= MeshBandChunkLo && CHi <= MeshBandChunkHi;
@@ -7087,44 +7064,6 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
} }
} }
// The sheet XY hole follows the outermost MC bridge level and shrinks by one tile, leaving
// an overlap at the mesh transition. Requeue sheets overlapping either the old or new hole
// when the player crosses a bridge-level tile.
{
int32 NewMinX = MAX_int32, NewMinY = MAX_int32;
int32 NewMaxX = MIN_int32, NewMaxY = MIN_int32;
const int32 TopMCLvl = VF_MaxMarchingCubesLevel(Settings);
if (Settings && Settings->bFarSheetRing && Settings->RenderDistanceChunks > 0)
{
const int32 RClip = FMath::Max(1, Settings->ClipRadius);
const int32 Shrink = FMath::Max(0, RClip - 1);
const int32 ExtM = CHUNK_SIZE << TopMCLvl; // tuile MaxLevel en voxels
const FIntVector CM = VF_FloorDiv(CenterChunk, 1 << TopMCLvl); // tuile MaxLevel du joueur
NewMinX = (CM.X - Shrink) * ExtM;
NewMinY = (CM.Y - Shrink) * ExtM;
NewMaxX = (CM.X + Shrink + 1) * ExtM; // EXCLUSIF
NewMaxY = (CM.Y + Shrink + 1) * ExtM;
}
if (NewMinX != SheetHoleMinXVox || NewMinY != SheetHoleMinYVox
|| NewMaxX != SheetHoleMaxXVox || NewMaxY != SheetHoleMaxYVox)
{
for (const FVoxelTileKey& T : LoadedTiles)
{
if (T.Level <= TopMCLvl) continue; // seules les feuilles portent le trou
const int32 Ext = CHUNK_SIZE << T.Level;
const int32 TMinX = T.Coord.X * Ext, TMinY = T.Coord.Y * Ext;
const bool bOldOv = TMinX < SheetHoleMaxXVox && TMinX + Ext > SheetHoleMinXVox
&& TMinY < SheetHoleMaxYVox && TMinY + Ext > SheetHoleMinYVox;
const bool bNewOv = TMinX < NewMaxX && TMinX + Ext > NewMinX
&& TMinY < NewMaxY && TMinY + Ext > NewMinY;
if ((bOldOv || bNewOv) && IsDesired(T)) BandRemeshQueue.Add(T);
}
SheetHoleMinXVox = NewMinX; SheetHoleMinYVox = NewMinY;
SheetHoleMaxXVox = NewMaxX; SheetHoleMaxYVox = NewMaxY;
bAllChunksLoaded = false;
}
}
// Cull rules — STRICT LOAD-BEFORE-UNLOAD so crossing a shell boundary NEVER leaves a hole: // Cull rules — STRICT LOAD-BEFORE-UNLOAD so crossing a shell boundary NEVER leaves a hole:
// - out of clip range → left the view entirely, no replacement coming → cull now. // - out of clip range → left the view entirely, no replacement coming → cull now.
// - in range (mid-LOD-transition) → cull ONLY once EVERY desired tile that overlaps its // - in range (mid-LOD-transition) → cull ONLY once EVERY desired tile that overlaps its
@@ -7391,20 +7330,9 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
const int32 FullRes = Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2; const int32 FullRes = Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2;
const int32 Extent = CHUNK_SIZE << Tile.Level; const int32 Extent = CHUNK_SIZE << Tile.Level;
// F18 — sheet-ring keys follow the last MC bridge level and mesh as two heightfields const int32 Cells = (Tile.Level < FullRes)
// (GenerateSheetMesh). Their sampling step matches that MC level; cells grow with sheet extent
// up to 128 per axis, after which the step increases.
const int32 TopMC = VF_MaxMarchingCubesLevel(Settings);
const bool bSheetTile = Tile.Level > TopMC;
int32 Cells = (Tile.Level < FullRes)
? CHUNK_SIZE ? CHUNK_SIZE
: (Settings ? FMath::Clamp(Settings->CoarseTileCells, 4, CHUNK_SIZE) : 16); : (Settings ? FMath::Clamp(Settings->CoarseTileCells, 4, CHUNK_SIZE) : 16);
if (bSheetTile)
{
const int32 StepMC = FMath::Max(1, (CHUNK_SIZE << TopMC) / Cells);
Cells = FMath::Clamp(Extent / StepMC, 4, 128);
}
const int32 Step = FMath::Max(1, Extent / Cells); const int32 Step = FMath::Max(1, Extent / Cells);
const uint32 TaskEpoch = GenerationEpoch; const uint32 TaskEpoch = GenerationEpoch;
const uint32 TaskDesiredEpoch = DesiredEpoch; const uint32 TaskDesiredEpoch = DesiredEpoch;
@@ -7425,40 +7353,8 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
// below the configured minimum and all LODs in the gap (no band) mesh full. // below the configured minimum and all LODs in the gap (no band) mesh full.
int32 BandVoxLo = INT32_MIN, BandVoxHi = INT32_MAX; int32 BandVoxLo = INT32_MIN, BandVoxHi = INT32_MAX;
int32 BandChunkLo = MIN_int32, BandChunkHi = MAX_int32; int32 BandChunkLo = MIN_int32, BandChunkHi = MAX_int32;
int32 SheetChunkZ = 0;
if (bSheetTile)
{
// F18 — la feuille a besoin de la STRATE de référence (les deux heightfields sont ceux de
// la strate du joueur) : pas de bande armée (gap inter-strates, ou bounds introuvables)
// ⇒ rien à mailler, tuile vide (re-queue automatique via BandRemeshQueue en atterrissant).
if (MeshBandChunkLo == MIN_int32)
{
FChunkResult Empty;
Empty.Tile = Tile;
Empty.Epoch = TaskEpoch;
Empty.DesiredEpoch = TaskDesiredEpoch;
Empty.ModificationEpoch = TaskModificationEpoch;
Empty.RequestStartCycles = RequestStartCycles;
Empty.WorkPriority = WorkPriority;
if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical)
CriticalProcessQueue.Enqueue(MoveTemp(Empty));
else if (WorkPriority == EVoxelTileWorkPriority::Edited)
EditedProcessQueue.Enqueue(MoveTemp(Empty));
else if (WorkPriority == EVoxelTileWorkPriority::LOD0)
NearProcessQueue.Enqueue(MoveTemp(Empty));
else
ProcessQueue.Enqueue(MoveTemp(Empty));
return;
}
BandChunkLo = MeshBandChunkLo; // pour la résolution matériaux sol/cap dans ApplyMeshToTile
BandChunkHi = MeshBandChunkHi;
SheetChunkZ = MeshBandChunkLo + (MeshBandChunkHi - MeshBandChunkLo) / 2; // chunk au cœur de la strate
}
// F18 — trou XY courant (zone couverte par les coquilles MC, découpée des feuilles).
const int32 HoleMinX = SheetHoleMinXVox, HoleMinY = SheetHoleMinYVox;
const int32 HoleMaxX = SheetHoleMaxXVox, HoleMaxY = SheetHoleMaxYVox;
const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9; const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9;
if (!bSheetTile && Tile.Level >= CutMin && MeshBandChunkLo != MIN_int32) if (Tile.Level >= CutMin && MeshBandChunkLo != MIN_int32)
{ {
BandChunkLo = MeshBandChunkLo; BandChunkLo = MeshBandChunkLo;
BandChunkHi = MeshBandChunkHi; BandChunkHi = MeshBandChunkHi;
@@ -7512,7 +7408,6 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
: UE::Tasks::ETaskPriority::BackgroundNormal; : UE::Tasks::ETaskPriority::BackgroundNormal;
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture, UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi, BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
RequestStartCycles, TaskDesiredEpoch, TaskModificationEpoch, Cancellation, RequestStartCycles, TaskDesiredEpoch, TaskModificationEpoch, Cancellation,
WorkPriority]() WorkPriority]()
{ {
@@ -7565,7 +7460,6 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
Result.GenerationStartCycles = GenerationStartCycles; Result.GenerationStartCycles = GenerationStartCycles;
GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture, GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi, BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
Result, &Cancellation->bObsolete); Result, &Cancellation->bObsolete);
Result.GenerationEndCycles = GenerationStartCycles != 0 Result.GenerationEndCycles = GenerationStartCycles != 0
? FPlatformTime::Cycles64() : 0; ? FPlatformTime::Cycles64() : 0;
@@ -7611,8 +7505,6 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels, void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture, int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi, int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
bool bSheetTile, int32 SheetChunkZ,
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
FChunkResult& Result, FChunkResult& Result,
const std::atomic<bool>* ObsoleteFlag) const std::atomic<bool>* ObsoleteFlag)
{ {
@@ -7666,7 +7558,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
/ static_cast<double>(WormEligibleSamples) / static_cast<double>(WormEligibleSamples)
: 0.0; : 0.0;
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d triangles=%d ceiling_triangles=%d " TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d aborted=%d empty=%d triangles=%d ceiling_triangles=%d "
"band=(%d,%d) " "band=(%d,%d) "
"verdict=%d proof=%d air_proof=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f " "verdict=%d proof=%d air_proof=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f "
"cache_build=%.6f evaluation=%.6f " "cache_build=%.6f evaluation=%.6f "
@@ -7686,7 +7578,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
"worm_eligible=%llu worm_block_proofs=%llu worm_block_skipped=%llu " "worm_eligible=%llu worm_block_proofs=%llu worm_block_skipped=%llu "
"worm_skip_rate=%.6f"), "worm_skip_rate=%.6f"),
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells, Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells,
bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0, Result.bAborted ? 1 : 0,
Result.bEmpty ? 1 : 0, Result.NumTriangles, Result.NumCeilingTriangles, Result.bEmpty ? 1 : 0, Result.NumTriangles, Result.NumCeilingTriangles,
BandChunkLo, BandChunkHi, BandChunkLo, BandChunkHi,
ClassifyVerdict, Result.bSealedSolidProof ? 1 : 0, ClassifyVerdict, Result.bSealedSolidProof ? 1 : 0,
@@ -7825,7 +7717,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
// (Gate IsoLevel == 0 : les verdicts du classifieur supposent l'iso MC à zéro exactement.) // (Gate IsoLevel == 0 : les verdicts du classifieur supposent l'iso MC à zéro exactement.)
bool bTrivialEmpty = false; bool bTrivialEmpty = false;
if (GVoxelForgeSealedSolidProof != 0 if (GVoxelForgeSealedSolidProof != 0
&& !bSheetTile && !bWantCapture && Generator && Mesher && !bWantCapture && Generator && Mesher
&& Mesher->IsoLevel == 0.0f) && Mesher->IsoLevel == 0.0f)
{ {
TotalSealedSolidProofCandidates.fetch_add(1, std::memory_order_relaxed); TotalSealedSolidProofCandidates.fetch_add(1, std::memory_order_relaxed);
@@ -7867,7 +7759,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
} }
const int32 OuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1); const int32 OuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1);
if (!bTrivialEmpty && OuterClassifierMode != 0 if (!bTrivialEmpty && OuterClassifierMode != 0
&& !bSheetTile && !bWantCapture && Generator && Mesher && Mesher->IsoLevel == 0.0f) && !bWantCapture && Generator && Mesher && Mesher->IsoLevel == 0.0f)
{ {
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile);
INC_DWORD_STAT(STAT_VoxelForgeTilesClassified); INC_DWORD_STAT(STAT_VoxelForgeTilesClassified);
@@ -7972,8 +7864,6 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
bTrivialEmpty = (ValidatedVerdict != EVoxelTileClass::Mixed); bTrivialEmpty = (ValidatedVerdict != EVoxelTileClass::Mixed);
} }
// F18 — feuille : deux heightfields sol/cap échantillonnés par colonne (pas de marching
// cubes, pas de classifieur — la classe de surface est vraie par construction).
// `TilesMeshed` peut dépasser `TilesClassified` : les tuiles qui ratent cette porte sont // `TilesMeshed` peut dépasser `TilesClassified` : les tuiles qui ratent cette porte sont
// maillées sans classification. / `TilesMeshed` may exceed `TilesClassified`: tiles that // maillées sans classification. / `TilesMeshed` may exceed `TilesClassified`: tiles that
// fail this gate are meshed without classification. // fail this gate are meshed without classification.
@@ -7982,10 +7872,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
{ {
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GenerateMesh); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GenerateMesh);
const double MeshStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0; const double MeshStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0;
MeshData = bSheetTile MeshData = Mesher->GenerateMesh(OriginVoxels, Step, Cells,
? Mesher->GenerateSheetMesh(OriginVoxels, Step, Cells, SheetChunkZ,
HoleMinX, HoleMinY, HoleMaxX, HoleMaxY)
: Mesher->GenerateMesh(OriginVoxels, Step, Cells,
bWantCapture ? &Result.CaptureGrid : nullptr, bWantCapture ? &Result.CaptureGrid : nullptr,
BandVoxLo, BandVoxHi, BandVoxLo, BandVoxHi,
&Result.MesherDensityCalls); &Result.MesherDensityCalls);
@@ -8041,7 +7928,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
// Parity instrumentation observes the same FVoxelMeshData that the runtime stream builder // Parity instrumentation observes the same FVoxelMeshData that the runtime stream builder
// consumes. It is intentionally after the abort fence, so a canceled worker cannot publish a // consumes. It is intentionally after the abort fence, so a canceled worker cannot publish a
// partial identity, and it records empty/proof tiles as well as visible meshes. // partial identity, and it records empty/proof tiles as well as visible meshes.
RecordTileHash(Tile, OriginVoxels, Step, Cells, BandChunkLo, BandChunkHi, bSheetTile, MeshData); RecordTileHash(Tile, OriginVoxels, Step, Cells, BandChunkLo, BandChunkHi, MeshData);
Result.ClassifySeconds = ClassifySeconds; Result.ClassifySeconds = ClassifySeconds;
Result.MeshSeconds = MeshSeconds; Result.MeshSeconds = MeshSeconds;
@@ -78,35 +78,6 @@ public:
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX, int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
int64* OutDensitySampleCount = nullptr); int64* OutDensitySampleCount = nullptr);
/**
* F18 — FEUILLE de champ lointain (anneau render-distance, cf. UVoxelSettings::bFarSheetRing).
* Dans une strate ouverte (SurfaceWorld) le champ lointain est exactement DEUX heightfields —
* TerrainZ (sol) et CeilSurf (plafond sky-cap), déjà calculés par colonne par l'oracle
* GetSurfaceHeightAt. On construit donc deux grilles déplacées régulières au lieu d'un marching
* cubes 3D : sol = polygroup 0, cap = polygroup 1 (classes vraies PAR CONSTRUCTION — pas de vote,
* pas de sonde de classification), mêmes conventions que GenerateMesh (positions monde cm, UVs
* planaires, masques couleur F6 biome/pente/fondu, normales du gradient de hauteur, jupes
* périmètre par seau, run d'indices sol‖cap + NumCeilingTriangles).
*
* @param OriginVoxels - Coin min de la tuile (voxels). Seul XY est utilisé (les hauteurs sont absolues).
* @param StepXY - Pas d'échantillonnage XY en voxels (aligné sur l'anneau MC pour la continuité).
* @param CellsXY - Cellules par axe XY (extent = CellsXY × StepXY).
* @param StrateChunkZ - Chunk Z DANS la strate de référence (le cœur de la bande) — identifie la
* strate dont on maille sol+cap. Hors SurfaceWorld ⇒ mesh vide.
* @param HoleMin/MaxX/YVox - TROU XY (voxels, Max EXCLUSIF ; sentinelles MAX/MIN = pas de trou) :
* les cellules ENTIÈREMENT dans ce rectangle (la zone couverte par les
* coquilles MC autour du joueur) sont sautées — sinon la feuille recouvre
* le terrain proche avec son échantillonnage grossier. Les cellules à
* cheval restent (anneau de recouvrement au raccord) ; pas de jupe sur
* les bords du trou (le terrain MC remplit derrière).
* Non couvert (accepté, cf. fable-idea F18) : passages/spine/chasms creusés (le heightfield pur ne
* les contient pas), diff layer — invisibles à distance de feuille, l'anneau MC proche les garde.
*/
FVoxelMeshData GenerateSheetMesh(FIntVector OriginVoxels, int32 StepXY, int32 CellsXY,
int32 StrateChunkZ,
int32 HoleMinXVox = INT32_MAX, int32 HoleMinYVox = INT32_MAX,
int32 HoleMaxXVox = INT32_MIN, int32 HoleMaxYVox = INT32_MIN);
//========================================================================= //=========================================================================
// SERVICES (injectés par AVoxelWorld) // SERVICES (injectés par AVoxelWorld)
//========================================================================= //=========================================================================
+4 -13
View File
@@ -183,22 +183,13 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0"))
int32 RenderDistanceChunks = 768; int32 RenderDistanceChunks = 768;
// F18 — the render-distance ring streams per-surface SHEETS instead of MC tiles: in an open // UNUSED: the far sheet ring was removed from the runtime; the render-distance ring is always
// strate the far field is exactly two heightfields (TerrainZ + sky-cap CeilSurf, both already // marching-cubes tiles at MaxClipLevel. Kept only so saved assets and the test harness's
// computed per column), so each far tile becomes two displaced grids (ground polygroup 0 / // `voxel.TestFarSheetRing` switch still load; it has no effect.
// cap polygroup 1 — same materials), ~3-6× cheaper to generate and far fewer components (one
// sheet spans 2^FarSheetSpanLevels MC-tile footprints per axis). Non-open strates produce
// empty sheets (their far ring was enclosed rock anyway). Carved features (passages, chasms,
// spine) don't show at sheet distance. Needs the strate band armed (StrateContentCutMinLevel
// active); in the inter-strate gap the sheet ring blanks until you land. The ship default is OFF:
// the ring stays real MC tiles at level MaxClipLevel (pre-F18 behaviour). Only matters when
// RenderDistanceChunks > 0.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
bool bFarSheetRing = false; bool bFarSheetRing = false;
// Sheet tile size = MaxClipLevel + this many levels (2 → one sheet covers 4×4 MC-tile // UNUSED (see bFarSheetRing). Kept so saved assets still load; it has no effect.
// footprints → ~16× fewer far components). Sampling density stays that of the MaxClipLevel
// MC ring (cell count grows instead), capped at 128 cells/axis (beyond, cells coarsen).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "1", ClampMax = "4", EditCondition = "bFarSheetRing")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "1", ClampMax = "4", EditCondition = "bFarSheetRing"))
int32 FarSheetSpanLevels = 2; int32 FarSheetSpanLevels = 2;
+1 -1
View File
@@ -13,7 +13,7 @@ namespace VoxelForgeStartupTrace
int32 TileY = 0; int32 TileY = 0;
int32 TileZ = 0; int32 TileZ = 0;
int32 Level = -1; int32 Level = -1;
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (sheet/early path) int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (early path)
bool bSealedSolidProof = false; bool bSealedSolidProof = false;
bool bEmpty = true; bool bEmpty = true;
int32 Vertices = 0; int32 Vertices = 0;
+3 -16
View File
@@ -160,7 +160,6 @@ struct FVoxelTileHashDumpRecord
int32 Cells = 0; int32 Cells = 0;
int32 BandChunkLo = MIN_int32; int32 BandChunkLo = MIN_int32;
int32 BandChunkHi = MAX_int32; int32 BandChunkHi = MAX_int32;
bool bSheetTile = false;
bool bEmpty = true; bool bEmpty = true;
int32 NumTriangles = 0; int32 NumTriangles = 0;
FString GeometryHash; FString GeometryHash;
@@ -567,7 +566,7 @@ private:
void LogReadyTransitionVisibilityMeasure(); void LogReadyTransitionVisibilityMeasure();
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels, void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi, int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
bool bSheetTile, const FVoxelMeshData& MeshData); const FVoxelMeshData& MeshData);
void WriteTileHashDump(); void WriteTileHashDump();
// Packed shader params, recomputed each Tick. ALL meaningful data is in .xyz — a material Vector // Packed shader params, recomputed each Tick. ALL meaningful data is in .xyz — a material Vector
@@ -761,7 +760,7 @@ public:
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon); EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon);
/** /**
* Worker-side gen for one tile: classify → GenerateMesh/GenerateSheetMesh → BuildTileStreamSet. * Worker-side gen for one tile: classify → GenerateMesh → BuildTileStreamSet.
* Fills Result (no enqueue, no bookkeeping). Called from the async ChunkGen task AND from the * Fills Result (no enqueue, no bookkeeping). Called from the async ChunkGen task AND from the
* synchronous carve path (SyncRemeshTile) — reads Generator/Mesher only, so it's safe on either * synchronous carve path (SyncRemeshTile) — reads Generator/Mesher only, so it's safe on either
* thread. See LoadTile for how the parameters are derived. * thread. See LoadTile for how the parameters are derived.
@@ -769,8 +768,6 @@ public:
void GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels, void GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture, int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi, int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
bool bSheetTile, int32 SheetChunkZ,
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
FChunkResult& Result, FChunkResult& Result,
const std::atomic<bool>* ObsoleteFlag = nullptr); const std::atomic<bool>* ObsoleteFlag = nullptr);
@@ -933,15 +930,6 @@ public:
// result cannot publish and the post-carve result is admitted immediately. // result cannot publish and the post-carve result is admitted immediately.
TSet<FVoxelTileKey> DirtyRemeshQueue; TSet<FVoxelTileKey> DirtyRemeshQueue;
// F18 — TROU XY de l'anneau feuille (voxels ; Max EXCLUSIF ; sentinelles MAX/MIN = pas de
// trou) : la zone couverte par les coquilles MC (boîte niveau-MaxClipLevel autour du joueur,
// rétrécie d'une tuile pour garder un anneau de recouvrement au raccord) est DÉCOUPÉE des
// feuilles — sinon une feuille partiellement couverte recouvre le terrain proche avec son
// échantillonnage grossier. Mis à jour par crossing ; changement ⇒ re-queue des feuilles
// chevauchantes via BandRemeshQueue (re-gen en place).
int32 SheetHoleMinXVox = MAX_int32, SheetHoleMinYVox = MAX_int32;
int32 SheetHoleMaxXVox = MIN_int32, SheetHoleMaxYVox = MIN_int32;
// Set to true during EndPlay — async tasks check this before accessing UObjects // Set to true during EndPlay — async tasks check this before accessing UObjects
std::atomic<bool> bShuttingDown{false}; std::atomic<bool> bShuttingDown{false};
@@ -1018,8 +1006,7 @@ public:
TArray<FReadyTransitionVisibilitySample> ReadyTransitionVisibilitySamples; TArray<FReadyTransitionVisibilitySample> ReadyTransitionVisibilitySamples;
TArray<FFarFieldSubmitSample> FarFieldSubmitSamples; TArray<FFarFieldSubmitSample> FarFieldSubmitSamples;
// Clean performance accounting. LOD 0..8 covers every marching-cubes clip level; sheet tiles // Clean performance accounting. LOD 0..8 covers every marching-cubes clip level.
// above the configured MC range never enter the outer classifier.
static constexpr int32 TrackedClassifierLODCount = 9; static constexpr int32 TrackedClassifierLODCount = 9;
std::atomic<uint64> TotalWorkerGenerationCycles{0}; std::atomic<uint64> TotalWorkerGenerationCycles{0};
std::atomic<uint64> TotalWorkerGenerationTasks{0}; std::atomic<uint64> TotalWorkerGenerationTasks{0};