|
| 1 | +# Texture |
| 2 | + |
| 3 | +With a large part of the complexity wrapped away in `vma`, a `Texture` is just a combination of three things: |
| 4 | + |
| 5 | +1. Sampled Image |
| 6 | +2. (Unique) Image View of above |
| 7 | +3. (Unique) Sampler |
| 8 | + |
| 9 | +In `texture.hpp`, create a default sampler: |
| 10 | + |
| 11 | +```cpp |
| 12 | +[[nodiscard]] constexpr auto create_sampler_ci(vk::SamplerAddressMode wrap, |
| 13 | + vk::Filter filter) { |
| 14 | + auto ret = vk::SamplerCreateInfo{}; |
| 15 | + ret.setAddressModeU(wrap) |
| 16 | + .setAddressModeV(wrap) |
| 17 | + .setAddressModeW(wrap) |
| 18 | + .setMinFilter(filter) |
| 19 | + .setMagFilter(filter) |
| 20 | + .setMaxLod(VK_LOD_CLAMP_NONE) |
| 21 | + .setBorderColor(vk::BorderColor::eFloatTransparentBlack) |
| 22 | + .setMipmapMode(vk::SamplerMipmapMode::eNearest); |
| 23 | + return ret; |
| 24 | +} |
| 25 | + |
| 26 | +constexpr auto sampler_ci_v = create_sampler_ci( |
| 27 | + vk::SamplerAddressMode::eClampToEdge, vk::Filter::eLinear); |
| 28 | +``` |
| 29 | +
|
| 30 | +Define the Create Info and Texture types: |
| 31 | +
|
| 32 | +```cpp |
| 33 | +struct TextureCreateInfo { |
| 34 | + vk::Device device; |
| 35 | + VmaAllocator allocator; |
| 36 | + std::uint32_t queue_family; |
| 37 | + CommandBlock command_block; |
| 38 | + Bitmap bitmap; |
| 39 | +
|
| 40 | + vk::SamplerCreateInfo sampler{sampler_ci_v}; |
| 41 | +}; |
| 42 | +
|
| 43 | +class Texture { |
| 44 | + public: |
| 45 | + using CreateInfo = TextureCreateInfo; |
| 46 | +
|
| 47 | + explicit Texture(CreateInfo create_info); |
| 48 | +
|
| 49 | + [[nodiscard]] auto descriptor_info() const -> vk::DescriptorImageInfo; |
| 50 | +
|
| 51 | + private: |
| 52 | + vma::Image m_image{}; |
| 53 | + vk::UniqueImageView m_view{}; |
| 54 | + vk::UniqueSampler m_sampler{}; |
| 55 | +}; |
| 56 | +``` |
| 57 | + |
| 58 | +Add a fallback bitmap constant, and the implementation: |
| 59 | + |
| 60 | +```cpp |
| 61 | +// 4-channels. |
| 62 | +constexpr auto white_pixel_v = std::array{std::byte{0xff}, std::byte{0xff}, |
| 63 | + std::byte{0xff}, std::byte{0xff}}; |
| 64 | +// fallback bitmap. |
| 65 | +constexpr auto white_bitmap_v = Bitmap{ |
| 66 | + .bytes = white_pixel_v, |
| 67 | + .size = {1, 1}, |
| 68 | +}; |
| 69 | + |
| 70 | +// ... |
| 71 | +Texture::Texture(CreateInfo create_info) { |
| 72 | + if (create_info.bitmap.bytes.empty() || create_info.bitmap.size.x <= 0 || |
| 73 | + create_info.bitmap.size.y <= 0) { |
| 74 | + create_info.bitmap = white_bitmap_v; |
| 75 | + } |
| 76 | + |
| 77 | + auto const image_ci = vma::ImageCreateInfo{ |
| 78 | + .allocator = create_info.allocator, |
| 79 | + .queue_family = create_info.queue_family, |
| 80 | + }; |
| 81 | + m_image = vma::create_sampled_image( |
| 82 | + image_ci, std::move(create_info.command_block), create_info.bitmap); |
| 83 | + |
| 84 | + auto image_view_ci = vk::ImageViewCreateInfo{}; |
| 85 | + auto subresource_range = vk::ImageSubresourceRange{}; |
| 86 | + subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) |
| 87 | + .setLayerCount(1) |
| 88 | + .setLevelCount(m_image.get().levels); |
| 89 | + |
| 90 | + image_view_ci.setImage(m_image.get().image) |
| 91 | + .setViewType(vk::ImageViewType::e2D) |
| 92 | + .setFormat(m_image.get().format) |
| 93 | + .setSubresourceRange(subresource_range); |
| 94 | + m_view = create_info.device.createImageViewUnique(image_view_ci); |
| 95 | + |
| 96 | + m_sampler = create_info.device.createSamplerUnique(create_info.sampler); |
| 97 | +} |
| 98 | + |
| 99 | +auto Texture::descriptor_info() const -> vk::DescriptorImageInfo { |
| 100 | + auto ret = vk::DescriptorImageInfo{}; |
| 101 | + ret.setImageView(*m_view) |
| 102 | + .setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal) |
| 103 | + .setSampler(*m_sampler); |
| 104 | + return ret; |
| 105 | +} |
| 106 | +``` |
| 107 | +
|
| 108 | +To sample textures, `Vertex` will need a UV coordinate: |
| 109 | +
|
| 110 | +```cpp |
| 111 | +struct Vertex { |
| 112 | + glm::vec2 position{}; |
| 113 | + glm::vec3 color{1.0f}; |
| 114 | + glm::vec2 uv{}; |
| 115 | +}; |
| 116 | +
|
| 117 | +// two vertex attributes: position at 0, color at 1. |
| 118 | +constexpr auto vertex_attributes_v = std::array{ |
| 119 | + // the format matches the type and layout of data: vec2 => 2x 32-bit floats. |
| 120 | + vk::VertexInputAttributeDescription2EXT{0, 0, vk::Format::eR32G32Sfloat, |
| 121 | + offsetof(Vertex, position)}, |
| 122 | + // vec3 => 3x 32-bit floats |
| 123 | + vk::VertexInputAttributeDescription2EXT{1, 0, vk::Format::eR32G32B32Sfloat, |
| 124 | + offsetof(Vertex, color)}, |
| 125 | + // vec2 => 2x 32-bit floats |
| 126 | + vk::VertexInputAttributeDescription2EXT{2, 0, vk::Format::eR32G32Sfloat, |
| 127 | + offsetof(Vertex, uv)}, |
| 128 | +}; |
| 129 | +``` |
| 130 | + |
| 131 | +Store a texture in `App` and create with the other shader resources: |
| 132 | + |
| 133 | +```cpp |
| 134 | +std::optional<Texture> m_texture{}; |
| 135 | + |
| 136 | +// ... |
| 137 | +using Pixel = std::array<std::byte, 4>; |
| 138 | +static constexpr auto rgby_pixels_v = std::array{ |
| 139 | + Pixel{std::byte{0xff}, {}, {}, std::byte{0xff}}, |
| 140 | + Pixel{std::byte{}, std::byte{0xff}, {}, std::byte{0xff}}, |
| 141 | + Pixel{std::byte{}, {}, std::byte{0xff}, std::byte{0xff}}, |
| 142 | + Pixel{std::byte{0xff}, std::byte{0xff}, {}, std::byte{0xff}}, |
| 143 | +}; |
| 144 | +static constexpr auto rgby_bytes_v = |
| 145 | + std::bit_cast<std::array<std::byte, sizeof(rgby_pixels_v)>>( |
| 146 | + rgby_pixels_v); |
| 147 | +static constexpr auto rgby_bitmap_v = Bitmap{ |
| 148 | + .bytes = rgby_bytes_v, |
| 149 | + .size = {2, 2}, |
| 150 | +}; |
| 151 | +auto texture_ci = Texture::CreateInfo{ |
| 152 | + .device = *m_device, |
| 153 | + .allocator = m_allocator.get(), |
| 154 | + .queue_family = m_gpu.queue_family, |
| 155 | + .command_block = create_command_block(), |
| 156 | + .bitmap = rgby_bitmap_v, |
| 157 | +}; |
| 158 | +// use Nearest filtering instead of Linear (interpolation). |
| 159 | +texture_ci.sampler.setMagFilter(vk::Filter::eNearest); |
| 160 | +m_texture.emplace(std::move(texture_ci)); |
| 161 | +``` |
| 162 | + |
| 163 | +Update the descriptor pool sizes to also contain Combined Image Samplers: |
| 164 | + |
| 165 | +```cpp |
| 166 | +/// ... |
| 167 | +vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2}, |
| 168 | +vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2}, |
| 169 | +``` |
| 170 | +
|
| 171 | +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): |
| 172 | +
|
| 173 | +```cpp |
| 174 | +static constexpr auto set_1_bindings_v = std::array{ |
| 175 | + layout_binding(0, vk::DescriptorType::eCombinedImageSampler), |
| 176 | +}; |
| 177 | +auto set_layout_cis = std::array<vk::DescriptorSetLayoutCreateInfo, 2>{}; |
| 178 | +set_layout_cis[0].setBindings(set_0_bindings_v); |
| 179 | +set_layout_cis[1].setBindings(set_1_bindings_v); |
| 180 | +``` |
| 181 | + |
| 182 | +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. |
| 183 | + |
| 184 | +```cpp |
| 185 | +static constexpr auto vertices_v = std::array{ |
| 186 | + Vertex{.position = {-200.0f, -200.0f}, .uv = {0.0f, 1.0f}}, |
| 187 | + Vertex{.position = {200.0f, -200.0f}, .uv = {1.0f, 1.0f}}, |
| 188 | + Vertex{.position = {200.0f, 200.0f}, .uv = {1.0f, 0.0f}}, |
| 189 | + Vertex{.position = {-200.0f, 200.0f}, .uv = {0.0f, 0.0f}}, |
| 190 | +}; |
| 191 | +``` |
| 192 | + |
| 193 | +Finally, update the descriptor writes: |
| 194 | + |
| 195 | +```cpp |
| 196 | +auto writes = std::array<vk::WriteDescriptorSet, 2>{}; |
| 197 | +// ... |
| 198 | +auto const set1 = descriptor_sets[1]; |
| 199 | +auto const image_info = m_texture->descriptor_info(); |
| 200 | +write.setImageInfo(image_info) |
| 201 | + .setDescriptorType(vk::DescriptorType::eCombinedImageSampler) |
| 202 | + .setDescriptorCount(1) |
| 203 | + .setDstSet(set1) |
| 204 | + .setDstBinding(0); |
| 205 | +writes[1] = write; |
| 206 | +``` |
| 207 | + |
| 208 | +Since set 1 is not N-buffered (because the Texture is "GPU const"), in this case the sets could also be updated once after texture creation instead of every frame. |
| 209 | + |
| 210 | +Add the UV vertex attribute the vertex shader and pass it to the fragment shader: |
| 211 | + |
| 212 | +```glsl |
| 213 | +layout (location = 2) in vec2 a_uv; |
| 214 | +
|
| 215 | +// ... |
| 216 | +layout (location = 1) out vec2 out_uv; |
| 217 | +
|
| 218 | +// ... |
| 219 | +out_color = a_color; |
| 220 | +out_uv = a_uv; |
| 221 | +``` |
| 222 | + |
| 223 | +Add set 1 and the incoming UV coords to the fragment shader, combine the sampled texture color with the vertex color: |
| 224 | + |
| 225 | +```glsl |
| 226 | +layout (set = 1, binding = 0) uniform sampler2D tex; |
| 227 | +
|
| 228 | +// ... |
| 229 | +layout (location = 1) in vec2 in_uv; |
| 230 | +
|
| 231 | +// ... |
| 232 | +out_color = vec4(in_color, 1.0) * texture(tex, in_uv); |
| 233 | +``` |
| 234 | + |
| 235 | + |
0 commit comments