Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basis plugins: support UASTC HDR #145

Closed
wants to merge 10 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ rdo_uastc_max_allowed_rms_increase_ratio=10.0
rdo_uastc_skip_block_rms_threshold=8.0
rdo_uastc_favor_simpler_modes_in_rdo_mode=true

# HDR options
# Quality level from 0 (fastest) to 4 (highest quality)
hdr_quality_level=1
hdr_favor_astc=false

# KTX2 options
ktx2_uastc_supercompression=true
ktx2_zstd_supercompression_level=6
Expand Down
170 changes: 114 additions & 56 deletions src/MagnumPlugins/BasisImageConverter/BasisImageConverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,90 @@ using namespace Containers::Literals;

namespace {

template<typename T> void copySlice(const ImageView3D& image3D, UnsignedInt slice, Containers::StridedArrayView2D<Math::Color4<T>>& dst, bool yFlip, bool hasCustomSwizzle) {
const UnsignedInt channelCount = pixelFormatChannelCount(image3D.format());

if(yFlip)
dst = dst.template flipped<0>();

/* basis image is always RGBA, fill in alpha if necessary */
if(channelCount == 4) {
const auto src = image3D.pixels<Math::Vector<4, T>>()[slice];
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = src[y][x];

} else if(channelCount == 3) {
const auto src = image3D.pixels<Math::Vector<3, T>>()[slice];
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = Math::Vector3<T>{src[y][x]}; /* Alpha implicitly opaque */

} else if(channelCount == 2) {
const auto src = image3D.pixels<Math::Vector<2, T>>()[slice];
/* If the user didn't specify a custom swizzle, assume they want
the two channels compressed in separate slices, R in RGB and G
in Alpha. This significantly improves quality. */
if(!hasCustomSwizzle)
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = Math::gather<'r', 'r', 'r', 'g'>(src[y][x]);
else
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = Math::Vector3<T>::pad(src[y][x]); /* Alpha implicitly opaque */

} else if(channelCount == 1) {
const auto src = image3D.pixels<Math::Vector<1, T>>()[slice];
/* If the user didn't specify a custom swizzle, assume they want
a gray-scale image. Alpha is always implicitly opaque. */
if(!hasCustomSwizzle)
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = Math::gather<'r', 'r', 'r'>(src[y][x]);
else
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = Math::Vector3<T>::pad(src[y][x]); /* Alpha implicitly opaque */

} else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this reminds me you requested the batch swizzle. On my list :)

}

