Generating Mipmaps

目录

Introduction

Image creation

Generating Mipmaps

Linear filtering support

Sampler


Introduction

我们的程序现在可以加载和渲染3D模型。在本章中,我们将添加另一个功能,mipmap生成。Mipmap在游戏和渲染软件中广泛使用,Vulkan让我们完全控制它们的创建方式。

Mipmap是预先计算的图像的缩小版本。每个新图像的宽度和高度都是前一个图像的一半。Mipmap用作详细等级或LOD的一种形式。远离相机的对象将从较小的mip图像中采样其纹理。使用较小的图像可以提高渲染速度,并避免诸如Moiré图案之类的伪影。mipmaps外观示例:

Image creation

在Vulkan中,每个mip图像存储在VkImage的不同mip级别中。Mip级别0是原始图像,级别0之后的Mip级别通常称为Mip链。

创建VkImage时指定mip级别数。到目前为止,我们始终将此值设置为1。我们需要从图像的维度计算mip级别的数量。首先,添加一个类成员以存储此编号:

...
uint32_t mipLevels;
VkImage textureImage;
...

在createTextureImage中加载纹理后,可以找到mipLevels的值:

int texWidth, texHeight, texChannels;
stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
...
mipLevels = static_cast<uint32_t>(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1;

这将计算mip链中的级别数。max函数选择最大尺寸。log2函数计算该维度可以被2除的次数。floor函数处理最大尺寸不是2的幂的情况。1,使得原始图像具有mip级别。

要使用此值,我们需要更改createImage、createImageView和transitionImageLayout函数,以允许我们指定mip级别的数量。向函数添加mipLevels参数:

void createImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) {
    ...
    imageInfo.mipLevels = mipLevels;
    ...
}
VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags, uint32_t mipLevels) {
    ...
    viewInfo.subresourceRange.levelCount = mipLevels;
    ...
void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout, uint32_t mipLevels) {
    ...
    barrier.subresourceRange.levelCount = mipLevels;
    ...

更新对这些函数的所有调用以使用正确的值:

createImage(swapChainExtent.width, swapChainExtent.height, 1, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);
...
createImage(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);
swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1);
...
depthImageView = createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT, 1);
...
textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT, mipLevels);
transitionImageLayout(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, 1);
...
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels);

Generating Mipmaps

我们的纹理图像现在有多个mip级别,但暂存缓冲区只能用于填充mip级别0。其他级别仍然未定义。为了填充这些级别,我们需要从现有的单个级别生成数据。我们将使用vkCmdBlitImage命令。此命令执行复制、缩放和过滤操作。我们将多次调用此函数,以将数据blit到纹理图像的每个级别。

vkCmdBlitImage被视为传输操作,因此我们必须通知Vulkan,我们打算将纹理图像用作传输的源和目标。将VK_IMAGE_USAGE_TRANSFER_SRC_BIT添加到createTextureImage中纹理图像的使用标志:

...
createImage(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);
...

与其他图像操作一样,vkCmdBlitImage取决于它所操作的图像的布局。我们可以将整个图像转换为VK_IMAGE_LAYOUT_GENERAL,但这很可能会很慢。为了获得最佳性能,源映像应位于VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL中,而目标映像应位于VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL中。Vulkan允许我们独立地转换图像的每个mip级别。每个blit一次只能处理两个mip级别,因此我们可以将每个级别转换为blits命令之间的最佳布局。

transitionImageLayout只对整个图像执行布局转换,因此我们需要再编写一些管道屏障命令。在createTextureImage中删除到VK_IMAGE_LAYOUT_SHADER_READER_READ_ONLY_OPTIMAL的现有转换:

...
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels);
    copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));
//transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL while generating mipmaps
...

这将使纹理图像的每个级别保留在VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL中。在完成blit命令读取后,每个级别将转换为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL。

现在我们将编写生成mipmaps的函数:

void generateMipmaps(VkImage image, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) {
    VkCommandBuffer commandBuffer = beginSingleTimeCommands();

    VkImageMemoryBarrier barrier{};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    barrier.image = image;
    barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    barrier.subresourceRange.baseArrayLayer = 0;
    barrier.subresourceRange.layerCount = 1;
    barrier.subresourceRange.levelCount = 1;

    endSingleTimeCommands(commandBuffer);
}

我们将进行几个转换,因此我们将重用这个VkImageMemoryBarrier。以上设置的字段对于所有障碍都将保持不变。subsourceRange.miplevel、oldLayout、newLayout、srcAccessMask和dstAccessMask将针对每个转换进行更改。

