diff --git a/assets/shader.frag b/assets/shader.frag index 782c404..9face2b 100644 Binary files a/assets/shader.frag and b/assets/shader.frag differ diff --git a/assets/shader.vert b/assets/shader.vert index ca41ab9..d91edd9 100644 Binary files a/assets/shader.vert and b/assets/shader.vert differ diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index a465269..76da5fc 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -43,3 +43,9 @@ - [Command Block](memory/command_block.md) - [Device Buffers](memory/device_buffers.md) - [Images](memory/images.md) +- [Descriptor Sets](descriptor_sets/README.md) + - [Pipeline Layout](descriptor_sets/pipeline_layout.md) + - [Shader Buffer](descriptor_sets/shader_buffer.md) + - [Texture](descriptor_sets/texture.md) + - [View Matrix](descriptor_sets/view_matrix.md) + - [Instanced Rendering](descriptor_sets/instanced_rendering.md) diff --git a/guide/src/descriptor_sets/README.md b/guide/src/descriptor_sets/README.md new file mode 100644 index 0000000..ecdea30 --- /dev/null +++ b/guide/src/descriptor_sets/README.md @@ -0,0 +1,5 @@ +# Descriptor Sets + +[Vulkan Descriptor](https://docs.vulkan.org/guide/latest/mapping_data_to_shaders.html#descriptors)s are essentially typed pointers to resources that shaders can use, eg uniform/storage buffers or combined image samplers (textures with samplers). A Descriptor Set is a collection of descriptors at various **bindings** that is bound together as an atomic unit. Shaders can declare input based on these set and binding numbers, and any sets the shader uses must have been updated and bound before drawing. A Descriptor Set Layout is a description of a collection of descriptor sets associated with a particular set number, usually describing all the sets in a shader. Descriptor sets are allocated using a Descriptor Pool and the desired set layout(s). + +Structuring set layouts and managing descriptor sets are complex topics with many viable approaches, each with their pros and cons. Some robust ones are described in this [page](https://docs.vulkan.org/samples/latest/samples/performance/descriptor_management/README.html). 2D frameworks - and even simple/basic 3D ones - can simply allocate and update sets every frame, as described in the docs as the "simplest approach". Here's an [extremely detailed](https://zeux.io/2020/02/27/writing-an-efficient-vulkan-renderer/) - albeit a bit dated now - post by Arseny on the subject. A more modern approach, namely "bindless" or Descriptor Indexing, is described in the official docs [here](https://docs.vulkan.org/samples/latest/samples/extensions/descriptor_indexing/README.html). diff --git a/guide/src/descriptor_sets/instanced_rendering.md b/guide/src/descriptor_sets/instanced_rendering.md new file mode 100644 index 0000000..019d2e6 --- /dev/null +++ b/guide/src/descriptor_sets/instanced_rendering.md @@ -0,0 +1,132 @@ +# Instanced Rendering + +When multiple copies of a drawable object are desired, one option is to use instanced rendering. The basic idea is to store per-instance data in a uniform/storage buffer and index into it in the vertex shader. We shall represent one model matrix per instance, feel free to add more data like an overall tint (color) that gets multiplied to the existing output color in the fragment shader. This will be bound to a Storage Buffer (SSBO), which can be "unbounded" in the shader (size is determined during invocation). + +Store the SSBO and a buffer for instance matrices: + +```cpp +std::vector m_instance_data{}; // model matrices. +std::optional m_instance_ssbo{}; +``` + +Add two `Transform`s as the source of rendering instances, and a function to update the matrices: + +```cpp +void update_instances(); + +// ... +std::array m_instances{}; // generates model matrices. + +// ... +void App::update_instances() { + m_instance_data.clear(); + m_instance_data.reserve(m_instances.size()); + for (auto const& transform : m_instances) { + m_instance_data.push_back(transform.model_matrix()); + } + // can't use bit_cast anymore, reinterpret data as a byte array instead. + auto const span = std::span{m_instance_data}; + void* data = span.data(); + auto const bytes = + std::span{static_cast(data), span.size_bytes()}; + m_instance_ssbo->write_at(m_frame_index, bytes); +} +``` + +Update the descriptor pool to also provide storage buffers: + +```cpp +// ... +vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2}, +vk::DescriptorPoolSize{vk::DescriptorType::eStorageBuffer, 2}, +``` + +This time add a new binding to set 1 (instead of a new set): + +```cpp +static constexpr auto set_1_bindings_v = std::array{ + layout_binding(0, vk::DescriptorType::eCombinedImageSampler), + layout_binding(1, vk::DescriptorType::eStorageBuffer), +}; +``` + +Create the instance SSBO after the view UBO: + +```cpp +m_instance_ssbo.emplace(m_allocator.get(), m_gpu.queue_family, + vk::BufferUsageFlagBits::eStorageBuffer); +``` + +Call `update_instances()` after `update_view()`: + +```cpp +// ... +update_view(); +update_instances(); +``` + +Extract transform inspection into a lambda and inspect each instance transform too: + +```cpp +static auto const inspect_transform = [](Transform& out) { + ImGui::DragFloat2("position", &out.position.x); + ImGui::DragFloat("rotation", &out.rotation); + ImGui::DragFloat2("scale", &out.scale.x, 0.1f); +}; + +ImGui::Separator(); +if (ImGui::TreeNode("View")) { + inspect_transform(m_view_transform); + ImGui::TreePop(); +} + +ImGui::Separator(); +if (ImGui::TreeNode("Instances")) { + for (std::size_t i = 0; i < m_instances.size(); ++i) { + auto const label = std::to_string(i); + if (ImGui::TreeNode(label.c_str())) { + inspect_transform(m_instances.at(i)); + ImGui::TreePop(); + } + } + ImGui::TreePop(); +} +``` + +Add another descriptor write for the SSBO: + +```cpp +auto writes = std::array{}; +// ... +auto const instance_ssbo_info = + m_instance_ssbo->descriptor_info_at(m_frame_index); +write.setBufferInfo(instance_ssbo_info) + .setDescriptorType(vk::DescriptorType::eStorageBuffer) + .setDescriptorCount(1) + .setDstSet(set1) + .setDstBinding(1); +writes[2] = write; +``` + +Finally, change the instance count in the draw call: + +```cpp +auto const instances = static_cast(m_instances.size()); +// m_vbo has 6 indices. +command_buffer.drawIndexed(6, instances, 0, 0, 0); +``` + +Update the vertex shader to incorporate the instance model matrix: + +```glsl +// ... +layout (set = 1, binding = 1) readonly buffer Instances { + mat4 mat_ms[]; +}; + +// ... +const mat4 mat_m = mat_ms[gl_InstanceIndex]; +const vec4 world_pos = mat_m * vec4(a_pos, 0.0, 1.0); +``` + +![Instanced Rendering](./instanced_rendering.png) diff --git a/guide/src/descriptor_sets/instanced_rendering.png b/guide/src/descriptor_sets/instanced_rendering.png new file mode 100644 index 0000000..766a49c Binary files /dev/null and b/guide/src/descriptor_sets/instanced_rendering.png differ diff --git a/guide/src/descriptor_sets/pipeline_layout.md b/guide/src/descriptor_sets/pipeline_layout.md new file mode 100644 index 0000000..9e16f3e --- /dev/null +++ b/guide/src/descriptor_sets/pipeline_layout.md @@ -0,0 +1,81 @@ +# Pipeline Layout + +A [Vulkan Pipeline Layout](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineLayout.html) represents a sequence of descriptor sets (and push constants) associated with a shader program. Even when using Shader Objects, a Pipeline Layout is needed to utilize descriptor sets. + +Starting with the layout of a single descriptor set containing a uniform buffer to set the view/projection matrices in, store a descriptor pool in `App` and create it before the shader: + +```cpp +vk::UniqueDescriptorPool m_descriptor_pool{}; + +// ... +void App::create_descriptor_pool() { + static constexpr auto pool_sizes_v = std::array{ + // 2 uniform buffers, can be more if desired. + vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2}, + }; + auto pool_ci = vk::DescriptorPoolCreateInfo{}; + // allow 16 sets to be allocated from this pool. + pool_ci.setPoolSizes(pool_sizes_v).setMaxSets(16); + m_descriptor_pool = m_device->createDescriptorPoolUnique(pool_ci); +} +``` + +Add new members to `App` to store the set layouts and pipeline layout. `m_set_layout_views` is just a copy of the descriptor set layout handles in a contiguous vector: + +```cpp +std::vector m_set_layouts{}; +std::vector m_set_layout_views{}; +vk::UniquePipelineLayout m_pipeline_layout{}; + +// ... +constexpr auto layout_binding(std::uint32_t binding, + vk::DescriptorType const type) { + return vk::DescriptorSetLayoutBinding{ + binding, type, 1, vk::ShaderStageFlagBits::eAllGraphics}; +} + +// ... +void App::create_pipeline_layout() { + static constexpr auto set_0_bindings_v = std::array{ + layout_binding(0, vk::DescriptorType::eUniformBuffer), + }; + auto set_layout_cis = std::array{}; + set_layout_cis[0].setBindings(set_0_bindings_v); + + for (auto const& set_layout_ci : set_layout_cis) { + m_set_layouts.push_back( + m_device->createDescriptorSetLayoutUnique(set_layout_ci)); + m_set_layout_views.push_back(*m_set_layouts.back()); + } + + auto pipeline_layout_ci = vk::PipelineLayoutCreateInfo{}; + pipeline_layout_ci.setSetLayouts(m_set_layout_views); + m_pipeline_layout = + m_device->createPipelineLayoutUnique(pipeline_layout_ci); +} +``` + +Add a helper function that allocates a set of descriptor sets for the entire layout: + +```cpp +auto App::allocate_sets() const -> std::vector { + auto allocate_info = vk::DescriptorSetAllocateInfo{}; + allocate_info.setDescriptorPool(*m_descriptor_pool) + .setSetLayouts(m_set_layout_views); + return m_device->allocateDescriptorSets(allocate_info); +} +``` + +Store a Buffered copy of descriptor sets for one drawable object: + +```cpp +Buffered> m_descriptor_sets{}; + +// ... + +void App::create_descriptor_sets() { + for (auto& descriptor_sets : m_descriptor_sets) { + descriptor_sets = allocate_sets(); + } +} +``` diff --git a/guide/src/descriptor_sets/rgby_texture.png b/guide/src/descriptor_sets/rgby_texture.png new file mode 100644 index 0000000..25b0c6d Binary files /dev/null and b/guide/src/descriptor_sets/rgby_texture.png differ diff --git a/guide/src/descriptor_sets/shader_buffer.md b/guide/src/descriptor_sets/shader_buffer.md new file mode 100644 index 0000000..d2e03ea --- /dev/null +++ b/guide/src/descriptor_sets/shader_buffer.md @@ -0,0 +1,173 @@ +# Shader Buffer + +Uniform and Storage buffers need to be N-buffered unless they are "GPU const", ie contents do not change after creation. Encapsulate a `vma::Buffer` per virtual frame in a `ShaderBuffer`: + +```cpp +class ShaderBuffer { + public: + explicit ShaderBuffer(VmaAllocator allocator, std::uint32_t queue_family, + vk::BufferUsageFlags usage); + + void write_at(std::size_t frame_index, std::span bytes); + + [[nodiscard]] auto descriptor_info_at(std::size_t frame_index) const + -> vk::DescriptorBufferInfo; + + private: + struct Buffer { + vma::Buffer buffer{}; + vk::DeviceSize size{}; + }; + + void write_to(Buffer& out, std::span bytes) const; + + VmaAllocator m_allocator{}; + std::uint32_t m_queue_family{}; + vk::BufferUsageFlags m_usage{}; + Buffered m_buffers{}; +}; +``` + +The implementation is fairly straightforward, it reuses existing buffers if they are large enough, else recreates them before copying data. It also ensures buffers are always valid to be bound to descriptors. + +```cpp +ShaderBuffer::ShaderBuffer(VmaAllocator allocator, + std::uint32_t const queue_family, + vk::BufferUsageFlags const usage) + : m_allocator(allocator), m_queue_family(queue_family), m_usage(usage) { + // ensure buffers are created and can be bound after returning. + for (auto& buffer : m_buffers) { write_to(buffer, {}); } +} + +void ShaderBuffer::write_at(std::size_t const frame_index, + std::span bytes) { + write_to(m_buffers.at(frame_index), bytes); +} + +auto ShaderBuffer::descriptor_info_at(std::size_t const frame_index) const + -> vk::DescriptorBufferInfo { + auto const& buffer = m_buffers.at(frame_index); + auto ret = vk::DescriptorBufferInfo{}; + ret.setBuffer(buffer.buffer.get().buffer).setRange(buffer.size); + return ret; +} + +void ShaderBuffer::write_to(Buffer& out, + std::span bytes) const { + static constexpr auto blank_byte_v = std::array{std::byte{}}; + // fallback to an empty byte if bytes is empty. + if (bytes.empty()) { bytes = blank_byte_v; } + out.size = bytes.size(); + if (out.buffer.get().size < bytes.size()) { + // size is too small (or buffer doesn't exist yet), recreate buffer. + auto const buffer_ci = vma::BufferCreateInfo{ + .allocator = m_allocator, + .usage = m_usage, + .queue_family = m_queue_family, + }; + out.buffer = vma::create_buffer(buffer_ci, vma::BufferMemoryType::Host, + out.size); + } + std::memcpy(out.buffer.get().mapped, bytes.data(), bytes.size()); +} +``` + +Store a `ShaderBuffer` in `App` and rename `create_vertex_buffer()` to `create_shader_resources()`: + +```cpp +std::optional m_view_ubo{}; + +// ... +m_vbo = vma::create_device_buffer(buffer_ci, create_command_block(), + total_bytes_v); + +m_view_ubo.emplace(m_allocator.get(), m_gpu.queue_family, + vk::BufferUsageFlagBits::eUniformBuffer); +``` + +Add functions to update the view/projection matrices and bind the frame's descriptor sets: + +```cpp +void App::update_view() { + auto const half_size = 0.5f * glm::vec2{m_framebuffer_size}; + auto const mat_projection = + glm::ortho(-half_size.x, half_size.x, -half_size.y, half_size.y); + auto const bytes = + std::bit_cast>( + mat_projection); + m_view_ubo->write_at(m_frame_index, bytes); +} + +// ... +void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const { + auto writes = std::array{}; + auto const& descriptor_sets = m_descriptor_sets.at(m_frame_index); + auto const set0 = descriptor_sets[0]; + auto write = vk::WriteDescriptorSet{}; + auto const view_ubo_info = m_view_ubo->descriptor_info_at(m_frame_index); + write.setBufferInfo(view_ubo_info) + .setDescriptorType(vk::DescriptorType::eUniformBuffer) + .setDescriptorCount(1) + .setDstSet(set0) + .setDstBinding(0); + writes[0] = write; + m_device->updateDescriptorSets(writes, {}); + + command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + *m_pipeline_layout, 0, descriptor_sets, + {}); +} +``` + +Add the descriptor set layouts to the Shader, call `update_view()` before `draw()`, and `bind_descriptor_sets()` in `draw()`: + +```cpp +auto const shader_ci = ShaderProgram::CreateInfo{ + .device = *m_device, + .vertex_spirv = vertex_spirv, + .fragment_spirv = fragment_spirv, + .vertex_input = vertex_input_v, + .set_layouts = m_set_layout_views, +}; + +// ... +inspect(); +update_view(); +draw(command_buffer); + +// ... +m_shader->bind(command_buffer, m_framebuffer_size); +bind_descriptor_sets(command_buffer); +// ... +``` + +Update the vertex shader to use the view UBO: + +```glsl +layout (set = 0, binding = 0) uniform View { + mat4 mat_vp; +}; + +// ... +void main() { + const vec4 world_pos = vec4(a_pos, 0.0, 1.0); + + out_color = a_color; + gl_Position = mat_vp * world_pos; +} +``` + +Since the projected space is now the framebuffer size instead of [-1, 1], update the vertex positions to be larger than 1 pixel: + +```cpp +static constexpr auto vertices_v = std::array{ + Vertex{.position = {-200.0f, -200.0f}, .color = {1.0f, 0.0f, 0.0f}}, + Vertex{.position = {200.0f, -200.0f}, .color = {0.0f, 1.0f, 0.0f}}, + Vertex{.position = {200.0f, 200.0f}, .color = {0.0f, 0.0f, 1.0f}}, + Vertex{.position = {-200.0f, 200.0f}, .color = {1.0f, 1.0f, 0.0f}}, +}; +``` + +![View UBO](./view_ubo.png) + +When such shader buffers are created and (more importantly) destroyed dynamically, they would need to store a `ScopedWaiter` to ensure all rendering with descriptor sets bound to them completes before destruction. Alternatively, the app can maintain a pool of scratch buffers (similar to small/dynamic vertex buffers) per virtual frame which get destroyed in a batch instead of individually. diff --git a/guide/src/descriptor_sets/texture.md b/guide/src/descriptor_sets/texture.md new file mode 100644 index 0000000..bac0b80 --- /dev/null +++ b/guide/src/descriptor_sets/texture.md @@ -0,0 +1,247 @@ +# Texture + +With a large part of the complexity wrapped away in `vma`, a `Texture` is just a combination of three things: + +1. Sampled Image +2. (Unique) Image View of above +3. (Unique) Sampler + +In `texture.hpp`, create a default sampler: + +```cpp +[[nodiscard]] constexpr auto +create_sampler_ci(vk::SamplerAddressMode const wrap, vk::Filter const filter) { + auto ret = vk::SamplerCreateInfo{}; + ret.setAddressModeU(wrap) + .setAddressModeV(wrap) + .setAddressModeW(wrap) + .setMinFilter(filter) + .setMagFilter(filter) + .setMaxLod(VK_LOD_CLAMP_NONE) + .setBorderColor(vk::BorderColor::eFloatTransparentBlack) + .setMipmapMode(vk::SamplerMipmapMode::eNearest); + return ret; +} + +constexpr auto sampler_ci_v = create_sampler_ci( + vk::SamplerAddressMode::eClampToEdge, vk::Filter::eLinear); +``` + +Define the Create Info and Texture types: + +```cpp +struct TextureCreateInfo { + vk::Device device; + VmaAllocator allocator; + std::uint32_t queue_family; + CommandBlock command_block; + Bitmap bitmap; + + vk::SamplerCreateInfo sampler{sampler_ci_v}; +}; + +class Texture { + public: + using CreateInfo = TextureCreateInfo; + + explicit Texture(CreateInfo create_info); + + [[nodiscard]] auto descriptor_info() const -> vk::DescriptorImageInfo; + + private: + vma::Image m_image{}; + vk::UniqueImageView m_view{}; + vk::UniqueSampler m_sampler{}; +}; +``` + +Add a fallback bitmap constant, and the implementation: + +```cpp +// 4-channels. +constexpr auto white_pixel_v = std::array{std::byte{0xff}, std::byte{0xff}, + std::byte{0xff}, std::byte{0xff}}; +// fallback bitmap. +constexpr auto white_bitmap_v = Bitmap{ + .bytes = white_pixel_v, + .size = {1, 1}, +}; + +// ... +Texture::Texture(CreateInfo create_info) { + if (create_info.bitmap.bytes.empty() || create_info.bitmap.size.x <= 0 || + create_info.bitmap.size.y <= 0) { + create_info.bitmap = white_bitmap_v; + } + + auto const image_ci = vma::ImageCreateInfo{ + .allocator = create_info.allocator, + .queue_family = create_info.queue_family, + }; + m_image = vma::create_sampled_image( + image_ci, std::move(create_info.command_block), create_info.bitmap); + + auto image_view_ci = vk::ImageViewCreateInfo{}; + auto subresource_range = vk::ImageSubresourceRange{}; + subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) + .setLayerCount(1) + .setLevelCount(m_image.get().levels); + + image_view_ci.setImage(m_image.get().image) + .setViewType(vk::ImageViewType::e2D) + .setFormat(m_image.get().format) + .setSubresourceRange(subresource_range); + m_view = create_info.device.createImageViewUnique(image_view_ci); + + m_sampler = create_info.device.createSamplerUnique(create_info.sampler); +} + +auto Texture::descriptor_info() const -> vk::DescriptorImageInfo { + auto ret = vk::DescriptorImageInfo{}; + ret.setImageView(*m_view) + .setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal) + .setSampler(*m_sampler); + return ret; +} +``` + +To sample textures, `Vertex` will need a UV coordinate: + +```cpp +struct Vertex { + glm::vec2 position{}; + glm::vec3 color{1.0f}; + glm::vec2 uv{}; +}; + +// two vertex attributes: position at 0, color at 1. +constexpr auto vertex_attributes_v = std::array{ + // the format matches the type and layout of data: vec2 => 2x 32-bit floats. + vk::VertexInputAttributeDescription2EXT{0, 0, vk::Format::eR32G32Sfloat, + offsetof(Vertex, position)}, + // vec3 => 3x 32-bit floats + vk::VertexInputAttributeDescription2EXT{1, 0, vk::Format::eR32G32B32Sfloat, + offsetof(Vertex, color)}, + // vec2 => 2x 32-bit floats + vk::VertexInputAttributeDescription2EXT{2, 0, vk::Format::eR32G32Sfloat, + offsetof(Vertex, uv)}, +}; +``` + +Store a texture in `App` and create with the other shader resources: + +```cpp +std::optional m_texture{}; + +// ... +using Pixel = std::array; +static constexpr auto rgby_pixels_v = std::array{ + Pixel{std::byte{0xff}, {}, {}, std::byte{0xff}}, + Pixel{std::byte{}, std::byte{0xff}, {}, std::byte{0xff}}, + Pixel{std::byte{}, {}, std::byte{0xff}, std::byte{0xff}}, + Pixel{std::byte{0xff}, std::byte{0xff}, {}, std::byte{0xff}}, +}; +static constexpr auto rgby_bytes_v = + std::bit_cast>( + rgby_pixels_v); +static constexpr auto rgby_bitmap_v = Bitmap{ + .bytes = rgby_bytes_v, + .size = {2, 2}, +}; +auto texture_ci = Texture::CreateInfo{ + .device = *m_device, + .allocator = m_allocator.get(), + .queue_family = m_gpu.queue_family, + .command_block = create_command_block(), + .bitmap = rgby_bitmap_v, +}; +// use Nearest filtering instead of Linear (interpolation). +texture_ci.sampler.setMagFilter(vk::Filter::eNearest); +m_texture.emplace(std::move(texture_ci)); +``` + +Update the descriptor pool sizes to also contain Combined Image Samplers: + +```cpp +/// ... +vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2}, +vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2}, +``` + +Set up a new descriptor set (number 1) with a combined image sampler at binding 0. This could be added to binding 1 of set 0 as well, since we are not optimizing binding calls (eg binding set 0 only once for multiple draws): + +```cpp +static constexpr auto set_1_bindings_v = std::array{ + layout_binding(0, vk::DescriptorType::eCombinedImageSampler), +}; +auto set_layout_cis = std::array{}; +set_layout_cis[0].setBindings(set_0_bindings_v); +set_layout_cis[1].setBindings(set_1_bindings_v); +``` + +Remove the vertex colors and set the UVs for the quad. In Vulkan UV space is the same as GLFW window space: origin is at the top left, +X moves right, +Y moves down. + +```cpp +static constexpr auto vertices_v = std::array{ + Vertex{.position = {-200.0f, -200.0f}, .uv = {0.0f, 1.0f}}, + Vertex{.position = {200.0f, -200.0f}, .uv = {1.0f, 1.0f}}, + Vertex{.position = {200.0f, 200.0f}, .uv = {1.0f, 0.0f}}, + Vertex{.position = {-200.0f, 200.0f}, .uv = {0.0f, 0.0f}}, +}; +``` + +Finally, update the descriptor writes: + +```cpp +auto writes = std::array{}; +// ... +auto const set1 = descriptor_sets[1]; +auto const image_info = m_texture->descriptor_info(); +write.setImageInfo(image_info) + .setDescriptorType(vk::DescriptorType::eCombinedImageSampler) + .setDescriptorCount(1) + .setDstSet(set1) + .setDstBinding(0); +writes[1] = write; +``` + +Since the Texture is not N-buffered (because it is "GPU const"), in this case the sets could also be updated once after texture creation instead of every frame. + +Add the UV vertex attribute the vertex shader and pass it to the fragment shader: + +```glsl +layout (location = 2) in vec2 a_uv; + +// ... +layout (location = 1) out vec2 out_uv; + +// ... +out_color = a_color; +out_uv = a_uv; +``` + +Add set 1 and the incoming UV coords to the fragment shader, combine the sampled texture color with the vertex color: + +```glsl +layout (set = 1, binding = 0) uniform sampler2D tex; + +// ... +layout (location = 1) in vec2 in_uv; + +// ... +out_color = vec4(in_color, 1.0) * texture(tex, in_uv); +``` + +![RGBY Texture](./rgby_texture.png) + +For generating mip-maps, follow the [sample in the Vulkan docs](https://docs.vulkan.org/samples/latest/samples/api/hpp_texture_mipmap_generation/README.html#_generating_the_mip_chain). The high-level steps are: + +1. Compute mip levels based on image size +1. Create an image with the desired mip levels +1. Copy the source data to the first mip level as usual +1. Transition the first mip level to TransferSrc +1. Iterate through all the remaining mip levels: + 1. Transition the current mip level to TransferDst + 1. Record an image blit operation from previous to current mip levels + 1. Transition the current mip level to TransferSrc +1. Transition all levels (entire image) to ShaderRead diff --git a/guide/src/descriptor_sets/view_matrix.md b/guide/src/descriptor_sets/view_matrix.md new file mode 100644 index 0000000..43cdc32 --- /dev/null +++ b/guide/src/descriptor_sets/view_matrix.md @@ -0,0 +1,79 @@ +# View Matrix + +Integrating the view matrix will be quite simple and short. First, transformations for objects and cameras/views can be encapsulated into a single struct: + +```cpp +struct Transform { + glm::vec2 position{}; + float rotation{}; + glm::vec2 scale{1.0f}; + + [[nodiscard]] auto model_matrix() const -> glm::mat4; + [[nodiscard]] auto view_matrix() const -> glm::mat4; +}; +``` + +Extracting the common logic into a helper, both member functions can be implemented easily: + +```cpp +namespace { +struct Matrices { + glm::mat4 translation; + glm::mat4 orientation; + glm::mat4 scale; +}; + +[[nodiscard]] auto to_matrices(glm::vec2 const position, float rotation, + glm::vec2 const scale) -> Matrices { + static constexpr auto mat_v = glm::identity(); + static constexpr auto axis_v = glm::vec3{0.0f, 0.0f, 1.0f}; + return Matrices{ + .translation = glm::translate(mat_v, glm::vec3{position, 0.0f}), + .orientation = glm::rotate(mat_v, glm::radians(rotation), axis_v), + .scale = glm::scale(mat_v, glm::vec3{scale, 1.0f}), + }; +} +} // namespace + +auto Transform::model_matrix() const -> glm::mat4 { + auto const [t, r, s] = to_matrices(position, rotation, scale); + // right to left: scale first, then rotate, then translate. + return t * r * s; +} + +auto Transform::view_matrix() const -> glm::mat4 { + // view matrix is the inverse of the model matrix. + // instead, perform translation and rotation in reverse order and with + // negative values. or, use glm::lookAt(). + // scale is kept unchanged as the first transformation for + // "intuitive" scaling on cameras. + auto const [t, r, s] = to_matrices(-position, -rotation, scale); + return r * t * s; +} +``` + +Add a `Transform` member to `App` to represent the view/camera, inspect its members, and combine with the existing projection matrix: + +```cpp +Transform m_view_transform{}; // generates view matrix. + +// ... +ImGui::Separator(); +if (ImGui::TreeNode("View")) { + ImGui::DragFloat2("position", &m_view_transform.position.x); + ImGui::DragFloat("rotation", &m_view_transform.rotation); + ImGui::DragFloat2("scale", &m_view_transform.scale.x); + ImGui::TreePop(); +} + +// ... +auto const mat_view = m_view_transform.view_matrix(); +auto const mat_vp = mat_projection * mat_view; +auto const bytes = + std::bit_cast>(mat_vp); +m_view_ubo->write_at(m_frame_index, bytes); +``` + +Naturally, moving the view left moves everything else - currently only a single RGBY quad - to the _right_. + +![View Matrix](./view_matrix.png) diff --git a/guide/src/descriptor_sets/view_matrix.png b/guide/src/descriptor_sets/view_matrix.png new file mode 100644 index 0000000..5070b91 Binary files /dev/null and b/guide/src/descriptor_sets/view_matrix.png differ diff --git a/guide/src/descriptor_sets/view_ubo.png b/guide/src/descriptor_sets/view_ubo.png new file mode 100644 index 0000000..0a56b53 Binary files /dev/null and b/guide/src/descriptor_sets/view_ubo.png differ diff --git a/src/app.cpp b/src/app.cpp index 3f5839e..5f2cdaa 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -18,6 +19,12 @@ template return std::bit_cast>(t); } +constexpr auto layout_binding(std::uint32_t binding, + vk::DescriptorType const type) { + return vk::DescriptorSetLayoutBinding{ + binding, type, 1, vk::ShaderStageFlagBits::eAllGraphics}; +} + [[nodiscard]] auto locate_assets_dir() -> fs::path { // look for '/assets/', starting from the working // directory and walking up the parent directory tree. @@ -88,10 +95,13 @@ void App::run() { create_swapchain(); create_render_sync(); create_imgui(); + create_descriptor_pool(); + create_pipeline_layout(); create_shader(); create_cmd_block_pool(); - create_vertex_buffer(); + create_shader_resources(); + create_descriptor_sets(); main_loop(); } @@ -243,6 +253,43 @@ void App::create_allocator() { m_allocator = vma::create_allocator(*m_instance, m_gpu.device, *m_device); } +void App::create_descriptor_pool() { + static constexpr auto pool_sizes_v = std::array{ + // 2 uniform buffers, can be more if desired. + vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2}, + vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2}, + vk::DescriptorPoolSize{vk::DescriptorType::eStorageBuffer, 2}, + }; + auto pool_ci = vk::DescriptorPoolCreateInfo{}; + // allow 16 sets to be allocated from this pool. + pool_ci.setPoolSizes(pool_sizes_v).setMaxSets(16); + m_descriptor_pool = m_device->createDescriptorPoolUnique(pool_ci); +} + +void App::create_pipeline_layout() { + static constexpr auto set_0_bindings_v = std::array{ + layout_binding(0, vk::DescriptorType::eUniformBuffer), + }; + static constexpr auto set_1_bindings_v = std::array{ + layout_binding(0, vk::DescriptorType::eCombinedImageSampler), + layout_binding(1, vk::DescriptorType::eStorageBuffer), + }; + auto set_layout_cis = std::array{}; + set_layout_cis[0].setBindings(set_0_bindings_v); + set_layout_cis[1].setBindings(set_1_bindings_v); + + for (auto const& set_layout_ci : set_layout_cis) { + m_set_layouts.push_back( + m_device->createDescriptorSetLayoutUnique(set_layout_ci)); + m_set_layout_views.push_back(*m_set_layouts.back()); + } + + auto pipeline_layout_ci = vk::PipelineLayoutCreateInfo{}; + pipeline_layout_ci.setSetLayouts(m_set_layout_views); + m_pipeline_layout = + m_device->createPipelineLayoutUnique(pipeline_layout_ci); +} + void App::create_shader() { auto const vertex_spirv = to_spir_v(asset_path("shader.vert")); auto const fragment_spirv = to_spir_v(asset_path("shader.frag")); @@ -256,7 +303,7 @@ void App::create_shader() { .vertex_spirv = vertex_spirv, .fragment_spirv = fragment_spirv, .vertex_input = vertex_input_v, - .set_layouts = {}, + .set_layouts = m_set_layout_views, }; m_shader.emplace(shader_ci); } @@ -271,13 +318,13 @@ void App::create_cmd_block_pool() { m_cmd_block_pool = m_device->createCommandPoolUnique(command_pool_ci); } -void App::create_vertex_buffer() { +void App::create_shader_resources() { // vertices of a quad. static constexpr auto vertices_v = std::array{ - Vertex{.position = {-0.5f, -0.5f}, .color = {1.0f, 0.0f, 0.0f}}, - Vertex{.position = {0.5f, -0.5f}, .color = {0.0f, 1.0f, 0.0f}}, - Vertex{.position = {0.5f, 0.5f}, .color = {0.0f, 0.0f, 1.0f}}, - Vertex{.position = {-0.5f, 0.5f}, .color = {1.0f, 1.0f, 0.0f}}, + Vertex{.position = {-200.0f, -200.0f}, .uv = {0.0f, 1.0f}}, + Vertex{.position = {200.0f, -200.0f}, .uv = {1.0f, 1.0f}}, + Vertex{.position = {200.0f, 200.0f}, .uv = {1.0f, 0.0f}}, + Vertex{.position = {-200.0f, 200.0f}, .uv = {0.0f, 0.0f}}, }; static constexpr auto indices_v = std::array{ 0u, 1u, 2u, 2u, 3u, 0u, @@ -298,6 +345,43 @@ void App::create_vertex_buffer() { }; m_vbo = vma::create_device_buffer(buffer_ci, create_command_block(), total_bytes_v); + + m_view_ubo.emplace(m_allocator.get(), m_gpu.queue_family, + vk::BufferUsageFlagBits::eUniformBuffer); + + m_instance_ssbo.emplace(m_allocator.get(), m_gpu.queue_family, + vk::BufferUsageFlagBits::eStorageBuffer); + + using Pixel = std::array; + static constexpr auto rgby_pixels_v = std::array{ + Pixel{std::byte{0xff}, {}, {}, std::byte{0xff}}, + Pixel{std::byte{}, std::byte{0xff}, {}, std::byte{0xff}}, + Pixel{std::byte{}, {}, std::byte{0xff}, std::byte{0xff}}, + Pixel{std::byte{0xff}, std::byte{0xff}, {}, std::byte{0xff}}, + }; + static constexpr auto rgby_bytes_v = + std::bit_cast>( + rgby_pixels_v); + static constexpr auto rgby_bitmap_v = Bitmap{ + .bytes = rgby_bytes_v, + .size = {2, 2}, + }; + auto texture_ci = Texture::CreateInfo{ + .device = *m_device, + .allocator = m_allocator.get(), + .queue_family = m_gpu.queue_family, + .command_block = create_command_block(), + .bitmap = rgby_bitmap_v, + }; + // use Nearest filtering instead of Linear (interpolation). + texture_ci.sampler.setMagFilter(vk::Filter::eNearest); + m_texture.emplace(std::move(texture_ci)); +} + +void App::create_descriptor_sets() { + for (auto& descriptor_sets : m_descriptor_sets) { + descriptor_sets = allocate_sets(); + } } auto App::asset_path(std::string_view const uri) const -> fs::path { @@ -308,6 +392,13 @@ auto App::create_command_block() const -> CommandBlock { return CommandBlock{*m_device, m_queue, *m_cmd_block_pool}; } +auto App::allocate_sets() const -> std::vector { + auto allocate_info = vk::DescriptorSetAllocateInfo{}; + allocate_info.setDescriptorPool(*m_descriptor_pool) + .setSetLayouts(m_set_layout_views); + return m_device->allocateDescriptorSets(allocate_info); +} + void App::main_loop() { while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) { glfwPollEvents(); @@ -397,6 +488,8 @@ void App::render(vk::CommandBuffer const command_buffer) { command_buffer.beginRendering(rendering_info); inspect(); + update_view(); + update_instances(); draw(command_buffer); command_buffer.endRendering(); @@ -476,18 +569,106 @@ void App::inspect() { ImGui::DragFloat("line width", &m_shader->line_width, 0.25f, line_width_range[0], line_width_range[1]); } + + static auto const inspect_transform = [](Transform& out) { + ImGui::DragFloat2("position", &out.position.x); + ImGui::DragFloat("rotation", &out.rotation); + ImGui::DragFloat2("scale", &out.scale.x, 0.1f); + }; + + ImGui::Separator(); + if (ImGui::TreeNode("View")) { + inspect_transform(m_view_transform); + ImGui::TreePop(); + } + + ImGui::Separator(); + if (ImGui::TreeNode("Instances")) { + for (std::size_t i = 0; i < m_instances.size(); ++i) { + auto const label = std::to_string(i); + if (ImGui::TreeNode(label.c_str())) { + inspect_transform(m_instances.at(i)); + ImGui::TreePop(); + } + } + ImGui::TreePop(); + } } ImGui::End(); } +void App::update_view() { + auto const half_size = 0.5f * glm::vec2{m_framebuffer_size}; + auto const mat_projection = + glm::ortho(-half_size.x, half_size.x, -half_size.y, half_size.y); + auto const mat_view = m_view_transform.view_matrix(); + auto const mat_vp = mat_projection * mat_view; + auto const bytes = + std::bit_cast>(mat_vp); + m_view_ubo->write_at(m_frame_index, bytes); +} + +void App::update_instances() { + m_instance_data.clear(); + m_instance_data.reserve(m_instances.size()); + for (auto const& transform : m_instances) { + m_instance_data.push_back(transform.model_matrix()); + } + // can't use bit_cast anymore, reinterpret data as a byte array instead. + auto const span = std::span{m_instance_data}; + void* data = span.data(); + auto const bytes = + std::span{static_cast(data), span.size_bytes()}; + m_instance_ssbo->write_at(m_frame_index, bytes); +} + void App::draw(vk::CommandBuffer const command_buffer) const { m_shader->bind(command_buffer, m_framebuffer_size); + bind_descriptor_sets(command_buffer); // single VBO at binding 0 at no offset. command_buffer.bindVertexBuffers(0, m_vbo.get().buffer, vk::DeviceSize{}); // u32 indices after offset of 4 vertices. command_buffer.bindIndexBuffer(m_vbo.get().buffer, 4 * sizeof(Vertex), vk::IndexType::eUint32); + auto const instances = static_cast(m_instances.size()); // m_vbo has 6 indices. - command_buffer.drawIndexed(6, 1, 0, 0, 0); + command_buffer.drawIndexed(6, instances, 0, 0, 0); +} + +void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const { + auto writes = std::array{}; + auto const& descriptor_sets = m_descriptor_sets.at(m_frame_index); + auto const set0 = descriptor_sets[0]; + auto write = vk::WriteDescriptorSet{}; + auto const view_ubo_info = m_view_ubo->descriptor_info_at(m_frame_index); + write.setBufferInfo(view_ubo_info) + .setDescriptorType(vk::DescriptorType::eUniformBuffer) + .setDescriptorCount(1) + .setDstSet(set0) + .setDstBinding(0); + writes[0] = write; + + auto const set1 = descriptor_sets[1]; + auto const image_info = m_texture->descriptor_info(); + write.setImageInfo(image_info) + .setDescriptorType(vk::DescriptorType::eCombinedImageSampler) + .setDescriptorCount(1) + .setDstSet(set1) + .setDstBinding(0); + writes[1] = write; + auto const instance_ssbo_info = + m_instance_ssbo->descriptor_info_at(m_frame_index); + write.setBufferInfo(instance_ssbo_info) + .setDescriptorType(vk::DescriptorType::eStorageBuffer) + .setDescriptorCount(1) + .setDstSet(set1) + .setDstBinding(1); + writes[2] = write; + + m_device->updateDescriptorSets(writes, {}); + + command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + *m_pipeline_layout, 0, descriptor_sets, + {}); } } // namespace lvk diff --git a/src/app.hpp b/src/app.hpp index 5bd094d..198518c 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -4,8 +4,11 @@ #include #include #include +#include #include #include +#include +#include #include #include #include @@ -38,12 +41,16 @@ class App { void create_render_sync(); void create_imgui(); void create_allocator(); + void create_descriptor_pool(); + void create_pipeline_layout(); void create_shader(); void create_cmd_block_pool(); - void create_vertex_buffer(); + void create_shader_resources(); + void create_descriptor_sets(); [[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path; [[nodiscard]] auto create_command_block() const -> CommandBlock; + [[nodiscard]] auto allocate_sets() const -> std::vector; void main_loop(); @@ -56,9 +63,13 @@ class App { // ImGui code goes here. void inspect(); + void update_view(); + void update_instances(); // Issue draw calls here. void draw(vk::CommandBuffer command_buffer) const; + void bind_descriptor_sets(vk::CommandBuffer command_buffer) const; + fs::path m_assets_dir{}; // the order of these RAII members is crucially important. @@ -82,14 +93,27 @@ class App { std::optional m_imgui{}; + vk::UniqueDescriptorPool m_descriptor_pool{}; + std::vector m_set_layouts{}; + std::vector m_set_layout_views{}; + vk::UniquePipelineLayout m_pipeline_layout{}; + std::optional m_shader{}; vma::Buffer m_vbo{}; + std::optional m_view_ubo{}; + std::optional m_texture{}; + std::vector m_instance_data{}; // model matrices. + std::optional m_instance_ssbo{}; + Buffered> m_descriptor_sets{}; glm::ivec2 m_framebuffer_size{}; std::optional m_render_target{}; bool m_wireframe{}; + Transform m_view_transform{}; // generates view matrix. + std::array m_instances{}; // generates model matrices. + // waiter must be the last member to ensure it blocks until device is idle // before other members get destroyed. ScopedWaiter m_waiter{}; diff --git a/src/glsl/shader.frag b/src/glsl/shader.frag index 93094d4..86be76b 100644 --- a/src/glsl/shader.frag +++ b/src/glsl/shader.frag @@ -1,9 +1,12 @@ #version 450 core +layout (set = 1, binding = 0) uniform sampler2D tex; + layout (location = 0) in vec3 in_color; +layout (location = 1) in vec2 in_uv; layout (location = 0) out vec4 out_color; void main() { - out_color = vec4(in_color, 1.0); + out_color = vec4(in_color, 1.0) * texture(tex, in_uv); } diff --git a/src/glsl/shader.vert b/src/glsl/shader.vert index edebf18..adf7066 100644 --- a/src/glsl/shader.vert +++ b/src/glsl/shader.vert @@ -2,12 +2,24 @@ layout (location = 0) in vec2 a_pos; layout (location = 1) in vec3 a_color; +layout (location = 2) in vec2 a_uv; + +layout (set = 0, binding = 0) uniform View { + mat4 mat_vp; +}; + +layout (set = 1, binding = 1) readonly buffer Instances { + mat4 mat_ms[]; +}; layout (location = 0) out vec3 out_color; +layout (location = 1) out vec2 out_uv; void main() { - const vec2 position = a_pos; + const mat4 mat_m = mat_ms[gl_InstanceIndex]; + const vec4 world_pos = mat_m * vec4(a_pos, 0.0, 1.0); out_color = a_color; - gl_Position = vec4(position, 0.0, 1.0); + out_uv = a_uv; + gl_Position = mat_vp * world_pos; } diff --git a/src/shader_buffer.cpp b/src/shader_buffer.cpp new file mode 100644 index 0000000..773c78a --- /dev/null +++ b/src/shader_buffer.cpp @@ -0,0 +1,43 @@ +#include + +namespace lvk { +ShaderBuffer::ShaderBuffer(VmaAllocator allocator, + std::uint32_t const queue_family, + vk::BufferUsageFlags const usage) + : m_allocator(allocator), m_queue_family(queue_family), m_usage(usage) { + // ensure buffers are created and can be bound after returning. + for (auto& buffer : m_buffers) { write_to(buffer, {}); } +} + +void ShaderBuffer::write_at(std::size_t const frame_index, + std::span bytes) { + write_to(m_buffers.at(frame_index), bytes); +} + +auto ShaderBuffer::descriptor_info_at(std::size_t const frame_index) const + -> vk::DescriptorBufferInfo { + auto const& buffer = m_buffers.at(frame_index); + auto ret = vk::DescriptorBufferInfo{}; + ret.setBuffer(buffer.buffer.get().buffer).setRange(buffer.size); + return ret; +} + +void ShaderBuffer::write_to(Buffer& out, + std::span bytes) const { + static constexpr auto blank_byte_v = std::array{std::byte{}}; + // fallback to an empty byte if bytes is empty. + if (bytes.empty()) { bytes = blank_byte_v; } + out.size = bytes.size(); + if (out.buffer.get().size < bytes.size()) { + // size is too small (or buffer doesn't exist yet), recreate buffer. + auto const buffer_ci = vma::BufferCreateInfo{ + .allocator = m_allocator, + .usage = m_usage, + .queue_family = m_queue_family, + }; + out.buffer = vma::create_buffer(buffer_ci, vma::BufferMemoryType::Host, + out.size); + } + std::memcpy(out.buffer.get().mapped, bytes.data(), bytes.size()); +} +} // namespace lvk diff --git a/src/shader_buffer.hpp b/src/shader_buffer.hpp new file mode 100644 index 0000000..c46a761 --- /dev/null +++ b/src/shader_buffer.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include + +namespace lvk { +class ShaderBuffer { + public: + explicit ShaderBuffer(VmaAllocator allocator, std::uint32_t queue_family, + vk::BufferUsageFlags usage); + + void write_at(std::size_t frame_index, std::span bytes); + + [[nodiscard]] auto descriptor_info_at(std::size_t frame_index) const + -> vk::DescriptorBufferInfo; + + private: + struct Buffer { + vma::Buffer buffer{}; + vk::DeviceSize size{}; + }; + + void write_to(Buffer& out, std::span bytes) const; + + VmaAllocator m_allocator{}; + std::uint32_t m_queue_family{}; + vk::BufferUsageFlags m_usage{}; + Buffered m_buffers{}; +}; +} // namespace lvk diff --git a/src/texture.cpp b/src/texture.cpp new file mode 100644 index 0000000..2c0b72d --- /dev/null +++ b/src/texture.cpp @@ -0,0 +1,51 @@ +#include +#include + +namespace lvk { +namespace { +// 4-channels. +constexpr auto white_pixel_v = std::array{std::byte{0xff}, std::byte{0xff}, + std::byte{0xff}, std::byte{0xff}}; +// fallback bitmap. +constexpr auto white_bitmap_v = Bitmap{ + .bytes = white_pixel_v, + .size = {1, 1}, +}; +} // namespace + +Texture::Texture(CreateInfo create_info) { + if (create_info.bitmap.bytes.empty() || create_info.bitmap.size.x <= 0 || + create_info.bitmap.size.y <= 0) { + create_info.bitmap = white_bitmap_v; + } + + auto const image_ci = vma::ImageCreateInfo{ + .allocator = create_info.allocator, + .queue_family = create_info.queue_family, + }; + m_image = vma::create_sampled_image( + image_ci, std::move(create_info.command_block), create_info.bitmap); + + auto image_view_ci = vk::ImageViewCreateInfo{}; + auto subresource_range = vk::ImageSubresourceRange{}; + subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) + .setLayerCount(1) + .setLevelCount(m_image.get().levels); + + image_view_ci.setImage(m_image.get().image) + .setViewType(vk::ImageViewType::e2D) + .setFormat(m_image.get().format) + .setSubresourceRange(subresource_range); + m_view = create_info.device.createImageViewUnique(image_view_ci); + + m_sampler = create_info.device.createSamplerUnique(create_info.sampler); +} + +auto Texture::descriptor_info() const -> vk::DescriptorImageInfo { + auto ret = vk::DescriptorImageInfo{}; + ret.setImageView(*m_view) + .setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal) + .setSampler(*m_sampler); + return ret; +} +} // namespace lvk diff --git a/src/texture.hpp b/src/texture.hpp new file mode 100644 index 0000000..185f25b --- /dev/null +++ b/src/texture.hpp @@ -0,0 +1,45 @@ +#pragma once +#include + +namespace lvk { +[[nodiscard]] constexpr auto +create_sampler_ci(vk::SamplerAddressMode const wrap, vk::Filter const filter) { + auto ret = vk::SamplerCreateInfo{}; + ret.setAddressModeU(wrap) + .setAddressModeV(wrap) + .setAddressModeW(wrap) + .setMinFilter(filter) + .setMagFilter(filter) + .setMaxLod(VK_LOD_CLAMP_NONE) + .setBorderColor(vk::BorderColor::eFloatTransparentBlack) + .setMipmapMode(vk::SamplerMipmapMode::eNearest); + return ret; +} + +constexpr auto sampler_ci_v = create_sampler_ci( + vk::SamplerAddressMode::eClampToEdge, vk::Filter::eLinear); + +struct TextureCreateInfo { + vk::Device device; + VmaAllocator allocator; + std::uint32_t queue_family; + CommandBlock command_block; + Bitmap bitmap; + + vk::SamplerCreateInfo sampler{sampler_ci_v}; +}; + +class Texture { + public: + using CreateInfo = TextureCreateInfo; + + explicit Texture(CreateInfo create_info); + + [[nodiscard]] auto descriptor_info() const -> vk::DescriptorImageInfo; + + private: + vma::Image m_image{}; + vk::UniqueImageView m_view{}; + vk::UniqueSampler m_sampler{}; +}; +} // namespace lvk diff --git a/src/transform.cpp b/src/transform.cpp new file mode 100644 index 0000000..f5a66be --- /dev/null +++ b/src/transform.cpp @@ -0,0 +1,39 @@ +#include +#include + +namespace lvk { +namespace { +struct Matrices { + glm::mat4 translation; + glm::mat4 orientation; + glm::mat4 scale; +}; + +[[nodiscard]] auto to_matrices(glm::vec2 const position, float rotation, + glm::vec2 const scale) -> Matrices { + static constexpr auto mat_v = glm::identity(); + static constexpr auto axis_v = glm::vec3{0.0f, 0.0f, 1.0f}; + return Matrices{ + .translation = glm::translate(mat_v, glm::vec3{position, 0.0f}), + .orientation = glm::rotate(mat_v, glm::radians(rotation), axis_v), + .scale = glm::scale(mat_v, glm::vec3{scale, 1.0f}), + }; +} +} // namespace + +auto Transform::model_matrix() const -> glm::mat4 { + auto const [t, r, s] = to_matrices(position, rotation, scale); + // right to left: scale first, then rotate, then translate. + return t * r * s; +} + +auto Transform::view_matrix() const -> glm::mat4 { + // view matrix is the inverse of the model matrix. + // instead, perform translation and rotation in reverse order and with + // negative values. or, use glm::lookAt(). + // scale is kept unchanged as the first transformation for + // "intuitive" scaling on cameras. + auto const [t, r, s] = to_matrices(-position, -rotation, scale); + return r * t * s; +} +} // namespace lvk diff --git a/src/transform.hpp b/src/transform.hpp new file mode 100644 index 0000000..f4507e5 --- /dev/null +++ b/src/transform.hpp @@ -0,0 +1,14 @@ +#pragma once +#include +#include + +namespace lvk { +struct Transform { + glm::vec2 position{}; + float rotation{}; + glm::vec2 scale{1.0f}; + + [[nodiscard]] auto model_matrix() const -> glm::mat4; + [[nodiscard]] auto view_matrix() const -> glm::mat4; +}; +} // namespace lvk diff --git a/src/vertex.hpp b/src/vertex.hpp index a8c003c..cefec31 100644 --- a/src/vertex.hpp +++ b/src/vertex.hpp @@ -7,6 +7,7 @@ namespace lvk { struct Vertex { glm::vec2 position{}; glm::vec3 color{1.0f}; + glm::vec2 uv{}; }; // two vertex attributes: position at 0, color at 1. @@ -17,6 +18,9 @@ constexpr auto vertex_attributes_v = std::array{ // vec3 => 3x 32-bit floats vk::VertexInputAttributeDescription2EXT{1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)}, + // vec2 => 2x 32-bit floats + vk::VertexInputAttributeDescription2EXT{2, 0, vk::Format::eR32G32Sfloat, + offsetof(Vertex, uv)}, }; // one vertex binding at location 0.