#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(FMath::Clamp(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(0, Radius); const int64 SafeAlignment = FMath::Max(1, Alignment); const int64 BaseMin = static_cast(CenterAxis) - SafeRadius; const int64 BaseEnd = static_cast(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(SafeLevel); const int64 Numerator = static_cast(Parameters.RenderDistanceChunks) + Scale - 1; const int64 DesiredRadius = Numerator / Scale; return static_cast(FMath::Clamp( FMath::Max(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(CenterChunkZ) - ViewDistanceDown); OutMaxChunkZ = ClampInt32(static_cast(CenterChunkZ) + ViewDistanceUp); if (bHasStrate) { OutMinChunkZ = ClampInt32(static_cast(StrateBottomChunkZ) - StrateViewMarginChunks); OutMaxChunkZ = ClampInt32(static_cast(StrateTopChunkZ) + StrateViewMarginChunks); } } FORCEINLINE void Build(const FIntVector& Center, int32 MinChunkZ, int32 MaxChunkZ, const FParameters& Parameters, TArray& 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(uint32{1} << static_cast(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(TileMinZ, FloorDiv(MinChunkZ, Pow)); TileMaxZ = FMath::Min(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(X), static_cast(Y), static_cast(Z)); const int64 TileBottomChunkZ = static_cast(Tile.Z) * Pow; const int64 TileTopChunkZ = (static_cast(Tile.Z) + 1) * Pow - 1; if (TileTopChunkZ < MinChunkZ || TileBottomChunkZ > MaxChunkZ) { continue; } if (Level > 0) { const bool bCovered = (2 * static_cast(Tile.X) >= PreviousRingMin.X) && (2 * static_cast(Tile.X) + 1 <= PreviousRingMax.X) && (2 * static_cast(Tile.Y) >= PreviousRingMin.Y) && (2 * static_cast(Tile.Y) + 1 <= PreviousRingMax.Y) && (2 * static_cast(Tile.Z) >= PreviousRingMin.Z) && (2 * static_cast(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& OutEntering, TArray& 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(uint32{1} << static_cast(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(ChildRingMax.X) - 1, 2)), ClampInt32(FloorDivWide(static_cast(ChildRingMax.Y) - 1, 2)), ClampInt32(FloorDivWide(static_cast(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(X), static_cast(Y), static_cast(Z))); } }; for (int32 Level = 0; Level <= MaxLevel; ++Level) { TArray> 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> 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); } }); } } } }