You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
989 lines
33 KiB
C++
989 lines
33 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
|
|
#include <Processing/ImageObjectImpl.h>
|
|
#include <Processing/PixelFormatInfo.h>
|
|
#include <Processing/ImageFlags.h>
|
|
#include <Converters/PixelOperation.h>
|
|
#include <AzCore/std/string/conversions.h>
|
|
|
|
#include <AzCore/IO/SystemFile.h>
|
|
#include <AzCore/IO/GenericStreams.h>
|
|
|
|
#include <AzFramework/StringFunc/StringFunc.h>
|
|
|
|
// Indicates a 2D texture is a cube-map texture.
|
|
#define DDS_RESOURCE_MISC_TEXTURECUBE 0x4
|
|
|
|
namespace ImageProcessingAtom
|
|
{
|
|
using namespace AZ;
|
|
|
|
IImageObject* IImageObject::CreateImage(AZ::u32 width, AZ::u32 height,
|
|
AZ::u32 maxMipCount, EPixelFormat pixelFormat)
|
|
{
|
|
return aznew CImageObject(width, height, maxMipCount, pixelFormat);
|
|
}
|
|
|
|
CImageObject::CImageObject(AZ::u32 width, AZ::u32 height, AZ::u32 maxMipCount, EPixelFormat pixelFormat)
|
|
: m_pixelFormat(pixelFormat)
|
|
, m_colMinARGB(0.0f, 0.0f, 0.0f, 0.0f)
|
|
, m_colMaxARGB(1.0f, 1.0f, 1.0f, 1.0f)
|
|
, m_averageBrightness(0.63f)
|
|
, m_imageFlags(0)
|
|
, m_numPersistentMips(0)
|
|
{
|
|
ResetImage(width, height, maxMipCount, pixelFormat);
|
|
}
|
|
|
|
EPixelFormat CImageObject::GetPixelFormat() const
|
|
{
|
|
return m_pixelFormat;
|
|
}
|
|
|
|
AZ::u32 CImageObject::GetPixelCount(AZ::u32 mip) const
|
|
{
|
|
AZ_Assert(mip < (AZ::u32)m_mips.size() && m_mips[mip], "Mip doesn't exist: %d", mip);
|
|
|
|
return m_mips[mip]->m_width * m_mips[mip]->m_height;
|
|
}
|
|
|
|
AZ::u32 CImageObject::GetWidth(AZ::u32 mip) const
|
|
{
|
|
AZ_Assert(mip < (AZ::u32)m_mips.size() && m_mips[mip], "Mip doesn't exist: %d", mip);
|
|
|
|
return m_mips[mip]->m_width;
|
|
}
|
|
|
|
AZ::u32 CImageObject::GetHeight(AZ::u32 mip) const
|
|
{
|
|
AZ_Assert(mip < (AZ::u32)m_mips.size() && m_mips[mip], "Mip doesn't exist: %d", mip);
|
|
|
|
return m_mips[mip]->m_height;
|
|
}
|
|
|
|
AZ::u32 CImageObject::GetMipCount() const
|
|
{
|
|
return (AZ::u32)m_mips.size();
|
|
}
|
|
|
|
void CImageObject::ResetImage(AZ::u32 width, AZ::u32 height, AZ::u32 maxMipCount, EPixelFormat pixelFormat)
|
|
{
|
|
//check input
|
|
AZ_Assert(width > 0 && height > 0, "image width and height need to larger than 0. width: %d, height: %d", width, height);
|
|
|
|
//clean up mipmaps
|
|
for (AZ::u32 mip = 0; mip < AZ::u32(m_mips.size()); ++mip)
|
|
{
|
|
delete m_mips[mip];
|
|
}
|
|
|
|
m_pixelFormat = pixelFormat;
|
|
m_colMinARGB = AZ::Color(0.0f, 0.0f, 0.0f, 0.0f);
|
|
m_colMaxARGB = AZ::Color(1.0f, 1.0f, 1.0f, 1.0f);
|
|
m_averageBrightness = 0.0f;
|
|
m_imageFlags = 0;
|
|
m_numPersistentMips = 0;
|
|
m_mips.clear();
|
|
|
|
const PixelFormatInfo* const pFmt = CPixelFormats::GetInstance().GetPixelFormatInfo(m_pixelFormat);
|
|
AZ_Assert(pFmt, "can't find pixe format info for %d", m_pixelFormat);
|
|
|
|
const AZ::u32 mipCount = AZStd::min<AZ::u32>(maxMipCount,
|
|
CPixelFormats::GetInstance().ComputeMaxMipCount(m_pixelFormat, width, height));
|
|
|
|
m_mips.reserve(mipCount);
|
|
|
|
for (AZ::u32 mip = 0; mip < mipCount; ++mip)
|
|
{
|
|
MipLevel* const pEntry = aznew MipLevel;
|
|
|
|
AZ::u32 localWidth = width >> mip;
|
|
AZ::u32 localHeight = height >> mip;
|
|
if (localWidth < 1)
|
|
{
|
|
localWidth = 1;
|
|
}
|
|
if (localHeight < 1)
|
|
{
|
|
localHeight = 1;
|
|
}
|
|
|
|
pEntry->m_width = localWidth;
|
|
pEntry->m_height = localHeight;
|
|
|
|
if (pFmt->bCompressed)
|
|
{
|
|
const AZ::u32 blocksInRow = (pEntry->m_width + (pFmt->blockWidth - 1)) / pFmt->blockWidth;
|
|
pEntry->m_pitch = (blocksInRow * pFmt->bitsPerBlock) / 8;
|
|
pEntry->m_rowCount = (localHeight + (pFmt->blockHeight - 1)) / pFmt->blockHeight;
|
|
}
|
|
else
|
|
{
|
|
pEntry->m_pitch = (pEntry->m_width * pFmt->bitsPerBlock) / 8;
|
|
pEntry->m_rowCount = localHeight;
|
|
}
|
|
|
|
pEntry->Alloc();
|
|
|
|
m_mips.push_back(pEntry);
|
|
}
|
|
}
|
|
|
|
bool CImageObject::CompareImage(const IImageObjectPtr otherImage) const
|
|
{
|
|
CImageObject* other = static_cast<CImageObject*>(otherImage.get());
|
|
if (other == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (m_pixelFormat == other->m_pixelFormat
|
|
&& m_colMinARGB == other->m_colMinARGB
|
|
&& m_colMaxARGB == other->m_colMaxARGB
|
|
&& m_averageBrightness == other->m_averageBrightness
|
|
&& m_imageFlags == other->m_imageFlags
|
|
&& m_numPersistentMips == other->m_numPersistentMips
|
|
&& m_mips.size() == other->m_mips.size())
|
|
{
|
|
for (int mip = 0; mip < m_mips.size(); mip++)
|
|
{
|
|
if (!(*m_mips[mip] == *other->m_mips[mip]))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
uint32_t CImageObject::GetTextureMemory() const
|
|
{
|
|
int totalSize = 0;
|
|
for (int mip = 0; mip < m_mips.size(); mip++)
|
|
{
|
|
totalSize += CPixelFormats::GetInstance().EvaluateImageDataSize(m_pixelFormat,
|
|
m_mips[mip]->m_width, m_mips[mip]->m_height);
|
|
}
|
|
|
|
return totalSize;
|
|
}
|
|
|
|
EAlphaContent CImageObject::GetAlphaContent() const
|
|
{
|
|
if (CPixelFormats::GetInstance().IsPixelFormatWithoutAlpha(m_pixelFormat))
|
|
{
|
|
return EAlphaContent::eAlphaContent_Absent;
|
|
}
|
|
|
|
//if it's compressed format, return indeterminate. if user really want to know the content, they may convert the format to ARGB8 first
|
|
if (!CPixelFormats::GetInstance().IsPixelFormatUncompressed(m_pixelFormat))
|
|
{
|
|
AZ_Assert(false, "the function only works right with uncompressed formats. convert to uncompressed format if you get accurate result");
|
|
return EAlphaContent::eAlphaContent_Indeterminate;
|
|
}
|
|
|
|
//go though alpha channel of first mip
|
|
//create pixel operation function to access pixel data
|
|
IPixelOperationPtr pixelOp = CreatePixelOperation(m_pixelFormat);
|
|
|
|
//get count of bytes per pixel for images
|
|
AZ::u32 pixelBytes = CPixelFormats::GetInstance().GetPixelFormatInfo(m_pixelFormat)->bitsPerBlock / 8;
|
|
|
|
// counts of blacks and white
|
|
uint32_t nBlacks = 0;
|
|
uint32_t nWhites = 0;
|
|
|
|
float r, g, b, a;
|
|
AZ::u8* pixelBuf;
|
|
AZ::u32 pitch;
|
|
GetImagePointer(0, pixelBuf, pitch);
|
|
|
|
const AZ::u32 pixelCount = GetPixelCount(0);
|
|
|
|
for (AZ::u32 i = 0; i < pixelCount; ++i, pixelBuf += pixelBytes)
|
|
{
|
|
pixelOp->GetRGBA(pixelBuf, r, g, b, a);
|
|
if (a == 0.0f)
|
|
{
|
|
++nBlacks;
|
|
}
|
|
else if (a == 1.0f)
|
|
{
|
|
++nWhites;
|
|
}
|
|
else
|
|
{
|
|
return EAlphaContent::eAlphaContent_Greyscale;
|
|
}
|
|
}
|
|
|
|
if (nBlacks == 0)
|
|
{
|
|
return EAlphaContent::eAlphaContent_OnlyWhite;
|
|
}
|
|
|
|
if (nWhites == 0)
|
|
{
|
|
return EAlphaContent::eAlphaContent_OnlyBlack;
|
|
}
|
|
|
|
return EAlphaContent::eAlphaContent_OnlyBlackAndWhite;
|
|
}
|
|
|
|
// clone this image-object's contents
|
|
IImageObject* CImageObject::Clone(uint32_t maxMipCount) const
|
|
{
|
|
const EPixelFormat srcPixelformat = GetPixelFormat();
|
|
|
|
IImageObject* outImage = AllocateImage(maxMipCount);
|
|
AZ::u32 mips = outImage->GetMipCount();
|
|
|
|
for (AZ::u32 mip = 0; mip < mips; ++mip)
|
|
{
|
|
AZ::u8* dstMem;
|
|
AZ::u32 pitch;
|
|
outImage->GetImagePointer(mip, dstMem, pitch);
|
|
|
|
memcpy(dstMem, m_mips[mip]->m_pData, GetMipBufSize(mip));
|
|
}
|
|
return outImage;
|
|
}
|
|
|
|
void CImageObject::ClearColor(float r, float g, float b, float a)
|
|
{
|
|
//if it's compressed format, return directly
|
|
if (!CPixelFormats::GetInstance().IsPixelFormatUncompressed(m_pixelFormat))
|
|
{
|
|
AZ_Assert(false, "The %s function only works with uncompressed formats", __FUNCTION__);
|
|
return;
|
|
}
|
|
//create pixel operation function to access pixel data
|
|
IPixelOperationPtr pixelOp = CreatePixelOperation(m_pixelFormat);
|
|
|
|
//get count of bytes per pixel for images
|
|
AZ::u32 pixelBytes = CPixelFormats::GetInstance().GetPixelFormatInfo(m_pixelFormat)->bitsPerBlock / 8;
|
|
|
|
AZ::u8* pixelBuf;
|
|
AZ::u32 pitch;
|
|
|
|
AZ::u32 mips = GetMipCount();
|
|
for (AZ::u32 mip = 0; mip < mips; ++mip)
|
|
{
|
|
GetImagePointer(mip, pixelBuf, pitch);
|
|
const AZ::u32 pixelCount = GetPixelCount(mip);
|
|
for (AZ::u32 i = 0; i < pixelCount; ++i, pixelBuf += pixelBytes)
|
|
{
|
|
pixelOp->SetRGBA(pixelBuf, r, g, b, a);
|
|
}
|
|
}
|
|
}
|
|
|
|
// allocate an empty image with the same properties as the given image and the requested format
|
|
IImageObject* CImageObject::AllocateImage(EPixelFormat pixelFormat, uint32_t maxMipCount) const
|
|
{
|
|
AZ::u32 width = GetWidth(0);
|
|
AZ::u32 height = GetHeight(0);
|
|
|
|
if (!CPixelFormats::GetInstance().IsImageSizeValid(pixelFormat, width, height, false))
|
|
{
|
|
AZ_Assert(false, "Cann't allocate image with format: %d", pixelFormat);
|
|
return nullptr;
|
|
}
|
|
|
|
maxMipCount = AZ::GetMin(maxMipCount, GetMipCount());
|
|
CImageObject* pRet = aznew CImageObject(width, height, maxMipCount, pixelFormat);
|
|
pRet->CopyPropertiesFrom(this);
|
|
return pRet;
|
|
}
|
|
|
|
IImageObject* CImageObject::AllocateImage(uint32_t maxMipCount) const
|
|
{
|
|
return AllocateImage(m_pixelFormat, maxMipCount);
|
|
}
|
|
|
|
CImageObject::~CImageObject()
|
|
{
|
|
for (size_t i = 0; i < m_mips.size(); ++i)
|
|
{
|
|
delete m_mips[i];
|
|
}
|
|
m_mips.clear();
|
|
}
|
|
|
|
//note: there are some unreasonable parts of the save files formats for cry textures. We might need to rethink about
|
|
// it for new renderer
|
|
bool CImageObject::SaveImage(const char* filename, IImageObjectPtr alphaImage, AZStd::vector<AZStd::string>& outFilePaths) const
|
|
{
|
|
AZ::IO::SystemFile file;
|
|
file.Open(filename, AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY);
|
|
|
|
AZ::IO::SystemFileStream fileSaveStream(&file, true);
|
|
if (!fileSaveStream.IsOpen())
|
|
{
|
|
AZ_Warning("Image Processing", false, "%s: failed to create file %s", __FUNCTION__, filename);
|
|
return false;
|
|
}
|
|
|
|
if (alphaImage)
|
|
{
|
|
AZ_Assert(HasImageFlags(EIF_AttachedAlpha), "attached alpha image flag wasn't set");
|
|
AZ_Assert(!alphaImage->HasImageFlags(EIF_AttachedAlpha), "alpha image shouldn't have attached alpha image flag");
|
|
|
|
// inherit cubemap and decal image flags to attached alpha image
|
|
alphaImage->AddImageFlags(GetImageFlags() & (EIF_Cubemap
|
|
| EIF_Decal | EIF_Splitted));
|
|
alphaImage->SetNumPersistentMips(m_numPersistentMips);
|
|
}
|
|
|
|
bool bOk = SaveImage(fileSaveStream);
|
|
bool hasSplitFlag = HasImageFlags(EIF_Splitted);
|
|
|
|
//append alpha image data in the end if there is no split
|
|
if (bOk && alphaImage && !hasSplitFlag)
|
|
{
|
|
//4 bytes extension tag, 4 bytes attached alpha tag, then 4 bytes of chunk size
|
|
fileSaveStream.Write(sizeof(FOURCC_CExt), &FOURCC_CExt); // marker for the start of O3DE Extended data
|
|
fileSaveStream.Write(sizeof(FOURCC_AttC), &FOURCC_AttC); // Attached Channel chunk
|
|
|
|
uint32_t size = 0;
|
|
uint32_t sizeBytes = sizeof(size);
|
|
fileSaveStream.Write(sizeBytes, &size); //size of attached chunk
|
|
|
|
//save alpha image and get the size
|
|
AZ::IO::SizeType startPos = fileSaveStream.GetCurPos();
|
|
bOk = alphaImage->SaveImage(fileSaveStream);
|
|
AZ::IO::SizeType endPos = fileSaveStream.GetCurPos();
|
|
size = static_cast<uint32_t>(endPos - startPos);
|
|
|
|
//move back to beginning of chunk and write chunk size then move back to end
|
|
fileSaveStream.Seek(startPos - sizeBytes, AZ::IO::GenericStream::ST_SEEK_BEGIN);
|
|
fileSaveStream.Write(sizeBytes, &size);
|
|
fileSaveStream.Seek(endPos, AZ::IO::GenericStream::ST_SEEK_BEGIN);
|
|
|
|
// marker for the end of O3DE Extended data
|
|
fileSaveStream.Write(sizeof(FOURCC_CEnd), &FOURCC_CEnd);
|
|
}
|
|
|
|
if (!bOk)
|
|
{
|
|
AZ::IO::SystemFile::Delete(filename);
|
|
return false;
|
|
}
|
|
|
|
// It's important to maintain the product output sequence. Asset Database/Browser will use the first product to determine the source type!
|
|
outFilePaths.push_back(filename);
|
|
|
|
// save stand alone products
|
|
if (hasSplitFlag)
|
|
{
|
|
// alpha
|
|
if (alphaImage)
|
|
{
|
|
AZStd::string alphaFile = AZStd::string::format("%s.a", filename);
|
|
|
|
AZ::IO::SystemFile outAlphaFile;
|
|
outAlphaFile.Open(alphaFile.c_str(), AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY);
|
|
|
|
AZ::IO::SystemFileStream alphaFileSaveStream(&outAlphaFile, true);
|
|
|
|
if (alphaFileSaveStream.IsOpen())
|
|
{
|
|
alphaImage->SaveImage(alphaFileSaveStream);
|
|
outFilePaths.push_back(alphaFile);
|
|
}
|
|
else
|
|
{
|
|
AZ_Warning("Image Processing", false, "%s: failed to create file %s", __FUNCTION__, alphaFile.c_str());
|
|
}
|
|
}
|
|
|
|
// mips
|
|
AZ::u32 numStreamable = GetMipCount() - m_numPersistentMips;
|
|
for (AZ::u32 mip = 0; mip < numStreamable; mip++)
|
|
{
|
|
AZ::u32 nameIdx = numStreamable - mip;
|
|
AZStd::string mipFileName = AZStd::string::format("%s.%d", filename, nameIdx);
|
|
SaveMipToFile(mip, mipFileName);
|
|
outFilePaths.push_back(mipFileName);
|
|
if (alphaImage)
|
|
{
|
|
AZStd::string mipAlphaFileName = mipFileName + "a";
|
|
alphaImage->SaveMipToFile(mip, mipAlphaFileName);
|
|
outFilePaths.push_back(mipAlphaFileName);
|
|
}
|
|
}
|
|
}
|
|
|
|
return bOk;
|
|
}
|
|
|
|
bool CImageObject::SaveMipToFile(AZ::u32 mip, const AZStd::string& filename) const
|
|
{
|
|
AZ::IO::SystemFile saveFile;
|
|
saveFile.Open(filename.c_str(), AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY);
|
|
|
|
AZ::IO::SystemFileStream saveFileStream(&saveFile, true);
|
|
|
|
if (!saveFileStream.IsOpen())
|
|
{
|
|
AZ_Warning("Image Processing", false, "%s: failed to create file %s", __FUNCTION__, filename.c_str());
|
|
return false;
|
|
}
|
|
|
|
saveFileStream.Write(GetMipBufSize(mip), m_mips[mip]->m_pData);
|
|
return true;
|
|
}
|
|
|
|
float CImageObject::CalculateAverageBrightness() const
|
|
{
|
|
//if it's compressed format, return a default value
|
|
if (!CPixelFormats::GetInstance().IsPixelFormatUncompressed(m_pixelFormat))
|
|
{
|
|
return 0.5f;
|
|
}
|
|
|
|
// Accumulate pixel colors of the top mip
|
|
double avgOverall[3] = { 0.0, 0.0, 0.0 };
|
|
|
|
//create pixel operation function to access pixel data
|
|
IPixelOperationPtr pixelOp = CreatePixelOperation(m_pixelFormat);
|
|
|
|
//get count of bytes per pixel for images
|
|
AZ::u32 pixelBytes = CPixelFormats::GetInstance().GetPixelFormatInfo(m_pixelFormat)->bitsPerBlock / 8;
|
|
|
|
//only calculate mip 0
|
|
AZ::u32 mip = 0;
|
|
float color[4];
|
|
AZ::u8* pixelBuf;
|
|
AZ::u32 pitch;
|
|
GetImagePointer(mip, pixelBuf, pitch);
|
|
const AZ::u32 pixelCount = GetPixelCount(mip);
|
|
for (AZ::u32 i = 0; i < pixelCount; ++i, pixelBuf += pixelBytes)
|
|
{
|
|
pixelOp->GetRGBA(pixelBuf, color[0], color[1], color[2], color[3]);
|
|
avgOverall[0] += color[0];
|
|
avgOverall[1] += color[1];
|
|
avgOverall[2] += color[2];
|
|
}
|
|
|
|
const double avg = (avgOverall[0] + avgOverall[1] + avgOverall[2]) / (3 * pixelCount);
|
|
|
|
return (float)avg;
|
|
}
|
|
|
|
bool CImageObject::BuildSurfaceHeader(DDS_HEADER_LEGACY& header) const
|
|
{
|
|
AZ::u32 dwWidth, dwMips, dwHeight;
|
|
GetExtent(dwWidth, dwHeight, dwMips);
|
|
|
|
if (dwMips <= 0)
|
|
{
|
|
AZ_Error("Image Processing", false, "%s: dwMips is %u", __FUNCTION__, (unsigned)dwMips);
|
|
return false;
|
|
}
|
|
|
|
const EPixelFormat format = GetPixelFormat();
|
|
if ((format < 0) || (format >= ePixelFormat_Count))
|
|
{
|
|
AZ_Error("Image Processing", false, "%s: Bad format %d", __FUNCTION__, (int)format);
|
|
return false;
|
|
}
|
|
|
|
const PixelFormatInfo* const pPixelFormatInfo = CPixelFormats::GetInstance().GetPixelFormatInfo(format);
|
|
|
|
memset(&header, 0, sizeof(DDS_HEADER_LEGACY));
|
|
|
|
header.dwSize = sizeof(DDS_HEADER_LEGACY);
|
|
header.dwHeaderFlags = DDSD_CAPS | DDSD_PIXELFORMAT | DDSD_WIDTH | DDSD_HEIGHT;
|
|
header.dwWidth = dwWidth;
|
|
header.dwHeight = dwHeight;
|
|
|
|
if (HasImageFlags(EIF_Cubemap))
|
|
{
|
|
header.dwSurfaceFlags |= DDS_SURFACE_FLAGS_CUBEMAP;
|
|
header.dwCubemapFlags |= DDS_CUBEMAP_ALLFACES;
|
|
//save face size instead of image size.
|
|
header.dwHeight /= 6;
|
|
}
|
|
|
|
header.ddspf.dwSize = sizeof(DDS_PIXELFORMAT);
|
|
header.ddspf.dwFlags = DDS_FOURCC;
|
|
|
|
header.ddspf.dwFourCC = pPixelFormatInfo->fourCC;
|
|
|
|
header.dwSurfaceFlags |= DDS_SURFACE_FLAGS_TEXTURE;
|
|
|
|
if (dwMips > 1)
|
|
{
|
|
header.dwHeaderFlags |= DDS_HEADER_FLAGS_MIPMAP;
|
|
header.dwMipMapCount = dwMips;
|
|
header.dwSurfaceFlags |= DDS_SURFACE_FLAGS_MIPMAP;
|
|
}
|
|
|
|
// non standardized way to expose some features in the header (same information is in attached chunk but then
|
|
// streaming would need to find this spot in the file)
|
|
// if this is causing problems we need to change it
|
|
header.dwTextureStage = FOURCC_FYRC;
|
|
header.dwReserved1 = GetImageFlags();
|
|
header.bNumPersistentMips = (AZ::u8)GetNumPersistentMips();
|
|
|
|
//tile mode for some platform native texture
|
|
if (HasImageFlags(EIF_RestrictedPlatformDNative))
|
|
{
|
|
header.tileMode = eTM_LinearPadded;
|
|
}
|
|
else if (HasImageFlags(EIF_RestrictedPlatformONative))
|
|
{
|
|
header.tileMode = eTM_Optimal;
|
|
}
|
|
|
|
// setting up min and max colors
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
header.cMinColor[i] = m_colMinARGB.GetElement(i);
|
|
header.cMaxColor[i] = m_colMaxARGB.GetElement(i);
|
|
}
|
|
|
|
// set avg brightness
|
|
header.fAvgBrightness = GetAverageBrightness();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CImageObject::BuildSurfaceExtendedHeader(DDS_HEADER_DXT10& exthead) const
|
|
{
|
|
const EPixelFormat format = GetPixelFormat();
|
|
|
|
const PixelFormatInfo* const pPixelFormatInfo = CPixelFormats::GetInstance().GetPixelFormatInfo(format);
|
|
|
|
DXGI_FORMAT dxgiformat = pPixelFormatInfo->d3d10Format;
|
|
|
|
// check if we hit a format which can't be stored into a DX10 DDS-file (fe. L8)
|
|
if (dxgiformat == DXGI_FORMAT_UNKNOWN)
|
|
{
|
|
AZ_Error("Image Processing", false, "%s: Format can not be stored in a DDS-file %d", __FUNCTION__, dxgiformat);
|
|
return false;
|
|
}
|
|
|
|
//the dxgi format are different for linear space or gamma space
|
|
if (HasImageFlags(EIF_SRGBRead))
|
|
{
|
|
switch (dxgiformat)
|
|
{
|
|
case DXGI_FORMAT_R8G8B8A8_UNORM:
|
|
dxgiformat = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
|
|
break;
|
|
case DXGI_FORMAT_BC1_UNORM:
|
|
dxgiformat = DXGI_FORMAT_BC1_UNORM_SRGB;
|
|
break;
|
|
case DXGI_FORMAT_BC2_UNORM:
|
|
dxgiformat = DXGI_FORMAT_BC2_UNORM_SRGB;
|
|
break;
|
|
case DXGI_FORMAT_BC3_UNORM:
|
|
dxgiformat = DXGI_FORMAT_BC3_UNORM_SRGB;
|
|
break;
|
|
case DXGI_FORMAT_BC7_UNORM:
|
|
dxgiformat = DXGI_FORMAT_BC7_UNORM_SRGB;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (dxgiformat)
|
|
{
|
|
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
|
|
dxgiformat = DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
break;
|
|
case DXGI_FORMAT_BC1_UNORM_SRGB:
|
|
dxgiformat = DXGI_FORMAT_BC1_UNORM;
|
|
break;
|
|
case DXGI_FORMAT_BC2_UNORM_SRGB:
|
|
dxgiformat = DXGI_FORMAT_BC2_UNORM;
|
|
break;
|
|
case DXGI_FORMAT_BC3_UNORM_SRGB:
|
|
dxgiformat = DXGI_FORMAT_BC3_UNORM;
|
|
break;
|
|
case DXGI_FORMAT_BC7_UNORM_SRGB:
|
|
dxgiformat = DXGI_FORMAT_BC7_UNORM;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
memset(&exthead, 0, sizeof(exthead));
|
|
|
|
exthead.dxgiFormat = dxgiformat;
|
|
exthead.resourceDimension = 3; //texture2d. not used
|
|
|
|
if (HasImageFlags(EIF_Volumetexture))
|
|
{
|
|
AZ_Assert(false, "There isn't any support for volume texture");
|
|
}
|
|
else if (HasImageFlags(EIF_Cubemap))
|
|
{
|
|
exthead.miscFlag = DDS_RESOURCE_MISC_TEXTURECUBE;
|
|
exthead.arraySize = 6;
|
|
}
|
|
else
|
|
{
|
|
exthead.miscFlag = 0;
|
|
exthead.arraySize = 1;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CImageObject::SaveImage(AZ::IO::SystemFileStream& saveFileStream) const
|
|
{
|
|
DDS_FILE_DESC_LEGACY desc;
|
|
DDS_HEADER_DXT10 exthead;
|
|
|
|
desc.dwMagic = FOURCC_DDS;
|
|
|
|
if (!BuildSurfaceHeader(desc.header))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (desc.header.IsDX10Ext() && !BuildSurfaceExtendedHeader(exthead))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
saveFileStream.Write(sizeof(desc), &desc);
|
|
|
|
if (desc.header.IsDX10Ext())
|
|
{
|
|
saveFileStream.Write(sizeof(exthead), &exthead);
|
|
}
|
|
|
|
AZ::u32 faces = 1;
|
|
|
|
//for cubemap. export each face and its mipmap
|
|
if (HasImageFlags(EIF_Cubemap))
|
|
{
|
|
faces = 6;
|
|
}
|
|
|
|
AZ::u32 mipStart = 0;
|
|
if (HasImageFlags(EIF_Splitted))
|
|
{
|
|
if (m_numPersistentMips < m_mips.size())
|
|
{
|
|
mipStart = (AZ::u32)m_mips.size() - m_numPersistentMips;
|
|
}
|
|
else
|
|
{
|
|
AZ_Assert(false, "numPersistentMips wasn't setup correctly");
|
|
}
|
|
}
|
|
|
|
for (AZ::u32 face = 0; face < faces; face++)
|
|
{
|
|
for (AZ::u32 mip = mipStart; mip < m_mips.size(); ++mip)
|
|
{
|
|
const MipLevel& level = *m_mips[mip];
|
|
AZ::u32 faceBufSize = level.m_pitch * level.m_rowCount / faces;
|
|
saveFileStream.Write(faceBufSize, level.m_pData + faceBufSize * face);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void CImageObject::GetExtent(AZ::u32& width, AZ::u32& height, AZ::u32& mipCount) const
|
|
{
|
|
mipCount = (AZ::u32)m_mips.size();
|
|
|
|
width = m_mips[0]->m_width;
|
|
height = m_mips[0]->m_height;
|
|
}
|
|
|
|
AZ::u32 CImageObject::GetMipDataSize(const AZ::u32 mip) const
|
|
{
|
|
AZ_Assert(mip < m_mips.size(), "mip %d doesn't exist", mip);
|
|
|
|
return m_mips[mip]->GetSize();
|
|
}
|
|
|
|
void CImageObject::GetImagePointer(const AZ::u32 mip, AZ::u8*& pMem, AZ::u32& pitch) const
|
|
{
|
|
AZ_Assert(mip < (AZ::u32)m_mips.size() && m_mips[mip], "requested mip doesn't exist");
|
|
|
|
pMem = m_mips[mip]->m_pData;
|
|
pitch = m_mips[mip]->m_pitch;
|
|
}
|
|
|
|
AZ::u32 CImageObject::GetMipBufSize(AZ::u32 mip) const
|
|
{
|
|
AZ_Assert(mip < (AZ::u32)m_mips.size() && m_mips[mip], "requested mip doesn't exist");
|
|
|
|
return m_mips[mip]->m_rowCount * m_mips[mip]->m_pitch;
|
|
}
|
|
|
|
|
|
void CImageObject::SetMipData(AZ::u32 mip, AZ::u8* mipBuf, AZ::u32 bufSize, AZ::u32 pitch)
|
|
{
|
|
if (mip >= m_mips.size())
|
|
{
|
|
return;
|
|
}
|
|
m_mips[mip]->m_pData = mipBuf;
|
|
m_mips[mip]->m_pitch = pitch;
|
|
m_mips[mip]->m_rowCount = bufSize / pitch;
|
|
AZ_Assert(bufSize == m_mips[mip]->m_rowCount * pitch, "Bad pitch size");
|
|
}
|
|
|
|
// ARGB
|
|
void CImageObject::GetColorRange(AZ::Color& minColor, AZ::Color& maxColor) const
|
|
{
|
|
minColor = m_colMinARGB;
|
|
maxColor = m_colMaxARGB;
|
|
}
|
|
|
|
// ARGB
|
|
void CImageObject::SetColorRange(const AZ::Color& minColor, const AZ::Color& maxColor)
|
|
{
|
|
m_colMinARGB = minColor;
|
|
m_colMaxARGB = maxColor;
|
|
}
|
|
|
|
float CImageObject::GetAverageBrightness() const
|
|
{
|
|
return m_averageBrightness;
|
|
}
|
|
|
|
void CImageObject::SetAverageBrightness(const float avgBrightness)
|
|
{
|
|
m_averageBrightness = avgBrightness;
|
|
}
|
|
|
|
AZ::u32 CImageObject::GetImageFlags() const
|
|
{
|
|
return m_imageFlags;
|
|
}
|
|
|
|
void CImageObject::SetImageFlags(const AZ::u32 imageFlags)
|
|
{
|
|
m_imageFlags = imageFlags;
|
|
}
|
|
|
|
void CImageObject::AddImageFlags(const AZ::u32 imageFlags)
|
|
{
|
|
m_imageFlags |= imageFlags;
|
|
}
|
|
|
|
void CImageObject::RemoveImageFlags(const AZ::u32 imageFlags)
|
|
{
|
|
m_imageFlags &= ~imageFlags;
|
|
}
|
|
|
|
bool CImageObject::HasImageFlags(const AZ::u32 imageFlags) const
|
|
{
|
|
return (m_imageFlags & imageFlags) != 0;
|
|
}
|
|
|
|
AZ::u32 CImageObject::GetNumPersistentMips() const
|
|
{
|
|
return m_numPersistentMips;
|
|
}
|
|
|
|
void CImageObject::SetNumPersistentMips(AZ::u32 nMips)
|
|
{
|
|
m_numPersistentMips = nMips;
|
|
}
|
|
|
|
bool CImageObject::HasPowerOfTwoSizes() const
|
|
{
|
|
AZ::u32 w, h, mips;
|
|
GetExtent(w, h, mips);
|
|
return ((w & (w - 1)) == 0) && ((h & (h - 1)) == 0);
|
|
}
|
|
|
|
// use when you convert an image to another one
|
|
void CImageObject::CopyPropertiesFrom(const IImageObjectPtr src)
|
|
{
|
|
const CImageObject* imageObj = static_cast<CImageObject*>(src.get());
|
|
CopyPropertiesFrom(imageObj);
|
|
}
|
|
|
|
void CImageObject::CopyPropertiesFrom(const CImageObject* src)
|
|
{
|
|
m_colMinARGB = src->m_colMinARGB;
|
|
m_colMaxARGB = src->m_colMaxARGB;
|
|
m_averageBrightness = src->m_averageBrightness;
|
|
m_imageFlags = src->GetImageFlags();
|
|
}
|
|
|
|
void CImageObject::Swizzle(const char channels[4])
|
|
{
|
|
if (!(CPixelFormats::GetInstance().IsPixelFormatUncompressed(m_pixelFormat)))
|
|
{
|
|
AZ_Assert(false, "%s function only works with uncompressed pixel format", __FUNCTION__);
|
|
return;
|
|
}
|
|
|
|
const AZ::u8 channelCnt = 4;
|
|
|
|
enum Channel_Id
|
|
{
|
|
ChannelR = 0,
|
|
ChannelG,
|
|
ChannelB,
|
|
ChannelA,
|
|
ChannelVal0,
|
|
ChannelVal1,
|
|
ChannelTypeCount
|
|
};
|
|
|
|
float values[ChannelTypeCount];
|
|
values[ChannelVal0] = 0.f;
|
|
values[ChannelVal1] = 1.f;
|
|
|
|
AZ::u8 channelIndics[channelCnt];
|
|
for (AZ::u8 idx = 0; idx < channelCnt; idx++)
|
|
{
|
|
switch (channels[idx])
|
|
{
|
|
case 'a':
|
|
channelIndics[idx] = ChannelA;
|
|
break;
|
|
case 'r':
|
|
channelIndics[idx] = ChannelR;
|
|
break;
|
|
case 'g':
|
|
channelIndics[idx] = ChannelG;
|
|
break;
|
|
case 'b':
|
|
channelIndics[idx] = ChannelB;
|
|
break;
|
|
case '0':
|
|
channelIndics[idx] = ChannelVal0;
|
|
break;
|
|
case '1':
|
|
channelIndics[idx] = ChannelVal1;
|
|
break;
|
|
default:
|
|
AZ_Assert(false, "%s function only works with channel name \"rgba01\"", __FUNCTION__);
|
|
return;
|
|
}
|
|
}
|
|
|
|
//create pixel operation function
|
|
IPixelOperationPtr pixelOp = CreatePixelOperation(m_pixelFormat);
|
|
|
|
//get count of bytes per pixel
|
|
uint32 pixelBytes = CPixelFormats::GetInstance().GetPixelFormatInfo(m_pixelFormat)->bitsPerBlock / 8;
|
|
|
|
const uint32 mips = (uint32)m_mips.size();
|
|
for (uint32 mip = 0; mip < mips; ++mip)
|
|
{
|
|
uint8* pixelBuf = m_mips[mip]->m_pData;
|
|
const uint32 pixelCount = GetPixelCount(mip);
|
|
|
|
for (uint32 i = 0; i < pixelCount; ++i, pixelBuf += pixelBytes)
|
|
{
|
|
pixelOp->GetRGBA(pixelBuf, values[ChannelR], values[ChannelG], values[ChannelB], values[ChannelA]);
|
|
pixelOp->SetRGBA(pixelBuf, values[channelIndics[0]], values[channelIndics[1]],
|
|
values[channelIndics[2]], values[channelIndics[3]]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CImageObject::GlossFromNormals(bool hasAuthoredGloss)
|
|
{
|
|
if (!(CPixelFormats::GetInstance().IsPixelFormatUncompressed(m_pixelFormat)))
|
|
{
|
|
AZ_Assert(false, "%s function only works with uncompressed pixel format", __FUNCTION__);
|
|
return;
|
|
}
|
|
|
|
// Derive new roughness from normal variance to preserve the bumpiness of normal map mips and to reduce specular aliasing.
|
|
// The derived roughness is combined with the artist authored roughness stored in the alpha channel of the normal map.
|
|
// The algorithm is based on the Frequency Domain Normal Mapping implementation presented by Neubelt and Pettineo at Siggraph 2013.
|
|
|
|
//create pixel operation function
|
|
IPixelOperationPtr pixelOp = CreatePixelOperation(m_pixelFormat);
|
|
|
|
//get count of bytes per pixel
|
|
AZ::u32 pixelBytes = CPixelFormats::GetInstance().GetPixelFormatInfo(m_pixelFormat)->bitsPerBlock / 8;
|
|
|
|
const AZ::u32 mips = (AZ::u32)m_mips.size();
|
|
float color[4];
|
|
for (AZ::u32 mip = 0; mip < mips; ++mip)
|
|
{
|
|
AZ::u8* pixelBuf = m_mips[mip]->m_pData;
|
|
const AZ::u32 pixelCount = GetPixelCount(mip);
|
|
|
|
for (AZ::u32 i = 0; i < pixelCount; ++i, pixelBuf += pixelBytes)
|
|
{
|
|
pixelOp->GetRGBA(pixelBuf, color[0], color[1], color[2], color[3]);
|
|
|
|
// Get length of the averaged normal
|
|
AZ::Vector3 normal(color[0] * 2.0f - 1.0f, color[1] * 2.0f - 1.0f, color[2] * 2.0f - 1.0f);
|
|
|
|
float len = AZ::GetMax<float>(normal.GetLength(), 1.0f / (1 << 15));
|
|
|
|
float authoredSmoothness = hasAuthoredGloss ? color[3] : 1.0f;
|
|
float finalSmoothness = authoredSmoothness;
|
|
|
|
if (len < 1.0f)
|
|
{
|
|
// Convert from smoothness to roughness (needs to match shader code)
|
|
float authoredRoughness = (1.0f - authoredSmoothness) * (1.0f - authoredSmoothness);
|
|
|
|
// Derive new roughness based on normal variance
|
|
float kappa = (3.0f * len - len * len * len) / (1.0f - len * len);
|
|
float variance = 1.0f / (2.0f * kappa);
|
|
float finalRoughness = AZ::GetMin(sqrtf(authoredRoughness * authoredRoughness + variance), 1.0f);
|
|
|
|
// Convert roughness back to smoothness
|
|
finalSmoothness = 1.0f - sqrtf(finalRoughness);
|
|
}
|
|
|
|
pixelOp->SetRGBA(pixelBuf, color[0], color[1], color[2], finalSmoothness);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CImageObject::ConvertLegacyGloss()
|
|
{
|
|
if (!(CPixelFormats::GetInstance().IsPixelFormatUncompressed(m_pixelFormat)))
|
|
{
|
|
AZ_Assert(false, "%s function only works with uncompressed pixel format", __FUNCTION__);
|
|
return;
|
|
}
|
|
|
|
//create pixel operation function
|
|
IPixelOperationPtr pixelOp = CreatePixelOperation(m_pixelFormat);
|
|
|
|
//get count of bytes per pixel
|
|
AZ::u32 pixelBytes = CPixelFormats::GetInstance().GetPixelFormatInfo(m_pixelFormat)->bitsPerBlock / 8;
|
|
|
|
const AZ::u32 mips = (AZ::u32)m_mips.size();
|
|
float color[4];
|
|
for (AZ::u32 mip = 0; mip < mips; ++mip)
|
|
{
|
|
AZ::u8* pixelBuf = m_mips[mip]->m_pData;
|
|
const AZ::u32 pixelCount = GetPixelCount(mip);
|
|
|
|
for (AZ::u32 i = 0; i < pixelCount; ++i, pixelBuf += pixelBytes)
|
|
{
|
|
pixelOp->GetRGBA(pixelBuf, color[0], color[1], color[2], color[3]);
|
|
// Convert from (1 - s * 0.7)^6 to (1 - s)^2
|
|
color[3] = 1 - pow(1.0f - color[3] * 0.7f, 3.0f);
|
|
pixelOp->SetRGBA(pixelBuf, color[0], color[1], color[2], color[3]);
|
|
}
|
|
}
|
|
}
|
|
} // namespace ImageProcessingAtom
|