template<UnsignedInt dimensions> Containers::Optional<Containers::Array<char>> convertLevelsToData(Containers::ArrayView<const BasicImageView<dimensions>> imageLevels, const Utility::ConfigurationGroup& configuration, ImageConverterFlags flags, BasisImageConverter::Format fileFormat) {
/* Check input */
const PixelFormat pixelFormat = imageLevels.front().format();
bool isSrgb;
#if BASISU_LIB_VERSION >= 150
bool isHdr;
#endif
switch(pixelFormat) {
case PixelFormat::RGBA8Unorm:
case PixelFormat::RGB8Unorm:
case PixelFormat::RG8Unorm:
case PixelFormat::R8Unorm:
isSrgb = false;
pezcode marked this conversation as resolved.
Show resolved Hide resolved
#if BASISU_LIB_VERSION >= 150
isHdr = false;
#endif
break;
case PixelFormat::RGBA8Srgb:
case PixelFormat::RGB8Srgb:
case PixelFormat::RG8Srgb:
case PixelFormat::R8Srgb:
isSrgb = true;
#if BASISU_LIB_VERSION >= 150
isHdr = false;
#endif
break;
#if BASISU_LIB_VERSION >= 150
case PixelFormat::R32F:
case PixelFormat::RG32F:
case PixelFormat::RGB32F:
case PixelFormat::RGBA32F:
isSrgb = false;
isHdr = true;
break;
#endif
pezcode marked this conversation as resolved.
Show resolved Hide resolved
default:
Error{} << "Trade::BasisImageConverter::convertToData(): unsupported format" << pixelFormat;
return {};
Expand Down Expand Up @@ -260,6 +327,15 @@ template<UnsignedInt dimensions> Containers::Optional<Containers::Array<char>> c
}
#endif

/* HDR */
#if BASISU_LIB_VERSION >= 150
params.m_hdr = isHdr;

params.m_uastc_hdr_options.init();
params.m_uastc_hdr_options.set_quality_level(configuration.value<int>("hdr_quality_level"));
PARAM_CONFIG(hdr_favor_astc, bool);
#endif

/* Set various fields in the Basis file header */
PARAM_CONFIG(userdata0, int);
PARAM_CONFIG(userdata1, int);
Expand All @@ -279,17 +355,29 @@ template<UnsignedInt dimensions> Containers::Optional<Containers::Array<char>> c
#else
params.m_write_output_basis_files = false;
#endif

/* One image per slice. The base mip is in m_source_images, mip 1 and
higher go into m_source_mipmap_images. */
const UnsignedInt numImages = Vector3i::pad(baseSize, 1).z();
params.m_source_images.resize(numImages);
if(numMipmaps > 1) {
params.m_source_mipmap_images.resize(numImages);
for(auto& slice: params.m_source_mipmap_images)
slice.resize(numMipmaps - 1);
#if BASISU_LIB_VERSION >= 150
if(isHdr) {
params.m_source_images_hdr.resize(numImages);
if(numMipmaps > 1) {
params.m_source_mipmap_images_hdr.resize(numImages);
for(auto& slice: params.m_source_mipmap_images_hdr)
slice.resize(numMipmaps - 1);
}
} else
#endif
{
params.m_source_images.resize(numImages);
if(numMipmaps > 1) {
params.m_source_mipmap_images.resize(numImages);
for(auto& slice: params.m_source_mipmap_images)
slice.resize(numMipmaps - 1);
}
}

const UnsignedInt channelCount = pixelFormatChannelCount(pixelFormat);
for(UnsignedInt level = 0; level != numMipmaps; ++level) {
const auto mipSize = Math::max(baseSize >> level, 1)*mipMask + baseSize*(Math::Vector<dimensions, Int>{1} - mipMask);
const auto& image = imageLevels[level];
Expand All @@ -306,57 +394,27 @@ template<UnsignedInt dimensions> Containers::Optional<Containers::Array<char>> c
/* Copy image data into the basis image. There is no way to construct a
basis image from existing data as it is based on basisu::vector,
moreover we need to tightly pack it and flip Y. */
basisu::image& basisImage = level > 0 ? params.m_source_mipmap_images[slice][level - 1] : params.m_source_images[slice];
basisImage.resize(mipSize[0], mipSize[1]);
auto dst = Containers::arrayCast<Color4ub>(Containers::StridedArrayView2D<basisu::color_rgba>({basisImage.get_ptr(), basisImage.get_total_pixels()}, {std::size_t(mipSize[1]), std::size_t(mipSize[0])}));
/* Y-flip the view to make the following loops simpler. basisu doesn't
apply m_y_flip to user-supplied mipmaps, so only do this for the
base image:
/* basisu doesn't apply m_y_flip to user-supplied mipmaps, so only
do this for the base image:
https://github.com/BinomialLLC/basis_universal/issues/257 */
if(!params.m_y_flip || level == 0)
dst = dst.flipped<0>();

/* basis image is always RGBA, fill in alpha if necessary */
if(channelCount == 4) {
const auto src = image3D.pixels<Math::Vector<4, UnsignedByte>>()[slice];
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = src[y][x];

} else if(channelCount == 3) {
const auto src = image3D.pixels<Math::Vector<3, UnsignedByte>>()[slice];
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = Vector3ub{src[y][x]}; /* Alpha implicitly 255 */

} else if(channelCount == 2) {
const auto src = image3D.pixels<Math::Vector<2, UnsignedByte>>()[slice];
/* If the user didn't specify a custom swizzle, assume they want
the two channels compressed in separate slices, R in RGB and G
in Alpha. This significantly improves quality. */
if(!swizzle)
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = Math::gather<'r', 'r', 'r', 'g'>(src[y][x]);
else
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = Vector3ub::pad(src[y][x]); /* Alpha implicitly 255 */

} else if(channelCount == 1) {
const auto src = image3D.pixels<Math::Vector<1, UnsignedByte>>()[slice];
/* If the user didn't specify a custom swizzle, assume they want
a gray-scale image. Alpha is always implicitly 255. */
if(!swizzle)
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = Math::gather<'r', 'r', 'r'>(src[y][x]);
else
for(std::size_t y = 0; y != src.size()[0]; ++y)
for(std::size_t x = 0; x != src.size()[1]; ++x)
dst[y][x] = Vector3ub::pad(src[y][x]);

} else CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */
const bool yFlip = !params.m_y_flip || level == 0;
const bool hasCustomSwizzle = !swizzle.isEmpty();
#if BASISU_LIB_VERSION >= 150
if(isHdr) {
basisu::imagef& basisImage = level > 0 ? params.m_source_mipmap_images_hdr[slice][level - 1] : params.m_source_images_hdr[slice];
basisImage.resize(mipSize[0], mipSize[1]);
auto dst = Containers::arrayCast<Color4>(Containers::StridedArrayView2D<basisu::vec4F>({basisImage.get_ptr(), basisImage.get_total_pixels()}, {std::size_t(mipSize[1]), std::size_t(mipSize[0])}));

copySlice(image3D, slice, dst, yFlip, hasCustomSwizzle);
} else
#endif
{
basisu::image& basisImage = level > 0 ? params.m_source_mipmap_images[slice][level - 1] : params.m_source_images[slice];
basisImage.resize(mipSize[0], mipSize[1]);
auto dst = Containers::arrayCast<Color4ub>(Containers::StridedArrayView2D<basisu::color_rgba>({basisImage.get_ptr(), basisImage.get_total_pixels()}, {std::size_t(mipSize[1]), std::size_t(mipSize[0])}));

copySlice(image3D, slice, dst, yFlip, hasCustomSwizzle);
}
}
}

Expand Down
23 changes: 16 additions & 7 deletions src/MagnumPlugins/BasisImageConverter/BasisImageConverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,16 @@ Accepts 2D, 2D array, cube map and cube map array images, recognizing
The @ref PixelFormat::R8Unorm, @relativeref{PixelFormat,R8Srgb},
@relativeref{PixelFormat,RG8Unorm}, @relativeref{PixelFormat,RG8Srgb},
@relativeref{PixelFormat,RGB8Unorm}, @relativeref{PixelFormat,RGB8Srgb},
@relativeref{PixelFormat,RGBA8Unorm} and @relativeref{PixelFormat,RGBA8Srgb}
@relativeref{PixelFormat,RGBA8Unorm}, @relativeref{PixelFormat,RGBA8Srgb},
@relativeref{PixelFormat,R32F} and @relativeref{PixelFormat,RG32F},
@relativeref{PixelFormat,RGB32F} and @relativeref{PixelFormat,RGBA32F}
formats are supported.

HDR floating point formats require at least version 1.50 of Basis.

The alpha channel of @relativeref{PixelFormat,RGBA32F} will be dropped and
transcoded to 1 because Basis UASTC HDR doesn't support alpha channels.

Even though the KTX container format supports 1D, 1D array and 3D images, Basis
Universal doesn't. In particular, if a 2D image with @ref ImageFlag2D::Array is
passed, the conversion will fail as it's not possible to represent 1D array
Expand Down Expand Up @@ -184,12 +191,14 @@ If no user-specified channel mapping is supplied through the
@cb{.ini} swizzle @ce @ref Trade-BasisImageConverter-configuration "configuration option",
the converter swizzles 1- and 2-channel formats before compression as follows:

- 1-channel formats (@ref PixelFormat::R8Unorm / @ref PixelFormat::R8Srgb)
are remapped as RRR, producing an opaque gray-scale image
- 2-channel formats (@ref PixelFormat::RG8Unorm / @ref PixelFormat::RG8Srgb)
are remapped as RRRG, ie. G becomes the alpha channel. This significantly
improves compressed image quality because RGB and alpha get separate slices
instead of the two channels being compressed into a single slice.
- 1-channel formats (@ref PixelFormat::R8Unorm / @ref PixelFormat::R8Srgb /
@ref PixelFormat::R32F) are remapped as RRR, producing an opaque gray-scale
image
- 2-channel formats (@ref PixelFormat::RG8Unorm / @ref PixelFormat::RG8Srgb /
@ref PixelFormat::RG32F) are remapped as RRRG, ie. G becomes the alpha
channel. This significantly improves compressed image quality because RGB
and alpha get separate slices instead of the two channels being compressed
into a single slice.

Setting the @cb{.ini} swizzle @ce option to any value disables this behavior.
To keep the original channel order, set @cb{.ini} swizzle=rgba @ce.
Expand Down
Loading