Files
UnrealEngine/Engine/Plugins/Mutable/Source/MutableRuntime/Private/MuR/OpImageTransformPrivate.cpp
2025-05-18 13:04:45 +08:00

445 lines
21 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "OpImageTransformPrivate.h"
#include "MuR/ParallelExecutionUtils.h"
#include "Math/Vector.h"
#include "Math/IntVector.h"
#include "Math/TransformCalculus.h"
namespace mu
{
//---------------------------------------------------------------------------------------------
//! Image transform, translate, scale, rotate
//---------------------------------------------------------------------------------------------
template<int32 NumChannels, EAddressMode AddressMode>
void ImageTransformNonVectorImpl(
uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect,
const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size,
float MipFactor,
const FTransform2f& Transform)
{
using FUint16Vector2 = UE::Math::TIntVector2<uint16>;
struct FPixelData
{
alignas(8) uint16 Data[NumChannels];
};
const FVector2f DestSizeF = FVector2f(DestSize.X, DestSize.Y);
const FVector2f DestNormFactor = FVector2f(1.0f) / DestSizeF;
check(DestCropRect.Min.X >= 0 && DestCropRect.Max.X <= DestSize.X);
check(DestCropRect.Min.Y >= 0 && DestCropRect.Max.Y <= DestSize.Y);
const FTransform2f InverseTransform = Transform.Inverse();
constexpr int32 BatchSize = 1 << 12;
const int32 NumBatchRows = FMath::DivideAndRoundUp(BatchSize, DestCropRect.Width());
const int32 NumBatches = FMath::DivideAndRoundUp(DestCropRect.Height(), NumBatchRows);
ParallelExecutionUtils::InvokeBatchParallelFor(NumBatches,
[=](int32 BatchId)
{
const int32 RowOffset = NumBatchRows * BatchId;
const FIntRect BatchRect = FIntRect(
DestCropRect.Min.X, DestCropRect.Min.Y + RowOffset,
DestCropRect.Max.X, FMath::Min(DestCropRect.Min.Y + RowOffset + NumBatchRows, DestCropRect.Max.Y));
const FVector2f Size0F(Src0Size.X, Src0Size.Y);
const FVector2f Size1F(Src1Size.X, Src1Size.Y);
for (int32 Y = BatchRect.Min.Y; Y < BatchRect.Max.Y; ++Y)
{
for (int32 X = BatchRect.Min.X; X < BatchRect.Max.X; ++X)
{
const FVector2f DestUv = InverseTransform.TransformPoint((FVector2f(X, Y) + 0.5f) * DestNormFactor);
// TODO: Make the black edge antialiazed?
if constexpr (AddressMode == EAddressMode::ClampToBlack)
{
if ((DestUv.X < 0.0f) | (DestUv.X > 1.0f) | (DestUv.Y < 0.0f) | (DestUv.Y > 1.0f))
{
// Black texture init is mandatory for ClampToBlack Address mode, no need to set black again.
//if constexpr (NumChannels == 4)
//{
// FMemory::Memzero(&DestData[(Y * DestSize.X + X) * NumChannels], 4);
//}
//else
//{
// for (uint32 Channel = 0; Channel < NumChannels; ++Channel)
// {
// DestData[(Y * DestSize.X + X) * NumChannels + Channel] = 0;
// }
//}
continue;
}
}
const FVector2f Uv = Invoke([&]() -> FVector2f
{
if constexpr (AddressMode == EAddressMode::Wrap)
{
return FVector2f(FMath::Frac(DestUv.X), FMath::Frac(DestUv.Y));
}
else if (AddressMode == EAddressMode::ClampToBlack || AddressMode == EAddressMode::ClampToEdge)
{
return FVector2f(FMath::Clamp(DestUv.X, 0.0f, 1.0f), FMath::Clamp(DestUv.Y, 0.0f, 1.0f));
}
else
{
return DestUv;
}
});
const FVector2f Coords0F = FVector2f(
FMath::Clamp(Uv.X * Size0F.X - 0.5f, 0.0f, Size0F.X - 1.0f),
FMath::Clamp(Uv.Y * Size0F.Y - 0.5f, 0.0f, Size0F.Y - 1.0f));
const FUint16Vector2 Frac0 = FUint16Vector2(FMath::Frac(Coords0F.X) * 255.0f, FMath::Frac(Coords0F.Y) * 255.0f);
const FIntVector2 Coords0 = FIntVector2(Coords0F.X, Coords0F.Y);
const FIntVector2 Coords0PlusOne = Invoke([&]() -> FIntVector2
{
if constexpr (AddressMode == EAddressMode::Wrap)
{
return FIntVector2(Coords0.X + 1 < Src0Size.X ? Coords0.X + 1 : 0,
Coords0.Y + 1 < Src0Size.Y ? Coords0.Y + 1 : 0);
}
else if (AddressMode == EAddressMode::ClampToBlack || AddressMode == EAddressMode::ClampToEdge)
{
return FIntVector2(FMath::Min(Src0Size.X - 1, Coords0.X + 1),
FMath::Min(Src0Size.Y - 1, Coords0.Y + 1));
}
});
const FVector2f Coords1F = FVector2f(
FMath::Clamp(Uv.X * Size1F.X - 0.5f, 0.0f, Size1F.X - 1.0f),
FMath::Clamp(Uv.Y * Size1F.Y - 0.5f, 0.0f, Size1F.Y - 1.0f));
const FUint16Vector2 Frac1 = FUint16Vector2(FMath::Frac(Coords1F.X) * 255.0f, FMath::Frac(Coords1F.Y) * 255.0f);
const FIntVector2 Coords1 = FIntVector2(Coords1F.X, Coords1F.Y);
const FIntVector2 Coords1PlusOne = Invoke([&]() -> FIntVector2
{
if constexpr (AddressMode == EAddressMode::Wrap)
{
return FIntVector2(Coords1.X + 1 < Src1Size.X ? Coords1.X + 1 : 0,
Coords1.Y + 1 < Src1Size.Y ? Coords1.Y + 1 : 0);
}
else if (AddressMode == EAddressMode::ClampToBlack || AddressMode == EAddressMode::ClampToEdge)
{
return FIntVector2(FMath::Min(Src1Size.X - 1, Coords1.X + 1),
FMath::Min(Src1Size.Y - 1, Coords1.Y + 1));
}
});
auto LoadPixel = [](const uint8* Ptr) -> FPixelData
{
FPixelData Result;
if constexpr (NumChannels == 4)
{
uint32 PackedData;
FMemory::Memcpy(&PackedData, Ptr, sizeof(uint32));
Result.Data[0] = static_cast<uint16>((PackedData >> (8 * 0)) & 0xFF);
Result.Data[1] = static_cast<uint16>((PackedData >> (8 * 1)) & 0xFF);
Result.Data[2] = static_cast<uint16>((PackedData >> (8 * 2)) & 0xFF);
Result.Data[3] = static_cast<uint16>((PackedData >> (8 * 3)) & 0xFF);
}
else
{
for (int32 C = 0; C < NumChannels; ++C)
{
Result.Data[C] = static_cast<uint16>(Ptr[C]);
}
}
return Result;
};
FPixelData PixelData000 = LoadPixel(Src0Data + (Coords0.Y * Src0Size.X + Coords0.X) * NumChannels);
FPixelData PixelData010 = LoadPixel(Src0Data + (Coords0.Y * Src0Size.X + Coords0PlusOne.X) * NumChannels);
FPixelData PixelData001 = LoadPixel(Src0Data + (Coords0PlusOne.Y * Src0Size.X + Coords0.X) * NumChannels);
FPixelData PixelData011 = LoadPixel(Src0Data + (Coords0PlusOne.Y * Src0Size.X + Coords0PlusOne.X) * NumChannels);
FPixelData PixelData100 = LoadPixel(Src1Data + (Coords1.Y * Src1Size.X + Coords1.X) * NumChannels);
FPixelData PixelData110 = LoadPixel(Src1Data + (Coords1.Y * Src1Size.X + Coords1PlusOne.X) * NumChannels);
FPixelData PixelData101 = LoadPixel(Src1Data + (Coords1PlusOne.Y * Src1Size.X + Coords1.X) * NumChannels);
FPixelData PixelData111 = LoadPixel(Src1Data + (Coords1PlusOne.Y * Src1Size.X + Coords1PlusOne.X) * NumChannels);
FPixelData ResultPixelData;
for (int32 C = 0; C < NumChannels; ++C)
{
const uint16 LerpY00 = ((PixelData010.Data[C] * Frac0.X) + PixelData000.Data[C] * (255 - Frac0.X)) / 255;
const uint16 LerpY01 = ((PixelData011.Data[C] * Frac0.X) + PixelData001.Data[C] * (255 - Frac0.X)) / 255;
const uint16 LerpY10 = ((PixelData110.Data[C] * Frac1.X) + PixelData100.Data[C] * (255 - Frac1.X)) / 255;
const uint16 LerpY11 = ((PixelData111.Data[C] * Frac1.X) + PixelData101.Data[C] * (255 - Frac1.X)) / 255;
const uint16 Mip0 = ((LerpY01 * Frac0.Y) + LerpY00 * (255 - Frac0.Y)) / 255;
const uint16 Mip1 = ((LerpY11 * Frac1.Y) + LerpY10 * (255 - Frac1.Y)) / 255;
ResultPixelData.Data[C] = ((Mip0 * MipFactor) + Mip1 * (255 - MipFactor)) / 255;
}
static_assert(NumChannels <= 4);
if constexpr (NumChannels == 4)
{
const uint32 PackedPixel =
(static_cast<uint32>((ResultPixelData.Data[0]) & 0xFF) << (8 * 0)) |
(static_cast<uint32>((ResultPixelData.Data[1]) & 0xFF) << (8 * 1)) |
(static_cast<uint32>((ResultPixelData.Data[2]) & 0xFF) << (8 * 2)) |
(static_cast<uint32>((ResultPixelData.Data[3]) & 0xFF) << (8 * 3));
*reinterpret_cast<uint32*>(&DestData[(Y * DestSize.X + X) * NumChannels]) = PackedPixel;
}
else
{
for (uint32 Channel = 0; Channel < NumChannels; ++Channel)
{
DestData[(Y * DestSize.X + X) * NumChannels + Channel] =
static_cast<uint8>(ResultPixelData.Data[Channel]);
}
}
}
}
});
}
template<int32 NumChannels, EAddressMode AddressMode>
void ImageTransformVectorImpl(
uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect,
const uint8* Src0Data, FIntVector2 Src0Size,
const uint8* Src1Data, FIntVector2 Src1Size,
float MipFactor,
const FTransform2f& Transform)
{
if (DestCropRect.Area() == 0)
{
return;
}
const FVector2f DestSizeF = FVector2f(DestSize.X, DestSize.Y);
const FVector2f DestNormFactor = FVector2f(1.0f) / DestSizeF;
check(DestCropRect.Min.X >= 0 && DestCropRect.Max.X <= DestSize.X);
check(DestCropRect.Min.Y >= 0 && DestCropRect.Max.Y <= DestSize.Y);
const FTransform2f InverseTransform = Transform.Inverse();
using namespace GlobalVectorConstants;
const VectorRegister4Int SizeI =
MakeVectorRegisterInt(Src0Size.X, Src0Size.Y, Src1Size.X, Src1Size.Y);
const VectorRegister4Float SizeF = VectorIntToFloat(SizeI);
constexpr int32 BatchSize = 1 << 12;
const int32 NumBatchRows = FMath::DivideAndRoundUp(BatchSize, DestCropRect.Width());
const int32 NumBatches = FMath::DivideAndRoundUp(DestCropRect.Height(), NumBatchRows);
ParallelExecutionUtils::InvokeBatchParallelFor(NumBatches,
[=](int32 BatchId)
{
const int32 RowOffset = NumBatchRows * BatchId;
const FIntRect BatchRect = FIntRect(
DestCropRect.Min.X, DestCropRect.Min.Y + RowOffset,
DestCropRect.Max.X, FMath::Min(DestCropRect.Min.Y + RowOffset + NumBatchRows, DestCropRect.Max.Y));
const VectorRegister4Float VectorMipFactor = VectorSetFloat1(MipFactor);
for (int32 Y = BatchRect.Min.Y; Y < BatchRect.Max.Y; ++Y)
{
for (int32 X = BatchRect.Min.X; X < BatchRect.Max.X; ++X)
{
const FVector2f DestUv = InverseTransform.TransformPoint((FVector2f(X, Y) + 0.5f) * DestNormFactor);
// TODO: Make the black edge antialiazed.
if constexpr (AddressMode == EAddressMode::ClampToBlack)
{
if ((DestUv.X < 0.0f) | (DestUv.X > 1.0f) | (DestUv.Y < 0.0f) | (DestUv.Y > 1.0f))
{
// Black texture init is mandatory for ClampToBlack Address mode, no need to set black again.
//if constexpr (NumChannels == 4)
//{
// FMemory::Memzero(&DestData[(Y * DestSize.X + X) * NumChannels], 4);
//}
//else
//{
// for (uint32 Channel = 0; Channel < NumChannels; ++Channel)
// {
// DestData[(Y * DestSize.X + X) * NumChannels + Channel] = 0;
// }
//}
continue;
}
}
const FVector2f Uv = Invoke([&]() -> FVector2f
{
if constexpr (AddressMode == EAddressMode::Wrap)
{
return FVector2f(FMath::Frac(DestUv.X), FMath::Frac(DestUv.Y));
}
else if (AddressMode == EAddressMode::ClampToBlack || AddressMode == EAddressMode::ClampToEdge)
{
return FVector2f(FMath::Clamp(DestUv.X, 0.0f, 1.0f), FMath::Clamp(DestUv.Y, 0.0f, 1.0f));
}
else
{
return DestUv;
}
});
const VectorRegister4Float CoordsF = VectorMin(VectorMax(
VectorMultiplyAdd(MakeVectorRegister(Uv.X, Uv.Y, Uv.X, Uv.Y), SizeF, FloatMinusOneHalf),
FloatZero),
VectorSubtract(SizeF, FloatOne));
const VectorRegister4Float Frac = VectorSubtract(CoordsF, VectorFloor(CoordsF));
const VectorRegister4Int CoordsI = VectorFloatToInt(CoordsF);
const VectorRegister4Int CoordsIPlusOne = VectorIntMin(
VectorIntAdd(CoordsI, IntOne), VectorIntSubtract(SizeI, IntOne));
alignas(VectorRegister4Int) int32 CoordsIData[4];
VectorIntStoreAligned(CoordsI, CoordsIData);
alignas(VectorRegister4Int) int32 CoordsIPlusOneData[4];
VectorIntStoreAligned(CoordsIPlusOne, CoordsIPlusOneData);
auto LoadPixel = [](const uint8* DataPtr) -> VectorRegister4Float
{
if constexpr (NumChannels == 4)
{
uint32 PackedData;
FMemory::Memcpy(&PackedData, DataPtr, sizeof(uint32));
return VectorIntToFloat(MakeVectorRegisterInt(
(PackedData >> (8 * 0)) & 0xFF,
(PackedData >> (8 * 1)) & 0xFF,
(PackedData >> (8 * 2)) & 0xFF,
(PackedData >> (8 * 3)) & 0xFF));
}
else
{
alignas(VectorRegister4Int) int32 PixelData[4] = {0};
for (int32 C = 0; C < NumChannels; ++C)
{
PixelData[C] = static_cast<int32>(DataPtr[C]);
}
return VectorIntToFloat(VectorIntLoadAligned(&PixelData[0]));
}
};
const VectorRegister4Float Pixel000 = LoadPixel(Src0Data + (CoordsIData[1] * Src0Size.X + CoordsIData[0]) * NumChannels);
const VectorRegister4Float Pixel010 = LoadPixel(Src0Data + (CoordsIData[1] * Src0Size.X + CoordsIPlusOneData[0]) * NumChannels);
const VectorRegister4Float Pixel001 = LoadPixel(Src0Data + (CoordsIPlusOneData[1] * Src0Size.X + CoordsIData[0]) * NumChannels);
const VectorRegister4Float Pixel011 = LoadPixel(Src0Data + (CoordsIPlusOneData[1] * Src0Size.X + CoordsIPlusOneData[0]) * NumChannels);
const VectorRegister4Float Pixel100 = LoadPixel(Src1Data + (CoordsIData[3] * Src1Size.X + CoordsIData[2]) * NumChannels);
const VectorRegister4Float Pixel110 = LoadPixel(Src1Data + (CoordsIData[3] * Src1Size.X + CoordsIPlusOneData[2]) * NumChannels);
const VectorRegister4Float Pixel101 = LoadPixel(Src1Data + (CoordsIPlusOneData[3] * Src1Size.X + CoordsIData[2]) * NumChannels);
const VectorRegister4Float Pixel111 = LoadPixel(Src1Data + (CoordsIPlusOneData[3] * Src1Size.X + CoordsIPlusOneData[2]) * NumChannels);
const VectorRegister4Float Frac0X = VectorReplicate(Frac, 0);
const VectorRegister4Float Frac0Y = VectorReplicate(Frac, 1);
// Bilerp image 0
VectorRegister4Float Sample0 = VectorMultiplyAdd(Frac0X, VectorSubtract(Pixel010, Pixel000), Pixel000);
Sample0 = VectorMultiplyAdd(
Frac0Y,
VectorSubtract(
VectorMultiplyAdd(Frac0X, VectorSubtract(Pixel011, Pixel001), Pixel001),
Sample0),
Sample0);
const VectorRegister4Float Frac1X = VectorReplicate(Frac, 2);
const VectorRegister4Float Frac1Y = VectorReplicate(Frac, 3);
// Bilerp image 1
VectorRegister4Float Sample1 = VectorMultiplyAdd(Frac1X, VectorSubtract(Pixel110, Pixel100), Pixel100);
Sample1 = VectorMultiplyAdd(
Frac1Y,
VectorSubtract(
VectorMultiplyAdd(Frac1X, VectorSubtract(Pixel111, Pixel101), Pixel101),
Sample1),
Sample1);
const VectorRegister4Float Result =
VectorMultiplyAdd(VectorMipFactor, VectorSubtract(Sample1, Sample0), Sample0);
if constexpr (NumChannels == 4)
{
VectorStoreByte4(Result, &DestData[(Y * DestSize.X + X) * NumChannels]);
VectorResetFloatRegisters();
}
else
{
const AlignedFloat4 ResultData(VectorMin(Result, Float255));
for (int32 C = 0; C < NumChannels; ++C)
{
DestData[(Y * DestSize.X + X) * NumChannels + C] = static_cast<uint8>(ResultData[C]);
}
}
}
}
});
}
template<int32 NumChannels, EAddressMode AddressMode, bool bUseVectorImpl>
void ImageTransformImpl(
uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect,
const uint8* Src0Data, FIntVector2 Src0Size,
const uint8* Src1Data, FIntVector2 Src1Size,
float MipFactor,
const FTransform2f& Transform)
{
if constexpr (bUseVectorImpl)
{
ImageTransformVectorImpl<NumChannels, AddressMode>(
DestData, DestSize, DestCropRect,
Src0Data, Src0Size, Src1Data, Src1Size,
MipFactor,
Transform);
}
else
{
ImageTransformNonVectorImpl<NumChannels, AddressMode>(
DestData, DestSize, DestCropRect,
Src0Data, Src0Size, Src1Data, Src1Size,
MipFactor,
Transform);
}
}
// Force instantiation of used templates. A link error will occur if any of them is missing.
template void ImageTransformImpl<1, EAddressMode::ClampToBlack, true>(uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<1, EAddressMode::ClampToEdge, true> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<1, EAddressMode::Wrap, true> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<3, EAddressMode::ClampToBlack, true>(uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<3, EAddressMode::ClampToEdge, true> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<3, EAddressMode::Wrap, true> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<4, EAddressMode::ClampToBlack, true>(uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<4, EAddressMode::ClampToEdge, true> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<4, EAddressMode::Wrap, true> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<1, EAddressMode::ClampToBlack, false>(uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<1, EAddressMode::ClampToEdge, false> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<1, EAddressMode::Wrap, false> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<3, EAddressMode::ClampToBlack, false>(uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<3, EAddressMode::ClampToEdge, false> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<3, EAddressMode::Wrap, false> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<4, EAddressMode::ClampToBlack, false>(uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<4, EAddressMode::ClampToEdge, false> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
template void ImageTransformImpl<4, EAddressMode::Wrap, false> (uint8* DestData, FIntVector2 DestSize, FIntRect DestCropRect, const uint8* Src0Data, FIntVector2 Src0Size, const uint8* Src1Data, FIntVector2 Src1Size, float MipFactor, const FTransform2f& Transform);
}