在 Vulkan 中使用 Basis Universal 超级压缩 GPU 纹理编解码器

此示例的源代码可以在 Khronos Vulkan 示例 github 存储库中找到。

概述

本教程以及随附的示例代码演示了如何在 Vulkan 应用程序中使用 Basis universal 超级压缩 GPU 纹理。

KTX2 格式

KTX 是一种 GPU 纹理容器格式,用于存储不同的纹理类型(2D、立方体贴图等)和纹理格式(未压缩和已压缩)。2.0 版本增加了对 Basis Universal 超级压缩纹理的支持。

Basis Universal

Basis Universal 是一种超级压缩 GPU 纹理数据交换系统,它实现了 UASTC 和 ETC1S 压缩格式,作为传输格式。两者都可以快速转码为各种 GPU 本机压缩和未压缩格式,如 RGB/RGBA、PVRTC1、BCn、ETC1、ETC2 等。这意味着与存储 BC3 纹理的 KTX 2.0 文件不同,数据需要在运行时进行转码。

KTX and BasisU

用于加载 KTX 2.0 文件的库

此示例(以及存储库)使用来自 官方 Khronos KTX 软件存储库libktx 库来加载和转码 Basis Universal 压缩的 KTX 2.0 纹理。它通过此 CMakeLists.txt 包含在框架中,并添加了 Basis Universal 转码器

# libktx
set(KTX_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ktx)

