Skip to content

Commit 2b2d4b2

Browse files
committed
Instanced rendering
1 parent 1e78e49 commit 2b2d4b2

File tree

8 files changed

+195
-8
lines changed

8 files changed

+195
-8
lines changed

assets/shader.vert

484 Bytes
Binary file not shown.

guide/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@
4848
- [Shader Buffer](descriptor_sets/shader_buffer.md)
4949
- [Texture](descriptor_sets/texture.md)
5050
- [View Matrix](descriptor_sets/view_matrix.md)
51+
- [Instanced Rendering](descriptor_sets/instanced_rendering.md)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Instanced Rendering
2+
3+
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).
4+
5+
Store the SSBO and a buffer for instance matrices:
6+
7+
```cpp
8+
std::vector<glm::mat4> m_instance_data{}; // model matrices.
9+
std::optional<ShaderBuffer> m_instance_ssbo{};
10+
```
11+
12+
Add two `Transform`s as the source of rendering instances, and a function to update the matrices:
13+
14+
```cpp
15+
void update_instances();
16+
17+
// ...
18+
std::array<Transform, 2> m_instances{}; // generates model matrices.
19+
20+
// ...
21+
void App::update_instances() {
22+
m_instance_data.clear();
23+
m_instance_data.reserve(m_instances.size());
24+
for (auto const& transform : m_instances) {
25+
m_instance_data.push_back(transform.model_matrix());
26+
}
27+
// can't use bit_cast anymore, reinterpret data as a byte array instead.
28+
auto const span = std::span{m_instance_data};
29+
void* data = span.data();
30+
auto const bytes =
31+
std::span{static_cast<std::byte const*>(data), span.size_bytes()};
32+
m_instance_ssbo->write_at(m_frame_index, bytes);
33+
}
34+
```
35+
36+
Update the descriptor pool to also provide storage buffers:
37+
38+
```cpp
39+
// ...
40+
vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2},
41+
vk::DescriptorPoolSize{vk::DescriptorType::eStorageBuffer, 2},
42+
```
43+
44+
This time add a new binding to set 1 (instead of a new set):
45+
46+
```cpp
47+
static constexpr auto set_1_bindings_v = std::array{
48+
layout_binding(0, vk::DescriptorType::eCombinedImageSampler),
49+
layout_binding(1, vk::DescriptorType::eStorageBuffer),
50+
};
51+
```
52+
53+
Create the instance SSBO after the view UBO:
54+
55+
```cpp
56+
m_instance_ssbo.emplace(m_allocator.get(), m_gpu.queue_family,
57+
vk::BufferUsageFlagBits::eStorageBuffer);
58+
```
59+
60+
Call `update_instances()` after `update_view()`:
61+
62+
```cpp
63+
// ...
64+
update_view();
65+
update_instances();
66+
```
67+
68+
Extract transform inspection into a lambda and inspect each instance transform too:
69+
70+
```cpp
71+
static auto const inspect_transform = [](Transform& out) {
72+
ImGui::DragFloat2("position", &out.position.x);
73+
ImGui::DragFloat("rotation", &out.rotation);
74+
ImGui::DragFloat2("scale", &out.scale.x, 0.1f);
75+
};
76+
77+
ImGui::Separator();
78+
if (ImGui::TreeNode("View")) {
79+
inspect_transform(m_view_transform);
80+
ImGui::TreePop();
81+
}
82+
83+
ImGui::Separator();
84+
if (ImGui::TreeNode("Instances")) {
85+
for (std::size_t i = 0; i < m_instances.size(); ++i) {
86+
auto const label = std::to_string(i);
87+
if (ImGui::TreeNode(label.c_str())) {
88+
inspect_transform(m_instances.at(i));
89+
ImGui::TreePop();
90+
}
91+
}
92+
ImGui::TreePop();
93+
}
94+
```
95+
96+
Add another descriptor write for the SSBO:
97+
98+
```cpp
99+
auto writes = std::array<vk::WriteDescriptorSet, 3>{};
100+
// ...
101+
auto const instance_ssbo_info =
102+
m_instance_ssbo->descriptor_info_at(m_frame_index);
103+
write.setBufferInfo(instance_ssbo_info)
104+
.setDescriptorType(vk::DescriptorType::eStorageBuffer)
105+
.setDescriptorCount(1)
106+
.setDstSet(set1)
107+
.setDstBinding(1);
108+
writes[2] = write;
109+
```
110+
111+
Finally, change the instance count in the draw call:
112+
113+
```cpp
114+
auto const instances = static_cast<std::uint32_t>(m_instances.size());
115+
// m_vbo has 6 indices.
116+
command_buffer.drawIndexed(6, instances, 0, 0, 0);
117+
```
118+
119+
Update the vertex shader to incorporate the instance model matrix:
120+
121+
```glsl
122+
// ...
123+
layout (set = 1, binding = 1) readonly buffer Instances {
124+
mat4 mat_ms[];
125+
};
126+
127+
// ...
128+
const mat4 mat_m = mat_ms[gl_InstanceIndex];
129+
const vec4 world_pos = mat_m * vec4(a_pos, 0.0, 1.0);
130+
```
131+
132+
![Instanced Rendering](./instanced_rendering.png)
Loading