int32_t mipWidth = texWidth;
int32_t mipHeight = texHeight;

for (uint32_t i = 1; i < mipLevels; i++) {

}

此循环将记录每个VkCmdBlitImage命令。请注意,循环变量从1开始,而不是0。

barrier.subresourceRange.baseMipLevel = i - 1;
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;

vkCmdPipelineBarrier(commandBuffer,
    VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
    0, nullptr,
    0, nullptr,
    1, &barrier);

首先,我们将级别i-1转换为VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL。此转换将等待从上一个blit命令或从vkCmdCopyBufferToImage填充级别i-1。当前blit命令将等待此转换。

VkImageBlit blit{};
blit.srcOffsets[0] = { 0, 0, 0 };
blit.srcOffsets[1] = { mipWidth, mipHeight, 1 };
blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blit.srcSubresource.mipLevel = i - 1;
blit.srcSubresource.baseArrayLayer = 0;
blit.srcSubresource.layerCount = 1;
blit.dstOffsets[0] = { 0, 0, 0 };
blit.dstOffsets[1] = { mipWidth > 1 ? mipWidth / 2 : 1, mipHeight > 1 ? mipHeight / 2 : 1, 1 };
blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
blit.dstSubresource.mipLevel = i;
blit.dstSubresource.baseArrayLayer = 0;
blit.dstSubresource.layerCount = 1;

接下来,我们指定将在blit操作中使用的区域。源mip级别是i-1,目标mip级别为i。srcOffsets数组的两个元素决定了数据将从哪个3D区域进行数据块传输。dstOffsets决定数据将被分块传输到的区域。dstOffsets[1]的X和Y维度除以2,因为每个mip级别的大小是前一级别的一半。srcOffsets[1]和dstOffsets[1]的Z维度必须为1,因为2D图像的深度为1。

vkCmdBlitImage(commandBuffer,
    image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
    image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
    1, &blit,
    VK_FILTER_LINEAR);

现在,我们记录blit命令。注意textureImage用于srcImage和dstImage参数。这是因为我们在同一图像的不同级别之间进行闪电扫描。源mip级别刚刚转换为VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,目标级别仍在createTextureImage中的VK_IMACE_LAYOUT_TRANSFER_DST_OPTIMA中。

如果您使用的是专用传输队列(如Vertex缓冲区中所建议的),请注意:vkCmdBlitImage必须提交到具有图形功能的队列。

最后一个参数允许我们指定要在blit中使用的VkFilter。我们这里有与制作VkSampler时相同的过滤选项。我们使用VK_FILTER_LINEAR来启用插值。

barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

vkCmdPipelineBarrier(commandBuffer,
    VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
    0, nullptr,
    0, nullptr,
    1, &barrier);

此屏障将mip级别i-1转换为VK_IMAGE_LAYOUT_SHADER_READER_READ_ONLY_OPTIMAL。此转换等待当前blit命令完成。所有采样操作将等待此转换完成。

    ...
    if (mipWidth > 1) mipWidth /= 2;
    if (mipHeight > 1) mipHeight /= 2;
}

在循环结束时,我们将当前mip维度除以2。我们在分割之前检查每个维度,以确保维度永远不会变为0。这将处理图像不是正方形的情况,因为其中一个mip维度将在另一个维度之前达到1。发生这种情况时,该维度对于所有剩余级别都应保持为1。

    barrier.subresourceRange.baseMipLevel = mipLevels - 1;
    barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
    barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

    vkCmdPipelineBarrier(commandBuffer,
        VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
        0, nullptr,
        0, nullptr,
        1, &barrier);

    endSingleTimeCommands(commandBuffer);
}

在结束命令缓冲区之前,我们再插入一个管道屏障。此屏障将最后一个mip级别从VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL转换为VK_IMACE_LAYOUT_SHADER_READER_ONLY_OPTIMAL。这不是由循环处理的,因为上一个mip级别从未从中进行过blit。

最后,在createTextureImage中添加对generateMaps的调用:

transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels);
    copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));
//transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL while generating mipmaps
...
generateMipmaps(textureImage, texWidth, texHeight, mipLevels);

我们的纹理图像的mipmap现在已完全填充。

Linear filtering support

