BuildDelta emits entering/leaving slabs per LOD (incl. coarser-ring coverage changes); cached Core key set updated from the delta at one-tile crossings, full rebuild for larger jumps or LOD>=4 changes. Equality audit vs full rebuild (45/45 crossings, 20/20 sampled; injected mismatch asserts) + 630 automation cases. 30 m/s UE, 5 pairs: BuildDesiredTiles p95 7.80 -> 4.08 ms, p99 11.46 -> 7.97 ms, worst tick 13.73 -> 12.11 ms (all pairs), near and forward readiness and arrival better in all pairs; far holes better in 4/5 (pair 5 +0.29 pp). Geometry unchanged. Source/: VoxelClipmapDesiredTiles.h, VoxelWorld.cpp, VoxelWorld.h, Tests/VoxelForgeClipmapNoOverlapTest.cpp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
400 lines
17 KiB
C++
400 lines
17 KiB
C++
#pragma once
|
|
|
|
#include "VoxelTypes.h"
|
|
|
|
namespace VoxelClipmapDesiredTiles
|
|
{
|
|
FORCEINLINE int64 FloorDivWide(int64 Value, int64 Divisor)
|
|
{
|
|
if (Divisor <= 0) { return 0; }
|
|
const int64 Quotient = Value / Divisor;
|
|
const int64 Remainder = Value % Divisor;
|
|
return Remainder < 0 ? Quotient - 1 : Quotient;
|
|
}
|
|
|
|
FORCEINLINE int64 CeilDivWide(int64 Value, int64 Divisor)
|
|
{
|
|
if (Divisor <= 0) { return 0; }
|
|
const int64 Quotient = Value / Divisor;
|
|
const int64 Remainder = Value % Divisor;
|
|
return Remainder > 0 ? Quotient + 1 : Quotient;
|
|
}
|
|
|
|
FORCEINLINE int32 ClampInt32(int64 Value)
|
|
{
|
|
return static_cast<int32>(FMath::Clamp<int64>(Value, MIN_int32, MAX_int32));
|
|
}
|
|
|
|
struct FParameters
|
|
{
|
|
int32 ClipRadius = 3;
|
|
int32 MaxClipLevel = 4;
|
|
int32 RenderDistanceChunks = 0;
|
|
};
|
|
|
|
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
|
|
{
|
|
if (Divisor <= 0) { return 0; }
|
|
const int32 Quotient = Value / Divisor;
|
|
const int32 Remainder = Value % Divisor;
|
|
return Remainder < 0 ? Quotient - 1 : Quotient;
|
|
}
|
|
|
|
FORCEINLINE FIntVector FloorDiv(const FIntVector& Value, int32 Divisor)
|
|
{
|
|
return FIntVector(FloorDiv(Value.X, Divisor), FloorDiv(Value.Y, Divisor),
|
|
FloorDiv(Value.Z, Divisor));
|
|
}
|
|
|
|
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
|
|
{
|
|
if (Divisor <= 0) { return 0; }
|
|
const int32 Quotient = Value / Divisor;
|
|
const int32 Remainder = Value % Divisor;
|
|
return Remainder > 0 ? Quotient + 1 : Quotient;
|
|
}
|
|
|
|
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
|
|
FIntVector& OutMin, FIntVector& OutMax)
|
|
{
|
|
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
|
int32& OutAxisMin, int32& OutAxisMax)
|
|
{
|
|
const int64 SafeRadius = FMath::Max<int64>(0, Radius);
|
|
const int64 SafeAlignment = FMath::Max<int64>(1, Alignment);
|
|
const int64 BaseMin = static_cast<int64>(CenterAxis) - SafeRadius;
|
|
const int64 BaseEnd = static_cast<int64>(CenterAxis) + SafeRadius + 1;
|
|
OutAxisMin = ClampInt32(FloorDivWide(BaseMin, SafeAlignment) * SafeAlignment);
|
|
OutAxisMax = ClampInt32(CeilDivWide(BaseEnd, SafeAlignment) * SafeAlignment - 1);
|
|
};
|
|
AlignAxis(Center.X, OutMin.X, OutMax.X);
|
|
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
|
AlignAxis(Center.Z, OutMin.Z, OutMax.Z);
|
|
}
|
|
|
|
FORCEINLINE int32 OuterShellRadius(const FParameters& Parameters, int32 Radius, int32 MaxLevel)
|
|
{
|
|
if (Parameters.RenderDistanceChunks <= 0)
|
|
{
|
|
return Radius;
|
|
}
|
|
const int32 SafeLevel = FMath::Clamp(MaxLevel, 0, 8);
|
|
const int64 Scale = int64{1} << static_cast<uint32>(SafeLevel);
|
|
const int64 Numerator = static_cast<int64>(Parameters.RenderDistanceChunks)
|
|
+ Scale - 1;
|
|
const int64 DesiredRadius = Numerator / Scale;
|
|
return static_cast<int32>(FMath::Clamp<int64>(
|
|
FMath::Max<int64>(Radius, DesiredRadius), 0, MAX_int32));
|
|
}
|
|
|
|
FORCEINLINE void OuterShell(const FParameters& Parameters, int32 Radius, int32 MaxLevel,
|
|
int32& OutLevel, int32& OutRadius)
|
|
{
|
|
OutLevel = MaxLevel;
|
|
OutRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
|
|
}
|
|
|
|
FORCEINLINE void ResolveVerticalBounds(
|
|
int32 CenterChunkZ, int32 ViewDistanceUp, int32 ViewDistanceDown,
|
|
int32 StrateViewMarginChunks, bool bClampToStrate, bool bHasStrate,
|
|
int32 StrateTopChunkZ, int32 StrateBottomChunkZ,
|
|
int32& OutMinChunkZ, int32& OutMaxChunkZ)
|
|
{
|
|
OutMinChunkZ = MIN_int32;
|
|
OutMaxChunkZ = MAX_int32;
|
|
if (!bClampToStrate)
|
|
{
|
|
return;
|
|
}
|
|
|
|
OutMinChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) - ViewDistanceDown);
|
|
OutMaxChunkZ = ClampInt32(static_cast<int64>(CenterChunkZ) + ViewDistanceUp);
|
|
if (bHasStrate)
|
|
{
|
|
OutMinChunkZ = ClampInt32(static_cast<int64>(StrateBottomChunkZ) - StrateViewMarginChunks);
|
|
OutMaxChunkZ = ClampInt32(static_cast<int64>(StrateTopChunkZ) + StrateViewMarginChunks);
|
|
}
|
|
}
|
|
|
|
FORCEINLINE void Build(const FIntVector& Center, int32 MinChunkZ, int32 MaxChunkZ,
|
|
const FParameters& Parameters, TArray<FVoxelTileKey>& OutTiles)
|
|
{
|
|
OutTiles.Reset();
|
|
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
|
|
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
|
|
const int32 OuterRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
|
|
FIntVector PreviousRingMin = FIntVector::ZeroValue;
|
|
FIntVector PreviousRingMax = FIntVector::ZeroValue;
|
|
|
|
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
|
{
|
|
const int32 Pow = static_cast<int32>(uint32{1} << static_cast<uint32>(Level));
|
|
const FIntVector CenterLevel = FloorDiv(Center, Pow);
|
|
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
|
|
|
// Every finer shell must end on a complete child-pair boundary so a coarser tile is
|
|
// either wholly inside it or wholly outside it. The outermost ring has no coarser
|
|
// neighbour and needs no alignment.
|
|
const int32 RingAlignment = Level < MaxLevel ? 2 : 1;
|
|
FIntVector RingMin, RingMax;
|
|
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
|
|
|
int64 TileMinZ = RingMin.Z;
|
|
int64 TileMaxZ = RingMax.Z;
|
|
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
|
{
|
|
TileMinZ = FMath::Max<int64>(TileMinZ, FloorDiv(MinChunkZ, Pow));
|
|
TileMaxZ = FMath::Min<int64>(TileMaxZ, FloorDiv(MaxChunkZ, Pow));
|
|
}
|
|
|
|
for (int64 Z = TileMinZ; Z <= TileMaxZ; ++Z)
|
|
for (int64 Y = RingMin.Y; Y <= RingMax.Y; ++Y)
|
|
for (int64 X = RingMin.X; X <= RingMax.X; ++X)
|
|
{
|
|
const FIntVector Tile(static_cast<int32>(X), static_cast<int32>(Y),
|
|
static_cast<int32>(Z));
|
|
const int64 TileBottomChunkZ = static_cast<int64>(Tile.Z) * Pow;
|
|
const int64 TileTopChunkZ = (static_cast<int64>(Tile.Z) + 1) * Pow - 1;
|
|
if (TileTopChunkZ < MinChunkZ || TileBottomChunkZ > MaxChunkZ)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (Level > 0)
|
|
{
|
|
const bool bCovered =
|
|
(2 * static_cast<int64>(Tile.X) >= PreviousRingMin.X)
|
|
&& (2 * static_cast<int64>(Tile.X) + 1 <= PreviousRingMax.X)
|
|
&& (2 * static_cast<int64>(Tile.Y) >= PreviousRingMin.Y)
|
|
&& (2 * static_cast<int64>(Tile.Y) + 1 <= PreviousRingMax.Y)
|
|
&& (2 * static_cast<int64>(Tile.Z) >= PreviousRingMin.Z)
|
|
&& (2 * static_cast<int64>(Tile.Z) + 1 <= PreviousRingMax.Z);
|
|
if (bCovered)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
OutTiles.Emplace(Tile, Level);
|
|
}
|
|
|
|
PreviousRingMin = RingMin;
|
|
PreviousRingMax = RingMax;
|
|
}
|
|
}
|
|
|
|
// Compute the exact key difference between two adjacent clipmap centers without visiting
|
|
// unchanged ring cells. A coarser ring can change membership when its finer child ring moves,
|
|
// even when the coarser ring bounds themselves stay fixed; the cover-box delta below includes
|
|
// those boundary keys as well.
|
|
FORCEINLINE void BuildDelta(
|
|
const FIntVector& PreviousCenter, int32 PreviousMinChunkZ, int32 PreviousMaxChunkZ,
|
|
const FIntVector& Center, int32 MinChunkZ, int32 MaxChunkZ,
|
|
const FParameters& Parameters,
|
|
TArray<FVoxelTileKey>& OutEntering, TArray<FVoxelTileKey>& OutLeaving)
|
|
{
|
|
struct FBox
|
|
{
|
|
FIntVector Min = FIntVector::ZeroValue;
|
|
FIntVector Max = FIntVector::ZeroValue;
|
|
bool bValid = false;
|
|
};
|
|
OutEntering.Reset();
|
|
OutLeaving.Reset();
|
|
|
|
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
|
|
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
|
|
const int32 OuterRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
|
|
FBox PreviousValid[9];
|
|
FBox CurrentValid[9];
|
|
FBox PreviousCovered[9];
|
|
FBox CurrentCovered[9];
|
|
const auto BuildRings = [&](const FIntVector& RingCenter, int32 RingMinChunkZ,
|
|
int32 RingMaxChunkZ, FBox* OutValid, FBox* OutCovered)
|
|
{
|
|
FIntVector ChildRingMin = FIntVector::ZeroValue;
|
|
FIntVector ChildRingMax = FIntVector::ZeroValue;
|
|
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
|
{
|
|
const int32 Pow = static_cast<int32>(uint32{1} << static_cast<uint32>(Level));
|
|
const FIntVector CenterLevel = FloorDiv(RingCenter, Pow);
|
|
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
|
|
const int32 RingAlignment = Level < MaxLevel ? 2 : 1;
|
|
FIntVector RingMin, RingMax;
|
|
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
|
FBox Valid;
|
|
Valid.Min = RingMin;
|
|
Valid.Max = RingMax;
|
|
Valid.Min.Z = FMath::Max(Valid.Min.Z, FloorDiv(RingMinChunkZ, Pow));
|
|
Valid.Max.Z = FMath::Min(Valid.Max.Z, FloorDiv(RingMaxChunkZ, Pow));
|
|
Valid.bValid = Valid.Min.X <= Valid.Max.X
|
|
&& Valid.Min.Y <= Valid.Max.Y && Valid.Min.Z <= Valid.Max.Z;
|
|
OutValid[Level] = Valid;
|
|
|
|
FBox Covered;
|
|
if (Level > 0)
|
|
{
|
|
Covered.Min = FIntVector(
|
|
CeilDiv(ChildRingMin.X, 2), CeilDiv(ChildRingMin.Y, 2),
|
|
CeilDiv(ChildRingMin.Z, 2));
|
|
Covered.Max = FIntVector(
|
|
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.X) - 1, 2)),
|
|
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.Y) - 1, 2)),
|
|
ClampInt32(FloorDivWide(static_cast<int64>(ChildRingMax.Z) - 1, 2)));
|
|
Covered.bValid = Covered.Min.X <= Covered.Max.X
|
|
&& Covered.Min.Y <= Covered.Max.Y && Covered.Min.Z <= Covered.Max.Z;
|
|
}
|
|
OutCovered[Level] = Covered;
|
|
ChildRingMin = RingMin;
|
|
ChildRingMax = RingMax;
|
|
}
|
|
};
|
|
BuildRings(PreviousCenter, PreviousMinChunkZ, PreviousMaxChunkZ,
|
|
PreviousValid, PreviousCovered);
|
|
BuildRings(Center, MinChunkZ, MaxChunkZ, CurrentValid, CurrentCovered);
|
|
|
|
const auto Intersect = [](const FBox& A, const FBox& B)
|
|
{
|
|
FBox Result;
|
|
if (!A.bValid || !B.bValid) return Result;
|
|
Result.Min = FIntVector(
|
|
FMath::Max(A.Min.X, B.Min.X), FMath::Max(A.Min.Y, B.Min.Y),
|
|
FMath::Max(A.Min.Z, B.Min.Z));
|
|
Result.Max = FIntVector(
|
|
FMath::Min(A.Max.X, B.Max.X), FMath::Min(A.Max.Y, B.Max.Y),
|
|
FMath::Min(A.Max.Z, B.Max.Z));
|
|
Result.bValid = Result.Min.X <= Result.Max.X
|
|
&& Result.Min.Y <= Result.Max.Y && Result.Min.Z <= Result.Max.Z;
|
|
return Result;
|
|
};
|
|
const auto AppendDifference = [&Intersect](const FBox& A, const FBox& B, auto& OutBoxes)
|
|
{
|
|
if (!A.bValid) return;
|
|
const FBox Common = Intersect(A, B);
|
|
if (!Common.bValid)
|
|
{
|
|
OutBoxes.Add(A);
|
|
return;
|
|
}
|
|
|
|
// Six disjoint slabs cover A minus Common. The later slabs are narrowed to the
|
|
// common extent on each already-partitioned axis, so no key is emitted twice.
|
|
FBox Slab = A;
|
|
if (A.Min.X < Common.Min.X)
|
|
{
|
|
Slab.Max.X = Common.Min.X - 1;
|
|
Slab.bValid = Slab.Min.X <= Slab.Max.X;
|
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
}
|
|
Slab = A;
|
|
if (Common.Max.X < A.Max.X)
|
|
{
|
|
Slab.Min.X = Common.Max.X + 1;
|
|
Slab.bValid = Slab.Min.X <= Slab.Max.X;
|
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
}
|
|
|
|
Slab = A;
|
|
Slab.Min.X = Common.Min.X;
|
|
Slab.Max.X = Common.Max.X;
|
|
if (A.Min.Y < Common.Min.Y)
|
|
{
|
|
Slab.Max.Y = Common.Min.Y - 1;
|
|
Slab.bValid = Slab.Min.Y <= Slab.Max.Y;
|
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
}
|
|
Slab = A;
|
|
Slab.Min.X = Common.Min.X;
|
|
Slab.Max.X = Common.Max.X;
|
|
if (Common.Max.Y < A.Max.Y)
|
|
{
|
|
Slab.Min.Y = Common.Max.Y + 1;
|
|
Slab.bValid = Slab.Min.Y <= Slab.Max.Y;
|
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
}
|
|
|
|
Slab = A;
|
|
Slab.Min.X = Common.Min.X;
|
|
Slab.Max.X = Common.Max.X;
|
|
Slab.Min.Y = Common.Min.Y;
|
|
Slab.Max.Y = Common.Max.Y;
|
|
if (A.Min.Z < Common.Min.Z)
|
|
{
|
|
Slab.Max.Z = Common.Min.Z - 1;
|
|
Slab.bValid = Slab.Min.Z <= Slab.Max.Z;
|
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
}
|
|
Slab = A;
|
|
Slab.Min.X = Common.Min.X;
|
|
Slab.Max.X = Common.Max.X;
|
|
Slab.Min.Y = Common.Min.Y;
|
|
Slab.Max.Y = Common.Max.Y;
|
|
if (Common.Max.Z < A.Max.Z)
|
|
{
|
|
Slab.Min.Z = Common.Max.Z + 1;
|
|
Slab.bValid = Slab.Min.Z <= Slab.Max.Z;
|
|
if (Slab.bValid) OutBoxes.Add(Slab);
|
|
}
|
|
};
|
|
const auto Contains = [](const FBox& Box, const FIntVector& Point)
|
|
{
|
|
return Box.bValid && Point.X >= Box.Min.X && Point.X <= Box.Max.X
|
|
&& Point.Y >= Box.Min.Y && Point.Y <= Box.Max.Y
|
|
&& Point.Z >= Box.Min.Z && Point.Z <= Box.Max.Z;
|
|
};
|
|
const auto ForEachBox = [](const FBox& Box, const auto& Callback)
|
|
{
|
|
if (!Box.bValid) return;
|
|
for (int64 Z = Box.Min.Z; Z <= Box.Max.Z; ++Z)
|
|
for (int64 Y = Box.Min.Y; Y <= Box.Max.Y; ++Y)
|
|
for (int64 X = Box.Min.X; X <= Box.Max.X; ++X)
|
|
{
|
|
Callback(FIntVector(static_cast<int32>(X), static_cast<int32>(Y),
|
|
static_cast<int32>(Z)));
|
|
}
|
|
};
|
|
|
|
for (int32 Level = 0; Level <= MaxLevel; ++Level)
|
|
{
|
|
TArray<FBox, TInlineAllocator<16>> ChangedBoxes;
|
|
AppendDifference(CurrentValid[Level], PreviousValid[Level], ChangedBoxes);
|
|
AppendDifference(PreviousValid[Level], CurrentValid[Level], ChangedBoxes);
|
|
|
|
if (Level > 0)
|
|
{
|
|
const FBox CommonValid = Intersect(CurrentValid[Level], PreviousValid[Level]);
|
|
if (CommonValid.bValid)
|
|
{
|
|
TArray<FBox, TInlineAllocator<16>> CoverDifferences;
|
|
AppendDifference(CurrentCovered[Level], PreviousCovered[Level], CoverDifferences);
|
|
AppendDifference(PreviousCovered[Level], CurrentCovered[Level], CoverDifferences);
|
|
for (const FBox& CoverDifference : CoverDifferences)
|
|
{
|
|
const FBox ChangedCover = Intersect(CoverDifference, CommonValid);
|
|
if (ChangedCover.bValid) ChangedBoxes.Add(ChangedCover);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const FBox& ChangedBox : ChangedBoxes)
|
|
{
|
|
ForEachBox(ChangedBox, [&](const FIntVector& Coord)
|
|
{
|
|
const bool bCurrentMember = Contains(CurrentValid[Level], Coord)
|
|
&& !Contains(CurrentCovered[Level], Coord);
|
|
const bool bPreviousMember = Contains(PreviousValid[Level], Coord)
|
|
&& !Contains(PreviousCovered[Level], Coord);
|
|
if (bCurrentMember && !bPreviousMember)
|
|
{
|
|
OutEntering.Emplace(Coord, Level);
|
|
}
|
|
else if (bPreviousMember && !bCurrentMember)
|
|
{
|
|
OutLeaving.Emplace(Coord, Level);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|