guide/src/descriptor_sets/view_matrix.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ auto Transform::view_matrix() const -> glm::mat4 {
5555
Add a `Transform` member to `App` to represent the view/camera, inspect its members, and combine with the existing projection matrix:
5656

5757
```cpp
58-
Transform m_view_transform{};
58+
Transform m_view_transform{}; // generates view matrix.
5959

6060
// ...
6161
ImGui::Separator();

src/app.cpp

+50-5
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ void App::create_descriptor_pool() {
258258
// 2 uniform buffers, can be more if desired.
259259
vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2},
260260
vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2},
261+
vk::DescriptorPoolSize{vk::DescriptorType::eStorageBuffer, 2},
261262
};
262263
auto pool_ci = vk::DescriptorPoolCreateInfo{};
263264
// allow 16 sets to be allocated from this pool.
@@ -271,6 +272,7 @@ void App::create_pipeline_layout() {
271272
};
272273
static constexpr auto set_1_bindings_v = std::array{
273274
layout_binding(0, vk::DescriptorType::eCombinedImageSampler),
275+
layout_binding(1, vk::DescriptorType::eStorageBuffer),
274276
};
275277
auto set_layout_cis = std::array<vk::DescriptorSetLayoutCreateInfo, 2>{};
276278
set_layout_cis[0].setBindings(set_0_bindings_v);
@@ -347,6 +349,9 @@ void App::create_shader_resources() {
347349
m_view_ubo.emplace(m_allocator.get(), m_gpu.queue_family,
348350
vk::BufferUsageFlagBits::eUniformBuffer);
349351

352+
m_instance_ssbo.emplace(m_allocator.get(), m_gpu.queue_family,
353+
vk::BufferUsageFlagBits::eStorageBuffer);
354+
350355
using Pixel = std::array<std::byte, 4>;
351356
static constexpr auto rgby_pixels_v = std::array{
352357
Pixel{std::byte{0xff}, {}, {}, std::byte{0xff}},
@@ -484,6 +489,7 @@ void App::render(vk::CommandBuffer const command_buffer) {
484489
command_buffer.beginRendering(rendering_info);
485490
inspect();
486491
update_view();
492+
update_instances();
487493
draw(command_buffer);
488494
command_buffer.endRendering();
489495

@@ -564,11 +570,27 @@ void App::inspect() {
564570
line_width_range[0], line_width_range[1]);
565571
}
566572

573+
static auto const inspect_transform = [](Transform& out) {
574+
ImGui::DragFloat2("position", &out.position.x);
575+
ImGui::DragFloat("rotation", &out.rotation);
576+
ImGui::DragFloat2("scale", &out.scale.x, 0.1f);
577+
};
578+
567579
ImGui::Separator();
568580
if (ImGui::TreeNode("View")) {
569-
ImGui::DragFloat2("position", &m_view_transform.position.x);
570-
ImGui::DragFloat("rotation", &m_view_transform.rotation);
571-
ImGui::DragFloat2("scale", &m_view_transform.scale.x, 0.1f);
581+
inspect_transform(m_view_transform);
582+
ImGui::TreePop();
583+
}
584+
585+
ImGui::Separator();
586+
if (ImGui::TreeNode("Instances")) {
587+
for (std::size_t i = 0; i < m_instances.size(); ++i) {
588+
auto const label = std::to_string(i);
589+
if (ImGui::TreeNode(label.c_str())) {
590+
inspect_transform(m_instances.at(i));
591+
ImGui::TreePop();
592+
}
593+
}
572594
ImGui::TreePop();
573595
}
574596
}
@@ -586,6 +608,20 @@ void App::update_view() {
586608
m_view_ubo->write_at(m_frame_index, bytes);
587609
}
588610

611+
void App::update_instances() {
612+
m_instance_data.clear();
613+
m_instance_data.reserve(m_instances.size());
614+
for (auto const& transform : m_instances) {
615+
m_instance_data.push_back(transform.model_matrix());
616+
}
617+
// can't use bit_cast anymore, reinterpret data as a byte array instead.
618+
auto const span = std::span{m_instance_data};
619+
void* data = span.data();
620+
auto const bytes =
621+
std::span{static_cast<std::byte const*>(data), span.size_bytes()};
622+
m_instance_ssbo->write_at(m_frame_index, bytes);
623+
}
624+
589625
void App::draw(vk::CommandBuffer const command_buffer) const {
590626
m_shader->bind(command_buffer, m_framebuffer_size);
591627
bind_descriptor_sets(command_buffer);
@@ -594,12 +630,13 @@ void App::draw(vk::CommandBuffer const command_buffer) const {
594630
// u32 indices after offset of 4 vertices.
595631
command_buffer.bindIndexBuffer(m_vbo.get().buffer, 4 * sizeof(Vertex),
596632
vk::IndexType::eUint32);
633+
auto const instances = static_cast<std::uint32_t>(m_instances.size());
597634
// m_vbo has 6 indices.
598-
command_buffer.drawIndexed(6, 1, 0, 0, 0);
635+
command_buffer.drawIndexed(6, instances, 0, 0, 0);
599636
}
600637

601638
void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const {
602-
auto writes = std::array<vk::WriteDescriptorSet, 2>{};
639+
auto writes = std::array<vk::WriteDescriptorSet, 3>{};
603640
auto const& descriptor_sets = m_descriptor_sets.at(m_frame_index);
604641
auto const set0 = descriptor_sets[0];
605642
auto write = vk::WriteDescriptorSet{};
@@ -619,6 +656,14 @@ void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const {
619656
.setDstSet(set1)
620657
.setDstBinding(0);
621658
writes[1] = write;
659+
auto const instance_ssbo_info =
660+
m_instance_ssbo->descriptor_info_at(m_frame_index);
661+
write.setBufferInfo(instance_ssbo_info)
662+
.setDescriptorType(vk::DescriptorType::eStorageBuffer)
663+
.setDescriptorCount(1)
664+
.setDstSet(set1)
665+
.setDstBinding(1);
666+
writes[2] = write;
622667

623668
m_device->updateDescriptorSets(writes, {});
624669

src/app.hpp

+5-1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class App {
6464
// ImGui code goes here.
6565
void inspect();
6666
void update_view();
67+
void update_instances();
6768
// Issue draw calls here.
6869
void draw(vk::CommandBuffer command_buffer) const;
6970

@@ -102,13 +103,16 @@ class App {
102103
vma::Buffer m_vbo{};
103104
std::optional<ShaderBuffer> m_view_ubo{};
104105
std::optional<Texture> m_texture{};
106+
std::vector<glm::mat4> m_instance_data{}; // model matrices.
107+
std::optional<ShaderBuffer> m_instance_ssbo{};
105108
Buffered<std::vector<vk::DescriptorSet>> m_descriptor_sets{};
106109

107110
glm::ivec2 m_framebuffer_size{};
108111
std::optional<RenderTarget> m_render_target{};
109112
bool m_wireframe{};
110113

111-
Transform m_view_transform{};
114+
Transform m_view_transform{}; // generates view matrix.
115+
std::array<Transform, 2> m_instances{}; // generates model matrices.
112116

113117
// waiter must be the last member to ensure it blocks until device is idle
114118
// before other members get destroyed.

src/glsl/shader.vert

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ layout (set = 0, binding = 0) uniform View {
88
mat4 mat_vp;
99
};
1010

11+
layout (set = 1, binding = 1) readonly buffer Instances {
12+
mat4 mat_ms[];
13+
};
14+
1115
layout (location = 0) out vec3 out_color;
1216
layout (location = 1) out vec2 out_uv;
1317

1418
void main() {
15-
const vec4 world_pos = vec4(a_pos, 0.0, 1.0);
19+
const mat4 mat_m = mat_ms[gl_InstanceIndex];
20+
const vec4 world_pos = mat_m * vec4(a_pos, 0.0, 1.0);
1621

1722
out_color = a_color;
1823
out_uv = a_uv;

0 commit comments

Comments
 (0)