init
This commit is contained in:
@@ -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();
|
||||
Reference in New Issue
Block a user