diff --git a/DirectXTex/DirectXTex.h b/DirectXTex/DirectXTex.h index 9e36d04..bb4381c 100644 --- a/DirectXTex/DirectXTex.h +++ b/DirectXTex/DirectXTex.h @@ -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, diff --git a/DirectXTex/DirectXTexMipmaps.cpp b/DirectXTex/DirectXTexMipmaps.cpp index 7cda4bf..6fa27c3 100644 --- a/DirectXTex/DirectXTexMipmaps.cpp +++ b/DirectXTex/DirectXTexMipmaps.cpp @@ -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(_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(_aligned_malloc((sizeof(XMVECTOR)*srcImage.width), 16))); + if (!row0) + { + return E_OUTOFMEMORY; + } + + ScopedAlignedArrayXMVECTOR row1(reinterpret_cast(_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(coverageCount) / static_cast((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; +} \ No newline at end of file diff --git a/Texconv/texconv.cpp b/Texconv/texconv.cpp index cec34fd..a2a6ede 100644 --- a/Texconv/texconv.cpp +++ b/Texconv/texconv.cpp @@ -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 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 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 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 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 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 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)