,

Vulkan PBR Renderer

Git : https://github.com/Ryan-Mus/VulkanPBRRenderer

Description

A physically-based rendering engine built with Vulkan that demonstrates modern real-time graphics techniques. This application renders 3D models with realistic lighting and materials using a deferred rendering pipeline.

Features

Modern Vulkan Implementation: Utilizes Vulkan 1.3 features including dynamic rendering and synchronization2

PBR Materials: Full physically-based rendering pipeline supporting metallic-roughness workflow

HDR Rendering: High dynamic range rendering with ACES tone mapping

Image-Based Lighting: Environment map-based ambient lighting using IBL cubemaps

Dynamic Lighting: Real-time directional and point light support with directional shadow mapping

How I made it

Vulkan Tutorial

I started with the vulkan tutorial where I followed until the loading models where I ended up with this rotating viking room.

Refactoring

From then on, I started on my own without a tutorial. The first thing I did was try to load different models with multiple textures. I ended up using the well-known Sponza scene. At this point, the whole project was still a single file, just like in the tutorial. So, I began a big refactor that encapsulated all the Vulkan objects and made a render class that uses these objects. I updated the model loading from OBJ to glTF and made use of Assimp for the model and material loading. I used STB Image to load the textures.

For the model, I loaded all the submeshes and calculated the Axis-Aligned Bounding Box, so I only render the submeshes that are in view.

For the Vulkan objects, I mostly used the builder pattern. I used this pattern because you can easily build complex objects with different “settings.” Take, for example, the graphics pipeline.

Thanks to the builder pattern, I was able to effortlessly create three distinct graphics pipelines, each serving its own unique setting. This patterns also avoid having a lot of constructor functions with loads of parameters.

Upgrading

When everything was encapsulated and the render class rendered the Sponza scene, I started using new Vulkan features, so I updated the code to use sync2 objects. Instead of using render passes, I started with dynamic rendering where I manually transition between formats.

Deferred shading

I started with making my GBuffer where I save 4 different textures: Depth, Normals, Metallic-Roughness, Material. For the depth I did a depth prepass then I render the normals, metallic-roughness and materials to then combine them in a lighting pass. Then I do a ACES tone mapping using compute shaders.

Tone Mapping and Exposure

For the tone mapping and exposure I used a compute shader that uses push constants to know the camera settings.

This is the before and after for the compute shader.

Shadow Mapping

I did shadow mapping for directional light at the start of the program because the directional light is static.

Skybox

I rendered a cubemap using an HDRI from polyhaven. I then used that cubemap to render to the pixels where the depth is greater than 1. I used that cubemap to render a irradiance cubemap that I use in the lighting pass as ambient lighting.

Result

Conclusion

While working on this project, I gained valuable insights into Vulkan and rendering in general. It has reinforced my passion for graphics programming, as I thoroughly enjoyed the journey and realized that there are still a LOT of things to learn in this branch of programming. With how the project currently stands, there are still a lot of problems such as the shadow map and irradiance cubemap being incorrect. The code is also not optimally efficient, and I probably did some unnecessary things. But finally, I’m happy with the results I got.

I’m looking forward to my next graphics programming projects, where I will learn new and interesting techniques.