[Translation]Vulkan Tutorial (32) Generate mipmap
Generating Mipmaps Generate mipmap
Introduction Getting Started
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.
Now our program can load and render 3D models. In this chapter, we will add a feature, mipmap generation. Mipmap is widely used in games and rendering software, and Vulkan gives us complete control-on how to create them.
Mipmaps are precalculated, downscaled versions of an image. Each new image is half the width and height of the previous one. Mipmaps are used as a form of Level of Detail or < em>LOD. Objects that are far away from the camera will sample their textures from the smaller mip images. Using smaller images increases the rendering speed and avoids artifacts such as Moiré patterns. An example of what mipmaps look like:< /p>
Mipmap is a pre-computed, reduced version of the image. Each new image is half the width and height of one. Mipmap is used as a way of Level of Detail (LOD). Objects far away from the camera will be sampled from the mip image with a smaller texture. Using a smaller image will increase the rendering speed and avoid aliasing such as Moiré patterns. An example of a mipmap is as follows:
< /p>
Image creation
In Vulkan, each of the mip images is stored in different mip levels of a VkImage
. Mip level 0 is the original image, and the mip levels after level 0 are commonly referred to as the mip chain.
In Vulkan, each mip image is saved in VkImage
in different mip layers. Mip layer 0 is the initial image, and the subsequent mip layer is called mipchain.
The number of mip levels is specified when the VkImage
is created. Up until now, we have always set this value to one. We need to calculate the number of mip levels from the dimensions of the image. First, add a class member to store this number:
The number of Mip layers is specified when VkImage
is created. Until now, we have set this value to 1 in total. We need to calculate the number of mip layers based on the dimensions of the image. First, add class members to record this number:
...
uint32_t mipLevels;
VkImage textureImage;
...
The value for mipLevels
can be found once we’ve loaded the texture The value in createTextureImage
:
mipLevels
can be obtained immediately after we load the texture in createTextureImage
:
int texWidth, texHeight, texChannels;
stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
...
mipLevels = static_cast(std::floor(std::log2(std::max(texWidth, texHeight))))) + 1;
This calculates the number of levels in the mip chain. The max
function selects the largest dimension. The log2
function calculates how many times that dimension can be divided by 2. The floor
function handles cases where the largest dimension is not a power of 2. 1
is added so that the original image has a mip level.
This calculates the number of levels in the mip chain. The max
function selects the largest dimension. The log2
function calculates how many times the dimension can be divided by 2. The floor
function deals with the problem that the largest dimension is not an exponent of 2. Adding 1
makes the original image have 1 mip layer.
To use this value, we need to change the createImage
, createImageView
, and transitionImageLayout
functions to allow us to specify the number of mip levels. Add a mipLevels
parameter to the functions:
To use this value, we need to modify createImage
, createImageView< /code>and
transitionImageLayout
The function to allows us to specify the number of mip layers. Add mipLevels
parameters to these functions:
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;
...
Update all calls to these functions to use the right values:
Update all For calls to these functions, use the correct values:
createImage(swapChainExtent.width, swapChainExtent.height, 1< /span>, 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_UNORM, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT |1);
...
depthImageView = createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT, 1);
...
textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_UNORM, 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_UNORM, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels);
Generating Mip>
Generating Mip
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 the vkCmdBlitImage
command. This command performs copying, scaling, and filtering operations. We will call this multiple times to blit data to each level of our texture image.
Our texture image now has multiple mip layers, but the temporary buffer can only be used to fill mp layer 0. The other layers are still undefined. Ask to fill in these layers, we need to generate data for each layer. We need to use the vkCmdBlitImage
command. Try this command to copy, zoom, and filter operations. We call it multiple times to bit block transmissionblit data to each layer.
VkCmdBlit
is considered a transfer operation, so we must inform Vulkan that we intend to use the texture image as both the source and destination of a transfer. AddVK_IMAGE_USAGE_TRANSFER_SRC_BIT< /code>to the texture image's usage flags in
createTextureImage
:
VkCmdBlit
It is considered a transfer operation, so we must notify Vulkan, we want Use texture images as both source and target. Add VK_IMAGE_USAGE_TRANSFER_SRC_BIT
to the usage flag of the texture image in createTextureImage
:
...
createImage(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFERAGE_DST_K_IMAGE_USAGE_PERAGE_Memory, texture, VK_IMAGE_USAGE_MEMAGE,,
...
Like other image operations, vkCmdBlitImage
depends on the layout of the image it operates on. We could transition the entire image to VK_IMAGE_LAYOUT_GENERAL
, but this will most likely be slow. 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
. Vulkan allows us to transition each mip level of an image independently. Each blit will only deal with two mip levels at a time, so we can transition each level into the optimal layout between blits commands .
Like other image operations, vkCmdBlitImage
depends on the layout of the image. We can convert the entire image to VK_IMAGE_LAYOUT_GENERAL
, but this will be very slow. For optimal performance, the source image should be VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
, and the target image should be VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
. Vulkan allows us to convert each mip layer independently. Each blit will only process 2 mip layers, so we can switch each layer to the optimal layout between two blit commands.
transitionImageLayout
only performs layout transitions on the entire image, so we'll need to write a few more pipeline barrier commands. Remove the existing transition to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
In createTextureImage
:
transitionImageLayout
Only the layout transformation is implemented on the entire image, so we need to write some more pipeline barrier commands. Change the existing conversion in createTextureImage
to 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(texWidth), static_cast (texHeight));
//transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL while generating mipmaps
...
This will leave each level of the texture image in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
. Each level will be transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
after the blit command reading from it is finished.
This will put each layer in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
. After the blit command is read from the layers, each layer will be converted to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
.
We're now going to write the function that generates the mipmaps:
We're now going to write the function that generates the 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);
}
We're going to make several transitions, so we'll reuse this VkImageMemoryBarrier
. The fields set above will remain the same for all barriers. subresourceRange.miplevel
, oldLayout
, newLayout
, srcAccessMask
, and dstAccessMask
will be changed for each transition.
We have to do several transitions, so we reuse this VkImageMemoryBarrier
. The setting of the above fields will figure out all barriers subresourceRange.miplevel
, oldLayout
, newLayout
, srcAccessMask
and < code>dstAccessMask will change with each conversion.
int32_t mipWidth = texWidth;
int32_t mipHeight = texHeight;
for (uint32_t i = 1; i) {
}
This loop will record each of the VkCmdBlitImage
commands. Note that the loop variable starts at 1, not 0.
This loop will record each VkCmdBlitImage
command. Note that the loop variable starts at 1, not 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);
First, we transition level i-1
to VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
. This transition will wait for level i-1
to be filled, either from the previous blit command, or from vkCmdCopyBufferToImage
. The current blit command will wait on this transition.
First, we convert the layer i-1
to VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
. This conversion will wait for the layer i-1
to be filled, either from the previous blit command, or from vkCmdCopyBufferToImage
. The current blit command will wait for this conversion.
VkImageBlit blit = {};
blit.srcOffsets[0] = {0, 0, 0 };
blit.srcOffsets[1] = {mipWidth, mipHeight, 1< /span> };
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;
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
determine 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.
Next, we know the area to be used by this blit operation. The source mip layer is i-1
, and the target mip layer is i
. The two elements of the srcOffsets
array determine the area from which the data will be filled. dstOffsets
determines the area where the data will be filled. The X and Y dimensions of dstOffsets[1]
are divided by 2, because each mip layer is half of the previous one. The Z dimension of srcOffsets[1]
and dstOffsets[1]
must be 1, because the depth of a 2D image is 1.
vkCmdBlitImage(commandBuffer,
image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &blit,
VK_FILTER_LINEAR);
Now, we record the blit command. Note that textureImage
is used for both the srcImage
and dstImage
parameter. This is because we're blitting between different levels of the same image. The source mip level was just transitioned to VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
code> and the destination level is still in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
from createTextureImage
.
Now, we record the blit command. Note that textureImage
is used for both srcImage
and dstImage
parameters. This is because we want to blit on different layers of the same image. The source mip layer is converted to VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
, and the target mip layer is still VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
from createTextureImage
.
The last parameter allows us to specify a VkFilter
to use in the blit. We have the same filtering options here that we had when making the VkSampler
. We use the VK_FILTER_LINEAR
to enable interpolation.
The last parameter allows us to specify the VkFilter
used for blit. We also used the same filtering options when making it. We use VK_FILTER_LINEAR
to enable interpolation.
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);
This barrier transitions mip level i-1
to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
. This transition waits on the current blit command to finish. All sampling operations will wait on this transition to finish.
This barrier converts the mip layer i-1
to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
. This conversion waits for the current blit command to complete. All sampling operations will wait for the conversion to complete.
...
if (mipWidth> 1) mipWidth /= < span style="color: #800080;">2;
if (mipHeight> 1) mipHeight /= < span style="color: #800080;">2;
}
At the end of the loop, we divide the current mip dimensions by two. We check each dimension before the division to ensure that dimension never becomes 0. This handles cases where the image is not square, since one of the mip dimensions would reach 1 before the other dimension. When this happens, that dimension should remain 1 for all remaining levels.
At the end of the loop, we divide the current mip dimension by 2. We check each dimension before division to ensure that the dimension does not become zero. This handles the case where the image is not square, because one of the mip dimensions will reach 1 first. At this time, that dimension should continue to be 1 for the remaining layers.
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);
}
Before we end the command buffer, we insert one more pipeline barrier. This barrier transitions the last mip level from
code>VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
. This wasn’t handled by the loop, since the last mip level is never blitted from.
Before we end the command buffer, We insert another pipe barrier. This barrier transforms the last mip layer from VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
. This is not processed in a loop, because the last mip layer is never blit to anyone.
Finally, add the call to generateMipmaps
in createTextureImage
:
Finally, in createTextureImage
Add a call to generateMipmaps
:
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_UNDEFINED, VK_TIMAL_LAYOUT_TRANSFER_D mipLevels);
copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast (texHeight));
//transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL while generating mipmaps
...
generateMipmaps(textureImage, texWidth, texHeight, mipLevels);
Our texture image’s mipmaps are now completely filled. p>
The mipmap of our texture image is now completely filled in.
Linear filtering support Support for linear filtering
It is very convenient to use a built-in function like vkCmdBlitImage
to generate all the mip levels, but Unfortunately it is not guaranteed to be supported on all platforms. It requires the texture image format we use to support linear filtering, which can be checked with the vkGetPhysicalDeviceFormatProperties
function. We will add a check to the generateMipmaps
function for this.
It is very convenient to use built-in functions such as vkCmdBlitImage
to generate all mip layers, but unfortunately, it does not guarantee that All are supported on the platform. It requires the texture image format to support linear filtering, which can be checked in the vkGetPhysicalDeviceFormatProperties
function. We need to add such a check to the generateMipmaps
function.
First add an additional parameter that specifies the image format:
First add an additional parameter that specifies the image format:
< span style="color: #0000ff;">void createTextureImage() {
...
generateMipmaps(textureImage, VK_FORMAT_R8G8B8A8_UNORM, texWidth, texHeight, mipLevels);
}
void generateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth, int32_t texHeight , uint32_t mipLevels) {
...
}
In the generateMipmaps
function, use vkGetPhysicalDeviceFormatProperties
to request the properties of the texture image format:
In the function, use vkGetPhysicalDeviceFormatProperties
to check the properties of the texture image format:
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);
...
The VkFormatProperties
struct has three fields named linearTilingFeatures
, optimalTilingFeatures
and bufferFeatures
that each describe how the format can be used depending on the way it is used. We create a texture image with the optimal tiling format, so we need to check optimalTilingFeatures
. Support for the linear filtering feature can be checked with the VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT
:
VkFormatProperties
There are 3 structures Two fields, linearTilingFeatures
, optimalTilingFeatures
, and bufferFeatures
, describe how the format is used according to how the format is used. We create a texture image in the optimal tiling format, so we need to check optimalTilingFeatures
. Support for linear filtering features can be queried with 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!");
}
There are two alternatives in this case. You could implement a function that searches common texture image formats for one that< em>does support linear blitting, or you could implement the mipmap generation in software with a library like stb_image_resize. Each mip level can then be loaded into the image in the same way that you loaded the original image.
There are 2 options at this time. You can implement a function that searches for the texture image format of the scene and find one that supports linear filtering, or you can use a library to softly implement mipmap generation, like stb_image_resize. Then each mip layer can be loaded into the image, just like you load the original image.
It should be noted that it is uncommon in practice to generate the mipmap levels at runtime anyway. Usually they are pregenerated and stored in the texture file alongside the base level to improve loading speed. Implementing resizing in software and loading multiple levels from a file is left as an exercise to the reader.
要注意到,实践中不常在运行时生成mipmap层的方式。一般的,它们都是预生成了,保存到纹理文件里to提升加载速度。软件实现resize和加载多mip层就留给读者作为练习了。
Sampler 采样器
While the VkImage
holds the mipmap data, VkSampler
controls how that data is read while rendering. Vulkan allows us to specify minLod
, maxLod
, mipLodBias
, and mipmapMode
(“Lod” means “Level of Detail”). When a texture is sampled, the sampler selects a mip level according to the following pseudocode:
VkImage
记录了mipmap数据,但VkSampler
控制了渲染时数据如何被读取。 Vulkan允许我们指定minLod
、maxLod
、mipLodBias
和mipmapMode
(Lod的意思是Level of Detail)。当一个纹理被采样时,采样器根据下述伪代码选择一个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));
}
If samplerInfo.mipmapMode
is VK_SAMPLER_MIPMAP_MODE_NEAREST
, lod
selects the mip level to sample from. If the mipmap mode is VK_SAMPLER_MIPMAP_MODE_LINEAR
, lod
is used to select two mip levels to be sampled. Those levels are sampled and the results are linearly blended.
如果samplerInfo.mipmapMode
是VK_SAMPLER_MIPMAP_MODE_NEAREST
,lod
选择mip层去采样。如果mipmap模式是VK_SAMPLER_MIPMAP_MODE_LINEAR
,lod
用于选择2个mi层来采样。这些层被采样,结果被线性混合。
The sample operation is also affected by lod
:
采样操作也被lod
影响:
if (lod <= 0) {
color = readTexture(uv, magFilter);
} else {
color = readTexture(uv, minFilter);
}
If the object is close to the camera, magFilter
is used as the filter. If the object is further from the camera, minFilter
is used. Normally, lod
is non-negative, and is only 0 when close the camera. mipLodBias
lets us force Vulkan to use lower lod
and level
than it would normally use.
如果对象距离摄像机很近,magFilter
就用于过滤。如果对象距离摄像机很远,minFilter
就用上了。一般地,lod
是非负数,只有接近摄像机时才为0。mipLodBias
让我们强制Vulkan使用比较低的lod
和level
than它一般用的。
To see the results of this chapter, we need to choose values for our textureSampler
. We‘ve already set the minFilter
and magFilter
to use VK_FILTER_LINEAR
. We just need to choose values for minLod
, maxLod
, mipLodBias
, and mipmapMode
.
为了看看本章的结果,我们需要选择我们的textureSampler
值,我们已经设置了minFilter
和magFilter
to使用VK_FILTER_LINEAR
。我们只需选择minLod
、maxLod
、mipLodBias
和mipmapMode
的值。
void createTextureSampler() {
...
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
samplerInfo.minLod = 0; // Optional
samplerInfo.maxLod = static_cast<float>(mipLevels);
samplerInfo.mipLodBias = 0; // Optional
...
}
To allow the full range of mip levels to be used, we set minLod
to 0, and maxLod
to the number of mip levels. We have no reason to change the lod
value , so we set mipLodBias
to 0.
为了使用全部范围内的mip层,我们设置minLod
为0,设置maxLod
为mip层的数量。我们没有理由修改lod
值,所以我们设置mipLodBias
为0。
Now run your program and you should see the following:
现在运行你的程序,你应当看到下述情景:
It‘s not a dramatic difference, since our scene is so simple. There are subtle differences if you look closely.
没什么打的区别,因为我们的场景太简单了。如果你靠近观看,会有微妙的区别。
The most noticeable difference is the writing on the signs. With mipmaps, the writing has been smoothed. Without mipmaps, the writing has harsh edges and gaps from Moiré artifacts.
最引人注意的区别是。有mipmap,写入被平滑了。没有mipmap,Moiré艺术品写入会有刺目的边界和裂缝。
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:
你可以鼓捣鼓捣采样器设置to看看它们如何影响mipmap。例如,通过修改minLod
,你可以强制采样器不使用最低的mip层:
samplerInfo.minLod = static_cast<float>(mipLevels / 2);
These settings will produce this image:
这些设置会产生这样的结果:
This is how higher mip levels will be used when objects are further away from the camera.
这就是更高的mip层会被使用的结果when对象原理摄像机。
C++ code / Vertex shader / Fragment shader
- Previous
- Next
...
uint32_t mipLevels;
VkImage textureImage;
...
int texWidth, texHeight, texChannels;
stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
...
mipLevels = static_cast(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1;
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_UNORM, 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_UNORM, 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_UNORM, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels);
...
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);
...
...
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels);
copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast (texHeight));
//transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL while generating 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);
}
int32_t mipWidth = texWidth;
int32_t mipHeight = texHeight;
for (uint32_t i = 1; i < mipLevels; i++) {
}
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);
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;
vkCmdBlitImage(commandBuffer,
image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &blit,
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);
...
if (mipWidth > 1) mipWidth /= 2;
if (mipHeight > 1) mipHeight /= 2;
}
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);
}
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels);
copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast (texHeight));
//transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL while generating mipmaps
...
generateMipmaps(textureImage, texWidth, texHeight, mipLevels);
void createTextureImage() {
...
generateMipmaps(textureImage, VK_FORMAT_R8G8B8A8_UNORM, texWidth, texHeight, mipLevels);
}
void generateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) {
...
}
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);
...
if (!(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT)) {
throw std::runtime_error("texture image format does not support linear blitting!");
}
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));
}
if (lod <= 0) {
color = readTexture(uv, magFilter);
} else {
color = readTexture(uv, minFilter);
}
void createTextureSampler() {
...
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
samplerInfo.minLod = 0; // Optional
samplerInfo.maxLod = static_cast<float>(mipLevels);
samplerInfo.mipLodBias = 0; // Optional
...
}
samplerInfo.minLod = static_cast<float>(mipLevels / 2);