Files
UnrealEngineUWP/Engine/Source/Developer/TextureCompressor/Private/TextureCompressorModule.cpp
Mikolaj Sieluzycki 45c031d576 #ttp 331438 UE4: PlatformMath refactor
#proj core
#branch UE4
#summary Rename Trunc, Round, Floor, Ceil to TruncToInt, RoundToInt, FloorToInt, CeilToInt. Added *ToFloat versions. Repeated for FGenericPlatformMath and all derived classes for all platforms wherever applicable. Corrected comment in Fractional and added Frac() which does HLSL-style fractional (x - floor(x)). Checked for compilation on all projects (with cooking levels wherever applicable). Didn't change Fractional to Frac, this will be done in second commit.
#codereview robert.manuszewski

[CL 2064306 by Mikolaj Sieluzycki in Main branch]
2014-05-06 06:26:25 -04:00

1878 lines
57 KiB
C++

// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.
#include "TextureCompressorPrivatePCH.h"
DEFINE_LOG_CATEGORY_STATIC(LogTextureCompressor, Log, All);
/*------------------------------------------------------------------------------
Mip-Map Generation
------------------------------------------------------------------------------*/
enum EMipGenAddressMode
{
MGTAM_Wrap,
MGTAM_Clamp,
MGTAM_BorderBlack,
};
/**
* 2D view into one slice of an image.
*/
struct FImageView2D
{
/** Pointer to colors in the slice. */
FLinearColor* SliceColors;
/** Width of the slice. */
int32 SizeX;
/** Height of the slice. */
int32 SizeY;
/** Initialization constructor. */
FImageView2D(FImage& Image, int32 SliceIndex)
{
SizeX = Image.SizeX;
SizeY = Image.SizeY;
SliceColors = Image.AsRGBA32F() + SliceIndex * SizeY * SizeX;
}
/** Access a single texel. */
FLinearColor& Access(int32 X, int32 Y)
{
return SliceColors[X + Y * SizeX];
}
/** Const access to a single texel. */
const FLinearColor& Access(int32 X, int32 Y) const
{
return SliceColors[X + Y * SizeX];
}
};
// 2D sample lookup with input conversion
// requires SourceImageData.SizeX and SourceImageData.SizeY to be power of two
template <EMipGenAddressMode AddressMode>
FLinearColor LookupSourceMip(const FImageView2D& SourceImageData, int32 X, int32 Y)
{
if(AddressMode == MGTAM_Wrap)
{
// wrap
X = (int32)((uint32)X) & (SourceImageData.SizeX - 1);
Y = (int32)((uint32)Y) & (SourceImageData.SizeY - 1);
}
else if(AddressMode == MGTAM_Clamp)
{
// clamp
X = FMath::Clamp(X, 0, SourceImageData.SizeX - 1);
Y = FMath::Clamp(Y, 0, SourceImageData.SizeY - 1);
}
else if(AddressMode == MGTAM_BorderBlack)
{
// border color 0
if((uint32)X >= (uint32)SourceImageData.SizeX
|| (uint32)Y >= (uint32)SourceImageData.SizeY)
{
return FLinearColor(0, 0, 0, 0);
}
}
else
{
check(0);
}
//return *(SourceImageData.AsRGBA32F() + X + Y * SourceImageData.SizeX);
return SourceImageData.Access(X,Y);
}
// Kernel class for image filtering operations like image downsampling
// at max MaxKernelExtend x MaxKernelExtend
class FImageKernel2D
{
public:
FImageKernel2D() :FilterTableSize(0)
{
}
// @param TableSize1D 2 for 2x2, 4 for 4x4, 6 for 6x6, 8 for 8x8
// @param SharpenFactor can be negative to blur
// generate normalized 2D Kernel with sharpening
void BuildSeparatableGaussWithSharpen(uint32 TableSize1D, float SharpenFactor = 0.0f)
{
if(TableSize1D > MaxKernelExtend)
{
TableSize1D = MaxKernelExtend;
}
float Table1D[MaxKernelExtend];
float NegativeTable1D[MaxKernelExtend];
FilterTableSize = TableSize1D;
if(SharpenFactor < 0.0f)
{
// blur only
BuildGaussian1D(Table1D, TableSize1D, 1.0f, -SharpenFactor);
BuildFilterTable2DFrom1D(KernelWeights, Table1D, TableSize1D);
return;
}
else if(TableSize1D == 2)
{
// 2x2 kernel: simple average
KernelWeights[0] = KernelWeights[1] = KernelWeights[2] = KernelWeights[3] = 0.25f;
return;
}
else if(TableSize1D == 4)
{
// 4x4 kernel with sharpen or blur: can alias a bit
BuildFilterTable1DBase(Table1D, TableSize1D, 1.0f + SharpenFactor);
BuildFilterTable1DBase(NegativeTable1D, TableSize1D, -SharpenFactor);
BlurFilterTable1D(NegativeTable1D, TableSize1D, 1);
}
else if(TableSize1D == 6)
{
// 6x6 kernel with sharpen or blur: still can alias
BuildFilterTable1DBase(Table1D, TableSize1D, 1.0f + SharpenFactor);
BuildFilterTable1DBase(NegativeTable1D, TableSize1D, -SharpenFactor);
BlurFilterTable1D(NegativeTable1D, TableSize1D, 2);
}
else if(TableSize1D == 8)
{
//8x8 kernel with sharpen or blur
// * 2 to get similar appearance as for TableSize 6
SharpenFactor = SharpenFactor * 2.0f;
BuildFilterTable1DBase(Table1D, TableSize1D, 1.0f + SharpenFactor);
// positive lobe is blurred a bit for better quality
BlurFilterTable1D(Table1D, TableSize1D, 1);
BuildFilterTable1DBase(NegativeTable1D, TableSize1D, -SharpenFactor);
BlurFilterTable1D(NegativeTable1D, TableSize1D, 3);
}
else
{
// not yet supported
check(0);
}
AddFilterTable1D(Table1D, NegativeTable1D, TableSize1D);
BuildFilterTable2DFrom1D(KernelWeights, Table1D, TableSize1D);
}
inline uint32 GetFilterTableSize() const
{
return FilterTableSize;
}
inline float GetAt(uint32 X, uint32 Y) const
{
checkSlow(X < FilterTableSize);
checkSlow(Y < FilterTableSize);
return KernelWeights[X + Y * FilterTableSize];
}
inline float& GetRefAt(uint32 X, uint32 Y)
{
checkSlow(X < FilterTableSize);
checkSlow(Y < FilterTableSize);
return KernelWeights[X + Y * FilterTableSize];
}
private:
inline static float NormalDistribution(float X, float Variance)
{
const float StandardDeviation = FMath::Sqrt(Variance);
return FMath::Exp(-FMath::Square(X) / (2.0f * Variance)) / (StandardDeviation * FMath::Sqrt(2.0f * (float)PI));
}
// support even and non even sized filters
static void BuildGaussian1D(float *InOutTable, uint32 TableSize, float Sum, float Variance)
{
float Center = TableSize * 0.5f;
float CurrentSum = 0;
for(uint32 i = 0; i < TableSize; ++i)
{
float Actual = NormalDistribution(i - Center + 0.5f, Variance);
InOutTable[i] = Actual;
CurrentSum += Actual;
}
// Normalize
float InvSum = Sum / CurrentSum;
for(uint32 i = 0; i < TableSize; ++i)
{
InOutTable[i] *= InvSum;
}
}
//
static void BuildFilterTable1DBase(float *InOutTable, uint32 TableSize, float Sum )
{
// we require a even sized filter
check(TableSize % 2 == 0);
float Inner = 0.5f * Sum;
uint32 Center = TableSize / 2;
for(uint32 x = 0; x < TableSize; ++x)
{
if(x == Center || x == Center - 1)
{
// center elements
InOutTable[x] = Inner;
}
else
{
// outer elements
InOutTable[x] = 0.0f;
}
}
}
// InOutTable += InTable
static void AddFilterTable1D( float *InOutTable, float *InTable, uint32 TableSize )
{
for(uint32 x = 0; x < TableSize; ++x)
{
InOutTable[x] += InTable[x];
}
}
// @param Times 1:box, 2:triangle, 3:pow2, 4:pow3, ...
// can be optimized with double buffering but doesn't need to be fast
static void BlurFilterTable1D( float *InOutTable, uint32 TableSize, uint32 Times )
{
check(Times>0);
check(TableSize<32);
float Intermediate[32];
for(uint32 Pass = 0; Pass < Times; ++Pass)
{
for(uint32 x = 0; x < TableSize; ++x)
{
Intermediate[x] = InOutTable[x];
}
for(uint32 x = 0; x < TableSize; ++x)
{
float sum = Intermediate[x];
if(x)
{
sum += Intermediate[x-1];
}
if(x < TableSize - 1)
{
sum += Intermediate[x+1];
}
InOutTable[x] = sum / 3.0f;
}
}
}
static void BuildFilterTable2DFrom1D( float *OutTable2D, float *InTable1D, uint32 TableSize )
{
for(uint32 y = 0; y < TableSize; ++y)
{
for(uint32 x = 0; x < TableSize; ++x)
{
OutTable2D[x + y * TableSize] = InTable1D[y] * InTable1D[x];
}
}
}
// at max we support MaxKernelExtend x MaxKernelExtend kernels
const static uint32 MaxKernelExtend = 12;
// 0 if no kernel was setup yet
uint32 FilterTableSize;
// normalized, means the sum of it should be 1.0f
float KernelWeights[MaxKernelExtend * MaxKernelExtend];
};
/**
* Generates a mip-map for an 2D B8G8R8A8 image using a 4x4 filter with sharpening
* @param SourceImageData - The source image's data.
* @param DestImageData - The destination image's data.
* @param ImageFormat - The format of both the source and destination images.
* @param FilterTable2D - [FilterTableSize * FilterTableSize]
* @param FilterTableSize - >= 2
* @param ScaleFactor 1 / 2:for downsampling
*/
template <EMipGenAddressMode AddressMode>
static void GenerateSharpenedMipB8G8R8A8Templ(
const FImageView2D& SourceImageData,
FImageView2D& DestImageData,
bool bDitherMipMapAlpha,
const FImageKernel2D& Kernel,
uint32 ScaleFactor,
bool bSharpenWithoutColorShift )
{
check( SourceImageData.SizeX == ScaleFactor * DestImageData.SizeX || DestImageData.SizeX == 1 );
check( SourceImageData.SizeY == ScaleFactor * DestImageData.SizeY || DestImageData.SizeY == 1 );
check( Kernel.GetFilterTableSize() >= 2 );
const int32 KernelCenter = (int32)Kernel.GetFilterTableSize() / 2 - 1;
// Set up a random number stream for dithering.
FRandomStream RandomStream(0);
for ( int32 DestY = 0;DestY < DestImageData.SizeY; DestY++ )
{
for ( int32 DestX = 0;DestX < DestImageData.SizeX; DestX++ )
{
const int32 SourceX = DestX * ScaleFactor;
const int32 SourceY = DestY * ScaleFactor;
FLinearColor FilteredColor(0, 0, 0, 0);
if ( bSharpenWithoutColorShift )
{
float NewLuminance = 0;
for ( uint32 KernelY = 0; KernelY < Kernel.GetFilterTableSize(); ++KernelY )
{
for ( uint32 KernelX = 0; KernelX < Kernel.GetFilterTableSize(); ++KernelX )
{
float Weight = Kernel.GetAt( KernelX, KernelY );
FLinearColor Sample = LookupSourceMip<AddressMode>( SourceImageData, SourceX + KernelX - KernelCenter, SourceY + KernelY - KernelCenter );
float LuminanceSample = Sample.ComputeLuminance();
NewLuminance += Weight * LuminanceSample;
}
}
// simple 2x2 kernel to compute the color
FilteredColor =
( LookupSourceMip<AddressMode>( SourceImageData, SourceX + 0, SourceY + 0 )
+ LookupSourceMip<AddressMode>( SourceImageData, SourceX + 1, SourceY + 0 )
+ LookupSourceMip<AddressMode>( SourceImageData, SourceX + 0, SourceY + 1 )
+ LookupSourceMip<AddressMode>( SourceImageData, SourceX + 1, SourceY + 1 ) ) * 0.25f;
float OldLuminance = FilteredColor.ComputeLuminance();
if ( OldLuminance > 0.001f )
{
float Factor = NewLuminance / OldLuminance;
FilteredColor.R *= Factor;
FilteredColor.G *= Factor;
FilteredColor.B *= Factor;
}
}
else
{
for ( uint32 KernelY = 0; KernelY < Kernel.GetFilterTableSize(); ++KernelY )
{
for ( uint32 KernelX = 0; KernelX < Kernel.GetFilterTableSize(); ++KernelX )
{
float Weight = Kernel.GetAt( KernelX, KernelY );
FLinearColor Sample = LookupSourceMip<AddressMode>( SourceImageData, SourceX + KernelX - KernelCenter, SourceY + KernelY - KernelCenter );
FilteredColor += Weight * Sample;
}
}
}
if ( bDitherMipMapAlpha )
{
// Dither the alpha of any pixel which passes an alpha threshold test.
const int32 AlphaThreshold = 5.0f / 255.0f;
const float MinRandomAlpha = 85.0f;
const float MaxRandomAlpha = 255.0f;
if ( FilteredColor.A > AlphaThreshold )
{
FilteredColor.A = FMath::TruncToInt( FMath::Lerp( MinRandomAlpha, MaxRandomAlpha, RandomStream.GetFraction() ) );
}
}
// Set the destination pixel.
//FLinearColor& DestColor = *(DestImageData.AsRGBA32F() + DestX + DestY * DestImageData.SizeX);
FLinearColor& DestColor = DestImageData.Access(DestX, DestY);
DestColor = FilteredColor;
}
}
}
// to switch conveniently between different texture wrapping modes for the mip map generation
// the template can optimize the inner loop using a constant AddressMode
static void GenerateSharpenedMipB8G8R8A8(
const FImageView2D& SourceImageData,
FImageView2D& DestImageData,
EMipGenAddressMode AddressMode,
bool bDitherMipMapAlpha,
const FImageKernel2D &Kernel,
uint32 ScaleFactor,
bool bSharpenWithoutColorShift
)
{
switch(AddressMode)
{
case MGTAM_Wrap:
GenerateSharpenedMipB8G8R8A8Templ<MGTAM_Wrap>(SourceImageData, DestImageData, bDitherMipMapAlpha, Kernel, ScaleFactor, bSharpenWithoutColorShift);
break;
case MGTAM_Clamp:
GenerateSharpenedMipB8G8R8A8Templ<MGTAM_Clamp>(SourceImageData, DestImageData, bDitherMipMapAlpha, Kernel, ScaleFactor, bSharpenWithoutColorShift);
break;
case MGTAM_BorderBlack:
GenerateSharpenedMipB8G8R8A8Templ<MGTAM_BorderBlack>(SourceImageData, DestImageData, bDitherMipMapAlpha, Kernel, ScaleFactor, bSharpenWithoutColorShift);
break;
default:
check(0);
}
}
// Update border texels after normal mip map generation to preserve the colors there (useful for particles and decals).
static void GenerateMipBorder(
const FImageView2D& SrcImageData,
FImageView2D& DestImageData
)
{
check( SrcImageData.SizeX == 2 * DestImageData.SizeX || DestImageData.SizeX == 1 );
check( SrcImageData.SizeY == 2 * DestImageData.SizeY || DestImageData.SizeY == 1 );
for ( int32 DestY = 0; DestY < DestImageData.SizeY; DestY++ )
{
for ( int32 DestX = 0; DestX < DestImageData.SizeX; )
{
FLinearColor FilteredColor(0, 0, 0, 0);
{
float WeightSum = 0.0f;
for ( int32 KernelY = 0; KernelY < 2; ++KernelY )
{
for ( int32 KernelX = 0; KernelX < 2; ++KernelX )
{
const int32 SourceX = DestX * 2 + KernelX;
const int32 SourceY = DestY * 2 + KernelY;
// only average the source border
if ( SourceX == 0 ||
SourceX == SrcImageData.SizeX - 1 ||
SourceY == 0 ||
SourceY == SrcImageData.SizeY - 1 )
{
FLinearColor Sample = LookupSourceMip<MGTAM_Wrap>( SrcImageData, SourceX, SourceY );
FilteredColor += Sample;
WeightSum += 1.0f;
}
}
}
FilteredColor /= WeightSum;
}
// Set the destination pixel.
//FLinearColor& DestColor = *(DestImageData.AsRGBA32F() + DestX + DestY * DestImageData.SizeX);
FLinearColor& DestColor = DestImageData.Access(DestX, DestY);
DestColor = FilteredColor;
++DestX;
if ( DestY > 0 &&
DestY < DestImageData.SizeY - 1 &&
DestX > 0 &&
DestX < DestImageData.SizeX - 1 )
{
// jump over the non border area
DestX += FMath::Max( 1, DestImageData.SizeX - 2 );
}
}
}
}
// how should be treat lookups outside of the image
static EMipGenAddressMode ComputeAdressMode(const FTextureBuildSettings& Settings)
{
EMipGenAddressMode AddressMode = MGTAM_Wrap;
if(Settings.bPreserveBorder)
{
AddressMode = Settings.bBorderColorBlack ? MGTAM_BorderBlack : MGTAM_Clamp;
}
return AddressMode;
}
static void GenerateTopMip(const FImage& SrcImage, FImage& DestImage, const FTextureBuildSettings& Settings)
{
EMipGenAddressMode AddressMode = ComputeAdressMode(Settings);
FImageKernel2D KernelDownsample;
// /2 as input resolution is same as output resolution and the settings assumed the output is half resolution
KernelDownsample.BuildSeparatableGaussWithSharpen( FMath::Max( 2u, Settings.SharpenMipKernelSize / 2 ), Settings.MipSharpening );
DestImage.Init(SrcImage.SizeX, SrcImage.SizeY, SrcImage.Format, SrcImage.bSRGB);
for (int32 SliceIndex = 0; SliceIndex < SrcImage.NumSlices; ++SliceIndex)
{
FImageView2D SrcView((FImage&)SrcImage, SliceIndex);
FImageView2D DestView(DestImage, SliceIndex);
// generate DestImage: down sample with sharpening
GenerateSharpenedMipB8G8R8A8(
SrcView,
DestView,
AddressMode,
Settings.bDitherMipMapAlpha,
KernelDownsample,
1,
Settings.bSharpenWithoutColorShift
);
}
}
/**
* Generate a full mip chain. The input mip chain must have one or more mips.
* @param Settings - Preprocess settings.
* @param BaseImage - An image that will serve as the source for the generation of the mip chain.
* @param OutMipChain - An array that will contain the resultant mip images. Generated mip levels are appended to the array.
* @param MipChainDepth - number of mip images to produce. Mips chain is finished when either a 1x1 mip is produced or 'MipChainDepth' images have been produced.
*/
static void GenerateMipChain(
const FTextureBuildSettings& Settings,
const FImage& BaseImage,
TArray<FImage> &OutMipChain,
uint32 MipChainDepth = MAX_uint32
)
{
check(BaseImage.Format == ERawImageFormat::RGBA32F);
const FImage& BaseMip = BaseImage;
const int32 SrcWidth = BaseMip.SizeX;
const int32 SrcHeight= BaseMip.SizeY;
const int32 SrcNumSlices = BaseMip.NumSlices;
const ERawImageFormat::Type ImageFormat = ERawImageFormat::RGBA32F;
// space for one source mip and one destination mip
FImage IntermediateSrc(SrcWidth, SrcHeight, SrcNumSlices, ImageFormat);
FImage IntermediateDst(FMath::Max<uint32>( 1, SrcWidth >> 1 ), FMath::Max<uint32>( 1, SrcHeight >> 1 ), SrcNumSlices, ImageFormat);
// copy base mip
BaseMip.CopyTo(IntermediateSrc, ERawImageFormat::RGBA32F, false);
// Filtering kernels.
FImageKernel2D KernelSimpleAverage;
FImageKernel2D KernelDownsample;
KernelSimpleAverage.BuildSeparatableGaussWithSharpen( 2 );
KernelDownsample.BuildSeparatableGaussWithSharpen( Settings.SharpenMipKernelSize, Settings.MipSharpening );
EMipGenAddressMode AddressMode = ComputeAdressMode(Settings);
bool bReDrawBorder = false;
if( Settings.bPreserveBorder )
{
bReDrawBorder = !Settings.bBorderColorBlack;
}
// Generate mips
for (; MipChainDepth != 0 ; --MipChainDepth)
{
FImage& DestImage = *new(OutMipChain) FImage(IntermediateDst.SizeX, IntermediateDst.SizeY, SrcNumSlices, ImageFormat);
for (int32 SliceIndex = 0; SliceIndex < SrcNumSlices; ++SliceIndex)
{
FImageView2D IntermediateSrcView(IntermediateSrc, SliceIndex);
FImageView2D DestView(DestImage, SliceIndex);
FImageView2D IntermediateDstView(IntermediateDst, SliceIndex);
// generate DestImage: down sample with sharpening
GenerateSharpenedMipB8G8R8A8(
IntermediateSrcView,
DestView,
AddressMode,
Settings.bDitherMipMapAlpha,
KernelDownsample,
2,
Settings.bSharpenWithoutColorShift
);
// generate IntermediateDstImage:
if ( Settings.bDownsampleWithAverage )
{
// down sample without sharpening for the next iteration
GenerateSharpenedMipB8G8R8A8(
IntermediateSrcView,
IntermediateDstView,
AddressMode,
Settings.bDitherMipMapAlpha,
KernelSimpleAverage,
2,
Settings.bSharpenWithoutColorShift
);
}
}
if ( Settings.bDownsampleWithAverage == false )
{
FMemory::Memcpy( IntermediateDst.AsRGBA32F(), DestImage.AsRGBA32F(),
IntermediateDst.SizeX * IntermediateDst.SizeY * SrcNumSlices * sizeof(FLinearColor) );
}
if ( bReDrawBorder )
{
for (int32 SliceIndex = 0; SliceIndex < SrcNumSlices; ++SliceIndex)
{
FImageView2D IntermediateSrcView(IntermediateSrc, SliceIndex);
FImageView2D DestView(DestImage, SliceIndex);
FImageView2D IntermediateDstView(IntermediateDst, SliceIndex);
GenerateMipBorder( IntermediateSrcView, DestView );
GenerateMipBorder( IntermediateSrcView, IntermediateDstView );
}
}
// Once we've created mip-maps down to 1x1, we're done.
if ( IntermediateDst.SizeX == 1 && IntermediateDst.SizeY == 1 )
{
break;
}
// last destination becomes next source
FMemory::Memcpy(IntermediateSrc.AsRGBA32F(), IntermediateDst.AsRGBA32F(),
IntermediateDst.SizeX * IntermediateDst.SizeY * SrcNumSlices * sizeof(FLinearColor));
// Sizes for the next iteration.
IntermediateSrc.SizeX = FMath::Max<uint32>( 1, IntermediateSrc.SizeX >> 1 );
IntermediateSrc.SizeY = FMath::Max<uint32>( 1, IntermediateSrc.SizeY >> 1 );
IntermediateDst.SizeX = FMath::Max<uint32>( 1, IntermediateDst.SizeX >> 1 );
IntermediateDst.SizeY = FMath::Max<uint32>( 1, IntermediateDst.SizeY >> 1 );
}
}
/*------------------------------------------------------------------------------
Angular Filtering for HDR Cubemaps.
------------------------------------------------------------------------------*/
/**
* View in to an image that allows access by converting a direction to longitude and latitude.
*/
struct FImageViewLongLat
{
/** Image colors. */
FLinearColor* ImageColors;
/** Width of the image. */
int32 SizeX;
/** Height of the image. */
int32 SizeY;
/** Initialization constructor. */
explicit FImageViewLongLat(FImage& Image)
{
ImageColors = Image.AsRGBA32F();
SizeX = Image.SizeX;
SizeY = Image.SizeY;
}
/** Wraps X around W. */
static void WrapTo(int32& X, int32 W)
{
X = X % W;
if(X < 0)
{
X += W;
}
}
/** Const access to a texel. */
FLinearColor Access(int32 X, int32 Y) const
{
return ImageColors[X + Y * SizeX];
}
/** Makes a filtered lookup. */
FLinearColor LookupFiltered(float X, float Y) const
{
int32 X0 = (int32)floor(X);
int32 Y0 = (int32)floor(Y);
float FracX = X - X0;
float FracY = Y - Y0;
int32 X1 = X0 + 1;
int32 Y1 = Y0 + 1;
WrapTo(X0, SizeX);
WrapTo(X1, SizeX);
Y0 = FMath::Clamp(Y0, 0, (int32)(SizeY - 1));
Y1 = FMath::Clamp(Y1, 0, (int32)(SizeY - 1));
FLinearColor CornerRGB00 = Access(X0, Y0);
FLinearColor CornerRGB10 = Access(X1, Y0);
FLinearColor CornerRGB01 = Access(X0, Y1);
FLinearColor CornerRGB11 = Access(X1, Y1);
FLinearColor CornerRGB0 = FMath::Lerp(CornerRGB00, CornerRGB10, FracX);
FLinearColor CornerRGB1 = FMath::Lerp(CornerRGB01, CornerRGB11, FracX);
return FMath::Lerp(CornerRGB0, CornerRGB1, FracY);
}
/** Makes a filtered lookup using a direction. */
FLinearColor LookupLongLat(FVector NormalizedDirection) const
{
// see http://gl.ict.usc.edu/Data/HighResProbes
// latitude-longitude panoramic format = equirectangular mapping
float X = (1 + atan2(NormalizedDirection.X, - NormalizedDirection.Z) / PI) / 2 * SizeX;
float Y = acos(NormalizedDirection.Y) / PI * SizeY;
return LookupFiltered(X, Y);
}
};
// transform world space vector to a space relative to the face
static FVector TransformSideToWorldSpace(uint32 CubemapFace, FVector InDirection)
{
float x = InDirection.X, y = InDirection.Y, z = InDirection.Z;
FVector Ret = FVector(0, 0, 0);
// see http://msdn.microsoft.com/en-us/library/bb204881(v=vs.85).aspx
switch(CubemapFace)
{
case 0: Ret = FVector(+z, -y, -x); break;
case 1: Ret = FVector(-z, -y, +x); break;
case 2: Ret = FVector(+x, +z, +y); break;
case 3: Ret = FVector(+x, -z, -y); break;
case 4: Ret = FVector(+x, -y, +z); break;
case 5: Ret = FVector(-x, -y, -z); break;
default:
checkSlow(0);
}
// this makes it with the Unreal way (z and y are flipped)
return FVector(Ret.X, Ret.Z, Ret.Y);
}
// transform vector relative to the face to world space
static FVector TransformWorldToSideSpace(uint32 CubemapFace, FVector InDirection)
{
// undo Unreal way (z and y are flipped)
float x = InDirection.X, y = InDirection.Z, z = InDirection.Y;
FVector Ret = FVector(0, 0, 0);
// see http://msdn.microsoft.com/en-us/library/bb204881(v=vs.85).aspx
switch(CubemapFace)
{
case 0: Ret = FVector(-z, -y, +x); break;
case 1: Ret = FVector(+z, -y, -x); break;
case 2: Ret = FVector(+x, +z, +y); break;
case 3: Ret = FVector(+x, -z, -y); break;
case 4: Ret = FVector(+x, -y, +z); break;
case 5: Ret = FVector(-x, -y, -z); break;
default:
checkSlow(0);
}
return Ret;
}
FVector ComputeSSCubeDirectionAtTexelCenter(uint32 x, uint32 y, float InvSideExtent)
{
// center of the texels
FVector DirectionSS((x + 0.5f) * InvSideExtent * 2 - 1, (y + 0.5f) * InvSideExtent * 2 - 1, 1);
DirectionSS.Normalize();
return DirectionSS;
}
static FVector ComputeWSCubeDirectionAtTexelCenter(uint32 CubemapFace, uint32 x, uint32 y, float InvSideExtent)
{
FVector DirectionSS = ComputeSSCubeDirectionAtTexelCenter(x, y, InvSideExtent);
FVector DirectionWS = TransformSideToWorldSpace(CubemapFace, DirectionSS);
return DirectionWS;
}
static int32 ComputeLongLatCubemapExtents(const FImage& SrcImage)
{
return FMath::Clamp(1 << FMath::FloorLog2(SrcImage.SizeX / 2), 32, 512);
}
/**
* Generates the base cubemap mip from a longitude-latitude 2D image.
* @param OutMip - The output mip.
* @param SrcImage - The source longlat image.
*/
static void GenerateBaseCubeMipFromLongitudeLatitude2D(FImage* OutMip, const FImage& SrcImage)
{
FImage LongLatImage;
SrcImage.CopyTo(LongLatImage, ERawImageFormat::RGBA32F, false);
FImageViewLongLat LongLatView(LongLatImage);
// TODO_TEXTURE: Expose target size to user.
int32 Extent = ComputeLongLatCubemapExtents(LongLatImage);
float InvExtent = 1.0f / Extent;
OutMip->Init(Extent, Extent, 6, ERawImageFormat::RGBA32F, false);
for(uint32 Face = 0; Face < 6; ++Face)
{
FImageView2D MipView(*OutMip, Face);
for(int32 y = 0; y < Extent; ++y)
{
for(int32 x = 0; x < Extent; ++x)
{
FVector DirectionWS = ComputeWSCubeDirectionAtTexelCenter(Face, x, y, InvExtent);
MipView.Access(x, y) = LongLatView.LookupLongLat(DirectionWS);
}
}
}
}
class FTexelProcessor
{
public:
// @param InConeAxisSS - normalized, in side space
// @param TexelAreaArray - precomputed area of each texel for correct weighting
FTexelProcessor(const FVector& InConeAxisSS, float ConeAngle, const FLinearColor* InSideData, const float* InTexelAreaArray, uint32 InFullExtent)
: ConeAxisSS(InConeAxisSS)
, AccumulatedColor(0, 0, 0, 0)
, SideData(InSideData)
, TexelAreaArray(InTexelAreaArray)
, FullExtent(InFullExtent)
{
ConeAngleSin = sinf(ConeAngle);
ConeAngleCos = cosf(ConeAngle);
// *2 as the position is from -1 to 1
// / InFullExtent as x and y is in the range 0..InFullExtent-1
PositionToWorldScale = 2.0f / InFullExtent;
InvFullExtent = 1.0f / FullExtent;
// examples: 0 to diffuse convolution, 0.95f for glossy
DirDot = FMath::Min(FMath::Cos(ConeAngle), 0.9999f);
InvDirOneMinusDot = 1.0f / (1.0f - DirDot);
// precomputed sqrt(2.0f * 2.0f + 2.0f * 2.0f)
float Sqrt8 = 2.8284271f;
RadiusToWorldScale = Sqrt8 / (float)InFullExtent;
}
// @return true: yes, traverse deeper, false: not relevant
bool TestIfRelevant(uint32 x, uint32 y, uint32 LocalExtent) const
{
float HalfExtent = LocalExtent * 0.5f;
float U = (x + HalfExtent) * PositionToWorldScale - 1.0f;
float V = (y + HalfExtent) * PositionToWorldScale - 1.0f;
float SphereRadius = RadiusToWorldScale * LocalExtent;
FVector SpherePos(U, V, 1);
return FMath::SphereConeIntersection(SpherePos, SphereRadius, ConeAxisSS, ConeAngleSin, ConeAngleCos);
}
void Process(uint32 x, uint32 y)
{
const FLinearColor* In = &SideData[x + y * FullExtent];
FVector DirectionSS = ComputeSSCubeDirectionAtTexelCenter(x, y, InvFullExtent);
float DotValue = ConeAxisSS | DirectionSS;
if(DotValue > DirDot)
{
// 0..1, 0=at kernel border..1=at kernel center
float KernelWeight = 1.0f - (1.0f - DotValue) * InvDirOneMinusDot;
// apply smoothstep function (softer, less linear result)
KernelWeight = KernelWeight * KernelWeight * (3 - 2 * KernelWeight);
float AreaCompensation = TexelAreaArray[x + y * FullExtent];
// AreaCompensation would be need for correctness but seems it has a but
// as it looks much better (no seam) without, the effect is minor so it's deactivated for now.
// float Weight = KernelWeight * AreaCompensation;
float Weight = KernelWeight;
AccumulatedColor.R += Weight * In->R;
AccumulatedColor.G += Weight * In->G;
AccumulatedColor.B += Weight * In->B;
AccumulatedColor.A += Weight;
}
}
// normalized, in side space
FVector ConeAxisSS;
FLinearColor AccumulatedColor;
// cached for better performance
float ConeAngleSin;
float ConeAngleCos;
float PositionToWorldScale;
float RadiusToWorldScale;
float InvFullExtent;
// 0 to diffuse convolution, 0.95f for glossy
float DirDot;
float InvDirOneMinusDot;
/** [x + y * FullExtent] */
const FLinearColor* SideData;
const float* TexelAreaArray;
uint32 FullExtent;
};
template <class TVisitor>
void TCubemapSideRasterizer(TVisitor &TexelProcessor, int32 x, uint32 y, uint32 Extent)
{
if(Extent > 1)
{
if(!TexelProcessor.TestIfRelevant(x, y, Extent))
{
return;
}
Extent /= 2;
TCubemapSideRasterizer(TexelProcessor, x, y, Extent);
TCubemapSideRasterizer(TexelProcessor, x + Extent, y, Extent);
TCubemapSideRasterizer(TexelProcessor, x, y + Extent, Extent);
TCubemapSideRasterizer(TexelProcessor, x + Extent, y + Extent, Extent);
}
else
{
TexelProcessor.Process(x, y);
}
}
static FLinearColor IntegrateAngularArea(FImage& Image, FVector FilterDirectionWS, float ConeAngle, const float* TexelAreaArray)
{
// Alpha channel is used to renormalize later
FLinearColor ret(0, 0, 0, 0);
int32 Extent = Image.SizeX;
for(uint32 Face = 0; Face < 6; ++Face)
{
FImageView2D ImageView(Image, Face);
FVector FilterDirectionSS = TransformWorldToSideSpace(Face, FilterDirectionWS);
FTexelProcessor Processor(FilterDirectionSS, ConeAngle, &ImageView.Access(0,0), TexelAreaArray, Extent);
// recursively split the (0,0)-(Extent-1,Extent-1), tests for intersection and processes only colors inside
TCubemapSideRasterizer(Processor, 0, 0, Extent);
ret += Processor.AccumulatedColor;
}
if(ret.A != 0)
{
float Inv = 1.0f / ret.A;
ret.R *= Inv;
ret.G *= Inv;
ret.B *= Inv;
}
else
{
// should not happen
// checkSlow(0);
}
ret.A = 0;
return ret;
}
// @return 2 * computed triangle area
static float TriangleArea2_3D(FVector A, FVector B, FVector C)
{
return ((A-B) ^ (C-B)).Size();
}
static float ComputeTexelArea(uint32 x, uint32 y, float InvSideExtentMul2)
{
float fU = x * InvSideExtentMul2 - 1;
float fV = y * InvSideExtentMul2 - 1;
FVector CornerA = FVector(fU, fV, 1);
FVector CornerB = FVector(fU + InvSideExtentMul2, fV, 1);
FVector CornerC = FVector(fU, fV + InvSideExtentMul2, 1);
FVector CornerD = FVector(fU + InvSideExtentMul2, fV + InvSideExtentMul2, 1);
CornerA.Normalize();
CornerB.Normalize();
CornerC.Normalize();
CornerD.Normalize();
return TriangleArea2_3D(CornerA, CornerB, CornerC) + TriangleArea2_3D(CornerC, CornerB, CornerD) * 0.5f;
}
/**
* Generate a mip using angular filtering.
* @param DestMip - The filtered mip.
* @param SrcMip - The source mip which will be filtered.
* @param ConeAngle - The cone angle with which to filter.
*/
static void GenerateAngularFilteredMip(FImage* DestMip, FImage& SrcMip, float ConeAngle)
{
int32 Extent = DestMip->SizeX;
float InvSideExtent = 1.0f / Extent;
TArray<float> TexelAreaArray;
TexelAreaArray.AddUninitialized(SrcMip.SizeX * SrcMip.SizeY);
// precompute the area size for one face (is the same for each face)
for(int32 y = 0; y < SrcMip.SizeY; ++y)
{
for(int32 x = 0; x < SrcMip.SizeX; ++x)
{
TexelAreaArray[x + y * SrcMip.SizeX] = ComputeTexelArea(x, y, InvSideExtent * 2);
}
}
for(int32 Face = 0; Face < 6; ++Face)
{
FImageView2D DestMipView(*DestMip, Face);
for(int32 y = 0; y < Extent; ++y)
{
for(int32 x = 0; x < Extent; ++x)
{
FVector DirectionWS = ComputeWSCubeDirectionAtTexelCenter(Face, x, y, InvSideExtent);
DestMipView.Access(x,y) = IntegrateAngularArea(SrcMip, DirectionWS, ConeAngle, TexelAreaArray.GetData());
}
}
}
}
/**
* Generates angularly filtered mips.
* @param InOutMipChain - The mip chain to angularly filter.
* @param NumMips - The number of mips the chain should have.
* @param DiffuseConvolveMipLevel - The mip level that contains the diffuse convolution.
*/
static void GenerateAngularFilteredMips(TArray<FImage>& InOutMipChain, int32 NumMips, uint32 DiffuseConvolveMipLevel)
{
TArray<FImage> SrcMipChain;
Exchange(SrcMipChain, InOutMipChain);
InOutMipChain.Empty(NumMips);
// Generate simple averaged mips to accelerate angular filtering.
for (int32 MipIndex = SrcMipChain.Num(); MipIndex < NumMips; ++MipIndex)
{
FImage& BaseMip = SrcMipChain[MipIndex - 1];
int32 BaseExtent = BaseMip.SizeX;
int32 MipExtent = FMath::Max(BaseExtent >> 1, 1);
FImage* Mip = new(SrcMipChain) FImage(MipExtent, MipExtent, BaseMip.NumSlices, BaseMip.Format);
for(int32 Face = 0; Face < 6; ++Face)
{
FImageView2D BaseMipView(BaseMip, Face);
FImageView2D MipView(*Mip, Face);
for(int32 y = 0; y < MipExtent; ++y)
{
for(int32 x = 0; x < MipExtent; ++x)
{
FLinearColor Sum = (
BaseMipView.Access(x*2, y*2) +
BaseMipView.Access(x*2+1, y*2) +
BaseMipView.Access(x*2, y*2+1) +
BaseMipView.Access(x*2+1, y*2+1)
) * 0.25f;
MipView.Access(x,y) = Sum;
}
}
}
}
int32 Extent = 1 << (NumMips - 1);
int32 BaseExtent = Extent;
for (int32 i = 0; i < NumMips; ++i)
{
// 0:top mip 1:lowest mip = diffuse convolve
float NormalizedMipLevel = i / (float)(NumMips - DiffuseConvolveMipLevel);
float AdjustedMipLevel = NormalizedMipLevel * NumMips;
float NormalizedWidth = BaseExtent * FMath::Pow(2.0f, -AdjustedMipLevel);
float TexelSize = 1.0f / NormalizedWidth;
// 0.001f:sharp .. PI/2: diffuse convolve
// all lower mips are used for diffuse convolve
// above that the angle blends from sharp to diffuse convolved version
float ConeAngle = PI / 2.0f * TexelSize;
// restrict to reasonable range
ConeAngle = FMath::Clamp(ConeAngle, 0.002f, (float)PI / 2.0f);
UE_LOG(LogTextureCompressor, Verbose, TEXT("GenerateAngularFilteredMips %f %f %f %f %f"), NormalizedMipLevel, AdjustedMipLevel, NormalizedWidth, TexelSize, ConeAngle * 180 / PI);
// 0:normal, -1:4x faster, +1:4 times slower but more precise, -2, 2 ...
float QualityBias = 3.0f;
// defined to result in a area of 1.0f (NormalizedArea)
// optimized = 0.5f * FMath::Sqrt(1.0f / PI);
float SphereRadius = 0.28209478f;
float SegmentHeight = SphereRadius * (1.0f - FMath::Cos(ConeAngle));
// compute SphereSegmentArea
float AreaCoveredInNormalizedArea = 2 * PI * SphereRadius * SegmentHeight;
checkSlow(AreaCoveredInNormalizedArea <= 0.5f);
// unoptimized
// float FloatInputMip = FMath::Log2(FMath::Sqrt(AreaCoveredInNormalizedArea)) + InputMipCount - QualityBias;
// optimized
float FloatInputMip = 0.5f * FMath::Log2(AreaCoveredInNormalizedArea) + NumMips - QualityBias;
uint32 InputMip = FMath::Clamp(FMath::TruncToInt(FloatInputMip), 0, NumMips - 1);
FImage* Mip = new(InOutMipChain) FImage(Extent, Extent, 6, ERawImageFormat::RGBA32F);
GenerateAngularFilteredMip(Mip, SrcMipChain[InputMip], ConeAngle);
Extent = FMath::Max(Extent >> 1, 1);
}
}
/*------------------------------------------------------------------------------
Image Processing.
------------------------------------------------------------------------------*/
/**
* Adjusts the colors of the image using the specified settings (alpha channel will not be modified.)
*
* @param Image Image to adjust
* @param InParams Color adjustment parameters
*/
static void AdjustImageColors( FImage& Image, const FColorAdjustmentParameters& InParams )
{
check( Image.SizeX > 0 && Image.SizeY > 0 );
if( !FMath::IsNearlyEqual( InParams.AdjustBrightness, 1.0f, (float)KINDA_SMALL_NUMBER ) ||
!FMath::IsNearlyEqual( InParams.AdjustBrightnessCurve, 1.0f, (float)KINDA_SMALL_NUMBER ) ||
!FMath::IsNearlyEqual( InParams.AdjustSaturation, 1.0f, (float)KINDA_SMALL_NUMBER ) ||
!FMath::IsNearlyEqual( InParams.AdjustVibrance, 0.0f, (float)KINDA_SMALL_NUMBER ) ||
!FMath::IsNearlyEqual( InParams.AdjustRGBCurve, 1.0f, (float)KINDA_SMALL_NUMBER ) ||
!FMath::IsNearlyEqual( InParams.AdjustHue, 0.0f, (float)KINDA_SMALL_NUMBER ) ||
!FMath::IsNearlyEqual( InParams.AdjustMinAlpha, 0.0f, (float)KINDA_SMALL_NUMBER ) ||
!FMath::IsNearlyEqual( InParams.AdjustMaxAlpha, 1.0f, (float)KINDA_SMALL_NUMBER ) )
{
const int32 NumPixels = Image.SizeX * Image.SizeY * Image.NumSlices;
FLinearColor* ImageColors = Image.AsRGBA32F();
const bool bIsSRGB = Image.bSRGB;
for( int32 CurPixelIndex = 0; CurPixelIndex < NumPixels; ++CurPixelIndex )
{
const FLinearColor OriginalColor = ImageColors[ CurPixelIndex ];
// Convert to HSV
FLinearColor HSVColor = OriginalColor.LinearRGBToHSV();
float& PixelHue = HSVColor.R;
float& PixelSaturation = HSVColor.G;
float& PixelValue = HSVColor.B;
// Apply brightness adjustment
PixelValue *= InParams.AdjustBrightness;
// Apply brightness power adjustment
if( !FMath::IsNearlyEqual( InParams.AdjustBrightnessCurve, 1.0f, (float)KINDA_SMALL_NUMBER ) && InParams.AdjustBrightnessCurve != 0.0f )
{
// Raise HSV.V to the specified power
PixelValue = FMath::Pow( PixelValue, InParams.AdjustBrightnessCurve );
}
// Apply "vibrance" adjustment
if( !FMath::IsNearlyZero( InParams.AdjustVibrance, (float)KINDA_SMALL_NUMBER ) )
{
const float SatRaisePow = 5.0f;
const float InvSatRaised = FMath::Pow( 1.0f - PixelSaturation, SatRaisePow );
const float ClampedVibrance = FMath::Clamp( InParams.AdjustVibrance, 0.0f, 1.0f );
const float HalfVibrance = ClampedVibrance * 0.5f;
const float SatProduct = HalfVibrance * InvSatRaised;
PixelSaturation += SatProduct;
}
// Apply saturation adjustment
PixelSaturation *= InParams.AdjustSaturation;
// Apply hue adjustment
PixelHue += InParams.AdjustHue;
// Clamp HSV values
{
PixelHue = FMath::Fmod( PixelHue, 360.0f );
if( PixelHue < 0.0f )
{
// Keep the hue value positive as HSVToLinearRGB prefers that
PixelHue += 360.0f;
}
PixelSaturation = FMath::Clamp( PixelSaturation, 0.0f, 1.0f );
PixelValue = FMath::Clamp( PixelValue, 0.0f, 1.0f );
}
// Convert back to a linear color
FLinearColor LinearColor = HSVColor.HSVToLinearRGB();
// Apply RGB curve adjustment (linear space)
if( !FMath::IsNearlyEqual( InParams.AdjustRGBCurve, 1.0f, (float)KINDA_SMALL_NUMBER ) && InParams.AdjustRGBCurve != 0.0f )
{
LinearColor.R = FMath::Pow( LinearColor.R, InParams.AdjustRGBCurve );
LinearColor.G = FMath::Pow( LinearColor.G, InParams.AdjustRGBCurve );
LinearColor.B = FMath::Pow( LinearColor.B, InParams.AdjustRGBCurve );
}
// Remap the alpha channel
LinearColor.A = FMath::Lerp(InParams.AdjustMinAlpha, InParams.AdjustMaxAlpha, OriginalColor.A);
ImageColors[ CurPixelIndex ] = LinearColor;
}
}
}
/**
* Compute the alpha channel how BokehDOF needs it setup
*
* @param Image Image to adjust
*/
static void ComputeBokehAlpha(FImage& Image)
{
check( Image.SizeX > 0 && Image.SizeY > 0 );
const int32 NumPixels = Image.SizeX * Image.SizeY * Image.NumSlices;
FLinearColor* ImageColors = Image.AsRGBA32F();
const bool bIsSRGB = Image.bSRGB;
// compute LinearAverage
FLinearColor LinearAverage;
{
FLinearColor LinearSum(0, 0, 0, 0);
for( int32 CurPixelIndex = 0; CurPixelIndex < NumPixels; ++CurPixelIndex )
{
LinearSum += ImageColors[ CurPixelIndex ];
}
LinearAverage = LinearSum / (float)NumPixels;
}
FLinearColor Scale(1, 1, 1, 1);
// we want to normalize the image to have 0.5 as average luminance, this is assuming clamping doesn't happen (can happen when using a very small Bokeh shape)
{
float RGBLum = (LinearAverage.R + LinearAverage.G + LinearAverage.B) / 3.0f;
// ideally this would be 1 but then some pixels would need to be >1 which is not supported for the textureformat we want to use.
// The value affects the occlusion computation of the BokehDOF
const float LumGoal = 0.25f;
// clamp to avoid division by 0
Scale *= LumGoal / FMath::Max(RGBLum, 0.001f);
}
{
for( int32 CurPixelIndex = 0; CurPixelIndex < NumPixels; ++CurPixelIndex )
{
const FLinearColor OriginalColor = ImageColors[ CurPixelIndex ];
// Convert to a linear color
FLinearColor LinearColor = OriginalColor * Scale;
float RGBLum = (LinearColor.R + LinearColor.G + LinearColor.B) / 3.0f;
LinearColor.A = FMath::Clamp(RGBLum, 0.0f, 1.0f);
ImageColors[ CurPixelIndex ] = LinearColor;
}
}
}
/**
* Replicates the contents of the red channel to the green, blue, and alpha channels.
*/
static void ReplicateRedChannel( TArray<FImage>& InOutMipChain )
{
const uint32 MipCount = InOutMipChain.Num();
for ( uint32 MipIndex = 0; MipIndex < MipCount; ++MipIndex )
{
FImage& SrcMip = InOutMipChain[MipIndex];
FLinearColor* FirstColor = SrcMip.AsRGBA32F();
FLinearColor* LastColor = FirstColor + (SrcMip.SizeX * SrcMip.SizeY * SrcMip.NumSlices);
for ( FLinearColor* Color = FirstColor; Color < LastColor; ++Color )
{
*Color = FLinearColor( Color->R, Color->R, Color->R, Color->R );
}
}
}
/**
* Replicates the contents of the alpha channel to the red, green, and blue channels.
*/
static void ReplicateAlphaChannel( TArray<FImage>& InOutMipChain )
{
const uint32 MipCount = InOutMipChain.Num();
for ( uint32 MipIndex = 0; MipIndex < MipCount; ++MipIndex )
{
FImage& SrcMip = InOutMipChain[MipIndex];
FLinearColor* FirstColor = SrcMip.AsRGBA32F();
FLinearColor* LastColor = FirstColor + (SrcMip.SizeX * SrcMip.SizeY * SrcMip.NumSlices);
for ( FLinearColor* Color = FirstColor; Color < LastColor; ++Color )
{
*Color = FLinearColor( Color->A, Color->A, Color->A, Color->A );
}
}
}
/**
* Flips the contents of the green channel.
* @param InOutMipChain - The mip chain on which the green channel shall be flipped.
*/
static void FlipGreenChannel( FImage& Image )
{
FLinearColor* FirstColor = Image.AsRGBA32F();
FLinearColor* LastColor = FirstColor + (Image.SizeX * Image.SizeY * Image.NumSlices);
for ( FLinearColor* Color = FirstColor; Color < LastColor; ++Color )
{
Color->G = 1.0f - FMath::Clamp(Color->G, 0.0f, 1.0f);
}
}
/**
* Detects whether or not the image contains an alpha channel where at least one texel is != 255.
*/
static bool DetectAlphaChannel(const FImage& InImage)
{
// Uncompressed data is required to check for an alpha channel.
const FLinearColor* SrcColors = InImage.AsRGBA32F();
const FLinearColor* LastColor = SrcColors + (InImage.SizeX * InImage.SizeY * InImage.NumSlices);
while (SrcColors < LastColor)
{
if (SrcColors->A < (1.0f - SMALL_NUMBER))
{
return true;
}
++SrcColors;
}
return false;
}
float RoughnessToSpecularPower(float Roughness)
{
float Div = FMath::Pow(Roughness, 4);
// Roughness of 0 should result in a high specular power
float MaxSpecPower = 10000000000.0f;
Div = FMath::Max(Div, 2.0f / (MaxSpecPower + 2.0f));
return 2.0f / Div - 2.0f;
}
float SpecularPowerToRoughness(float SpecularPower)
{
float Out = FMath::Pow( SpecularPower * 0.5f + 1.0f, -0.25f );
return Out;
}
// @param CompositeTextureMode original type ECompositeTextureMode
void ApplyCompositeTexture(FImage& RoughnessSourceMips, const FImage& NormalSourceMips, uint8 CompositeTextureMode, float CompositePower)
{
check(RoughnessSourceMips.SizeX == NormalSourceMips.SizeX);
check(RoughnessSourceMips.SizeY == NormalSourceMips.SizeY);
FLinearColor* FirstColor = RoughnessSourceMips.AsRGBA32F();
const FLinearColor* NormalColors = NormalSourceMips.AsRGBA32F();
FLinearColor* LastColor = FirstColor + (RoughnessSourceMips.SizeX * RoughnessSourceMips.SizeY * RoughnessSourceMips.NumSlices);
for ( FLinearColor* Color = FirstColor; Color < LastColor; ++Color, ++NormalColors )
{
FVector Normal = FVector(NormalColors->R * 2.0f - 1.0f, NormalColors->G * 2.0f - 1.0f, NormalColors->B * 2.0f - 1.0f);
// to prevent crash for unknown CompositeTextureMode
float Dummy;
float* RefValue = &Dummy;
switch((ECompositeTextureMode)CompositeTextureMode)
{
case CTM_NormalRoughnessToRed:
RefValue = &Color->R;
break;
case CTM_NormalRoughnessToGreen:
RefValue = &Color->G;
break;
case CTM_NormalRoughnessToBlue:
RefValue = &Color->B;
break;
case CTM_NormalRoughnessToAlpha:
RefValue = &Color->A;
break;
default:
checkSlow(0);
}
// Toksvig estimation of variance
float LengthN = FMath::Min( Normal.Size(), 1.0f );
float Variance = ( 1.0f - LengthN ) / LengthN;
Variance = FMath::Max( 0.0f, Variance - 0.00004f );
Variance *= CompositePower;
float Roughness = *RefValue;
#if 0
float Power = RoughnessToSpecularPower( Roughness );
Power = Power / ( 1.0f + Variance * Power );
Roughness = SpecularPowerToRoughness( Power );
#else
// Refactored above to avoid divide by zero
float a = Roughness * Roughness;
float a2 = a * a;
float B = 2.0f * Variance * (a2 - 1.0f);
a2 = ( B - a2 ) / ( B - 1.0f );
Roughness = FMath::Pow( a2, 0.25f );
#endif
*RefValue = Roughness;
}
}
/*------------------------------------------------------------------------------
Image Compression.
------------------------------------------------------------------------------*/
/**
* Asynchronous compression, used for compressing mips simultaneously.
*/
class FAsyncCompressionWorker : public FNonAbandonableTask
{
public:
/**
* Initializes the data and creates the async compression task.
*/
FAsyncCompressionWorker(const ITextureFormat* InTextureFormat, const FImage* InImage, const FTextureBuildSettings& InBuildSettings, bool bInImageHasAlphaChannel)
: TextureFormat(*InTextureFormat)
, SourceImage(*InImage)
, BuildSettings(InBuildSettings)
, bImageHasAlphaChannel(bInImageHasAlphaChannel)
, bCompressionResults(false)
{
}
/**
* Compresses the texture
*/
void DoWork()
{
bCompressionResults = TextureFormat.CompressImage(
SourceImage,
BuildSettings,
bImageHasAlphaChannel,
CompressedImage
);
}
/**
* Give the name for external event viewers
* @return the name to display in external event viewers
*/
static const TCHAR* Name()
{
return TEXT("FAsyncCompressionTask");
}
bool GetCompressionResults(FCompressedImage2D& OutCompressedImage) const
{
OutCompressedImage = CompressedImage;
return bCompressionResults;
}
private:
/** Texture format interface with which to compress. */
const ITextureFormat& TextureFormat;
/** The image to compress. */
const FImage& SourceImage;
/** The resulting compressed image. */
FCompressedImage2D CompressedImage;
/** Build settings. */
FTextureBuildSettings BuildSettings;
/** true if the image has a non-white alpha channel. */
bool bImageHasAlphaChannel;
/** true if compression was successful. */
bool bCompressionResults;
};
typedef FAsyncTask<FAsyncCompressionWorker> FAsyncCompressionTask;
FTextureFormatCompressorCaps GetTextureFormatCaps(const FTextureBuildSettings& Settings)
{
ITargetPlatformManagerModule* TPM = GetTargetPlatformManager();
if (TPM)
{
const ITextureFormat* TextureFormat = TPM->FindTextureFormat(Settings.TextureFormatName);
if (TextureFormat != nullptr)
{
return TextureFormat->GetFormatCapabilities();
}
}
return FTextureFormatCompressorCaps();
}
// compress mip-maps in InMipChain and add mips to Texture, might alter the source content
static bool CompressMipChain(
const TArray<FImage>& MipChain,
const FTextureBuildSettings& Settings,
TArray<FCompressedImage2D>& OutMips
)
{
ITargetPlatformManagerModule* TPM = GetTargetPlatformManager();
if (TPM)
{
const ITextureFormat* TextureFormat = TPM->FindTextureFormat(Settings.TextureFormatName);
if (TextureFormat)
{
TIndirectArray<FAsyncCompressionTask> AsyncCompressionTasks;
const int32 MipCount = MipChain.Num();
const bool bImageHasAlphaChannel = DetectAlphaChannel(MipChain[0]);
const int32 MinAsyncCompressionSize = 128;
const bool bAllowParallelBuild = TextureFormat->AllowParallelBuild();
bool bCompressionSucceeded = true;
uint32 StartCycles = FPlatformTime::Cycles();
OutMips.Empty(MipCount);
for (int32 MipIndex = 0; MipIndex < MipCount; ++MipIndex)
{
const FImage& SrcMip = MipChain[MipIndex];
FCompressedImage2D& DestMip = *new(OutMips) FCompressedImage2D;
if (bAllowParallelBuild && FMath::Min(SrcMip.SizeX, SrcMip.SizeY) >= MinAsyncCompressionSize)
{
FAsyncCompressionTask* AsyncTask = new(AsyncCompressionTasks) FAsyncCompressionTask(
TextureFormat,
&SrcMip,
Settings,
bImageHasAlphaChannel
);
AsyncTask->StartBackgroundTask();
}
else
{
bCompressionSucceeded = bCompressionSucceeded && TextureFormat->CompressImage(
SrcMip,
Settings,
bImageHasAlphaChannel,
DestMip
);
}
}
for (int32 TaskIndex = 0; TaskIndex < AsyncCompressionTasks.Num(); ++TaskIndex)
{
FAsyncCompressionTask& AsynTask = AsyncCompressionTasks[TaskIndex];
AsynTask.EnsureCompletion();
FCompressedImage2D& DestMip = OutMips[TaskIndex];
bCompressionSucceeded = bCompressionSucceeded && AsynTask.GetTask().GetCompressionResults(DestMip);
}
if (!bCompressionSucceeded)
{
OutMips.Empty();
}
uint32 EndCycles = FPlatformTime::Cycles();
UE_LOG(LogTextureCompressor,Verbose,TEXT("Compressed %dx%dx%d %s in %fms"),
MipChain[0].SizeX,
MipChain[0].SizeY,
MipChain[0].NumSlices,
*Settings.TextureFormatName.ToString(),
FPlatformTime::ToMilliseconds( EndCycles-StartCycles )
);
return bCompressionSucceeded;
}
else
{
UE_LOG(LogTextureCompressor, Warning,
TEXT("Failed to find compressor for texture format '%s'."),
*Settings.TextureFormatName.ToString()
);
return false;
}
}
UE_LOG(LogTextureCompressor, Warning,
TEXT("Failed to load target platform manager module. Unable to compress textures.")
);
return false;
}
// only useful for normal maps, fixed bad input (denormalized normals) and improved quality (quantization artifacts)
static void NormalizeMip(FImage& InOutMip)
{
const uint32 NumPixels = InOutMip.SizeX * InOutMip.SizeY * InOutMip.NumSlices;
FLinearColor* ImageColors = InOutMip.AsRGBA32F();
for(uint32 CurPixelIndex = 0; CurPixelIndex < NumPixels; ++CurPixelIndex)
{
FLinearColor& Color = ImageColors[CurPixelIndex];
FVector Normal = FVector(Color.R * 2.0f - 1.0f, Color.G * 2.0f - 1.0f, Color.B * 2.0f - 1.0f);
Normal = Normal.SafeNormal();
Color = FLinearColor(Normal.X * 0.5f + 0.5f, Normal.Y * 0.5f + 0.5f, Normal.Z * 0.5f + 0.5f, Color.A);
}
}
/**
* Texture compression module
*/
class FTextureCompressorModule : public ITextureCompressorModule
{
public:
FTextureCompressorModule()
#if PLATFORM_WINDOWS
: nvTextureToolsHandle(0)
#endif //PLATFORM_WINDOWS
{
}
virtual bool BuildTexture(
const TArray<FImage>& SourceMips,
const TArray<FImage>& AssociatedNormalSourceMips,
const FTextureBuildSettings& BuildSettings,
TArray<FCompressedImage2D>& OutTextureMips
)
{
TArray<FImage> IntermediateMipChain;
if(!BuildTextureMips(SourceMips, BuildSettings, IntermediateMipChain))
{
return false;
}
// apply roughness adjustment depending on normal map variation
if(AssociatedNormalSourceMips.Num())
{
// check AssociatedNormalSourceMips.Format;
TArray<FImage> IntermediateAssociatedNormalSourceMipChain;
FTextureBuildSettings DefaultSettings;
// helps to reduce aliasing further
DefaultSettings.MipSharpening = -4.0f;
DefaultSettings.SharpenMipKernelSize = 4;
DefaultSettings.bApplyKernelToTopMip = true;
// important to make accurate computation with normal length
DefaultSettings.bRenormalizeTopMip = true;
if(!BuildTextureMips(AssociatedNormalSourceMips, DefaultSettings, IntermediateAssociatedNormalSourceMipChain))
{
UE_LOG(LogTexture, Warning, TEXT("Failed to generate texture mips for composite texture"));
}
if(!ApplyCompositeTexture(IntermediateMipChain, IntermediateAssociatedNormalSourceMipChain, BuildSettings.CompositeTextureMode, BuildSettings.CompositePower))
{
UE_LOG(LogTexture, Warning, TEXT("Failed to apply composite texture"));
}
}
// Set the correct biased texture size so that the compressor understands the original source image size
// This is requires for platforms that may need to tile based on the original source texture size
BuildSettings.OriginalTextureSize.X = IntermediateMipChain[0].SizeX;
BuildSettings.OriginalTextureSize.Y = IntermediateMipChain[0].SizeY;
return CompressMipChain(IntermediateMipChain, BuildSettings, OutTextureMips);
}
// IModuleInterface implementation.
void StartupModule()
{
#if PLATFORM_WINDOWS
#if PLATFORM_64BITS
nvTextureToolsHandle = LoadLibraryW(TEXT("../../../Engine/Binaries/ThirdParty/nvTextureTools/Win64/nvtt_64.dll"));
#else //32-bit platform
nvTextureToolsHandle = LoadLibraryW(TEXT("../../../Engine/Binaries/ThirdParty/nvTextureTools/Win32/nvtt_.dll"));
#endif
#endif //PLATFORM_WINDOWS
}
void ShutdownModule()
{
#if PLATFORM_WINDOWS
FreeLibrary(nvTextureToolsHandle);
nvTextureToolsHandle = 0;
#endif
}
private:
#if PLATFORM_WINDOWS
// Handle to the nvtt dll
HMODULE nvTextureToolsHandle;
#endif //PLATFORM_WINDOWS
bool BuildTextureMips(
const TArray<FImage>& InSourceMips,
const FTextureBuildSettings& BuildSettings,
TArray<FImage>& OutMipChain)
{
check(InSourceMips.Num());
check(InSourceMips[0].SizeX > 0 && InSourceMips[0].SizeY > 0 && InSourceMips[0].NumSlices > 0);
const FTextureFormatCompressorCaps CompressorCaps = GetTextureFormatCaps(BuildSettings);
// Identify long-lat cubemaps.
bool bLongLatCubemap = BuildSettings.bCubemap && InSourceMips[0].NumSlices == 1;
if (BuildSettings.bCubemap && InSourceMips[0].NumSlices != 6 && !bLongLatCubemap)
{
return false;
}
// Determine the maximum possible mip counts for source and dest.
const int32 MaxSourceMipCount = bLongLatCubemap ?
1 + FMath::CeilLogTwo(ComputeLongLatCubemapExtents(InSourceMips[0])) :
1 + FMath::CeilLogTwo(FMath::Max(InSourceMips[0].SizeX, InSourceMips[0].SizeY));
const int32 MaxDestMipCount = 1 + FMath::CeilLogTwo(CompressorCaps.MaxTextureDimension);
// Determine the number of mips required by BuildSettings.
int32 NumOutputMips = (BuildSettings.MipGenSettings == TMGS_NoMipmaps) ? 1 : MaxSourceMipCount;
NumOutputMips = FMath::Min(NumOutputMips, MaxDestMipCount);
int32 NumSourceMips = InSourceMips.Num();
if (BuildSettings.MipGenSettings != TMGS_LeaveExistingMips ||
BuildSettings.MipGenSettings == TMGS_NoMipmaps ||
bLongLatCubemap)
{
NumSourceMips = 1;
}
// Check our source can be exported by our current compressor.
int32 LevelsToUsableSource = FMath::Max(0, MaxSourceMipCount - MaxDestMipCount);
int32 StartMip = FMath::Max(0, LevelsToUsableSource);
bool bBuildSourceImage = StartMip > (NumSourceMips - 1);
TArray<FImage> GeneratedSourceMips;
if (bBuildSourceImage)
{
// the source is larger than the compressor allows and no mip image exists to act as a smaller source.
// We must generate a suitable source image:
bool bSuitableFormat = InSourceMips.Last().Format == ERawImageFormat::RGBA32F;
const FImage& BaseImage = InSourceMips.Last();
if (BaseImage.SizeX != FMath::RoundUpToPowerOfTwo(BaseImage.SizeX) || BaseImage.SizeY != FMath::RoundUpToPowerOfTwo(BaseImage.SizeY))
{
UE_LOG(LogTextureCompressor, Warning,
TEXT("Source image %dx%d (npot) prevents resizing and is too large for compressors max dimension (%d)."),
BaseImage.SizeX,
BaseImage.SizeY,
CompressorCaps.MaxTextureDimension
);
return false;
}
FImage Temp;
if (!bSuitableFormat)
{
// convert to RGBA32F
BaseImage.CopyTo(Temp, ERawImageFormat::RGBA32F, false);
}
UE_LOG(LogTextureCompressor, Verbose,
TEXT("Source image %dx%d too large for compressors max dimension (%d). Resizing."),
BaseImage.SizeX,
BaseImage.SizeY,
CompressorCaps.MaxTextureDimension
);
GenerateMipChain(BuildSettings, bSuitableFormat ? BaseImage : Temp, GeneratedSourceMips, LevelsToUsableSource);
check(GeneratedSourceMips.Num() != 0);
// Note: The newly generated mip chain does not include the original top level mip.
StartMip--;
}
const TArray<FImage>& SourceMips = bBuildSourceImage ? GeneratedSourceMips : InSourceMips;
OutMipChain.Empty(NumOutputMips);
// Copy over base mips.
int32 CopyCount = FMath::Min(NumOutputMips - StartMip, SourceMips.Num());
check(StartMip <= SourceMips.Num());
for (int32 MipIndex = StartMip; MipIndex < CopyCount; ++MipIndex)
{
const FImage& Image = SourceMips[MipIndex];
const int32 SrcWidth = Image.SizeX;
const int32 SrcHeight = Image.SizeY;
ERawImageFormat::Type MipFormat = ERawImageFormat::RGBA32F;
// create base for the mip chain
FImage* Mip = new(OutMipChain) FImage();
if (bLongLatCubemap)
{
// Generate the base mip from the long-lat source image.
GenerateBaseCubeMipFromLongitudeLatitude2D(Mip, Image);
}
else
{
// copy base source content to the base of the mip chain
if(BuildSettings.bApplyKernelToTopMip)
{
FImage Temp;
Image.CopyTo(Temp, MipFormat, false);
if(BuildSettings.bRenormalizeTopMip)
{
NormalizeMip(Temp);
}
GenerateTopMip(Temp, *Mip, BuildSettings);
}
else
{
Image.CopyTo(*Mip, MipFormat, false);
if(BuildSettings.bRenormalizeTopMip)
{
NormalizeMip(*Mip);
}
}
}
// Apply color adjustments
AdjustImageColors(*Mip, BuildSettings.ColorAdjustment);
if (BuildSettings.bComputeBokehAlpha)
{
// To get the occlusion in the BokehDOF shader working for all Bokeh textures.
ComputeBokehAlpha(*Mip);
}
if (BuildSettings.bFlipGreenChannel)
{
FlipGreenChannel(*Mip);
}
}
// Generate any missing mips in the chain.
if (NumOutputMips > OutMipChain.Num())
{
// Do angular filtering of cubemaps if requested.
if (BuildSettings.bCubemap)
{
GenerateAngularFilteredMips(OutMipChain, NumOutputMips, BuildSettings.DiffuseConvolveMipLevel);
}
else
{
GenerateMipChain(BuildSettings, OutMipChain.Last(), OutMipChain);
}
}
check(OutMipChain.Num() == NumOutputMips);
// Apply post-mip generation adjustments.
if (BuildSettings.bReplicateRed)
{
ReplicateRedChannel(OutMipChain);
}
else if (BuildSettings.bReplicateAlpha)
{
ReplicateAlphaChannel(OutMipChain);
}
return true;
}
// @param CompositeTextureMode original type ECompositeTextureMode
// @return true on success, false on failure. Can fail due to bad mismatched dimensions of incomplete mip chains.
bool ApplyCompositeTexture(TArray<FImage>& RoughnessSourceMips, const TArray<FImage>& NormalSourceMips, uint8 CompositeTextureMode, float CompositePower)
{
uint32 MinLevel = FMath::Min(RoughnessSourceMips.Num(), NormalSourceMips.Num());
if( RoughnessSourceMips[RoughnessSourceMips.Num() - MinLevel].SizeX != NormalSourceMips[NormalSourceMips.Num() - MinLevel].SizeX ||
RoughnessSourceMips[RoughnessSourceMips.Num() - MinLevel].SizeY != NormalSourceMips[NormalSourceMips.Num() - MinLevel].SizeY )
{
//incomplete mip chain or mismatched dimensions so bail
return false;
}
for(uint32 Level = 0; Level < MinLevel; ++Level)
{
::ApplyCompositeTexture(RoughnessSourceMips[RoughnessSourceMips.Num() - 1 - Level], NormalSourceMips[NormalSourceMips.Num() - 1 - Level], CompositeTextureMode, CompositePower);
}
return true;
}
};
IMPLEMENT_MODULE(FTextureCompressorModule, TextureCompressor)