Files
UnrealEngine/Engine/Source/Developer/MaterialBaking/Private/MaterialBakingHelpers.cpp
2025-05-18 13:04:45 +08:00

337 lines
10 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MaterialBakingHelpers.h"
#include "Math/Color.h"
#include "Async/ParallelFor.h"
namespace FMaterialBakingHelpersImpl
{
static FColor BoxBlurSample(FColor* InBMP, int32 X, int32 Y, int32 InImageWidth, int32 InImageHeight, uint32 BackgroundDWColor)
{
int32 PixelsSampled = 0;
int32 CombinedColorR = 0;
int32 CombinedColorG = 0;
int32 CombinedColorB = 0;
int32 CombinedColorA = 0;
const int32 BaseIndex = (Y * InImageWidth) + X;
int32 SampleIndex = BaseIndex - (InImageWidth + 1);
for (int32 YI = 0; YI < 3; ++YI)
{
for (int32 XI = 0; XI < 3; ++XI)
{
const FColor SampledColor = InBMP[SampleIndex++];
// Check if the pixel is a rendered one (not clear color)
if (SampledColor.DWColor() != BackgroundDWColor)
{
CombinedColorR += SampledColor.R;
CombinedColorG += SampledColor.G;
CombinedColorB += SampledColor.B;
CombinedColorA += SampledColor.A;
++PixelsSampled;
}
}
SampleIndex += InImageWidth - 3;
}
if (PixelsSampled == 0)
{
return FColor(BackgroundDWColor);
}
return FColor(CombinedColorR / PixelsSampled, CombinedColorG / PixelsSampled, CombinedColorB / PixelsSampled, CombinedColorA / PixelsSampled);
}
static bool HasBorderingPixel(FColor* InBMP, int32 X, int32 Y, int32 InImageWidth, int32 InImageHeight, uint32 BackgroundDWColor)
{
const int32 BaseIndex = Y*InImageWidth + X;
int32 SampleIndex = BaseIndex - (InImageWidth + 1);
for (int32 YI = 0; YI < 3; ++YI)
{
for (int32 XI = 0; XI < 3; ++XI)
{
if (InBMP[SampleIndex++].DWColor() != BackgroundDWColor)
{
return true;
}
}
SampleIndex += InImageWidth - 3;
}
return false;
}
static void PerformShrinking(TArray<FColor>& InOutPixels, int32& ImageWidth, int32& ImageHeight, uint32 BackgroundDWColor)
{
FColor* Current = InOutPixels.GetData();
// We can hopefully skip the entire smearing process if there is a single
// non-background color in the entire array since smearing that completely
// would lead to a monochome output.
uint32 SingleColor = BackgroundDWColor;
bool bHasSingleColor = true;
for (int32 Index = 0, Num = InOutPixels.Num(); Index < Num; ++Index)
{
const uint32 Color = Current[Index].DWColor();
if (Color != BackgroundDWColor)
{
if (SingleColor == BackgroundDWColor)
{
// This is the first time we stumble on a color other than background color, keep it.
SingleColor = Color;
}
// Compare with the known non-background color
else if (SingleColor != Color)
{
bHasSingleColor = false;
break;
}
}
}
// Shrink to a single texel
if (bHasSingleColor)
{
InOutPixels.SetNum(1);
ImageWidth = ImageHeight = 1;
Current = InOutPixels.GetData();
InOutPixels[0] = FColor(SingleColor);
}
}
static void PerformUVBorderSmear(TArray<FColor>& InOutPixels, int32& ImageWidth, int32& ImageHeight, int32 MaxIterations, uint32 BackgroundDWColor)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FMaterialBakingHelpers::PerformUVBorderSmear)
check(InOutPixels.Num() == ImageWidth*ImageHeight);
// No smearing needed for a single pixel
if (ImageWidth * ImageHeight == 1)
{
return;
}
const int32 PaddedImageWidth = ImageWidth + 2;
const int32 PaddedImageHeight = ImageHeight + 2;
TArray<uint32> RowsSwap1;
TArray<uint32> RowsSwap2;
TArray<uint32>* CurrentRowsLeft = &RowsSwap1;
TArray<uint32>* NextRowsLeft = &RowsSwap2;
//
// Create a copy of the image with a 1 pixel background color border around the edge.
// This avoids needing to bounds check in inner loops.
//
TArray<FColor> ScratchBuffer;
ScratchBuffer.SetNumUninitialized(PaddedImageWidth*PaddedImageHeight);
FColor* Scratch = ScratchBuffer.GetData();
// Set top and bottom bordering pixels to background color
for (int32 X = 0; X < PaddedImageWidth; ++X)
{
Scratch[X] = FColor(BackgroundDWColor);
Scratch[(PaddedImageHeight - 1)*PaddedImageWidth + X] = FColor(BackgroundDWColor);
}
FColor* Current = InOutPixels.GetData();
// Set left and right border pixels to background color and copy image data into rows
for (int32 Y = 1; Y <= ImageHeight; ++Y)
{
const int32 YOffset = Y * PaddedImageWidth;
Scratch[YOffset] = FColor(BackgroundDWColor);
FMemory::Memcpy(&Scratch[YOffset + 1], &Current[(Y - 1)*ImageWidth], ImageWidth * sizeof(FColor));
Scratch[YOffset + ImageWidth + 1] = FColor(BackgroundDWColor);
}
//
// Find our initial workset of all rows that have background colored pixels bordering non-background pixels.
// Also find all rows that have zero background colored pixels - these will never be added to the workset.
//
TArray<bool> RowCompleted;
RowCompleted.SetNumZeroed(PaddedImageHeight);
// Set border rows to start completed
RowCompleted[0] = true;
RowCompleted[PaddedImageHeight - 1] = true;
bool bHasAnyData = false;
for (int32 Y = 1; Y <= ImageHeight; ++Y)
{
bool bHasBackground = false;
bool bBordersNonBackground = false;
const int32 YOffset = Y * PaddedImageWidth;
for (int32 X = 1; X <= ImageWidth; ++X)
{
if (Scratch[YOffset + X].DWColor() == BackgroundDWColor)
{
bHasBackground = true;
if (HasBorderingPixel(Scratch, X, Y, PaddedImageWidth, PaddedImageHeight, BackgroundDWColor))
{
bBordersNonBackground = true;
break;
}
}
}
if (!bHasBackground)
{
bHasAnyData = true;
RowCompleted[Y] = true;
}
else if (bBordersNonBackground)
{
bHasAnyData = true;
CurrentRowsLeft->Add(Y);
}
}
// early-out if no valid pixels were found.
if (!bHasAnyData)
{
FMemory::Memzero(InOutPixels.GetData(), ImageWidth * ImageHeight * sizeof(FColor));
return;
}
// Early out if all pixels were valid.
if (CurrentRowsLeft->Num() == 0)
{
return;
}
TArray<uint32> RowRemainingPixels;
RowRemainingPixels.SetNumZeroed(PaddedImageHeight);
const int32 MaxThreads = FPlatformProcess::SupportsMultithreading() ? FPlatformMisc::NumberOfCoresIncludingHyperthreads() : 1;
//
// Iteratively smear until all rows are filled.
//
int32 LoopCount = 0;
while (CurrentRowsLeft->Num() && (MaxIterations <= 0 || LoopCount <= MaxIterations))
{
const int32 NumThreads = FMath::Min(CurrentRowsLeft->Num(), MaxThreads);
const int32 LinesPerThread = FMath::CeilToInt((float)CurrentRowsLeft->Num() / (float)NumThreads);
// split up rows that still have background colored pixels amongst threads
ParallelFor(NumThreads, [ImageWidth, PaddedImageWidth, PaddedImageHeight, Current, Scratch, CurrentRowsLeft, LinesPerThread, &RowRemainingPixels, BackgroundDWColor](int32 ThreadIndex)
{
const int32 StartY = ThreadIndex*LinesPerThread;
const int32 EndY = FMath::Min((ThreadIndex + 1) * LinesPerThread, CurrentRowsLeft->Num());
for (int32 Index = StartY; Index < EndY; Index++)
{
const int32 Y = (*CurrentRowsLeft)[Index];
RowRemainingPixels[Index] = 0;
int32 PixelIndex = (Y - 1) * ImageWidth;
for (int32 X = 1; X <= ImageWidth; X++)
{
FColor& Color = Current[PixelIndex++];
if (Color.DWColor() == BackgroundDWColor)
{
const FColor SampledColor = BoxBlurSample(Scratch, X, Y, PaddedImageWidth, PaddedImageHeight, BackgroundDWColor);
// If it's a valid pixel
if (SampledColor.DWColor() != BackgroundDWColor)
{
Color = SampledColor;
}
else
{
RowRemainingPixels[Index]++;
}
}
}
}
});
// Mark all completed rows and copy data between buffers
const int32 RowMemorySize = ImageWidth * sizeof(FColor);
for (int32 RowIndex = 0; RowIndex < CurrentRowsLeft->Num(); ++RowIndex)
{
const int32 Y = (*CurrentRowsLeft)[RowIndex];
if (!RowRemainingPixels[RowIndex])
{
RowCompleted[Y] = true;
}
FMemory::Memcpy(&Scratch[Y * PaddedImageWidth + 1], &Current[(Y - 1) * ImageWidth], RowMemorySize);
}
// Find the next set of rows to process.
// This will be our current rows that haven't completed, plus any bordering rows that haven't been marked completed.
NextRowsLeft->SetNum(0, EAllowShrinking::No);
{
int32 PreviousY = -1;
for (int32 RowIndex = 0; RowIndex < CurrentRowsLeft->Num(); ++RowIndex)
{
const int32 Y = (*CurrentRowsLeft)[RowIndex];
if (!RowCompleted[Y - 1] && PreviousY < Y - 1)
{
PreviousY = Y - 1;
NextRowsLeft->Add(Y - 1);
}
if (PreviousY < Y && !RowCompleted[Y] && RowRemainingPixels[RowIndex])
{
PreviousY = Y;
NextRowsLeft->Add(Y);
}
if (!RowCompleted[Y + 1])
{
PreviousY = Y + 1;
NextRowsLeft->Add(Y + 1);
}
}
}
// Swap rows left buffers
{
TArray<uint32>* Temp = NextRowsLeft;
NextRowsLeft = CurrentRowsLeft;
CurrentRowsLeft = Temp;
}
LoopCount++;
}
// If we finished before replacing all pixels, replace the remaining pixels with black.
for (int32 Index = 0; Index < CurrentRowsLeft->Num(); Index++)
{
const int32 Y = (*CurrentRowsLeft)[Index];
int32 PixelIndex = (Y - 1) * ImageWidth;
for (int32 X = 1; X <= ImageWidth; X++)
{
FColor& Color = Current[PixelIndex++];
if (Color.DWColor() == BackgroundDWColor)
{
Color = FColor::Black;
}
}
}
}
}
void FMaterialBakingHelpers::PerformUVBorderSmear(TArray<FColor>& InOutPixels, int32 ImageWidth, int32 ImageHeight, int32 MaxIterations, FColor BackgroundColor)
{
FMaterialBakingHelpersImpl::PerformUVBorderSmear(InOutPixels, ImageWidth, ImageHeight, MaxIterations, BackgroundColor.DWColor());
}
void FMaterialBakingHelpers::PerformShrinking(TArray<FColor>& InOutPixels, int32& InOutImageWidth, int32& InOutImageHeight, FColor BackgroundColor)
{
FMaterialBakingHelpersImpl::PerformShrinking(InOutPixels, InOutImageWidth, InOutImageHeight, BackgroundColor.DWColor());
}
void FMaterialBakingHelpers::PerformUVBorderSmearAndShrink(TArray<FColor>& InOutPixels, int32& InOutImageWidth, int32& InOutImageHeight, FColor BackgroundColor)
{
const uint32 BackgroundDWColor = BackgroundColor.DWColor();
FMaterialBakingHelpersImpl::PerformShrinking(InOutPixels, InOutImageWidth, InOutImageHeight, BackgroundDWColor);
const int32 DefaultMaxIteration = -1; // Perform smearing over the whole image
FMaterialBakingHelpersImpl::PerformUVBorderSmear(InOutPixels, InOutImageWidth, InOutImageHeight, DefaultMaxIteration, BackgroundDWColor);
}