[texconv] Implemented alpha coverage preservation option (#82)

This commit is contained in:
Julian McKinlay 2019-01-28 19:57:19 +00:00 committed by Chuck Walbourn
parent a79989a4f0
commit f2c4d94a35
3 changed files with 331 additions and 5 deletions

View File

@ -534,6 +534,11 @@ namespace DirectX
// levels of '0' indicates a full mipchain, otherwise is generates that number of total levels (including the source base image)
// Defaults to Fant filtering which is equivalent to a box filter
HRESULT __cdecl ScaleMipMapsAlphaForCoverage(
_In_ const Image* srcImages, _In_ const TexMetadata& metadata, _In_ size_t item,
_In_ float alphaReference, _Out_ ScratchImage& mipChain);
enum TEX_PMALPHA_FLAGS
{
TEX_PMALPHA_DEFAULT = 0,

View File

@ -116,6 +116,211 @@ namespace
return hr;
}
HRESULT ScaleAlpha(
_In_ const Image& srcImage,
_In_ float alphaScale,
_Out_ const Image& destImage)
{
assert(srcImage.width == destImage.width);
assert(srcImage.height == destImage.height);
ScopedAlignedArrayXMVECTOR scanline(reinterpret_cast<XMVECTOR*>(_aligned_malloc((sizeof(XMVECTOR)*srcImage.width), 16)));
if (!scanline)
{
return E_OUTOFMEMORY;
}
const uint8_t* pSrc = srcImage.pixels;
uint8_t* pDest = destImage.pixels;
if (!pSrc || !pDest)
{
return E_POINTER;
}
const XMVECTOR vscale = XMVectorReplicate(alphaScale);
for (size_t h = 0; h < srcImage.height; ++h)
{
if (!_LoadScanline(scanline.get(), srcImage.width, pSrc, srcImage.rowPitch, srcImage.format))
{
return E_FAIL;
}
XMVECTOR* ptr = scanline.get();
for (size_t w = 0; w < srcImage.width; ++w)
{
XMVECTOR v = *ptr;
XMVECTOR alpha = XMVectorMultiply(XMVectorSplatW(v), vscale);
*(ptr++) = XMVectorSelect(alpha, v, g_XMSelect1110);
}
if (!_StoreScanline(pDest, destImage.rowPitch, destImage.format, scanline.get(), srcImage.width))
{
return E_FAIL;
}
pSrc += srcImage.rowPitch;
pDest += destImage.rowPitch;
}
return S_OK;
}
void GenerateAlphaCoverageConvolutionVectors(
_In_ size_t N,
_Out_writes_(N*N) XMVECTOR* vectors)
{
for (size_t sy = 0; sy < N; ++sy)
{
const float fy = (sy + 0.5f) / N;
const float ify = 1.0f - fy;
for (size_t sx = 0; sx < N; ++sx)
{
const float fx = (sx + 0.5f) / N;
const float ifx = 1.0f - fx;
// [0]=(x+0, y+0), [1]=(x+0, y+1), [2]=(x+1, y+0), [3]=(x+1, y+1)
vectors[sy * N + sx] = XMVectorSet(ifx * ify, ifx * fy, fx * ify, fx * fy);
}
}
}
HRESULT CalculateAlphaCoverage(
const Image& srcImage,
float alphaReference,
float alphaScale,
float& coverage)
{
coverage = 0.0f;
ScopedAlignedArrayXMVECTOR row0(reinterpret_cast<XMVECTOR*>(_aligned_malloc((sizeof(XMVECTOR)*srcImage.width), 16)));
if (!row0)
{
return E_OUTOFMEMORY;
}
ScopedAlignedArrayXMVECTOR row1(reinterpret_cast<XMVECTOR*>(_aligned_malloc((sizeof(XMVECTOR)*srcImage.width), 16)));
if (!row1)
{
return E_OUTOFMEMORY;
}
const DWORD flags = 0;
const XMVECTOR scale = XMVectorReplicate(alphaScale);
const uint8_t *pSrcRow0 = srcImage.pixels;
if (!pSrcRow0)
{
return E_POINTER;
}
const size_t N = 8;
XMVECTOR convolution[N * N];
GenerateAlphaCoverageConvolutionVectors(N, convolution);
XMMATRIX alpha;
size_t coverageCount = 0;
for (size_t y = 0; y < srcImage.height - 1; ++y)
{
if (!_LoadScanlineLinear(row0.get(), srcImage.width, pSrcRow0, srcImage.rowPitch, srcImage.format, flags))
{
return E_FAIL;
}
const uint8_t *pSrcRow1 = pSrcRow0 + srcImage.rowPitch;
if (!_LoadScanlineLinear(row1.get(), srcImage.width, pSrcRow1, srcImage.rowPitch, srcImage.format, flags))
{
return E_FAIL;
}
const XMVECTOR* pRow0 = row0.get();
const XMVECTOR* pRow1 = row1.get();
for (size_t x = 0; x < srcImage.width - 1; ++x)
{
// [0]=(x+0, y+0), [1]=(x+0, y+1), [2]=(x+1, y+0), [3]=(x+1, y+1)
alpha.r[0] = XMVectorSaturate(XMVectorMultiply(XMVectorSplatW(*pRow0), scale));
alpha.r[1] = XMVectorSaturate(XMVectorMultiply(XMVectorSplatW(*pRow1), scale));
alpha.r[2] = XMVectorSaturate(XMVectorMultiply(XMVectorSplatW(*(pRow0++)), scale));
alpha.r[3] = XMVectorSaturate(XMVectorMultiply(XMVectorSplatW(*(pRow1++)), scale));
XMVECTOR v = XMVectorSet(XMVectorGetX(alpha.r[0]), XMVectorGetX(alpha.r[1]), XMVectorGetX(alpha.r[2]), XMVectorGetX(alpha.r[3]));
for (size_t sy = 0; sy < N; ++sy)
{
const size_t ry = sy * N;
for (size_t sx = 0; sx < N; ++sx)
{
v = XMVectorSum(XMVectorMultiply(v, convolution[ry + sx]));
if (XMVectorGetX(v) > alphaReference)
{
++coverageCount;
}
}
}
}
pSrcRow0 = pSrcRow1;
}
coverage = static_cast<float>(coverageCount) / static_cast<float>((srcImage.width - 1) * (srcImage.height - 1) * N * N);
return S_OK;
}
HRESULT EstimateAlphaScaleForCoverage(
_In_ const Image& srcImage,
_In_ float alphaReference,
_In_ float targetCoverage,
_Out_ float& alphaScale)
{
float minAlphaScale = 0.0f;
float maxAlphaScale = 4.0f;
float bestAlphaScale = 1.0f;
float bestError = FLT_MAX;
// Determine desired scale using a binary search. Hardcoded to 10 steps max.
alphaScale = 1.0f;
const size_t N = 10;
for (size_t i = 0; i < N; ++i)
{
float currentCoverage = 0.0f;
HRESULT hr = CalculateAlphaCoverage(srcImage, alphaReference, alphaScale, currentCoverage);
if (FAILED(hr))
{
return hr;
}
const float error = fabsf(currentCoverage - targetCoverage);
if (error < bestError)
{
bestError = error;
bestAlphaScale = alphaScale;
}
if (currentCoverage < targetCoverage)
{
minAlphaScale = alphaScale;
}
else if (currentCoverage > targetCoverage)
{
maxAlphaScale = alphaScale;
}
else
{
break;
}
alphaScale = (minAlphaScale + maxAlphaScale) * 0.5f;
}
return S_OK;
}
}
@ -3179,3 +3384,40 @@ HRESULT DirectX::GenerateMipMaps3D(
return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED);
}
}
_Use_decl_annotations_
HRESULT DirectX::ScaleMipMapsAlphaForCoverage(
const Image* srcImages,
const TexMetadata& metadata,
size_t item,
float alphaReference,
ScratchImage& mipChain)
{
HRESULT hr;
float targetCoverage = 0.0f;
hr = CalculateAlphaCoverage(*srcImages, alphaReference, 1.0f, targetCoverage);
if (FAILED(hr))
{
return hr;
}
const size_t levels = metadata.mipLevels;
for (size_t level = 1; level < levels; ++level)
{
float alphaScale = 0.0f;
hr = EstimateAlphaScaleForCoverage(srcImages[level], alphaReference, targetCoverage, alphaScale);
if (FAILED(hr))
{
return hr;
}
hr = ScaleAlpha(srcImages[level], alphaScale, *mipChain.GetImage(level, item, 0));
if (FAILED(hr))
{
return hr;
}
}
return S_OK;
}

View File

@ -101,6 +101,7 @@ enum OPTIONS
OPT_COLORKEY,
OPT_TONEMAP,
OPT_X2_BIAS,
OPT_PRESERVE_ALPHA_COVERAGE,
OPT_INVERT_Y,
OPT_FILELIST,
OPT_ROTATE_COLOR,
@ -187,6 +188,7 @@ const SValue g_pOptions[] =
{ L"c", OPT_COLORKEY },
{ L"tonemap", OPT_TONEMAP },
{ L"x2bias", OPT_X2_BIAS },
{ L"keepcoverage", OPT_PRESERVE_ALPHA_COVERAGE },
{ L"inverty", OPT_INVERT_Y },
{ L"flist", OPT_FILELIST },
{ L"rotatecolor", OPT_ROTATE_COLOR },
@ -751,6 +753,7 @@ namespace
wprintf(L" -nits <value> paper-white value in nits to use for HDR10 (def: 200.0)\n");
wprintf(L" -tonemap Apply a tonemap operator based on maximum luminance\n");
wprintf(L" -x2bias Enable *2 - 1 conversion cases for unorm/pos-only-float\n");
wprintf(L" -keepcoverage <ref> Preserve alpha coverage in generated mips for alpha test ref\n");
wprintf(L" -inverty Invert Y (i.e. green) channel values\n");
wprintf(L" -flist <filename> use text file with a list of input files (one per line)\n");
@ -1116,6 +1119,7 @@ int __cdecl wmain(_In_ int argc, _In_z_count_(argc) wchar_t* argv[])
DWORD colorKey = 0;
DWORD dwRotateColor = 0;
float paperWhiteNits = 200.f;
float preserveAlphaCoverageRef = 0.0f;
wchar_t szPrefix[MAX_PATH];
wchar_t szSuffix[MAX_PATH];
@ -1183,6 +1187,7 @@ int __cdecl wmain(_In_ int argc, _In_z_count_(argc) wchar_t* argv[])
case OPT_FILELIST:
case OPT_ROTATE_COLOR:
case OPT_PAPER_WHITE_NITS:
case OPT_PRESERVE_ALPHA_COVERAGE:
if (!*pValue)
{
if ((iArg + 1 >= argc))
@ -1569,15 +1574,27 @@ int __cdecl wmain(_In_ int argc, _In_z_count_(argc) wchar_t* argv[])
case OPT_PAPER_WHITE_NITS:
if (swscanf_s(pValue, L"%f", &paperWhiteNits) != 1)
{
wprintf(L"Invalid value specified with -nits (%ls)\n", pValue);
wprintf(L"\n");
wprintf(L"Invalid value specified with -nits (%ls)\n\n", pValue);
PrintUsage();
return 1;
}
else if (paperWhiteNits > 10000.f || paperWhiteNits <= 0.f)
{
wprintf(L"-nits (%ls) parameter must be between 0 and 10000\n", pValue);
wprintf(L"\n");
wprintf(L"-nits (%ls) parameter must be between 0 and 10000\n\n", pValue);
return 1;
}
break;
case OPT_PRESERVE_ALPHA_COVERAGE:
if (swscanf_s(pValue, L"%f", &preserveAlphaCoverageRef) != 1)
{
wprintf(L"Invalid value specified with -keepcoverage (%ls)\n\n", pValue);
PrintUsage();
return 1;
}
else if (preserveAlphaCoverageRef < 0.0f || preserveAlphaCoverageRef > 1.0f)
{
wprintf(L"-keepcoverage (%ls) parameter must be between 0.0 and 1.0\n\n", pValue);
return 1;
}
break;
@ -1655,6 +1672,7 @@ int __cdecl wmain(_In_ int argc, _In_z_count_(argc) wchar_t* argv[])
// Convert images
bool nonpow2warn = false;
bool non4bc = false;
bool preserveAlphaCoverage = false;
ComPtr<ID3D11Device> pDevice;
for (auto pConv = conversion.begin(); pConv != conversion.end(); ++pConv)
@ -2526,6 +2544,12 @@ int __cdecl wmain(_In_ int argc, _In_z_count_(argc) wchar_t* argv[])
cimage.reset();
}
// --- Determine whether preserve alpha coverage is required (if requested) ----
if (preserveAlphaCoverageRef > 0.0f && HasAlpha(info.format) && !image->IsAlphaAllOpaque())
{
preserveAlphaCoverage = true;
}
// --- Generate mips -----------------------------------------------------------
DWORD dwFilter3D = dwFilter;
if (!ispow2(info.width) || !ispow2(info.height) || !ispow2(info.depth))
@ -2542,9 +2566,11 @@ int __cdecl wmain(_In_ int argc, _In_z_count_(argc) wchar_t* argv[])
}
}
if ((!tMips || info.mipLevels != tMips) && (info.mipLevels != 1))
if ((!tMips || info.mipLevels != tMips || preserveAlphaCoverage) && (info.mipLevels != 1))
{
// Mips generation only works on a single base image, so strip off existing mip levels
// Also required for preserve alpha coverage so that existing mips are regenerated
std::unique_ptr<ScratchImage> timage(new (std::nothrow) ScratchImage);
if (!timage)
{
@ -2670,6 +2696,59 @@ int __cdecl wmain(_In_ int argc, _In_z_count_(argc) wchar_t* argv[])
cimage.reset();
}
// --- Preserve mipmap alpha coverage (if requested) ---------------------------
if (preserveAlphaCoverage && info.mipLevels != 1)
{
std::unique_ptr<ScratchImage> timage(new (std::nothrow) ScratchImage);
if (!timage)
{
wprintf(L"\nERROR: Memory allocation failed\n");
return 1;
}
hr = timage->Initialize(image->GetMetadata());
if (FAILED(hr))
{
wprintf(L" FAILED [keepcoverage] (%x)\n", hr);
return 1;
}
const size_t items = image->GetMetadata().arraySize;
for (size_t item = 0; item < items; ++item)
{
auto img = image->GetImage(0, item, 0);
assert(img);
hr = CopyRectangle(*img, Rect(0, 0, info.width, info.height), *timage->GetImage(0, item, 0), TEX_FILTER_DEFAULT, 0, 0);
if (FAILED(hr))
{
wprintf(L" FAILED [keepcoverage] (%x)\n", hr);
return 1;
}
hr = ScaleMipMapsAlphaForCoverage(img, info, item, preserveAlphaCoverageRef, *timage);
if (FAILED(hr))
{
wprintf(L" FAILED [keepcoverage] (%x)\n", hr);
return 1;
}
}
auto& tinfo = timage->GetMetadata();
tinfo;
assert(info.width == tinfo.width);
assert(info.height == tinfo.height);
assert(info.depth == tinfo.depth);
assert(info.arraySize == tinfo.arraySize);
assert(info.mipLevels == tinfo.mipLevels);
assert(info.miscFlags == tinfo.miscFlags);
assert(info.dimension == tinfo.dimension);
image.swap(timage);
cimage.reset();
}
// --- Premultiplied alpha (if requested) --------------------------------------
if ((dwOptions & (DWORD64(1) << OPT_PREMUL_ALPHA))
&& HasAlpha(info.format)