在 Vulkan 中使用 Basis Universal 超级压缩 GPU 纹理编解码器
此示例的源代码可以在 Khronos Vulkan 示例 github 存储库中找到。 |
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 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);