This commit is contained in:
xsl
2025-09-04 10:54:47 +08:00
commit 6bc8f61b18
1808 changed files with 208268 additions and 0 deletions
@@ -0,0 +1,33 @@
# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 the "License";
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
add_sample_with_tags(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Sascha Willems"
NAME "HPP Texture mipmap generation"
DESCRIPTION "Generate mipmaps for a texture"
SHADER_FILES_GLSL
"texture_mipmap_generation/glsl/texture.vert"
"texture_mipmap_generation/glsl/texture.frag"
SHADER_FILES_HLSL
"texture_mipmap_generation/hlsl/texture.vert.hlsl"
"texture_mipmap_generation/hlsl/texture.frag.hlsl")
@@ -0,0 +1,233 @@
////
- Copyright (c) 2022-2024, The Khronos Group
-
- SPDX-License-Identifier: Apache-2.0
-
- Licensed under the Apache License, Version 2.0 the "License";
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
////
:doctype: book
:pp: {plus}{plus}
= Run-time mip-map generation with Vulkan-Hpp
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/hpp_texture_mipmap_generation[Khronos Vulkan samples github repository].
endif::[]
NOTE: A transcoded version of the API sample https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/texture_mipmap_generation[Texture mipmap generation] that illustrates the usage of the C{pp} bindings of vulkan provided by vulkan.hpp.
== Overview
Generates a complete texture mip-chain at runtime from a base image using image blits and proper image barriers.
This examples demonstrates how to generate a complete texture mip-chain at runtime instead of loading offline generated mip-maps from a texture file.
While usually not applied for textures stored on the disk (that usually have the mips generated offline and stored in the file) this technique is often used for dynamic textures like cubemaps for reflections or other render-to-texture effects.
Having mip-maps for runtime generated textures offers lots of benefits, both in terms of image stability and performance.
Without mip mapping the image will become noisy, especially with high frequency textures (and texture components like specular) and using mip mapping will result in higher performance due to caching.
Though this example only generates one mip-chain for a single texture at the beginning this technique can also be used during normal frame rendering to generate mip-chains for dynamic textures.
Some GPUs also offer `asynchronous transfer queues` that may be used for doing such operations in the background.
To detect this, check for queue families with only the `vk::QueueFlagBits::eTransfer` set.
== Comparison
Without mip mapping:
image::samples/api/texture_mipmap_generation/images/mip_mapping_off.jpg[Off,512px]
Using mip mapping with a bilinear filter:
image::samples/api/texture_mipmap_generation/images/mip_mapping_bilinear.jpg[Bilinear,512px]
Using mip mapping with an anisotropic filter:
image::samples/api/texture_mipmap_generation/images/mip_mapping_anisotropic.jpg[Anisotropic,512px]
== Requirements
To downsample from one mip level to the next, we will be using https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkCmdBlitImage.html[`vk::CommandBuffer::blitImage`].
This requires the format used to support the `vk::FormatFeatureFlagBits::eBlitSrc` and the `vk::FormatFeatureFlagBits::eBlitDst` flags.
If these are not supported, the image format can't be used to blit and you'd either have to choose a different format or use a custom shader to generate mip levels.
The example uses the `vk::Format::eR8G8B8A8Srgb` that should support these flags on most implementations.
*_Note:_* Use https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkGetPhysicalDeviceFormatProperties.html[`vk::PhysicalDevice::getFormatProperties`] to check if the format supports the blit flags first.
== Points of interest
=== Image setup
Even though we'll only upload the first mip level initially, we create the image with number of desired mip levels.
The following formula is used to calculate the number of mip levels based on the max.
image extent:
[,cpp]
----
texture.mip_levels = static_cast<uint32_t>(floor(log2(std::max(texture.width, texture.height))) + 1);
----
This is then passed to the image create info:
[,cpp]
----
vk::ImageCreateInfo image_create_info({},
vk::ImageType::e2D,
format,
vk::Extent3D(texture.extent, 1),
texture.mip_levels,
...
----
Setting the number of desired mip levels is necessary as this is used for allocating the correct amount of memory required by the image (`vk::Device::allocateMemory`).
=== Upload base mip level
Before generating the mip-chain we need to copy the image data loaded from disk into the newly generated image.
This image will be the base for our mip-chain:
[,cpp]
----
vk::BufferImageCopy buffer_copy_region({}, {}, {}, {vk::ImageAspectFlagBits::eColor, 0, 0, 1}, {}, vk::Extent3D(texture.extent, 1));
copy_command.copyBufferToImage(staging_buffer, texture.image, vk::ImageLayout::eTransferDstOptimal, buffer_copy_region);
----
=== Prepare base mip level
As we are going to blit *_from_* the base mip-level just uploaded we also need to insert an image memory barrier that transitions the image layout to `vk::ImageLayout::eTransferSrcOptimal` for the base mip level:
[,cpp]
----
image_memory_barrier = vk::ImageMemoryBarrier(vk::AccessFlagBits::eTransferWrite,
vk::AccessFlagBits::eTransferRead,
vk::ImageLayout::eTransferDstOptimal,
vk::ImageLayout::eTransferSrcOptimal,
VK_QUEUE_FAMILY_IGNORED,
VK_QUEUE_FAMILY_IGNORED,
texture.image,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1});
copy_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, image_memory_barrier);
----
=== Generating the mip-chain
There are two different ways of generating the mip-chain.
The first one is to blit down the whole mip-chain from level n-1 to n, the other way would be to always use the base image and blit down from that to all levels.
This example uses the first one.
*_Note:_* Blitting (same for copying) images is done inside of a command buffer that has to be submitted and as such has to be synchronized before using the new image with e.g.
a `vk::Fence`.
We simply loop over all remaining mip levels (level 0 was loaded from disk) and prepare a `vk::ImageBlit` structure for each blit from mip level i-1 to level i.
First the source for our blit.
This is the previous mip level:
// {% raw %}
[,cpp]
----
for (int32_t i = 1; i < texture.mipLevels; i++)
{
vk::ImageBlit image_blit(// Source
{vk::ImageAspectFlagBits::eColor, i - 1, 0, 1},
{{{}, {int32_t(texture.extent.width >> (i - 1)), int32_t(texture.extent.height >> (i - 1)), int32_t(1)}}},
// Destination
{vk::ImageAspectFlagBits::eColor, i, 0, 1},
{{{}, {int32_t(texture.extent.width >> i), int32_t(texture.extent.height >> i), int32_t(1)}}});
}
----
// {% endraw %}
Before we can blit to this mip level, we need to transition it's image layout to `vk::ImageLayout::eTransferDstOptimal`:
[,cpp]
----
// Prepare current mip level as image blit destination
image_memory_barrier = vk::ImageMemoryBarrier({},
vk::AccessFlagBits::eTransferWrite,
vk::ImageLayout::eUndefined,
vk::ImageLayout::eTransferDstOptimal,
VK_QUEUE_FAMILY_IGNORED,
VK_QUEUE_FAMILY_IGNORED,
texture.image,
{vk::ImageAspectFlagBits::eColor, i, 1, 0, 1});
copy_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, image_memory_barrier);
----
Note that we set the `baseMipLevel` of the subresource range to `i`, so the image memory barrier will only affect the one mip level we want to copy to.
Now that the mip level we want to copy from and the one we'll copy to are in the proper layout (transfer source and destination) we can issue the https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkCmdBlitImage.html[`vk::CommandBuffer::blitImage`] to copy from mip level (i-1) to mip level (i):
[,cpp]
----
blit_command.blitImage(texture.image, vk::ImageLayout::eTransferSrcOptimal, texture.image, vk::ImageLayout::eTransferDstOptimal, image_blit, vk::Filter::eLinear);
----
`vk::CommandBuffer::blitImage` does the down sampling from mip level (i-1) to mip level (i) using a linear filter, if you need better or more advanced filtering for this you need to resort to using custom shaders for generating the mip chain instead of blitting.
After the blit is done we can use this mip level as a base for the next level, so we transition the layout from `vk::ImageLayout::eTransferDstOptimal` to `vk::ImageLayout::eTransferSrcOptimal` so we can use this level as transfer source for the next level:
[,cpp]
----
image_memory_barrier = vk::ImageMemoryBarrier(vk::AccessFlagBits::eTransferWrite,
vk::AccessFlagBits::eTransferRead,
vk::ImageLayout::eTransferDstOptimal,
vk::ImageLayout::eTransferSrcOptimal,
VK_QUEUE_FAMILY_IGNORED,
VK_QUEUE_FAMILY_IGNORED,
texture.image,
{vk::ImageAspectFlagBits::eColor, i, 1, 0, 1});
copy_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eTransfer, {}, {}, {}, image_memory_barrier);
}
----
=== Final image layout transitions
Once the loop is done we need to transition all mip levels of the image to their actual usage layout, which is `vk::ImageLayout::eShaderReadOnlyOptimal` for this example.
Note that after the loop above all levels will be in the `vk::ImageLayout::eTransferSrcOptimal` layout allowing us to transfer the whole image with a single barrier:
[,cpp]
----
image_memory_barrier = vk::ImageMemoryBarrier(vk::AccessFlagBits::eTransferRead,
vk::AccessFlagBits::eShaderRead,
vk::ImageLayout::eTransferSrcOptimal,
vk::ImageLayout::eShaderReadOnlyOptimal,
VK_QUEUE_FAMILY_IGNORED,
VK_QUEUE_FAMILY_IGNORED,
texture.image,
{vk::ImageAspectFlagBits::eColor, 0, texture.mip_levels, 0, 1});
copy_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eFragmentShader, {}, {}, {}, image_memory_barrier);
----
Submitting that command buffer will result in an image with a complete mip-chain and all mip levels being transitioned to the proper image layout for shader reads.
=== Image View creation
The Image View also requires information about how many Mip Levels are used.
This is specified in the `vk::ImageViewCreateInfo.subresourceRange.levelCount` field.
[,cpp]
----
vk::ImageViewCreateInfo image_view_create_info({},
texture.image,
vk::ImageViewType::e2D,
format,
{vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eG, vk::ComponentSwizzle::eB, vk::ComponentSwizzle::eA},
{vk::ImageAspectFlagBits::eColor, 0, texture.mip_levels, 0, 1});
texture.view = get_device()->get_handle().createImageView(image_view_create_info);
----
@@ -0,0 +1,455 @@
/* Copyright (c) 2022-2025, NVIDIA CORPORATION. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Runtime mip map generation, using vulkan.hpp
*/
#include "hpp_texture_mipmap_generation.h"
#include "common/hpp_vk_common.h"
#include "common/ktx_common.h"
#include "common/vk_initializers.h"
#include "core/command_pool.h"
HPPTextureMipMapGeneration::HPPTextureMipMapGeneration()
{
title = "Texture MipMap generation";
zoom = -2.5f;
rotation = {0.0f, 15.0f, 0.0f};
}
HPPTextureMipMapGeneration::~HPPTextureMipMapGeneration()
{
if (has_device() && get_device().get_handle())
{
vk::Device device = get_device().get_handle();
device.destroyPipeline(pipeline);
device.destroyPipelineLayout(pipeline_layout);
device.destroyDescriptorSetLayout(descriptor_set_layout);
for (auto sampler : samplers)
{
device.destroySampler(sampler);
}
device.destroyImageView(texture.view);
device.destroyImage(texture.image);
device.freeMemory(texture.device_memory);
uniform_buffer.reset();
}
}
bool HPPTextureMipMapGeneration::prepare(const vkb::ApplicationOptions &options)
{
assert(!prepared);
if (HPPApiVulkanSample::prepare(options))
{
prepare_camera();
load_assets();
prepare_uniform_buffers();
descriptor_set_layout = create_descriptor_set_layout();
pipeline_layout = get_device().get_handle().createPipelineLayout({.setLayoutCount = 1, .pSetLayouts = &descriptor_set_layout});
pipeline = create_pipeline();
descriptor_pool = create_descriptor_pool();
descriptor_set = vkb::common::allocate_descriptor_set(get_device().get_handle(), descriptor_pool, descriptor_set_layout);
update_descriptor_set();
build_command_buffers();
prepared = true;
}
return prepared;
}
// Enable physical device features required for this example
void HPPTextureMipMapGeneration::request_gpu_features(vkb::core::HPPPhysicalDevice &gpu)
{
// Enable anisotropic filtering if supported
if (gpu.get_features().samplerAnisotropy)
{
gpu.get_mutable_requested_features().samplerAnisotropy = true;
}
}
void HPPTextureMipMapGeneration::build_command_buffers()
{
vk::CommandBufferBeginInfo command_buffer_begin_info;
std::array<vk::ClearValue, 2> clear_values = {{default_clear_color, vk::ClearDepthStencilValue{1.0f, 0}}};
vk::RenderPassBeginInfo render_pass_begin_info{.renderPass = render_pass,
.renderArea = {{0, 0}, extent},
.clearValueCount = static_cast<uint32_t>(clear_values.size()),
.pClearValues = clear_values.data()};
for (int32_t i = 0; i < draw_cmd_buffers.size(); ++i)
{
auto command_buffer = draw_cmd_buffers[i];
render_pass_begin_info.framebuffer = framebuffers[i];
command_buffer.begin(command_buffer_begin_info);
command_buffer.beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
vk::Viewport viewport{0.0f, 0.0f, static_cast<float>(extent.width), static_cast<float>(extent.height), 0.0f, 1.0f};
command_buffer.setViewport(0, viewport);
vk::Rect2D scissor{{0, 0}, extent};
command_buffer.setScissor(0, scissor);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, descriptor_set, {});
command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline);
draw_model(scene, command_buffer);
draw_ui(command_buffer);
command_buffer.endRenderPass();
command_buffer.end();
}
}
void HPPTextureMipMapGeneration::on_update_ui_overlay(vkb::Drawer &drawer)
{
if (drawer.header("Settings"))
{
drawer.checkbox("Rotate", &rotate_scene);
if (drawer.slider_float("LOD bias", &ubo.lod_bias, 0.0f, static_cast<float>(texture.mip_levels)))
{
update_uniform_buffers();
}
if (drawer.combo_box("Sampler type", &ubo.sampler_index, sampler_names))
{
update_uniform_buffers();
}
}
}
void HPPTextureMipMapGeneration::render(float delta_time)
{
if (prepared)
{
draw();
if (rotate_scene)
{
update_uniform_buffers(delta_time);
}
}
}
void HPPTextureMipMapGeneration::view_changed()
{
update_uniform_buffers();
}
void HPPTextureMipMapGeneration::check_format_features(vk::Format format) const
{
// Get device properties for the requested texture format
vk::FormatProperties format_properties = get_device().get_gpu().get_handle().getFormatProperties(format);
// Check if the selected format supports blit source and destination, which is required for generating the mip levels
vk::FormatFeatureFlags format_feature_flags = vk::FormatFeatureFlagBits::eBlitSrc | vk::FormatFeatureFlagBits::eBlitDst;
// If this is not supported you could implement a fallback via compute shader image writes and stores
if ((format_properties.optimalTilingFeatures & format_feature_flags) != format_feature_flags)
{
throw std::runtime_error("Selected image format does not support blit source and destination");
}
}
vk::DescriptorPool HPPTextureMipMapGeneration::create_descriptor_pool()
{
// Example uses one ubo and one image sampler
std::array<vk::DescriptorPoolSize, 3> pool_sizes = {
{{vk::DescriptorType::eUniformBuffer, 1}, {vk::DescriptorType::eSampledImage, 1}, {vk::DescriptorType::eSampler, 3}}};
vk::DescriptorPoolCreateInfo descriptor_pool_create_info{.maxSets = 2,
.poolSizeCount = static_cast<uint32_t>(pool_sizes.size()),
.pPoolSizes = pool_sizes.data()};
return get_device().get_handle().createDescriptorPool(descriptor_pool_create_info);
}
vk::DescriptorSetLayout HPPTextureMipMapGeneration::create_descriptor_set_layout()
{
std::array<vk::DescriptorSetLayoutBinding, 3> set_layout_bindings = {
{{0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment}, // Binding 0 : Parameter uniform buffer
{1, vk::DescriptorType::eSampledImage, 1, vk::ShaderStageFlagBits::eFragment}, // Binding 1 : Fragment shader image sampler
{2, vk::DescriptorType::eSampler, 3, vk::ShaderStageFlagBits::eFragment}}}; // Binding 2 : Sampler array (3 descriptors)
vk::DescriptorSetLayoutCreateInfo descriptor_layout{.bindingCount = static_cast<uint32_t>(set_layout_bindings.size()),
.pBindings = set_layout_bindings.data()};
return get_device().get_handle().createDescriptorSetLayout(descriptor_layout);
}
vk::Pipeline HPPTextureMipMapGeneration::create_pipeline()
{
// Load shaders
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages = {
load_shader("texture_mipmap_generation", "texture.vert.spv", vk::ShaderStageFlagBits::eVertex),
load_shader("texture_mipmap_generation", "texture.frag.spv", vk::ShaderStageFlagBits::eFragment)};
// Vertex bindings and attributes
vk::VertexInputBindingDescription vertex_input_binding{0, sizeof(HPPVertex), vk::VertexInputRate::eVertex};
std::array<vk::VertexInputAttributeDescription, 2> vertex_input_attributes = {{
{0, 0, vk::Format::eR32G32B32Sfloat, 0}, // Position
{1, 0, vk::Format::eR32G32Sfloat, sizeof(float) * 6}, // UV
}};
vk::PipelineVertexInputStateCreateInfo vertex_input_state{.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &vertex_input_binding,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_input_attributes.size()),
.pVertexAttributeDescriptions = vertex_input_attributes.data()};
vk::PipelineColorBlendAttachmentState blend_attachment_state{.colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA};
vk::PipelineDepthStencilStateCreateInfo depth_stencil_state;
depth_stencil_state.depthCompareOp = vk::CompareOp::eLessOrEqual;
depth_stencil_state.depthTestEnable = true;
depth_stencil_state.depthWriteEnable = true;
depth_stencil_state.back.compareOp = vk::CompareOp::eAlways;
depth_stencil_state.front = depth_stencil_state.back;
return vkb::common::create_graphics_pipeline(get_device().get_handle(),
pipeline_cache,
shader_stages,
vertex_input_state,
vk::PrimitiveTopology::eTriangleList,
0,
vk::PolygonMode::eFill,
vk::CullModeFlagBits::eNone,
vk::FrontFace::eCounterClockwise,
{blend_attachment_state},
depth_stencil_state,
pipeline_layout,
render_pass);
}
void HPPTextureMipMapGeneration::draw()
{
HPPApiVulkanSample::prepare_frame();
// Command buffer to be submitted to the queue
submit_info.setCommandBuffers(draw_cmd_buffers[current_buffer]);
// Submit to queue
queue.submit(submit_info);
HPPApiVulkanSample::submit_frame();
}
void HPPTextureMipMapGeneration::load_assets()
{
scene = load_model("scenes/tunnel_cylinder.gltf");
// Load the base texture containing only the first mip level and generate the whole mip-chain at runtime
ktxTexture *ktx_texture = vkb::ktx::load_texture(vkb::fs::path::get(vkb::fs::path::Assets, "textures/checkerboard_rgba.ktx"));
texture.extent = vk::Extent2D{ktx_texture->baseWidth, ktx_texture->baseHeight};
// Calculate number of mip levels as per Vulkan specs:
// numLevels = 1 + floor(log2(max(w, h, d)))
texture.mip_levels = static_cast<uint32_t>(floor(log2(std::max(texture.extent.width, texture.extent.height))) + 1);
// ktx1 doesn't know whether the content is sRGB or linear, but most tools save in sRGB, so assume that.
constexpr vk::Format format = vk::Format::eR8G8B8A8Srgb;
check_format_features(format);
// Create a host-visible staging buffer that contains the raw image data
vkb::core::BufferCpp staging_buffer = vkb::core::BufferCpp::create_staging_buffer(get_device(), ktx_texture->dataSize, ktx_texture->pData);
// now, the ktx_texture can be destroyed
ktxTexture_Destroy(ktx_texture);
// Create optimal tiled target image on the device
auto device = get_device().get_handle();
vk::ImageCreateInfo image_create_info{.imageType = vk::ImageType::e2D,
.format = format,
.extent = {.width = texture.extent.width, .height = texture.extent.height, .depth = 1},
.mipLevels = texture.mip_levels,
.arrayLayers = 1,
.samples = vk::SampleCountFlagBits::e1,
.tiling = vk::ImageTiling::eOptimal,
.usage =
vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eSampled,
.sharingMode = vk::SharingMode::eExclusive};
texture.image = device.createImage(image_create_info);
vk::MemoryRequirements memory_requirements = device.getImageMemoryRequirements(texture.image);
vk::MemoryAllocateInfo memory_allocation{.allocationSize = memory_requirements.size,
.memoryTypeIndex = get_device().get_gpu().get_memory_type(memory_requirements.memoryTypeBits,
vk::MemoryPropertyFlagBits::eDeviceLocal)};
texture.device_memory = device.allocateMemory(memory_allocation);
device.bindImageMemory(texture.image, texture.device_memory, 0);
vk::CommandBuffer copy_command = vkb::common::allocate_command_buffer(get_device().get_handle(), get_device().get_command_pool().get_handle());
copy_command.begin(vk::CommandBufferBeginInfo());
// Optimal image will be used as destination for the copy, so we must transfer from our initial undefined image layout to the transfer destination layout
vkb::common::image_layout_transition(copy_command, texture.image, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal);
// Copy the first mip of the chain, remaining mips will be generated
vk::BufferImageCopy buffer_copy_region{.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1},
.imageExtent = {texture.extent.width, texture.extent.height, 1}};
copy_command.copyBufferToImage(staging_buffer.get_handle(), texture.image, vk::ImageLayout::eTransferDstOptimal, buffer_copy_region);
// Transition first mip level to transfer source so we can blit(read) from it
vkb::common::image_layout_transition(copy_command, texture.image, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eTransferSrcOptimal);
get_device().flush_command_buffer(copy_command, queue, true);
// Generate the mip chain
// ---------------------------------------------------------------
// We copy down the whole mip chain doing a blit from mip-1 to mip
// An alternative way would be to always blit from the first mip level and sample that one down
vk::CommandBuffer blit_command = vkb::common::allocate_command_buffer(get_device().get_handle(), get_device().get_command_pool().get_handle());
blit_command.begin(vk::CommandBufferBeginInfo());
// Copy down mips from n-1 to n
for (uint32_t i = 1; i < texture.mip_levels; i++)
{
vk::ImageBlit image_blit{
{vk::ImageAspectFlagBits::eColor, i - 1, 0, 1},
{{{{},
{static_cast<int32_t>(texture.extent.width >> (i - 1)),
static_cast<int32_t>(texture.extent.height >> (i - 1)),
static_cast<int32_t>(1)}}}},
{vk::ImageAspectFlagBits::eColor, i, 0, 1},
{{{{}, {static_cast<int32_t>(texture.extent.width >> i), static_cast<int32_t>(texture.extent.height >> i), static_cast<int32_t>(1)}}}}};
// Prepare current mip level as image blit destination
vk::ImageSubresourceRange image_subresource_range{vk::ImageAspectFlagBits::eColor, i, 1, 0, 1};
vkb::common::image_layout_transition(
blit_command, texture.image, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, image_subresource_range);
// Blit from previous level
blit_command.blitImage(texture.image, vk::ImageLayout::eTransferSrcOptimal, texture.image, vk::ImageLayout::eTransferDstOptimal, image_blit, vk::Filter::eLinear);
// Prepare current mip level as image blit source for next level
vkb::common::image_layout_transition(
blit_command, texture.image, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eTransferSrcOptimal, image_subresource_range);
}
// After the loop, all mip layers are in TRANSFER_SRC layout, so transition all to SHADER_READ
vkb::common::image_layout_transition(blit_command,
texture.image,
vk::ImageLayout::eTransferSrcOptimal,
vk::ImageLayout::eShaderReadOnlyOptimal,
{vk::ImageAspectFlagBits::eColor, 0, texture.mip_levels, 0, 1});
get_device().flush_command_buffer(blit_command, queue, true);
// ---------------------------------------------------------------
// Create samplers for different mip map demonstration cases
// Without mip mapping
samplers[0] = vkb::common::create_sampler(get_device().get_gpu().get_handle(), get_device().get_handle(), format,
vk::Filter::eLinear, vk::SamplerAddressMode::eRepeat, 1.0f, 0.0f);
// With mip mapping
samplers[1] =
vkb::common::create_sampler(get_device().get_gpu().get_handle(), get_device().get_handle(), format,
vk::Filter::eLinear, vk::SamplerAddressMode::eRepeat, 1.0f, static_cast<float>(texture.mip_levels));
// With mip mapping and anisotropic filtering (when supported)
samplers[2] = vkb::common::create_sampler(
get_device().get_gpu().get_handle(),
get_device().get_handle(),
format,
vk::Filter::eLinear,
vk::SamplerAddressMode::eRepeat,
get_device().get_gpu().get_features().samplerAnisotropy ? (get_device().get_gpu().get_properties().limits.maxSamplerAnisotropy) : 1.0f,
static_cast<float>(texture.mip_levels));
// Create image view
texture.view = vkb::common::create_image_view(
get_device().get_handle(), texture.image, vk::ImageViewType::e2D, format, vk::ImageAspectFlagBits::eColor, 0, texture.mip_levels);
}
void HPPTextureMipMapGeneration::prepare_camera()
{
camera.type = vkb::CameraType::FirstPerson;
camera.set_perspective(60.0f, static_cast<float>(extent.width) / static_cast<float>(extent.height), 0.1f, 1024.0f);
camera.set_translation(glm::vec3(0.0f, 0.0f, -12.5f));
}
void HPPTextureMipMapGeneration::prepare_uniform_buffers()
{
// Shared parameter uniform buffer block
uniform_buffer = std::make_unique<vkb::core::BufferCpp>(get_device(),
sizeof(ubo),
vk::BufferUsageFlagBits::eUniformBuffer,
VMA_MEMORY_USAGE_CPU_TO_GPU);
update_uniform_buffers();
}
void HPPTextureMipMapGeneration::update_descriptor_set()
{
vk::DescriptorBufferInfo buffer_descriptor{uniform_buffer->get_handle(), 0, vk::WholeSize};
vk::DescriptorImageInfo image_descriptor{nullptr, texture.view, vk::ImageLayout::eShaderReadOnlyOptimal};
std::array<vk::DescriptorImageInfo, 3> sampler_descriptors = {{{samplers[0], nullptr, vk::ImageLayout::eShaderReadOnlyOptimal},
{samplers[1], nullptr, vk::ImageLayout::eShaderReadOnlyOptimal},
{samplers[2], nullptr, vk::ImageLayout::eShaderReadOnlyOptimal}}};
assert(samplers.size() == sampler_descriptors.size());
std::array<vk::WriteDescriptorSet, 3> write_descriptor_sets = {{{.dstSet = descriptor_set,
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &buffer_descriptor}, // Binding 0 : Vertex shader uniform buffer
{.dstSet = descriptor_set,
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eSampledImage,
.pImageInfo = &image_descriptor}, // Binding 1 : Fragment shader texture sampler
{.dstSet = descriptor_set,
.dstBinding = 2,
.descriptorCount = static_cast<uint32_t>(sampler_descriptors.size()),
.descriptorType = vk::DescriptorType::eSampler,
.pImageInfo = sampler_descriptors.data()}}}; // Binding 2: Sampler array
get_device().get_handle().updateDescriptorSets(write_descriptor_sets, {});
}
void HPPTextureMipMapGeneration::update_uniform_buffers(float delta_time)
{
ubo.projection = camera.matrices.perspective;
ubo.model = camera.matrices.view;
ubo.model = glm::rotate(ubo.model, glm::radians(90.0f + timer * 360.0f), glm::vec3(0.0f, 0.0f, 1.0f));
ubo.model = glm::scale(ubo.model, glm::vec3(0.5f));
timer += delta_time * 0.005f;
if (timer > 1.0f)
{
timer -= 1.0f;
}
uniform_buffer->convert_and_update(ubo);
}
std::unique_ptr<vkb::Application> create_hpp_texture_mipmap_generation()
{
return std::make_unique<HPPTextureMipMapGeneration>();
}
@@ -0,0 +1,90 @@
/* Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 the "License";
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Runtime mip map generation, using vulkan.hpp
*/
#pragma once
#include <hpp_api_vulkan_sample.h>
#include <ktx.h>
class HPPTextureMipMapGeneration : public HPPApiVulkanSample
{
public:
HPPTextureMipMapGeneration();
~HPPTextureMipMapGeneration();
private:
struct Texture
{
vk::Image image;
vk::DeviceMemory device_memory;
vk::ImageView view;
vk::Extent2D extent;
uint32_t mip_levels;
};
struct UBO
{
glm::mat4 projection;
glm::mat4 model;
float lod_bias = 0.0f;
int32_t sampler_index = 2;
};
private:
// from vkb::Application
bool prepare(const vkb::ApplicationOptions &options) override;
// from vkb::VulkanSample
void request_gpu_features(vkb::core::HPPPhysicalDevice &gpu) override;
// from HPPApiVulkanSample
void build_command_buffers() override;
void on_update_ui_overlay(vkb::Drawer &drawer) override;
void render(float delta_time) override;
void view_changed() override;
void check_format_features(vk::Format) const;
vk::DescriptorPool create_descriptor_pool();
vk::DescriptorSetLayout create_descriptor_set_layout();
vk::Pipeline create_pipeline();
void draw();
void load_assets();
void prepare_camera();
void prepare_uniform_buffers();
void update_descriptor_set();
void update_uniform_buffers(float delta_time = 0.0f);
private:
vk::DescriptorSet descriptor_set;
vk::DescriptorSetLayout descriptor_set_layout;
vk::Pipeline pipeline;
vk::PipelineLayout pipeline_layout;
bool rotate_scene = false;
// To demonstrate mip mapping and filtering this example uses separate samplers
std::vector<std::string> sampler_names{"No mip maps", "Mip maps (bilinear)", "Mip maps (anisotropic)"};
std::array<vk::Sampler, 3> samplers;
std::unique_ptr<vkb::scene_graph::components::HPPSubMesh> scene;
Texture texture;
UBO ubo;
std::unique_ptr<vkb::core::BufferCpp> uniform_buffer;
};
std::unique_ptr<vkb::Application> create_hpp_texture_mipmap_generation();