赞
踩
目录
我们的程序现在可以加载和渲染3D模型。在本章中,我们将添加另一个功能,mipmap生成。Mipmap在游戏和渲染软件中广泛使用,Vulkan让我们完全控制它们的创建方式。
Mipmap是预先计算的图像的缩小版本。每个新图像的宽度和高度都是前一个图像的一半。Mipmap用作详细等级或LOD的一种形式。远离相机的对象将从较小的mip图像中采样其纹理。使用较小的图像可以提高渲染速度,并避免诸如Moiré图案之类的伪影。mipmaps外观示例:
在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);
我们的纹理图像现在有多个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现在已完全填充。
使用像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级别是不常见的。通常,它们是预先生成的,并存储在基本级别旁边的纹理文件中,以提高加载速度。在软件中实现大小调整和从文件中加载多个级别是留给读者的练习。
当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级别的方式。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。