|
| 1 | +## Introduction |
| 2 | +Our program can now load and render 3D models. In this chapter, we will add one more feature, mipmap generation. Mipmaps are widely used in games and rendering software, and Vulkan gives us complete control over how they are created. |
| 3 | + |
| 4 | +Mipmaps are precalculated, downscaled versions of an image. Each new image is half the width and height of the previous one. The smaller images are used when an object is far away from the camera. Since the smaller images use less memory, they are faster to sample from. Since they are precalculated, using them avoids artifacts such as [Moiré patterns](https://en.wikipedia.org/wiki/Moir%C3%A9_pattern). An example of what mip maps look like: |
| 5 | + |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | + |
| 10 | +## Image creation |
| 11 | + |
| 12 | +In Vulkan, each of the mip images is stored in different *mip levels* of a `VkImage`. Mip level 0 is the original image, and each mip level after that is half the size of previous level. The number of mip levels is specified when the `VkImage` is created. Up until now, we have always set this value to one. In `createTextureImage`, we need to calculate the number of mip levels from the dimensions of the image. First, add a class member to store this number: |
| 13 | + |
| 14 | +```c++ |
| 15 | + ... |
| 16 | + uint32_t mipLevels; |
| 17 | + VkImage textureImage; |
| 18 | + ... |
| 19 | +``` |
| 20 | +The value for `mipLevels` can be found with a simple loop in `createTextureImage`: |
| 21 | + |
| 22 | +```c++ |
| 23 | + int texWidth, texHeight, texChannels; |
| 24 | + stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); |
| 25 | + ... |
| 26 | + mipLevels = 1; |
| 27 | + int32_t mipWidth = texWidth; |
| 28 | + int32_t mipHeight = texHeight; |
| 29 | + while (mipWidth > 1 && mipHeight > 1) { |
| 30 | + mipLevels++; |
| 31 | + mipWidth /= 2; |
| 32 | + mipHeight /= 2; |
| 33 | + } |
| 34 | +``` |
| 35 | +
|
| 36 | +The loop counts the number of mip levels by halving the dimensions of the current mip level, until either dimension is 1. To use this value, we need to change the `createImage` and `createImageView` functions to allow us to specify the number of mip levels. Add a `mipLevels` parameter to the functions: |
| 37 | +
|
| 38 | +```c++ |
| 39 | + void createImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) { |
| 40 | + ... |
| 41 | + imageInfo.mipLevels = mipLevels; |
| 42 | + ... |
| 43 | + } |
| 44 | +``` |
| 45 | +```c++ |
| 46 | + VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags, uint32_t mipLevels) { |
| 47 | + ... |
| 48 | + viewInfo.subresourceRange.levelCount = mipLevels; |
| 49 | + ... |
| 50 | +``` |
| 51 | +
|
| 52 | +Update all calls to these functions to use the right values: |
| 53 | +
|
| 54 | +```c++ |
| 55 | + 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); |
| 56 | + ... |
| 57 | + 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); |
| 58 | +``` |
| 59 | +```c++ |
| 60 | + swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1); |
| 61 | + ... |
| 62 | + depthImageView = createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT, 1); |
| 63 | + ... |
| 64 | + textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT, mipLevels); |
| 65 | +``` |
| 66 | + |
| 67 | + |
| 68 | + |
| 69 | +## Generating Mipmaps |
| 70 | + |
| 71 | +Our texture image now has multiple mip levels, but the staging buffer can only be used to fill mip level 0. The other levels are still undefined. To fill these levels we need to generate the data from the single level that we have. We will use `vkCmdBlitImage` command. This command performs copying, scaling, filtering operations. We will use this to *blit* data from each level of our texture image. |
| 72 | + |
| 73 | +Like other image operations, `vkCmdBlitImage` depends on the layout of the image it operates on. For optimal performance, the source image should be in `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL` and the destination image should be in `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL` while blitting data. Vulkan allows us to transition each mip level of an image independently. `transitionImageLayout` only performs layout transitions on the entire image, so first we need to remove the existing transition to `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL` in `createTextureImage`: |
| 74 | + |
| 75 | +```c++ |
| 76 | + ... |
| 77 | + 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); |
| 78 | + |
| 79 | + transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels); |
| 80 | + copyBufferToImage(stagingBuffer, textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight)); |
| 81 | + //transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL while generating mipmaps |
| 82 | + ... |
| 83 | +``` |
| 84 | +This will leave each level of the texture image in `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`. |
| 85 | +
|
| 86 | +We're now going to write the function generates the mip maps: |
| 87 | +
|
| 88 | +```c++ |
| 89 | + void generateMipmaps(int32_t texWidth, int32_t texHeight) { |
| 90 | + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); |
| 91 | + ... |
| 92 | + endSingleTimeCommands(commandBuffer); |
| 93 | + } |
| 94 | +``` |
| 95 | + |
| 96 | +Since we can't use `transitionImageLayout`, we need to record and submit another `VkCommandBuffer`. |
| 97 | + |
| 98 | + |
| 99 | +```c++ |
| 100 | + VkImageMemoryBarrier barrier = {}; |
| 101 | + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; |
| 102 | + barrier.image = textureImage; |
| 103 | + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; |
| 104 | + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; |
| 105 | + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; |
| 106 | + barrier.subresourceRange.baseArrayLayer = 0; |
| 107 | + barrier.subresourceRange.layerCount = 1; |
| 108 | + barrier.subresourceRange.levelCount = 1; |
| 109 | +``` |
| 110 | + |
| 111 | +We're going to insert several pipeline barriers, so we'll reuse this `VkImageMemoryBarrier` for each transition. |
| 112 | + |
| 113 | +```c++ |
| 114 | + int32_t mipWidth = texWidth; |
| 115 | + int32_t mipHeight = texHeight; |
| 116 | + |
| 117 | + for (uint32_t i = 1; i < mipLevels; i++) { |
| 118 | + |
| 119 | + } |
| 120 | +``` |
| 121 | +
|
| 122 | +This loop will record each of the `VkCmdBlitImage` commands. Note that the loop variable starts at 1, not 0. |
| 123 | +
|
| 124 | +```c++ |
| 125 | + barrier.subresourceRange.baseMipLevel = i - 1; |
| 126 | + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; |
| 127 | + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; |
| 128 | + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; |
| 129 | + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; |
| 130 | +
|
| 131 | + vkCmdPipelineBarrier(commandBuffer, |
| 132 | + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, |
| 133 | + 0, nullptr, |
| 134 | + 0, nullptr, |
| 135 | + 1, &barrier); |
| 136 | +``` |
| 137 | + |
| 138 | +First, we transition level `i - 1` to `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`. |
| 139 | + |
| 140 | +```c++ |
| 141 | + VkImageBlit blit = {}; |
| 142 | + blit.srcOffsets[0] = { 0, 0, 0 }; |
| 143 | + blit.srcOffsets[1] = { mipWidth, mipHeight, 1 }; |
| 144 | + blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; |
| 145 | + blit.srcSubresource.mipLevel = i - 1; |
| 146 | + blit.srcSubresource.baseArrayLayer = 0; |
| 147 | + blit.srcSubresource.layerCount = 1; |
| 148 | + blit.dstOffsets[0] = { 0, 0, 0 }; |
| 149 | + blit.dstOffsets[1] = { mipWidth / 2, mipHeight / 2, 1 }; |
| 150 | + blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; |
| 151 | + blit.dstSubresource.mipLevel = i; |
| 152 | + blit.dstSubresource.baseArrayLayer = 0; |
| 153 | + blit.dstSubresource.layerCount = 1; |
| 154 | +``` |
| 155 | +
|
| 156 | +Next, we specify the regions that will be used in the blit operation. The source mip level is `i - 1` and the destination mip level is `i`. The two elements of the `srcOffsets` array determine the 3D region that data will be blitted from. `dstOffsets` determines the region that data will be blitted to. The X and Y dimensions of the `dstOffsets[1]` are divided by two since each mip level is half the size of the previous level. The Z dimension of `srcOffsets[1]` and `dstOffsets[1]` must be 1, since a 2D image has a depth of 1. |
| 157 | +
|
| 158 | +```c++ |
| 159 | + vkCmdBlitImage(commandBuffer, |
| 160 | + textureImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, |
| 161 | + textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, |
| 162 | + 1, &blit, VK_FILTER_LINEAR); |
| 163 | +``` |
| 164 | + |
| 165 | +Now, we record the blit command. Note that `textureImage` is used for both the `srcImage` and `dstImage` parameter. The source mip level was just transitioned to `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL` and the destination level is still in `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL` from `createTextureImage`. The last parameter says to use a linear filter when scaling the data. |
| 166 | + |
| 167 | +```c++ |
| 168 | + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; |
| 169 | + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; |
| 170 | + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; |
| 171 | + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; |
| 172 | + |
| 173 | + vkCmdPipelineBarrier(commandBuffer, |
| 174 | + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, |
| 175 | + 0, nullptr, |
| 176 | + 0, nullptr, |
| 177 | + 1, &barrier); |
| 178 | +``` |
| 179 | +
|
| 180 | +This barrier transitions mip level `i - 1` to `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`. |
| 181 | +
|
| 182 | +```c++ |
| 183 | + ... |
| 184 | + mipWidth /= 2; |
| 185 | + mipHeight /= 2; |
| 186 | + } |
| 187 | +``` |
| 188 | + |
| 189 | +At the end of the loop, we divide the current mip dimensions by two. |
| 190 | + |
| 191 | +```c++ |
| 192 | + |
| 193 | + barrier.subresourceRange.baseMipLevel = mipLevels - 1; |
| 194 | + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; |
| 195 | + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; |
| 196 | + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; |
| 197 | + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; |
| 198 | + |
| 199 | + vkCmdPipelineBarrier(commandBuffer, |
| 200 | + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, |
| 201 | + 0, nullptr, |
| 202 | + 0, nullptr, |
| 203 | + 1, &barrier); |
| 204 | + |
| 205 | + endSingleTimeCommands(commandBuffer); |
| 206 | + } |
| 207 | +``` |
| 208 | +
|
| 209 | +Before we end the command buffer, we insert one more pipeline barrier. This barrier transitions the last mip level from `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL` to `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`, since that wasn't handled by the loop. |
| 210 | +
|
| 211 | +Finally, add the call to `generateMipmaps` in `createTextureImage`: |
| 212 | +
|
| 213 | +```c++ |
| 214 | + generateMipmaps(texWidth, texHeight); |
| 215 | +``` |
| 216 | + |
| 217 | +Our texture image's mip maps are now completely filled. |
| 218 | + |
| 219 | +## Sampler |
| 220 | + |
| 221 | +One last thing we need to do before we see the results is modify `textureSampler`. |
| 222 | + |
| 223 | +```c++ |
| 224 | + void createTextureSampler() { |
| 225 | + ... |
| 226 | + samplerInfo.maxLod = static_cast<float>(mipLevels); |
| 227 | + ... |
| 228 | + } |
| 229 | +``` |
| 230 | +
|
| 231 | +This configures the sampler to sample from all mip levels up to `mipLevels`. |
| 232 | +
|
| 233 | +Now run your program and you should see the following: |
| 234 | +
|
| 235 | + |
| 236 | +
|
| 237 | +It's not a dramatic difference, since our scene is so simple. There are subtle differences if you look closely. |
| 238 | +
|
| 239 | +
|
| 240 | + |
| 241 | +
|
| 242 | +The left image is our new program. The right image is our program without mipmaps. The most noticeable difference is the writing on the signs. With mipmaps, the writing has been blurred. Without mipmaps, the writing has harsh edges and gaps from Moiré artifacts. |
| 243 | +
|
| 244 | +You can play around with the sampler settings to see how they affect mipmapping. For example, by changing `minLod`, you can force the sampler to not use the lowest mip levels: |
| 245 | +
|
| 246 | +```c++ |
| 247 | + samplerInfo.minLod = static_cast<float>(mipLevels / 2); |
| 248 | +``` |
| 249 | + |
| 250 | +These settings will produce this image: |
| 251 | + |
| 252 | + |
| 253 | + |
| 254 | + |
| 255 | +This is how higher mip levels will be used when objects are further away from the camera. |
| 256 | + |
| 257 | + |
| 258 | +## Conclusion |
| 259 | + |
| 260 | +It has taken a lot of work to get to this point, but now you finally have a good |
| 261 | +base for a Vulkan program. The knowledge of the basic principles of Vulkan that |
| 262 | +you now possess should be sufficient to start exploring more of the features, |
| 263 | +like: |
| 264 | + |
| 265 | +* Push constants |
| 266 | +* Instanced rendering |
| 267 | +* Dynamic uniforms |
| 268 | +* Separate images and sampler descriptors |
| 269 | +* Pipeline cache |
| 270 | +* Multi-threaded command buffer generation |
| 271 | +* Multiple subpasses |
| 272 | +* Compute shaders |
| 273 | + |
| 274 | +The current program can be extended in many ways, like adding Blinn-Phong |
| 275 | +lighting, post-processing effects and shadow mapping. You should be able to |
| 276 | +learn how these effects work from tutorials for other APIs, because despite |
| 277 | +Vulkan's explicitness, many concepts still work the same. |
| 278 | + |
| 279 | +[C++ code](/code/28_mipmapping.cpp) / |
| 280 | +[Vertex shader](/code/26_shader_depth.vert) / |
| 281 | +[Fragment shader](/code/26_shader_depth.frag) |
0 commit comments