// Copyright 2020 Phyronnaz #pragma once #include "CoreMinimal.h" #include "VoxelMinimal.h" #include "VoxelEnums.h" #include "VoxelIntBox.h" #include "VoxelContainers/VoxelStaticArray.h" #include "VoxelUtilities/VoxelIntVectorUtilities.h" namespace FVoxelUtilities { #define CHECK_CHUNK_SIZE() static_assert(FVoxelUtilities::IsPowerOfTwo(ChunkSize), "ChunkSize must be a power of 2") // Get required depth such that ChunkSize << Depth >= Size template inline int32 GetDepthFromSize(uint32 Size) { CHECK_CHUNK_SIZE(); if (Size <= 0) { return 0; } else { const int32 Depth = 31 - FPlatformMath::CountLeadingZeros(Size / ChunkSize); if (ChunkSize << Depth == Size) { return Depth; } else { return Depth + 1; } } } template inline constexpr uint32 GetSizeFromDepth(int32 Depth) { CHECK_CHUNK_SIZE(); return ChunkSize << Depth; } template inline int32 GetDepthFromBounds(const FVoxelIntBox& Bounds) { CHECK_CHUNK_SIZE(); return GetDepthFromSize(Bounds.Size().GetMax()); } template inline FVoxelIntBox GetBoundsFromDepth(int32 Depth) { CHECK_CHUNK_SIZE(); const FIntVector Size = FIntVector((ChunkSize << Depth) / 2); return FVoxelIntBox(-Size, Size); } template inline FVoxelIntBox GetCustomBoundsForDepth(FVoxelIntBox Bounds, int32 Depth) { CHECK_CHUNK_SIZE(); Bounds = Bounds.MakeMultipleOfBigger(ChunkSize); Bounds = FVoxelUtilities::GetBoundsFromDepth(Depth).Overlap(Bounds); check(Bounds.IsMultipleOf(ChunkSize)); return Bounds; } template inline FVoxelIntBox GetBoundsFromPositionAndDepth(const FIntVector& Position, int32 Depth) { CHECK_CHUNK_SIZE(); return FVoxelIntBox(Position, Position + FIntVector(ChunkSize << Depth)); } // Valid for root node only template inline int32 GetOctreeDepthContainingBounds(const FVoxelIntBox& Bounds) { CHECK_CHUNK_SIZE(); const uint32 Max = FMath::Max(FVoxelUtilities::Abs(Bounds.Min).GetMax(), FVoxelUtilities::Abs(Bounds.Max).GetMax()); return GetDepthFromSize(2 * Max); // 2x: octree doesn't start at 0 0 0 } template inline constexpr int32 ConvertDepth(int32 Depth) { static_assert(IsPowerOfTwo(FromChunkSize), "FromChunkSize must be a power of 2"); static_assert(IsPowerOfTwo(ToChunkSize), "ToChunkSize must be a power of 2"); if (FromChunkSize == ToChunkSize) { return Depth; } else if (FromChunkSize < ToChunkSize) { // Depth should be lower return Depth - IntLog2(ToChunkSize / FromChunkSize); } else { // FromChunkSize > ToChunkSize // Depth should be higher return Depth + IntLog2(FromChunkSize / ToChunkSize); } } template inline int32 ClampDepth(int32 Depth) { CHECK_CHUNK_SIZE(); constexpr int32 ChunkSizeDepth = IntLog2(ChunkSize); // In theory MaxDepth could be 31 // To avoid overflows when doing math we use 30 constexpr int32 MaxDepth = 30; // ChunkSizeDepth + Depth <= MaxDepth // Depth <= MaxDepth - ChunkSizeDepth return FMath::Clamp(Depth, 0, MaxDepth - ChunkSizeDepth); } #undef CHECK_CHUNK_SIZE inline int32 ClampMesherDepth(int32 Depth) { // 2x: Bounds.Size() needs to fit in a int32 for Meshers return ClampDepth<2 * RENDER_CHUNK_SIZE>(Depth); } template inline T MergeAsset(T A, T B, bool bSubtractiveAsset) { return bSubtractiveAsset ? FMath::Max(A, B) : FMath::Min(A, B); } // Falloff: between 0 and 1 inline v_flt RoundCylinder(const FVoxelVector& PositionRelativeToCenter, v_flt Radius, v_flt Height, v_flt Falloff) { const v_flt InternalRadius = Radius * (1.f - Falloff); const v_flt ExternalRadius = Radius * Falloff; const v_flt DistanceToCenterXY = FVector2D(PositionRelativeToCenter.X, PositionRelativeToCenter.Y).Size(); const v_flt DistanceToCenterZ = FMath::Abs(PositionRelativeToCenter.Z); const v_flt SidesSDF = DistanceToCenterXY - InternalRadius; const v_flt TopSDF = DistanceToCenterZ - Height / 2 + ExternalRadius; return FMath::Min(FMath::Max(SidesSDF, TopSDF), 0.0f) + FVector2D(FMath::Max(SidesSDF, 0.f), FMath::Max(TopSDF, 0.f)).Size() + -ExternalRadius; } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// template FORCEINLINE void XWayBlend_AlphasToStrengths_Impl(int32 NumChannels, const TIn& Alphas, TOut& Strengths) { ensureVoxelSlow(NumChannels > 1); // Unpack the strengths from the lerp values for (int32 Index = 0; Index < NumChannels; Index++) { Strengths[Index] = Index == 0 ? 1.f : Alphas[Index - 1]; for (int32 AlphaIndex = Index; AlphaIndex < NumChannels - 1; AlphaIndex++) { Strengths[Index] *= 1.f - Alphas[AlphaIndex]; } } #if VOXEL_DEBUG float Sum = 0.f; for (int32 Index = 0; Index < NumChannels; Index++) { Sum += Strengths[Index]; } ensure(FMath::IsNearlyEqual(Sum, 1.f, KINDA_SMALL_NUMBER)); #endif } template FORCEINLINE void XWayBlend_StrengthsToAlphas_Impl(int32 NumChannels, TIn Strengths, TOut& Alphas, uint32 ChannelsToKeepIntact = 0) { ensureVoxelSlow(NumChannels > 1); if (!ChannelsToKeepIntact) { // Normalize: we want the sum to be 1 float Sum = 0.f; for (int32 Index = 0; Index < NumChannels; Index++) { Sum += Strengths[Index]; } if (Sum != 0.f) // This can very rarely happen if we subtracted all the data when editing { for (int32 Index = 0; Index < NumChannels; Index++) { Strengths[Index] /= Sum; } } } else { // Normalize so that Strengths[Index] doesn't change if (1 << Index) & ChannelsToKeepIntact { // Sum of all the other components float SumToKeepIntact = 0.f; float SumToChange = 0.f; for (int32 Index = 0; Index < NumChannels; Index++) { if ((1u << Index) & ChannelsToKeepIntact) { SumToKeepIntact += Strengths[Index]; } else { SumToChange += Strengths[Index]; } } // If the sum to keep intact is above 1, normalize these channels too // (but on their own) if (SumToKeepIntact > 1.f) { for (int32 Index = 0; Index < NumChannels; Index++) { if ((1u << Index) & ChannelsToKeepIntact) { Strengths[Index] /= SumToKeepIntact; } } SumToKeepIntact = 1.f; } // We need to split this into the other channels. const float SumToSplit = 1.f - SumToKeepIntact; if (SumToChange == 0.f) { // If the sum is 0, increase all the other channels the same way const float Value = SumToSplit / (NumChannels - FMath::CountBits(ChannelsToKeepIntact)); for (int32 Index = 0; Index < NumChannels; Index++) { if (!((1u << Index) & ChannelsToKeepIntact)) { Strengths[Index] = Value; } } } else { // Else scale them const float Value = SumToSplit / SumToChange; for (int32 Index = 0; Index < NumChannels; Index++) { if (!((1u << Index) & ChannelsToKeepIntact)) { Strengths[Index] *= Value; } } } } } #if VOXEL_DEBUG float Sum = 0.f; for (int32 Index = 0; Index < NumChannels; Index++) { Sum += Strengths[Index]; } ensure(FMath::IsNearlyEqual(Sum, 1.f, 0.001f)); #endif const auto SafeDivide = [](float X, float Y) { // Here we resolve Y * A = X. If Y = 0, X should be 0 and A can be anything (here return 0) ensureVoxelSlowNoSideEffects(Y != 0.f || FMath::IsNearlyZero(X)); return Y == 0.f ? 0.f : X / Y; }; // Pack them back in: do the maths in reverse order const int32 NumAlphas = NumChannels - 1; for (int32 AlphaIndex = NumAlphas - 1; AlphaIndex >= 0; AlphaIndex--) { float Divisor = 1.f; for (int32 DivisorIndex = AlphaIndex + 1; DivisorIndex < NumAlphas; DivisorIndex++) { Divisor *= 1.f - Alphas[DivisorIndex]; } Alphas[AlphaIndex] = SafeDivide(Strengths[AlphaIndex + 1], Divisor); } } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// template FORCEINLINE TVoxelStaticArray XWayBlend_AlphasToStrengths_Static(const TVoxelStaticArray& Alphas) { TVoxelStaticArray Strengths; XWayBlend_AlphasToStrengths_Impl(NumChannels, Alphas, Strengths); return Strengths; } template FORCEINLINE TVoxelStaticArray XWayBlend_StrengthsToAlphas_Static(const TVoxelStaticArray& Strengths, uint32 ChannelsToKeepIntact = 0) { TVoxelStaticArray Alphas; XWayBlend_StrengthsToAlphas_Impl(NumChannels, Strengths, Alphas, ChannelsToKeepIntact); return Alphas; } // Avoid mistakes with ChannelsToKeepIntact not being a mask template void XWayBlend_StrengthsToAlphas_Static(const TVoxelStaticArray&, T) = delete; ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// template FORCEINLINE TArray XWayBlend_AlphasToStrengths_Dynamic(const TInArray& Alphas) { TArray Strengths; Strengths.SetNumUninitialized(Alphas.Num() + 1); XWayBlend_AlphasToStrengths_Impl(Alphas.Num() + 1, Alphas, Strengths); return Strengths; } template FORCEINLINE TArray XWayBlend_StrengthsToAlphas_Dynamic(const TInArray& Strengths, uint32 ChannelsToKeepIntact = 0) { TArray Alphas; Alphas.SetNumUninitialized(Strengths.Num() - 1); XWayBlend_StrengthsToAlphas_Impl(Strengths.Num(), Strengths, Alphas, ChannelsToKeepIntact); return Alphas; } // Avoid mistakes with ChannelsToKeepIntact not being a mask template void XWayBlend_StrengthsToAlphas_Dynamic(const TInArray&, T) = delete; ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// template FORCEINLINE TVoxelStaticArray, N> FindTopXElements_Impl(const ArrayType& Array, TLambda LessThan = TLess()) { checkVoxelSlow(Array.Num() >= N); // Biggest elements on top TVoxelStaticArray, N> Stack; // Fill stack with first N values for (int32 Index = 0; Index < N; Index++) { Stack[Index].template Get<0>() = Index; Stack[Index].template Get<1>() = Array[Index]; } // Sort the stack Algo::Sort(Stack, [&](const auto& A, const auto& B) { return LessThan(B.template Get<1>(), A.template Get<1>()); }); for (int32 Index = N; Index < Array.Num(); Index++) { const T& ArrayValue = Array[Index]; if (!LessThan(Stack[N - 1].template Get<1>(), ArrayValue)) { // Smaller than the entire stack continue; } // Find the element to replace int32 StackToReplace = N - 1; while (StackToReplace >= 1 && LessThan(Stack[StackToReplace - 1].template Get<1>(), ArrayValue)) { StackToReplace--; } // Move existing elements down for (int32 StackIndex = N - 1; StackIndex > StackToReplace; StackIndex--) { Stack[StackIndex] = Stack[StackIndex - 1]; } // Write new element Stack[StackToReplace].template Get<0>() = Index; Stack[StackToReplace].template Get<1>() = ArrayValue; } return Stack; } template FORCEINLINE TVoxelStaticArray, N> FindTopXElements(const TArray& Array, TLambda LessThan = TLess()) { return FindTopXElements_Impl(Array, LessThan); } template FORCEINLINE TVoxelStaticArray, N> FindTopXElements(const TVoxelStaticArray& Array, TLambda LessThan = TLess()) { return FindTopXElements_Impl(Array, LessThan); } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // Skips expensive bound checks outside of debug template FORCEINLINE auto& Get(T& Array, int32 Index) { checkVoxelSlow(0 <= Index && Index < GetNum(Array)); return GetData(Array)[Index]; } FORCEINLINE int32 Get3DIndex(const FIntVector& Size, int32 X, int32 Y, int32 Z, const FIntVector& Offset = FIntVector(0, 0, 0)) { X -= Offset.X; Y -= Offset.Y; Z -= Offset.Z; checkVoxelSlow(0 <= X && X < Size.X); checkVoxelSlow(0 <= Y && Y < Size.Y); checkVoxelSlow(0 <= Z && Z < Size.Z); checkVoxelSlow(int64(Size.X) * int64(Size.Y) * int64(Size.X) < MAX_int32); return X + Y * Size.X + Z * Size.X * Size.Y; } FORCEINLINE int32 Get3DIndex(const FIntVector& Size, const FIntVector& Position, const FIntVector& Offset = FIntVector(0, 0, 0)) { return Get3DIndex(Size, Position.X, Position.Y, Position.Z, Offset); } template FORCEINLINE T& Get3D(T* RESTRICT Array, const FIntVector& Size, int32 X, int32 Y, int32 Z, const FIntVector& Offset = FIntVector(0, 0, 0)) { return Array[Get3DIndex(Size, X, Y, Z, Offset)]; } template FORCEINLINE T& Get3D(T* RESTRICT Array, const FIntVector& Size, const FIntVector& Position, const FIntVector& Offset = FIntVector(0, 0, 0)) { return Get3D(Array, Size, Position.X, Position.Y, Position.Z, Offset); } template FORCEINLINE auto& Get3D(T& Array, const FIntVector& Size, int32 X, int32 Y, int32 Z, const FIntVector& Offset = FIntVector(0, 0, 0)) { checkVoxelSlow(GetNum(Array) == Size.X * Size.Y * Size.Z); return Get3D(GetData(Array), Size, X, Y, Z, Offset); } template FORCEINLINE auto& Get3D(T& Array, const FIntVector& Size, const FIntVector& Position, const FIntVector& Offset = FIntVector(0, 0, 0)) { return Get3D(Array, Size, Position.X, Position.Y, Position.Z, Offset); } template FORCEINLINE auto Create3DGetter(T& Array, const FIntVector& Size, const FIntVector& Offset = FIntVector(0, 0, 0)) -> decltype(auto) { return [&Array, Size, Offset](int32 X, int32 Y, int32 Z) -> decltype(auto) { return Get3D(Array, Size, X, Y, Z, Offset); }; } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// FORCEINLINE float LinearFalloff(float Distance, float Radius, float Falloff) { return Distance <= Radius ? 1.0f : Radius + Falloff <= Distance ? 0.f : 1.0f - (Distance - Radius) / Falloff; } FORCEINLINE float SmoothFalloff(float Distance, float Radius, float Falloff) { const float X = LinearFalloff(Distance, Radius, Falloff); return FMath::SmoothStep(0, 1, X); } FORCEINLINE float SphericalFalloff(float Distance, float Radius, float Falloff) { return Distance <= Radius ? 1.0f : Radius + Falloff <= Distance ? 0.f : FMath::Sqrt(1.0f - FMath::Square((Distance - Radius) / Falloff)); } FORCEINLINE float TipFalloff(float Distance, float Radius, float Falloff) { return Distance <= Radius ? 1.0f : Radius + Falloff <= Distance ? 0.f : 1.0f - FMath::Sqrt(1.0f - FMath::Square((Falloff + Radius - Distance) / Falloff)); } // Falloff: between 0 and 1 template FORCEINLINE auto DispatchFalloff(EVoxelFalloff FalloffType, float Radius, float Falloff, T Lambda) -> decltype(auto) { Falloff = FMath::Clamp(Falloff, 0.f, 1.f); if (Falloff == 0.f) { return Lambda([&](float Distance) { return 1.f; }); } const float RelativeRadius = Radius * (1.f - Falloff); const float RelativeFalloff = Radius * Falloff; switch (FalloffType) { default: ensure(false); case EVoxelFalloff::Linear: { return Lambda([=](float Distance) { return LinearFalloff(Distance, RelativeRadius, RelativeFalloff); }); } case EVoxelFalloff::Smooth: { return Lambda([=](float Distance) { return SmoothFalloff(Distance, RelativeRadius, RelativeFalloff); }); } case EVoxelFalloff::Spherical: { return Lambda([=](float Distance) { return SphericalFalloff(Distance, RelativeRadius, RelativeFalloff); }); } case EVoxelFalloff::Tip: { return Lambda([=](float Distance) { return TipFalloff(Distance, RelativeRadius, RelativeFalloff); }); } } } }