Vulkan Deferred Renderer

PostProcess_Final_Outdoor.webp

Overview

A Vulkan-based deferred rendering pipeline written in C++. Built to explore how modern rendering works and to learn Vulkan’s low-level API. It supports PBR materials, multiple light types, HDR tone mapping, and uses newer Vulkan features like Dynamic Rendering, Synchronization2, and Bindless Rendering.

Core
cplusplus

cplusplus

Vulkan

Vulkan

Libraries
GLFW

GLFW

GLM

GLM

stb_image

stb_image

tinyobjloader

tinyobjloader

Tools
RenderDoc

RenderDoc

Nsight_Graphics

Nsight_Graphics


Main Features

Deferred Rendering

Physically-based materials (PBR)

Directional/Point Lights

Skybox

IBL (diffuse irradiance)

Physical Camera Settings

Tone Mapping


Frame Breakdown

LightingPass_Ambient_Albedo.webp
LightingPass_DirLight_Albedo.webp

Code Snippets

Here some code snippets demonstrating key part of my Deferred Renderer.

This function handles my Depth Pre-Pass stage. I first transition the depth image into the proper layout for depth writes, then begin a depth-only rendering pass using Vulkan’s dynamic rendering. I bind the depth pre-pass pipeline, set the viewport and scissor, and draw all meshes to fill the depth buffer. This ensures early depth testing works efficiently in later passes like the G-buffer and lighting stages.

This is my deferred lighting fragment shader. It reconstructs the world position from the depth buffer, samples the G-buffer textures (albedo, normal, metallic, roughness), and applies physically-based lighting (PBR). It supports both directional and point lights, using GGX microfacet BRDF for specular reflection, Schlick’s Fresnel approximation, and Smith’s geometry term. When no geometry is present (depth ≥ 1), it renders the skybox. It also includes image-based lighting (IBL) using a diffuse irradiance cubemap for ambient contribution. The final output is the combined diffuse + specular lighting written to outColor.

This function creates all the descriptor set layouts used in my renderer. To make this process easier and less error-prone, I used a builder pattern for the Vulkan descriptor bindings and layouts. Instead of manually filling in big Vulkan structs every time, I can call methods like .SetDescriptorType(...).SetStageFlags(...).Build(). This makes it a lot cleaner and faster to set up all my descriptor sets — for the camera, materials, G-buffer, lights, and post-processing — without repeating tons of Vulkan boilerplate.


Technical Challenges & Learnings

One of the main challenges I faced was properly managing synchronization between Vulkan’s different subpasses and stages. Making sure each resource was in the right state at the right moment required quite a bit of experimentation and debugging. Another challenge was handling the lifetime of resources, especially separating those that need to exist per frame from those that persist across frames. Setting up a clean deletion queue system helped a lot with that. I also had to carefully design the descriptor set layout, deciding how often each set should update and how to organize them to keep things efficient and maintainable.