SPARK  0.1.0
A general purpose game engine written in C++.
Loading...
Searching...
No Matches
Renderer2D.h
1#pragma once
2
3#include "spark/base/Exception.h"
4#include "spark/imgui/ImGui.h"
5#include "spark/lib/Pointers.h"
6#include "spark/log/Logger.h"
7#include "spark/path/Paths.h"
8#include "spark/render/Buffer.h"
9#include "spark/render/DepthStencilState.h"
10#include "spark/render/Format.h"
11#include "spark/render/GraphicsFactory.h"
12#include "spark/render/IndexBuffer.h"
13#include "spark/render/InputAssembler.h"
14#include "spark/render/Rasterizer.h"
15#include "spark/render/RenderTarget.h"
16#include "spark/render/ShaderStages.h"
17
18#include "glm/matrix.hpp"
19#include "glm/gtc/matrix_transform.hpp"
20
21namespace spark::core
22{
23 template <typename Backend>
25 std::function<typename surface_type::handle_type(const typename backend_type::handle_type&)> surface_factory,
26 std::span<std::string> required_extensions)
27 {
28 // Setup validation layers if needed
29 std::vector<std::string> layers;
30#ifndef NDEBUG
31 layers.emplace_back("VK_LAYER_KHRONOS_validation");
32#endif
33
34 log::info("Creating 2D renderer with a render area of {}x{}", render_area.x, render_area.y);
35
36 // Configure the render backend
37 m_renderBackend = std::make_unique<backend_type>(required_extensions, layers);
38 m_viewport = std::make_unique<render::Viewport>(math::Rectangle {{}, render_area.castTo<float>()});
39 m_scissor = std::make_unique<render::Scissor>(math::Rectangle {{}, render_area.castTo<float>()});
40
41 // Query the available graphics adapters and find a suitable one
42 const adapter_type* selected_adapter = m_renderBackend->findAdapter(std::nullopt);
43 if (!selected_adapter)
44 throw base::NullPointerException("No suitable graphics adapter found");
45
46 // Create the surface and the device
47 auto surface = m_renderBackend->createSurface(surface_factory);
48 m_device = m_renderBackend->template createDevice<backend_type>("Default",
49 *selected_adapter,
50 std::move(surface),
51 render::Format::B8G8R8A8_UNORM,
52 m_viewport->rectangle().extent.castTo<unsigned>(),
53 3);
54
55 // Vertex and index buffer layouts
56 auto vertex_buffer_layout = std::make_unique<vertex_buffer_layout_type>(sizeof(glm::vec3), 0);
57 vertex_buffer_layout->addAttribute(render::BufferAttribute(0, 0, render::BufferFormat::XYZ32F, render::AttributeSemantic::Position, 0));
58
59 std::vector<std::unique_ptr<vertex_buffer_layout_type>> vertex_buffer_layouts;
60 vertex_buffer_layouts.push_back(std::move(vertex_buffer_layout));
61
62 auto index_buffer_layout = std::make_unique<index_buffer_layout_type>(render::IndexType::UInt16);
63
64 // Setup input assembler
65 m_inputAssembler = std::make_shared<input_assembler_type>(std::move(vertex_buffer_layouts), std::move(index_buffer_layout), render::PrimitiveTopology::TriangleList);
66
67 // Create a simple render pass
68 std::vector<render::RenderTarget> render_targets;
69 render_targets.emplace_back("Color Target",
70 0,
71 render::RenderTargetType::Present,
72 render::Format::B8G8R8A8_UNORM,
73 true,
74 math::Vector4 {0.2f, 0.2f, 0.2f, 1.f},
75 false,
76 false);
77 render_targets.emplace_back("Depth/Stencil Target",
78 1,
79 render::RenderTargetType::DepthStencil,
80 render::Format::D32_SFLOAT,
81 true,
82 math::Vector4 {1.f, 1.f, 1.f, 1.f},
83 false,
84 false);
85
86 auto render_pass = std::make_unique<render_pass_type>(*m_device, "Opaque", render_targets);
87
88 // Create the shader program
89 std::vector<std::unique_ptr<shader_module_type>> modules;
90 modules.push_back(std::make_unique<shader_module_type>(*m_device, render::ShaderStage::Vertex, spark::path::engine_assets_path() / "shaders" / "2d_vert.spv"));
91 modules.push_back(std::make_unique<shader_module_type>(*m_device, render::ShaderStage::Fragment, spark::path::engine_assets_path() / "shaders" / "2d_frag.spv"));
92
93 auto shader_program = std::make_shared<shader_program_type>(*m_device, std::move(modules));
94
95 // Create the rasterizer
96 render::DepthStencilState depth_stencil_state(render::DepthStencilState::DepthState {.operation = render::CompareOperation::LessEqual},
99 auto rasterizer = std::make_shared<rasterizer_type>(render::PolygonMode::Solid, render::CullMode::BackFaces, render::CullOrder::CounterClockWise, 1.f, depth_stencil_state);
100
101 // Create the render pipeline
102 auto render_pipeline = std::make_unique<render_pipeline_type>(*render_pass,
103 shader_program,
104 std::static_pointer_cast<pipeline_layout_type>(shader_program->reflectPipelineLayout()),
105 m_inputAssembler,
106 rasterizer,
107 false,
108 "Geometry");
109
110 // Add everything to the device state
111 m_device->state().add(std::move(render_pass));
112 m_device->state().add(std::move(render_pipeline));
113
114 // Init the render graph
115 initRenderGraph();
116 }
117
118 template <typename Backend>
120 {
121 m_renderBackend->releaseDevice("Default");
122 }
123
124 template <typename Backend>
126 {
127 // Wait for all frames to be finished
128 m_device->wait();
129
130 // Recreate the swap chain
131 const auto surface_format = m_device->swapChain().surfaceFormat();
132 m_device->swapChain().reset(surface_format, new_size, 3);
133
134 // Resize the frame buffers for the render passes. It should be done in order to avoid since dependencies
135 // (i.e. input attachments) are re-created and might be mapped to images that do no longer exist.
136 m_device->state().renderPass("Opaque").resizeFrameBuffers(new_size);
137
138 // Resize viewport and scissor.
139 m_viewport->setRectangle(math::Rectangle({0.f, 0.f}, new_size.castTo<float>()));
140 m_scissor->setRectangle(math::Rectangle({0.f, 0.f}, new_size.castTo<float>()));
141 }
142
143 template <typename Backend>
145 {
146 auto& render_pipeline = m_device->state().pipeline("Geometry");
147 auto command_buffer = m_device->transferQueue().createCommandBuffer(true, false);
148
149 // Create the vertex buffer
150 auto staged_vertex_buffer = m_device->factory().createVertexBuffer(m_inputAssembler->vertexBufferLayout(0),
151 render::BufferUsage::Staging,
152 static_cast<unsigned>(s_rectangleVertices.size()));
153
154 auto vertex_buffer = m_device->factory().createVertexBuffer("Vertex Buffer",
155 m_inputAssembler->vertexBufferLayout(0),
156 render::BufferUsage::Resource,
157 static_cast<unsigned>(s_rectangleVertices.size()));
158
159 staged_vertex_buffer->map(s_rectangleVertices.data(), s_rectangleVertices.size() * sizeof(glm::vec3), 0);
160 command_buffer->transfer(std::move(staged_vertex_buffer), *vertex_buffer, 0, 0, static_cast<unsigned>(s_rectangleVertices.size()));
161
162 // Create the index buffer
163 auto staged_indices_buffer = m_device->factory().createIndexBuffer(m_inputAssembler->indexBufferLayout(),
164 render::BufferUsage::Staging,
165 static_cast<unsigned>(s_rectangleIndices.size()));
166
167 auto index_buffer = m_device->factory().createIndexBuffer("Index Buffer",
168 m_inputAssembler->indexBufferLayout(),
169 render::BufferUsage::Resource,
170 static_cast<unsigned>(s_rectangleIndices.size()));
171
172 staged_indices_buffer->map(s_rectangleIndices.data(), s_rectangleIndices.size() * sizeof(uint16_t), 0);
173 command_buffer->transfer(std::move(staged_indices_buffer), *index_buffer, 0, 0, static_cast<unsigned>(s_rectangleIndices.size()));
174
175 // Create the transform bindings and buffer
176 auto& instance_binding_layout = render_pipeline.layout()->descriptorSet(0);
177
178 // Since we are using an unstructured storage buffer, we need to specify the element size manually.
179 auto staged_instance_buffer = lib::dynamic_unique_pointer_cast<buffer_type>(dynamic_cast<const render::IGraphicsFactory&>(m_device->factory()).
180 createBuffer("Instance Staging Buffer",
181 instance_binding_layout,
182 0,
183 render::BufferUsage::Staging,
184 sizeof(InstanceBuffer),
185 s_maxInstances));
186
187 auto instance_buffer = lib::dynamic_unique_pointer_cast<buffer_type>(dynamic_cast<const render::IGraphicsFactory&>(m_device->factory()).createBuffer("Instance Buffer",
188 instance_binding_layout,
189 0,
190 render::BufferUsage::Resource,
191 sizeof(InstanceBuffer),
192 s_maxInstances));
193
194 auto instance_binding = instance_binding_layout.allocate(s_maxInstances, {{0, *instance_buffer}});
195
196 m_transferFences.push_back(m_device->transferQueue().submit(command_buffer));
197
198 m_device->state().add(lib::static_unique_pointer_cast<render::IVertexBuffer>(std::move(vertex_buffer)));
199 m_device->state().add(lib::static_unique_pointer_cast<render::IIndexBuffer>(std::move(index_buffer)));
200 m_device->state().add(std::move(staged_instance_buffer));
201 m_device->state().add(std::move(instance_buffer));
202 m_device->state().add("Instance Binding", std::move(instance_binding));
203 }
204
205 template <typename Backend>
206 void Renderer2D<Backend>::updateCamera(const render::ICommandBuffer& command_buffer)
207 {
208 // Calculate the view matrix
209 const glm::mat4 view = glm::ortho(0.f, m_viewport->rectangle().extent.x, m_viewport->rectangle().extent.y, 0.f, -1.f, 1.f);
210
211 auto& pipeline = dynamic_cast<render_pipeline_type&>(m_device->state().pipeline("Geometry"));
212 command_buffer.pushConstants(*pipeline.layout()->pushConstants(), &view);
213 }
214
215 template <typename Backend>
216 void Renderer2D<Backend>::upload()
217 {
218 if (m_instanceData.empty()) // Nothing to upload
219 return;
220
221 // Create a command buffer
222 auto command_buffer = m_device->transferQueue().createCommandBuffer(true, false);
223
224 // Transfer frame data to the instance buffer
225 auto& staged_instance_buffer = dynamic_cast<buffer_type&>(m_device->state().buffer("Instance Staging Buffer"));
226 staged_instance_buffer.map(static_cast<const void*>(m_instanceData.data()), m_instanceData.size() * sizeof(InstanceBuffer), 0);
227
228 command_buffer->transfer(staged_instance_buffer,
229 dynamic_cast<buffer_type&>(m_device->state().buffer("Instance Buffer")),
230 0,
231 0,
232 static_cast<unsigned>(m_instanceData.size()));
233
234 // Submit the command buffer
235 m_transferFences.push_back(m_device->transferQueue().submit(command_buffer));
236 }
237
238 template <typename Backend>
240 {
241 // Swap the back buffers for the next frame
242 const auto back_buffer = m_device->swapChain().swapBackBuffer();
243
244 // TODO: Cache this instead of looking them up every frame
245 auto& render_pass = m_device->state().renderPass("Opaque");
246 const auto& geometry_pipeline = m_device->state().pipeline("Geometry");
247 const auto& instance_binding = m_device->state().descriptorSet("Instance Binding");
248 const auto& vertex_buffer = m_device->state().vertexBuffer("Vertex Buffer");
249 const auto& index_buffer = m_device->state().indexBuffer("Index Buffer");
250
251 // Upload the new instance data for the current frame
252 upload();
253
254 // Begin rendering on the render pass and use the only pipeline created for it
255 render_pass.begin(back_buffer);
256
257 // Wait for all transfers to finish
258 for (const auto& fence : m_transferFences)
259 m_device->transferQueue().waitFor(fence);
260 m_transferFences.clear();
261
262 const auto command_buffer = render_pass.activeFrameBuffer().commandBuffer(0);
263 command_buffer->use(geometry_pipeline);
264 command_buffer->setViewport(m_viewport.get());
265 command_buffer->setScissor(m_scissor.get());
266
267 // Set up the camera
268 updateCamera(*command_buffer);
269
270 // Bind the vertex and index buffers
271 command_buffer->bind(instance_binding);
272 command_buffer->bind(vertex_buffer);
273 command_buffer->bind(index_buffer);
274
275 // Draw the object and present the frame by ending the render pass
276 command_buffer->drawIndexed(index_buffer.elements(), static_cast<unsigned>(m_instanceData.size()));
277 // TODO: Render ImGui on another render pass (so this can be ordered as we want)
278 imgui::render(*command_buffer);
279 render_pass.end();
280
281 // Clean up the instance data for the next frame
282 m_instanceData.clear();
283 }
284
285 template <typename Backend>
286 void Renderer2D<Backend>::drawQuad(const glm::mat4& transform_matrix, const spark::math::Vector4<float>& color)
287 {
288 m_instanceData.emplace_back(InstanceBuffer {
289 .transform = transform_matrix,
290 .color = glm::vec4(color.x, color.y, color.z, color.w),
291 });
292 }
293
294 template <typename Backend>
295 void Renderer2D<Backend>::drawCircle(const glm::mat4& transform_matrix, const float radius, const spark::math::Vector4<float>& color)
296 {
297 m_instanceData.emplace_back(InstanceBuffer {
298 .transform = transform_matrix,
299 .color = glm::vec4(color.x, color.y, color.z, color.w),
300 .radius = radius
301 });
302 }
303}
Exception thrown when a pointer is null and should't be.
Definition Exception.h:66
An object that can render 2D graphics on a surface.
Definition Renderer2D.h:22
void render()
Draws the current frame.
Definition Renderer2D.h:239
void drawQuad(const glm::mat4 &transform_matrix, const spark::math::Vector4< float > &color={1.f, 1.f, 1.f, 1.f})
Draws a 1x1 quad with the given transformation_matrix.
Definition Renderer2D.h:286
void drawCircle(const glm::mat4 &transform_matrix, float radius, const spark::math::Vector4< float > &color={1.f, 1.f, 1.f, 1.f})
Draws a circle with the given transformation_matrix and radius.
Definition Renderer2D.h:295
void recreateSwapChain(const math::Vector2< unsigned > &new_size)
Recreates the swap chain with frame buffers of the new size.
Definition Renderer2D.h:125
Renderer2D(const math::Vector2< unsigned > &render_area, std::function< typename surface_type::handle_type(const typename backend_type::handle_type &)> surface_factory, std::span< std::string > required_extensions)
Creates a new 2D renderer.
Definition Renderer2D.h:24
Represents a rectangle in 2D space.
Definition Rectangle.h:15
A vector with two components.
Definition Vector2.h:13
constexpr Vector2< To > castTo() const noexcept
Casts all components of the Vector2 to the type To.
Definition Vector2.h:45
A vector with four components.
Definition Vector4.h:13
Stores metadata about a buffer attribute, member or field of a descriptor or buffer.
Definition Buffer.h:193
Stores the depth/stencil state of a see IRasterizer.
Definition DepthStencilState.h:75
Definition GraphicsFactory.h:16
std::unique_ptr< IBuffer > createBuffer(BufferType type, BufferUsage usage, std::size_t element_size, unsigned elements=1, bool allow_write=false) const
Creates a IBuffer of type type.
Definition GraphicsFactory.h:29
Describes the rasterizer depth bias.
Definition DepthStencilState.h:101
Describes the depth test state.
Definition DepthStencilState.h:81
CompareOperation operation
The compare operation used to pass the depth test (default: CompareOperation::Always).
Definition DepthStencilState.h:90
Describes the rasterizer stencil state.
Definition DepthStencilState.h:139