// Copyright Epic Games, Inc. All Rights Reserved. #include "TextureImportUtils.h" #include "ImageCore.h" #include "Async/ParallelFor.h" namespace UE { namespace TextureUtilitiesCommon { /** * Detect the existence of gray scale image in some formats and convert those to a gray scale equivalent image * * @return true if the image was converted */ bool AutoDetectAndChangeGrayScale(FImage& Image) { if (Image.Format != ERawImageFormat::BGRA8) { return false; } // auto-detect gray BGRA8 and change to G8 const FColor* Colors = (const FColor*)Image.RawData.GetData(); int64 NumPixels = Image.GetNumPixels(); for (int64 i = 0; i < NumPixels; i++) { if (Colors[i].A != 255 || Colors[i].R != Colors[i].B || Colors[i].G != Colors[i].B) { return false ; } } // yes, it's gray, do it : Image.ChangeFormat(ERawImageFormat::G8, Image.GammaSpace); return true; } /** * This fills any pixels of a texture with have an alpha value of zero and RGB=white, * with an RGB from the nearest neighboring pixel which has non-zero alpha. PNG images with "simple transparency" (eg. indexed color transparency) don't store RGB color in the transparent area libpng decodes those pels are {RGB=white, A=0} we replace them by filling in the RGB from neighbors note that this does NOT fill in the RGB of PNGs with a full alpha channel. -> it does now, if PNGInfill == Always */ template class TPNGDataFill { public: explicit TPNGDataFill( int32 SizeX, int32 SizeY, uint8* SourceTextureData ) : SourceData( reinterpret_cast(SourceTextureData) ) , TextureWidth(SizeX) , TextureHeight(SizeY) { // libpng decodes simple transparent (binary A or indexed color) as { RGB=white, A = 0} if ( sizeof(ColorDataType) == 4 ) { WhiteWithZeroAlpha = FColor(255, 255, 255, 0).DWColor(); } else { uint16 RGBA[4] = { 0xFFFF,0xFFFF,0xFFFF, 0 }; checkSlow( sizeof(ColorDataType) == 8 ); checkSlow( sizeof(RGBA) == 8 ); memcpy(&WhiteWithZeroAlpha,RGBA, sizeof(ColorDataType)); } // falloff weights for near neighbor extrapolation // fast falloff -> just extend neighbor pels out // slow falloff -> blur neighbor pels together for(int r1=0;r1<=NearNeighborRadius;r1++) { for(int r2=0;r2<=NearNeighborRadius;r2++) { int rsqr = r1*r1 + r2*r2; if( rsqr == 0 ) { NearNeighborWeights[0][0] = 0.f; // center self-weight = zero continue; } float W = expf( - 1.1f * sqrtf((float)rsqr) ); NearNeighborWeights[r1][r2] = W; } } } void ProcessData(bool bDoOnComplexAlphaNotJustBinaryTransparency) { // first identify alpha type : bool HasWhiteWithZeroAlpha=false; bool HasComplexAlpha=false; for (int64 Y = 0; Y < TextureHeight; ++Y) { const ColorDataType* RowData = (const ColorDataType *)SourceData + Y * TextureWidth; for(int64 X = 0; X < TextureWidth; ++X) { if ( IsOpaque(RowData[X]) ) { } else if ( RowData[X] == WhiteWithZeroAlpha ) { HasWhiteWithZeroAlpha = true; } else { HasComplexAlpha = true; if ( ! bDoOnComplexAlphaNotJustBinaryTransparency ) { UE_LOG(LogCore, Log, TEXT("PNG has complex alpha channel, will not fill RGB in transparent background, due to setting PNGInfill == OnlyOnBinaryTransparency")); // do not modify png's with full alpha channels : return; } } } } if ( ! HasWhiteWithZeroAlpha ) { // all opaque return; } if ( HasComplexAlpha ) { UE_LOG(LogCore, Log, TEXT("PNG has alpha channel, doing fill of RGB in transparent background, due to setting PNGInfill == Always")); } else { UE_LOG(LogCore, Log, TEXT("PNG has binary transparency, doing fill of RGB in transparent background, due to setting PNGInfill != Never")); } // first do good fill with limited distance : // this ensures near pels within NearNeighborRadius get a good neighbor fill for interpolation FillFromNearNeighbors(); // then do simple fill, rows one by one : // this can be a very poor fill, but it's fast for filling large empty areas // @todo oodle : fill from nearest row (up or down) rather than always filling downward int64 NumZeroedTopRowsToProcess = 0; int64 FillColorRow = -1; for (int64 Y = 0; Y < TextureHeight; ++Y) { if (!ProcessHorizontalRow(Y)) { if (FillColorRow != -1) { FillRowColorPixels(FillColorRow, Y); } else { NumZeroedTopRowsToProcess = Y+1; } } else { FillColorRow = Y; } } // Can only fill upwards if image not fully zeroed if (NumZeroedTopRowsToProcess > 0 && NumZeroedTopRowsToProcess < TextureHeight) { for (int64 Y = 0; Y < NumZeroedTopRowsToProcess; ++Y) { // fill row at Y from row at NumZeroedTopRowsToProcess FillRowColorPixels(NumZeroedTopRowsToProcess, Y); } } } static bool IsOpaque(const ColorDataType InColor) { if ( sizeof(ColorDataType) == 4 ) { return InColor >= 0xFF000000U; } else if ( sizeof(ColorDataType) == 8 ) { return InColor >= 0xFFFF000000000000ULL; } else { check(false); } } static ColorDataType MakeColorWithZeroAlpha(const ColorDataType InColor) { // take the RGB from InColor // set A to zero // return that if ( sizeof(ColorDataType) == 4 ) { return InColor & 0xFFFFFFU; } else if ( sizeof(ColorDataType) == 8 ) { return InColor & 0xFFFFFFFFFFFFULL; } else { check(false); } } static ColorDataType MakeColorOpaque(const ColorDataType InColor) { // take the RGB from InColor // set A to opaque // return that if ( sizeof(ColorDataType) == 4 ) { return InColor | 0xFF000000U; } else if ( sizeof(ColorDataType) == 8 ) { return InColor | 0xFFFF000000000000ULL; } else { check(false); } } /* returns False if requires further processing because entire row is filled with zeroed alpha values */ bool ProcessHorizontalRow(int64 Y) { ColorDataType* RowData = (ColorDataType *)SourceData + Y * TextureWidth; int64 X = 0; // note this is done after the NN fill // the NN fill will have RGB != white but A = 0 // so we will fill out using those if ( RowData[0] == WhiteWithZeroAlpha ) { // transparent run at start of row // find X which is the first opaque pel // ( "opaque" is a misnomer; actually transparent but not WhiteWithZeroAlpha ) for(;;) { if ( RowData[X] != WhiteWithZeroAlpha ) { break; } X++; if ( X == TextureWidth ) { // whole row was transparent return false; } } check( X < TextureWidth ); check( RowData[X] != WhiteWithZeroAlpha ); // RowData[X] is opaque // fill initial run from it ColorDataType FillColor = MakeColorWithZeroAlpha(RowData[X]); for(int64 FillX=0;FillX 0 ); check( RowData[X] == WhiteWithZeroAlpha ); int64 FirstTransparent = X; while( RowData[X] == WhiteWithZeroAlpha ) { X++; if ( X == TextureWidth ) { //reached end in transparent run // fill right-only transparent run from left : ColorDataType FillColor = MakeColorWithZeroAlpha(RowData[FirstTransparent-1]); for(int64 FillX=FirstTransparent;FillX ScratchRowArray; ScratchRowArray.SetNum(TextureWidth); ColorDataType * ScratchRow = &ScratchRowArray[0]; for (int64 Y = StartIndex; Y < EndIndex; ++Y) { ColorDataType * ImageRow = ImageColors + Y * TextureWidth; for (int64 X = 0; X < TextureWidth; ++X) { //@todo Oodle : we could more quickly detect large areas where no fill within NearNeighborRadius is possible if ( ImageRow[X] == WhiteWithZeroAlpha ) { // could write to ImageRow[X] immediately, but using ScratchRow reduces cache sharing across cores ScratchRow[X] = GetFilledFromNearNeighbors(X,Y); // ScratchRow[X] still has zero alpha, but no longer white } else { ScratchRow[X] = ImageRow[X]; } } memcpy(ImageRow,ScratchRow,TextureWidth*sizeof(ColorDataType)); } }); } PixelDataType* SourceData; int64 TextureWidth; int64 TextureHeight; ColorDataType WhiteWithZeroAlpha; enum { NearNeighborRadius = 4 }; float NearNeighborWeights[NearNeighborRadius+1][NearNeighborRadius+1]; }; void FillZeroAlphaPNGData(int32 SizeX, int32 SizeY, ETextureSourceFormat SourceFormat, uint8* SourceData, bool bDoOnComplexAlphaNotJustBinaryTransparency) { // These conditions should be checked by IsImportResolutionValid, but just in case we get here // via another path. check(SizeX > 0 && SizeY > 0); if (SizeX < 0 || SizeY < 0) { return; } switch (SourceFormat) { case TSF_BGRA8: { TPNGDataFill PNGFill(SizeX, SizeY, SourceData); PNGFill.ProcessData(bDoOnComplexAlphaNotJustBinaryTransparency); break; } case TSF_RGBA16: { TPNGDataFill PNGFill(SizeX, SizeY, SourceData); PNGFill.ProcessData(bDoOnComplexAlphaNotJustBinaryTransparency); break; } default: { // G8, G16, no alpha to fill break; } } } } }