生成细化贴图

我们的纹理图像现在有了多个细化级别,但我们目前还只有0级细化的图像数据(也就是原始图像数据),我们需要使用原始图像数据来生成其它细化级别的图像数据。这可以通过使用vkCmdBlitImage指令来完成。vkCmdBlitImage指令可以进行复制,缩放和过滤图像的操作。我们会多次调用这一指令来生成多个不同细化级别的纹理图像数据。

vkCmdBlitImage指令执行的是传输操作,使用这一指令,需要我们为纹理图像添加作为数据来源和接收目的使用标记。我们在createTextureImage函数中为创建的纹理图像添加这些使用标记:

...
createImage(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_UNORM, 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允许我们独立地对一张图像的不同细化级别进行布局变换。而每个传输操作一次只会对两个细化级别进行处理,所以,我们可以在使用传输指令前,将使用的两个细化级别的图像布局转换到最佳布局。

transitionImageLayout函数会对整个图像进行布局变换,需要我们编写管线屏障指令。将createTextureImage函数图像布局变换到VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL布局的代码:

...
    transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, 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布局。在传输指令读取完成后,每个细化级别会被变换到VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL布局。

接着,我们编写用于生成原始纹理图像不同细化级别的generateMipmaps函数:

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对象对多次图像布局变换进行同步。上面代码中对于barrier的设置,只需设置一次,无需修改。subresourceRange.miplevel、oldLayout、srcAccessMask和dstAccessMask这几个barrier的成员变量则需要在每次变换前根据需要进行修改。

int32_t mipWidth = texWidth;
int32_t mipHeight = texHeight;

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

}

我们使用循环来遍历所有细化级别,记录每个细化级别使用的VkCmdBlitImage指令到指令缓冲中。需要注意,这里的循环变量i是从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布局。这一变换会在细化级别为i-1的纹理图像数据被写入后(上一次的传输指令写入或vkCmdCopyBufferToImage指令写入)进行。当前的传输指令会等待这一变换结束才会执行。

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;

接着,我们指定传输操作使用的纹理图像范围。这里我们将srcSubresource.mipLevel成员变量设置为i-1,也就是上一细化级别的纹理图像。将dstSubresource.mipLevel成员变量设置为i,也就是我们要生成的纹理图像的细化级别。srcOffsets数组用于指定要传输的数据所在的三维图像区域。dstOffsets数组用于指定接收数据的三维图像区域。dstOffsets[1]的X分量和Y分量的值需要设置为上一细化级别纹理图像的一半。由于这里我们使用的是二维图像,二维图像的深度值都为1,所以srcOffsets[1]和dstOffsets[1]的Z分量都必须设置为1。

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

现在,我们可以开始记录传输指令到指令缓冲。可以注意到我们将textureImage变量同时作为vkCmdBlitImage指令的源图像和目的图像。 这是因为我们的传输操作是在同一纹理对象的不同细化级别间进行的。传输指令开始执行时源细化级别图像布局刚刚被变换为VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,目的细化级别图像布局仍处于创建纹理时设置的VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL布局。

vkCmdBlitImage指令的最后一个参数用于指定传输操作使用的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);

上面代码,我们设置屏障将细化级别为i-1的图像布局变换到VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL。这一变换会等待当前的传输指令结束会才会进行。所有采样操作需要等待这一变换结束才能进行。

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

在遍历细化级别的循环结尾处,我们将mipWidth变量和mipHeight变量的值除以2,计算出下一次循环要使用的细化级别的图像大小。这里的代码,可以处理图像的长宽不同的情况。当图像的长或宽为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);
}

在我们开始执行指令缓冲前,我们需要插入一个管线障碍用于将最后一个细化级别的图像布局从VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL变换为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL。这样做是因为最后一个细化级别的图像不会被作为传输指令的数据来源,所以就不会将布局变换为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,需要我们手动进行变换。

最后,在createTextureImage函数中添加对generateMipmaps函数的调用:

transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, 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);

至此,我们就生成了所有细化级别的纹理图像。