使用像vkCmdBlitImage这样的内置函数来生成所有mip级别非常方便,但遗憾的是,不能保证所有平台都支持它。它需要我们用来支持线性过滤的纹理图像格式,这可以通过vkGetPhysicalDeviceFormatProperties函数进行检查。我们将为此向generateIpmaps函数添加一个检查。

首先添加一个指定图像格式的附加参数:

void createTextureImage() {
    ...

    generateMipmaps(textureImage, VK_FORMAT_R8G8B8A8_SRGB, texWidth, texHeight, mipLevels);
}

void generateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) {

    ...
}

在generateMipmaps函数中,使用vkGetPhysicalDeviceFormatProperties请求纹理图像格式的特性:

void generateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) {

    // Check if image format supports linear blitting
    VkFormatProperties formatProperties;
    vkGetPhysicalDeviceFormatProperties(physicalDevice, imageFormat, &formatProperties);

    ...

VkFormatProperties结构有三个名为linearTilingFeatures、opticalTilingFeature和bufferFeatures的字段,每个字段都描述了格式的使用方式。我们创建了一个具有最佳平铺格式的纹理图像,因此我们需要检查optimalTilingFeatures。可以使用VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT:

if (!(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT)) {
    throw std::runtime_error("texture image format does not support linear blitting!");
}

在这种情况下有两种选择。您可以实现一个函数,该函数搜索普通纹理图像格式以查找支持线性blitting的格式,或者可以使用stb_image_resize这样的库在软件中实现mipmap生成。然后,可以按照加载原始图像的方式将每个mip级别加载到图像中。

应该注意的是,实际上在运行时生成mipmap级别是不常见的。通常,它们是预先生成的,并存储在基本级别旁边的纹理文件中,以提高加载速度。在软件中实现大小调整和从文件中加载多个级别是留给读者的练习。

Sampler

VkImage保存mipmap数据时,VkSampler控制渲染时如何读取该数据。Vulkan允许我们指定minLod、maxLod、mipLodBias和mipmapMode(“Lod”表示“细节级别”)。对纹理进行采样时,采样器根据以下伪代码选择mip级别:

lod = getLodLevelFromScreenSize(); //smaller when the object is close, may be negative
lod = clamp(lod + mipLodBias, minLod, maxLod);

level = clamp(floor(lod), 0, texture.mipLevels - 1);  //clamped to the number of mip levels in the texture

if (mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST) {
    color = sample(level);
} else {
    color = blend(sample(level), sample(level + 1));
}

如果samplerInfo.mipmapMode为VK_SAMPLER_IPMAP_MODE_NEAREST,lod将选择要采样的mip级别。如果mipmap模式为VK_SAMPLER_MIPMAP_MODE_LINEAR,则lod用于选择要采样的两个mip级别。对这些级别进行采样,并对结果进行线性混合。

示例操作也受lod的影响:

if (lod <= 0) {
    color = readTexture(uv, magFilter);
} else {
    color = readTexture(uv, minFilter);
}

如果对象靠近相机,magFilter将用作过滤器。如果对象距离摄影机更远,则使用minFilter。通常,lod是非负的,并且在关闭相机时仅为0。mipLodBias允许我们强制Vulkan使用比正常情况更低的lod和level。

要查看本章的结果,我们需要为textureSampler选择值。我们已经将minFilter和magFilter设置为使用VK_FILTER_LINEAR。我们只需要选择minLod、maxLod、mipLodBias和mipmapMode的值。

void createTextureSampler() {
    ...
    samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
    samplerInfo.minLod = 0.0f; // Optional
    samplerInfo.maxLod = static_cast<float>(mipLevels);
    samplerInfo.mipLodBias = 0.0f; // Optional
    ...
}

为了允许使用全部mip级别,我们将minLod设置为0.0f,将maxLod设置成mip级别的数量。我们没有理由更改lod值,因此我们将mipLodBias设置为0.0f。

现在运行程序,您应该看到以下内容:

这并不是一个戏剧性的区别,因为我们的场景很简单。如果你仔细观察,会有细微的差别。

最明显的区别是纸上的字迹。使用mipmaps,书写变得平滑。如果没有mipmap,文字会有粗糙的边缘和Moiré伪影的间隙。

您可以使用采样器设置来查看它们如何影响mipmapping。例如,通过更改minLod,可以强制采样器不使用最低mip级别:

samplerInfo.minLod = static_cast<float>(mipLevels / 2);

这些设置将生成此图像:

这是当对象远离摄影机时,使用更高mip级别的方式。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值