Files
UnrealEngine/Engine/Source/Developer/NaniteUtilities/Private/DisplacementMap.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

531 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DisplacementMap.h"
#include "ImageCore.h"
#include "ImageCoreUtils.h"
#include "Containers/Array.h"
#include "Containers/ContainerAllocationPolicies.h"
#include "Serialization/Archive.h"
namespace Nanite
{
FDisplacementMap::FDisplacementMap()
: SourceFormat( TSF_G8 )
, BytesPerPixel(1)
, SizeX(1)
, SizeY(1)
, NumLevels(1)
, Magnitude( 0.0f )
, Center( 0.0f )
, AddressX( TA_Wrap )
, AddressY( TA_Wrap )
{
SourceData.Add(0);
}
FDisplacementMap::FDisplacementMap( FImage&& TextureSourceImage, float InMagnitude, float InCenter, TextureAddress InAddressX, TextureAddress InAddressY )
: NumLevels(1)
, Magnitude( InMagnitude )
, Center( InCenter )
, AddressX( InAddressX )
, AddressY( InAddressY )
{
SourceData = MoveTemp(TextureSourceImage.RawData);
check(!SourceData.IsEmpty());
SourceFormat = FImageCoreUtils::ConvertToTextureSourceFormat(TextureSourceImage.Format);
BytesPerPixel = ERawImageFormat::GetBytesPerPixel(TextureSourceImage.Format);
SizeX = TextureSourceImage.GetWidth();
SizeY = TextureSourceImage.GetHeight();
uint32 PrevSizeX = SizeX;
uint32 PrevSizeY = SizeY;
for( uint32 Level = 1; ; Level++ )
{
uint32 MipSizeX = ( ( SizeX - 1 ) >> Level ) + 1;
uint32 MipSizeY = ( ( SizeY - 1 ) >> Level ) + 1;
MipData[ Level - 1 ].AddUninitialized( MipSizeX * MipSizeY );
MipDataFiltered[ Level - 1 ].AddUninitialized( MipSizeX * MipSizeY );
for( uint32 y = 0; y < MipSizeY; y++ )
{
for( uint32 x = 0; x < MipSizeX; x++ )
{
uint32 x0 = x*2;
uint32 y0 = y*2;
uint32 x1 = FMath::Min( x0 + 1, PrevSizeX - 1 );
uint32 y1 = FMath::Min( y0 + 1, PrevSizeY - 1 );
if ( Level == 1 )
{
float d0 = Load( x0, y0 );
float d1 = Load( x1, y0 );
float d2 = Load( x0, y1 );
float d3 = Load( x1, y1 );
MipData[ Level - 1 ][ x + y * MipSizeX ] = FVector2f(
FMath::Min( d0, FMath::Min3( d1, d2, d3 ) ),
FMath::Max( d0, FMath::Max3( d1, d2, d3 ) ) );
MipDataFiltered[ Level - 1 ][ x + y * MipSizeX ] = 0.25f * (d0 + d1 + d2 + d3);
}
else
{
FVector2f d0 = Load( x0, y0, Level - 1 );
FVector2f d1 = Load( x1, y0, Level - 1 );
FVector2f d2 = Load( x0, y1, Level - 1 );
FVector2f d3 = Load( x1, y1, Level - 1 );
MipData[ Level - 1 ][ x + y * MipSizeX ] = FVector2f(
FMath::Min( d0.X, FMath::Min3( d1.X, d2.X, d3.X ) ),
FMath::Max( d0.Y, FMath::Max3( d1.Y, d2.Y, d3.Y ) ) );
float v0 = LoadFiltered( x0, y0, Level - 1 );
float v1 = LoadFiltered( x1, y0, Level - 1 );
float v2 = LoadFiltered( x0, y1, Level - 1 );
float v3 = LoadFiltered( x1, y1, Level - 1 );
MipDataFiltered[Level - 1][x + y * MipSizeX] = 0.25f * (v0 + v1 + v2 + v3);
}
}
}
PrevSizeX = MipSizeX;
PrevSizeY = MipSizeY;
NumLevels++;
// Level NumLevel-1 corresponds to coarsest 1x1 average
if( MipSizeX == 1 && MipSizeY == 1 )
{
break;
}
}
}
// Bilinear filtered
float FDisplacementMap::Sample( FVector2f UV ) const
{
// Half texel
UV.X = UV.X * SizeX - 0.5f;
UV.Y = UV.Y * SizeY - 0.5f;
int32 x0 = FMath::FloorToInt32( UV.X );
int32 y0 = FMath::FloorToInt32( UV.Y );
int32 x1 = x0 + 1;
int32 y1 = y0 + 1;
float wx1 = UV.X - x0;
float wy1 = UV.Y - y0;
float wx0 = 1.0f - wx1;
float wy0 = 1.0f - wy1;
return
Sample( x0, y0 ) * wx0 * wy0 +
Sample( x1, y0 ) * wx1 * wy0 +
Sample( x0, y1 ) * wx0 * wy1 +
Sample( x1, y1 ) * wx1 * wy1;
}
// Returns min/max over bilinear footprint
FVector2f FDisplacementMap::Sample( FVector2f MinUV, FVector2f MaxUV ) const
{
// Half texel
MinUV = MinUV * FVector2f( SizeX, SizeY ) - FVector2f( 0.5f );
MaxUV = MaxUV * FVector2f( SizeX, SizeY ) - FVector2f( 0.5f );
int32 x0 = FMath::FloorToInt32( MinUV.X );
int32 y0 = FMath::FloorToInt32( MinUV.Y );
int32 x1 = FMath::FloorToInt32( MaxUV.X ) + 1;
int32 y1 = FMath::FloorToInt32( MaxUV.Y ) + 1;
uint32 Level = FMath::FloorLog2( FMath::Max( x1 - x0, y1 - y0 ) );
if( (x1 >> Level) - (x0 >> Level) > 1 ||
(y1 >> Level) - (y0 >> Level) > 1 )
Level++;
Level = FMath::Min( Level, NumLevels - 1 );
if ( Level == 0 )
{
float d0 = Sample( x0, y0 );
float d1 = Sample( x1, y0 );
float d2 = Sample( x0, y1 );
float d3 = Sample( x1, y1 );
return FVector2f(
FMath::Min( d0, FMath::Min3( d1, d2, d3 ) ),
FMath::Max( d0, FMath::Max3( d1, d2, d3 ) ) );
}
else
{
FVector2f d0 = Sample( x0, y0, Level );
FVector2f d1 = Sample( x1, y0, Level );
FVector2f d2 = Sample( x0, y1, Level );
FVector2f d3 = Sample( x1, y1, Level );
return FVector2f(
FMath::Min( d0.X, FMath::Min3( d1.X, d2.X, d3.X ) ),
FMath::Max( d0.Y, FMath::Max3( d1.Y, d2.Y, d3.Y ) ) );
}
}
// Returns min/max over rectangular region footprint
FVector2f FDisplacementMap::SampleHierarchical(FVector2f MinUV, FVector2f MaxUV, const uint32 MaxRefinements) const
{
if (MaxRefinements == 0)
{
// equivalent
return Sample(MinUV, MaxUV);
}
// Half texel
MinUV = MinUV * FVector2f(SizeX, SizeY) - FVector2f(0.5f);
MaxUV = MaxUV * FVector2f(SizeX, SizeY) - FVector2f(0.5f);
int32 X0 = FMath::FloorToInt32(MinUV.X);
int32 Y0 = FMath::FloorToInt32(MinUV.Y);
// inclusive, +1 to account for bilinear filtering
int32 X1 = FMath::FloorToInt32(MaxUV.X) + 1;
int32 Y1 = FMath::FloorToInt32(MaxUV.Y) + 1;
const FIntRect QueryWindow(X0, Y0, X1 + 1, Y1 + 1); // FIntRect is half-open
struct FNode
{
uint32 Level;
uint32 X, Y;
};
TArray<FNode, TInlineAllocator<64>> Stack;
// initialize root notes
uint32 Level = FMath::FloorLog2(FMath::Max(X1 - X0, Y1 - Y0));
if ((X1 >> Level) - (X0 >> Level) > 1 ||
(Y1 >> Level) - (Y0 >> Level) > 1)
{
Level++;
}
Level = FMath::Min(Level, NumLevels - 1);
const uint32 Mask = ~((1<<Level)-1);
const uint32 X0M = X0 & Mask;
const uint32 X1M = X1 & Mask;
const uint32 Y0M = Y0 & Mask;
const uint32 Y1M = Y1 & Mask;
Stack.Push( { Level, X0M, Y0M } );
if ( X1M != X0M )
{
Stack.Push( { Level, X1M, Y0M } );
}
if ( Y1M != Y0M )
{
Stack.Push( { Level, X0M, Y1M } );
if ( X1M != X0M )
{
Stack.Push( { Level, X1M, Y1M } );
}
}
const uint32 MinLevel = Level - FMath::Min(Level, MaxRefinements);
// result
FVector2f MinMax( std::numeric_limits<float>::max(),
-std::numeric_limits<float>::max() );
while (!Stack.IsEmpty())
{
FNode Node = Stack.Pop();
const uint32 NodeSize = 1u << Node.Level;
const FIntRect NodeRect(Node.X, Node.Y, Node.X + NodeSize, Node.Y + NodeSize);
if (!QueryWindow.Intersect(NodeRect))
{
continue;
}
// if node fully contained in query region or we can't refine do accumulate
if (QueryWindow.Contains(NodeRect) || (MinLevel > 0 && Node.Level == MinLevel))
{
FVector2f Bounds;
if (Node.Level == 0)
{
Bounds[0] = Bounds[1] = Sample(Node.X, Node.Y);
}
else
{
Bounds = Sample(Node.X, Node.Y, Node.Level);
}
MinMax[0] = FMath::Min(MinMax[0], Bounds[0]);
MinMax[1] = FMath::Max(MinMax[1], Bounds[1]);
continue;
}
if (Node.Level > MinLevel)
{
// partially overlaps -> refine
const uint32 NextNodeSize = NodeSize >> 1;
Stack.Push( { Node.Level-1, Node.X , Node.Y } );
Stack.Push( { Node.Level-1, Node.X + NextNodeSize, Node.Y } );
Stack.Push( { Node.Level-1, Node.X , Node.Y + NextNodeSize } );
Stack.Push( { Node.Level-1, Node.X + NextNodeSize, Node.Y + NextNodeSize } );
}
}
return MinMax;
}
float FDisplacementMap::SampleEWA(FVector2f UV, FVector2f Axis0, FVector2f Axis1) const
{
// major/minor
float Len1 = Axis1.Length();
float LODLevelf = FMath::Max(0.f, NumLevels - 1.f + FMath::Log2(Len1));
uint32 LODLeveli = FMath::FloorToInt(LODLevelf);
float d = LODLevelf - LODLeveli;
return ((1.f - d) * EWA(LODLeveli , UV, Axis0, Axis1)
+ d * EWA(LODLeveli + 1, UV, Axis0, Axis1) - Center) * Magnitude;
}
float FDisplacementMap::EWA(uint32 Level, FVector2f UV, FVector2f Axis0, FVector2f Axis1) const
{
// adapted from [ Pharr, Jakob, Humphreys 2023, "Physically Based Rendering" (4th Edition) ]
if (Level >= NumLevels)
{
return LoadFiltered(0, 0, NumLevels-1);
}
const uint32 MipSizeX = ((SizeX - 1) >> Level) + 1;
const uint32 MipSizeY = ((SizeY - 1) >> Level) + 1;
FVector2f MipScale(MipSizeX, MipSizeY);
UV.X = UV.X * (float)MipScale.X - 0.5f;
UV.Y = UV.Y * (float)MipScale.Y - 0.5f;
Axis0 *= MipScale;
Axis1 *= MipScale;
float A = Axis0.Y * Axis0.Y + Axis1.Y * Axis1.Y + 1.f;
float B = -2.f * (Axis0.X * Axis0.Y + Axis1.X * Axis1.Y);
float C = Axis0.X * Axis0.X + Axis1.X * Axis1.X + 1.f;
float InvF = 1.f / (A * C - 0.25f * B * B);
A *= InvF;
B *= InvF;
C *= InvF;
float Det = -B * B + 4.f * A * C;
float InvDet = 1.f / Det;
if (!FMath::IsFinite(InvDet))
{
return Sample(UV) / Magnitude + Center;
}
float USqrt = FMath::Sqrt(Det * C);
float VSqrt = FMath::Sqrt(Det * A);
const int32 U0 = FMath::CeilToInt32(UV.X - 2.f * InvDet * USqrt);
const int32 U1 = FMath::FloorToInt32(UV.X + 2.f * InvDet * USqrt);
const int32 V0 = FMath::CeilToInt32(UV.Y - 2.f * InvDet * VSqrt);
const int32 V1 = FMath::FloorToInt32(UV.Y + 2.f * InvDet * VSqrt);
float Sum = 0.f;
float SumWts = 0.f;
auto ClippedGaussian = [](const float XSqr) -> float
{
constexpr float Offset = 1.f / (UE_EULERS_NUMBER * UE_EULERS_NUMBER);
return FMath::Exp(-XSqr * 2.f) - Offset;
};
// goes to 0 smoothly, avoids clipping, sharper than Gaussian
// @todo needs testing
auto BlackmanHarris = [](const float XSqr) -> float
{
const float X = FMath::Sqrt(XSqr);
return 0.35875f + 0.48829f * FMath::Cos(UE_PI * X) + 0.14128f * FMath::Cos(2.f * UE_PI * X) + 0.01168f * FMath::Cos(3.f * UE_PI * X);
};
for (int Vi = V0; Vi <= V1; ++Vi)
{
const float Vf = Vi - UV.Y;
for (int Ui = U0; Ui <= U1; ++Ui)
{
const float Uf = Ui - UV.X;
const float RSqr = A * Uf * Uf + B * Uf * Vf + C * Vf * Vf;
if (RSqr < 1.f)
{
const float Weight = ClippedGaussian(RSqr);
const int32 X = FMath::Clamp(Ui, 0, MipSizeX - 1);
const int32 Y = FMath::Clamp(Vi, 0, MipSizeY - 1);
if (Level == 0)
{
Sum += Weight * Load(X, Y);
}
else
{
Sum += Weight * MipDataFiltered[Level - 1][X + Y * MipSizeX];
}
SumWts += Weight;
}
}
}
return Sum / SumWts;
}
FVector2f FDisplacementMap::WarpSample(const FVector2f& UV) const
{
float U = UV[0];
float V = UV[1];
{
uint32 MipSizeX = ( ( SizeX - 1 ) >> (NumLevels-1) ) + 1;
uint32 MipSizeY = ( ( SizeY - 1 ) >> (NumLevels-1) ) + 1;
check(MipSizeX == 1 && MipSizeY == 1);
}
uint32 X = 0;
uint32 Y = 0;
for (uint32 Level = NumLevels-1; Level > 0; --Level )
{
const int ChildLevel = Level - 1;
const uint32 MipSizeX = ((SizeX - 1) >> ChildLevel) + 1;
const uint32 MipSizeY = ((SizeY - 1) >> ChildLevel) + 1;
const uint32 ChildX = X << 1;
const uint32 ChildY = Y << 1;
float Child00 = 0.f, Child01 = 0.f, Child10 = 0.f, Child11 = 0.f;
if (ChildLevel == 0)
{
Child00 = Load(ChildX , ChildY );
if (ChildX+1 < MipSizeX)
{
Child10 = Load(ChildX+1, ChildY );
}
if (ChildY+1 < MipSizeY)
{
Child01 = Load(ChildX , ChildY+1);
if (ChildX+1 < MipSizeX)
{
Child11 = Load(ChildX+1, ChildY+1);
}
}
}
else
{
const TArray<float>& ChildMipData = MipDataFiltered[ChildLevel-1];
Child00 = ChildMipData[ ChildX + (ChildY ) * MipSizeX ];
if (ChildX+1 < MipSizeX)
{
Child10 = ChildMipData[ ChildX+1 + (ChildY ) * MipSizeX ];
}
if (ChildY+1 < MipSizeY)
{
Child01 = ChildMipData[ ChildX + (ChildY+1) * MipSizeX ];
if (ChildX+1 < MipSizeX)
{
Child11 = ChildMipData[ ChildX+1 + (ChildY+1) * MipSizeX ];
}
}
}
const float Sum0 = Child00 + Child10;
const float Sum1 = Child01 + Child11;
float ChildLeft, ChildRight;
// top-bottom
if (V * (Sum0 + Sum1) < Sum0 || ChildY+1 >= MipSizeY)
{
V *= (Sum0 + Sum1) / Sum0;
Y = ChildY;
ChildLeft = Child00;
ChildRight = Child10;
}
else
{
V = (V * (Sum0 + Sum1) - Sum0) / Sum1;
Y = ChildY + 1;
ChildLeft = Child01;
ChildRight = Child11;
}
// left-right
if (U*(ChildLeft + ChildRight) < ChildLeft || ChildX+1 >= MipSizeX)
{
U *= (ChildLeft + ChildRight) / ChildLeft;
X = ChildX;
}
else
{
U = (U*(ChildLeft + ChildRight) - ChildLeft) / ChildRight;
X = ChildX + 1;
}
}
return FVector2f( (static_cast<float>(X) + U) / static_cast<float>(SizeX),
(static_cast<float>(Y) + V) / static_cast<float>(SizeY) );
}
void FDisplacementMap::Serialize(FArchive& Ar)
{
int SourceFormatRaw = static_cast<int>(SourceFormat);
Ar << SourceFormatRaw;
if (Ar.IsLoading())
{
SourceFormat = static_cast<ETextureSourceFormat>(SourceFormatRaw);
}
Ar << BytesPerPixel;
Ar << SizeX;
Ar << SizeY;
Ar << NumLevels;
Ar << Magnitude;
Ar << Center;
int AddressXRaw = static_cast<int>(AddressX);
int AddressYRaw = static_cast<int>(AddressY);
Ar << AddressXRaw;
Ar << AddressYRaw;
if (Ar.IsLoading())
{
AddressX = static_cast<TextureAddress>(AddressXRaw);
AddressY = static_cast<TextureAddress>(AddressYRaw);
}
Ar << SourceData;
for (int Level = 0; Level < MAX_NUM_MIP_LEVELS; ++Level)
{
Ar << MipData[Level];
}
for (int Level = 0; Level < MAX_NUM_MIP_LEVELS; ++Level)
{
Ar << MipDataFiltered[Level];
}
}
} // namespace Nanite