set(KTX_SOURCES
    ...

    # Basis Universal
    ${KTX_DIR}/lib/basis_sgd.h
    ${KTX_DIR}/lib/basis_transcode.cpp
    ${KTX_DIR}/lib/basisu/transcoder/basisu_containers.h
    ${KTX_DIR}/lib/basisu/transcoder/basisu_containers_impl.h
    ${KTX_DIR}/lib/basisu/transcoder/basisu_file_headers.h
    ${KTX_DIR}/lib/basisu/transcoder/basisu_global_selector_cb.h
    ${KTX_DIR}/lib/basisu/transcoder/basisu_global_selector_palette.h
    ${KTX_DIR}/lib/basisu/transcoder/basisu_transcoder_internal.h
    ${KTX_DIR}/lib/basisu/transcoder/basisu_transcoder_uastc.h
    ${KTX_DIR}/lib/basisu/transcoder/basisu_transcoder.cpp
    ${KTX_DIR}/lib/basisu/transcoder/basisu_transcoder.h
    ${KTX_DIR}/lib/basisu/transcoder/basisu.h
    ${KTX_DIR}/lib/basisu/zstd/zstd.c

    ...
    # KTX2
    ${KTX_DIR}/lib/texture2.c
    ${KTX_DIR}/lib/texture2.h

通过 CMake 包含 libktx 的替代方法是使用 KTX-Software 存储库中提供的预构建二进制文件。

对于只想使用 KTX 2.0 和 Basis Universal 纹理压缩的项目,libktx 的轻量级替代方案是 basisu basist::ktx2_transcoder。有关如何使用它的更多信息可以在 Binomial 的文档中找到,如何配置和使用转码器

选择 GPU 本机目标格式

如上所述,本示例中使用的 KTX 2.0 文件以 Basis Universal ETC1S 和 UASTC 传输格式存储纹理数据,GPU 无法直接使用这些格式。

因此,在将数据转码为本机 GPU 格式之前,我们需要选择一个有效的本机 GPU 目标格式。在本示例中,我们使用一个简单的机制,仅基于 GPU 支持的 Vulkan 格式

void TextureCompressionBasisu::get_available_target_formats()
{
	available_target_formats.clear();

	VkPhysicalDeviceFeatures device_features = get_device().get_gpu().get_features();

	// Block compression
	if (device_features.textureCompressionBC)
	{
		// BC7 is the preferred block compression if available
		if (format_supported(VK_FORMAT_BC7_SRGB_BLOCK))
		{
            // Target formats from the KTX library, and prefixed with KTX_
			available_target_formats.push_back(KTX_TTF_BC7_RGBA);
		}

        ...
	}

	// Adaptive scalable texture compression
	if (device_features.textureCompressionASTC_LDR)
	{
        ...
    }

	// Ericsson texture compression
	if (device_features.textureCompressionETC2)
	{
        ...
    }

	// PowerVR texture compression support needs to be checked via an extension
	if (get_device().is_extension_supported(VK_IMG_FORMAT_PVRTC_EXTENSION_NAME))
	{
        ...
    }

	// Always add uncompressed RGBA as a valid target
	available_target_formats.push_back(KTX_TTF_RGBA32);
	available_target_formats_names.push_back("KTX_TTF_RGBA32");
}

这为我们提供了一个 Basis Universal 转码器的可能目标格式列表(前缀为 KTX_),我们稍后可以从 ETC1S 和 UASTC 传输格式转码到这些格式。

在实际应用程序中,转码目标格式的选择很可能会更加复杂。有关如何选择目标格式的良好参考可以在 KTX 2.0 / Basis Universal 纹理 — 开发者指南 中找到。

加载 KTX 2.0 文件

加载和转码 KTX 2.0 纹理图像文件由 libktx 处理,并在 TextureCompressionBasisu::transcode_texture 函数内部完成。

从磁盘加载文件

加载 KTX 2.0 文件与加载 KTX1.0 文件相同,但我们使用 ktxTexture2 类,在某些函数调用中需要将其转换为 ktxTexture

// We are working with KTX 2.0 files, so we need to use the ktxTexture2 class
ktxTexture2 *ktx_texture;
// Load the KTX 2.0 file into memory. This is agnostic to the KTX version, so we cast the ktxTexture2 down to ktxTexture
KTX_error_code result = ktxTexture_CreateFromNamedFile(file_name.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, (ktxTexture **) &ktx_texture);
if (result != KTX_SUCCESS)
{
    throw std::runtime_error("Could not load the requested image file.");
}

转码为原生格式

一旦我们成功从磁盘加载了文件,我们就可以将其从 ETCS1/UASTC 转码为我们之前创建的列表中所需的目标格式。

我们首先通过 ktxTexture2_NeedsTranscoding 检查源 KTX 2.0 文件是否真的需要转码。对于此示例中使用的所有 KTX 2.0 纹理文件来说,情况总是如此,但如果文件例如已经包含像 BCn 这样的原生格式,那么我们就不必对其进行转码。

如果文件需要转码,我们然后通过 libktx 调用 Basis Universal 转码器,使用 ktxTexture2_TranscodeBasis。这会将纹理数据转码为 GPU 原生目标格式。

if (ktxTexture2_NeedsTranscoding(ktx_texture))
{
    result = ktxTexture2_TranscodeBasis(ktx_texture, target_format, 0);
    if (result != KTX_SUCCESS)
    {
        throw std::runtime_error("Could not transcode the input texture to the selected target format.");
    }
}

例如,如果我们为 UASTC 压缩文件选择 KTX_TTF_BC7_RGBA 作为转码目标格式,这会将 UASTC 纹理数据转码为 GPU 原生 BC7 数据。

上传纹理数据

一旦转码完成,ktxTexture 对象包含原生 GPU 格式的纹理数据(例如上面示例中的 BC7),然后可以直接上传到支持 BC7 纹理压缩的 GPU。从这一点开始,它就像使用常规纹理一样。我们然后可以使用 ktxTexture 对象中的原生 Vulkan 格式来创建 Vulkan 图像。

VkFormat format = (VkFormat)ktx_texture->vkFormat;

// Create a buffer to store the transcoded ktx texture data for staging to the GPU
VkBufferCreateInfo buffer_create_info = vkb::initializers::buffer_create_info();
buffer_create_info.size               = ktx_texture->dataSize;

...

// Copy the ktx texture into the host local buffer
uint8_t *data;
vkMapMemory(get_device().get_handle(), staging_memory, 0, memory_requirements.size, 0, (void **) &data);
memcpy(data, ktx_image_data, ktx_texture->dataSize);
vkUnmapMemory(get_device().get_handle(), staging_memory);

// Setup buffer copy regions for each mip level
std::vector<VkBufferImageCopy> buffer_copy_regions;
for (uint32_t mip_level = 0; mip_level < texture.mip_levels; mip_level++)
{
    ktx_size_t        offset;
    KTX_error_code    result                           = ktxTexture_GetImageOffset((ktxTexture *) ktx_texture, mip_level, 0, 0, &offset);
    VkBufferImageCopy buffer_copy_region               = {};
    buffer_copy_region.imageSubresource.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
    buffer_copy_region.imageSubresource.mipLevel       = mip_level;
    buffer_copy_region.imageSubresource.baseArrayLayer = 0;
    buffer_copy_region.imageSubresource.layerCount     = 1;
    buffer_copy_region.imageExtent.width               = ktx_texture->baseWidth >> mip_level;
    buffer_copy_region.imageExtent.height              = ktx_texture->baseHeight >> mip_level;
    buffer_copy_region.imageExtent.depth               = 1;
    buffer_copy_region.bufferOffset                    = offset;
    buffer_copy_regions.push_back(buffer_copy_region);
}

...

VkImageCreateInfo image_create_info = vkb::initializers::image_create_info();
image_create_info.imageType         = VK_IMAGE_TYPE_2D;
image_create_info.format            = format;
image_create_info.mipLevels         = texture.mip_levels;
image_create_info.arrayLayers       = 1;
image_create_info.samples           = VK_SAMPLE_COUNT_1_BIT;
image_create_info.tiling            = VK_IMAGE_TILING_OPTIMAL;
image_create_info.sharingMode       = VK_SHARING_MODE_EXCLUSIVE;
image_create_info.initialLayout     = VK_IMAGE_LAYOUT_UNDEFINED;
image_create_info.extent            = {texture.width, texture.height, 1};
image_create_info.usage             = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
vkCreateImage(get_device().get_handle(), &image_create_info, nullptr, &texture.image);

...

// Upload data to the Vulkan image using a command buffer

VkCommandBuffer copy_command = device->create_command_buffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
...
vkCmdCopyBufferToImage(
    copy_command,
    staging_buffer,
    texture.image,
    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
    static_cast<uint32_t>(buffer_copy_regions.size()),
    buffer_copy_regions.data());
...
device->flush_command_buffer(copy_command, queue, true);

示例

Sample image

该示例允许在运行时将一组固定的 ETC1S/UASTC 转码为支持的原生 GPU 目标格式。可能的目标列表取决于设备的性能。您还可以放大和旋转图像以查看不同输入和目标格式组合的效果。

在调试版本中,转码速度会大幅下降。为了获得最佳性能,建议运行发布版本。