Vulkan Renderer
After finishing the Vulkan Tutorial, I understood how the main concepts of the API work, but couldn’t quite wrap my head around how they would work together in a more complex project.
In the end, I ended up going on a long and deep dive into more of the details of the Vulkan API and how to write a game engine of sorts. I absolutely loved working on this project, and loved learning a lot from all the little challenges that stood in my way.
Below the overview video I go into more details on some of the technical aspects of the project.
Abstraction layer over Vulkan
Because Vulkan is a very verbose API, I knew that building an abstraction layer on top of Vulkan would be vital.
The main pain point I identified was setting up the pipelines, as they require a lot of boilerplate structs to be filled out. With my PipelineBuilder abstraction, it was super easy to set up a new pipeline:
void VulkanRenderer::CreateGraphicsPipeline()
{
// Load our shader
VulkanShader* pShader = new VulkanShader();
pShader->AddShader(ShaderType::Vertex, "res/shaders/vert.spv");
pShader->AddShader(ShaderType::Fragment, "res/shaders/frag.spv");
// Setup all our parameters in the pipeline
PipelineBuilder builder{ m_pDevice->GetDevice() };
builder.SetShader(pShader);
builder.SetInputAssembly(vk::PrimitiveTopology::eTriangleList, false);
builder.SetViewport(0.0f, 0.0f, swapChainWidth, swapChainHeight, 0.0f, 1.0f);
builder.SetScissor(vk::Offset2D(0, 0), m_pSwapChain->GetExtent());
builder.SetRasterizer(vk::PolygonMode::eFill, vk::CullModeFlagBits::eBack);
builder.SetMultisampling();
builder.SetDepthStencil(true, true, vk::CompareOp::eLess);
builder.SetColorBlend(true, vk::BlendOp::eAdd, vk::BlendOp::eAdd, false, vk::LogicOp::eCopy);
builder.SetDescriptorSetLayout(1, &m_DescriptorSetLayout);
// Build the filled rendering pipeline
m_Pipelines[static_cast<int>(RenderMode::Filled)] = builder.BuildGraphics(m_RenderPass);
// Build the line rendering pipeline
builder.SetRasterizer(vk::PolygonMode::eLine, vk::CullModeFlagBits::eBack);
m_Pipelines[static_cast<int>(RenderMode::Lines)] = builder.BuildGraphics(m_RenderPass);
// Build the point rendering pipeline
builder.SetRasterizer(vk::PolygonMode::ePoint, vk::CullModeFlagBits::eBack);
m_Pipelines[static_cast<int>(RenderMode::Points)] = builder.BuildGraphics(m_RenderPass);
// Clean up shader, we don't need it anymore
delete pShader;
pShader = nullptr;
}
Shader hot-reloading
Implementing shader hot-reloading was a fun little side-quest to figure out, because going into this I had no clue where to start. In the end I realized that all I had to do was simply destroy the current render pass and graphics pipeline, reload the shader files and recreate the render pass and graphics pipeline.
Because of my small abstraction layer, this was fairly easy to implement, and the end code resulted in something like this:
void VulkanRenderer::ReloadShaders()
{
m_pDevice->WaitIdle();
m_Pipeline.Cleanup(m_pDevice->GetDevice());
m_pDevice->GetDevice().destroyRenderPass(m_RenderPass);
CreateRenderPass();
CreateGraphicsPipeline();
}
ECS scene system
After hearing a lot of positive voices around using an ECS system, I decided to check out EnTT and implement it in my project.
Its implementation in the project ended up being very trivial, as it mostly just needs to call EnTT functions:
class Entity
{
public:
Entity() = delete;
template<typename T, typename... Args>
T& AddComponent(Args&&... args)
{
ASSERT_MSG(!HasComponent<T>(), "Entity already has component!");
T& component = m_pScene->m_Registry.emplace<T>(m_Entity, std::forward<Args>(args)...);
return component;
}
template<typename T>
void RemoveComponent()
{
ASSERT_MSG(HasComponent<T>(), "Entity does not have component!");
m_pScene->m_Registry.remove<T>(m_Entity);
}
template<typename T>
bool HasComponent()
{
return m_pScene->m_Registry.all_of<T>(m_Entity);
}
private:
// Scene holds the entt::registry, from which entities are created.
// Only the scene is allowed to create entities
friend class Scene;
Entity(entt::entity entity);
entt::entity m_Entity{ entt::null };
Scene* m_pScene;
};
EnTT makes it really easy to query entities which have certain components attached to them:
void Scene::Draw()
{
// Insert a debug marker for graphics debuggers
vk::CommandBuffer cmd = VulkanRenderer::GetCurrentBuffer();
VkDebugMarker::BeginRegion(cmd, "Scene Render");
// Get all entities that have a transform and a model component,
// and record their draw calls.
auto view = m_Registry.view<TransformComponent, ModelComponent>();
for (auto [entity, transformComp, modelComp] : view.each())
{
modelComp.pModel->Draw();
}
VkDebugMarker::EndRegion(cmd);
}
Media
Here you can see the shader hot-reloading system in action:
Here is the Dear ImGui debug UI doing its job:
And finally, a sneak preview of the PBR renderer (currently, only simple lighting is implemented)
used libraries: