Book Image

Mastering Graphics Programming with Vulkan

By : Marco Castorina, Gabriel Sassone
5 (2)
Book Image

Mastering Graphics Programming with Vulkan

5 (2)
By: Marco Castorina, Gabriel Sassone

Overview of this book

Vulkan is now an established and flexible multi-platform graphics API. It has been adopted in many industries, including game development, medical imaging, movie productions, and media playback but learning it can be a daunting challenge due to its low-level, complex nature. Mastering Graphics Programming with Vulkan is designed to help you overcome this difficulty, providing a practical approach to learning one of the most advanced graphics APIs. In Mastering Graphics Programming with Vulkan, you’ll focus on building a high-performance rendering engine from the ground up. You’ll explore Vulkan’s advanced features, such as pipeline layouts, resource barriers, and GPU-driven rendering, to automate tedious tasks and create efficient workflows. Additionally, you'll delve into cutting-edge techniques like mesh shaders and real-time ray tracing, elevating your graphics programming to the next level. By the end of this book, you’ll have a thorough understanding of modern rendering engines to confidently handle large-scale projects. Whether you're developing games, simulations, or visual effects, this guide will equip you with the skills and knowledge to harness Vulkan’s full potential.
Table of Contents (21 chapters)
1
Part 1: Foundations of a Modern Rendering Engine
7
Part 2: GPU-Driven Rendering
13
Part 3: Advanced Rendering Techniques

PBR in a nutshell

PBR is at the heart of many rendering engines. It was originally developed for offline rendering, but thanks to the advances in hardware capabilities and research efforts by the graphics community, it can now be used for real-time rendering as well.

As the name implies, this technique aims at modeling the physical interactions of light and matter and, in some implementations, ensuring that the amount of energy in the system is preserved.

There are plenty of in-depth resources available that describe PBR in great detail. Nonetheless, we want to give a brief overview of our implementation for reference. We have followed the implementation presented in the glTF spec.

To compute the final color of our surface, we have to determine the diffuse and specular components. The amount of specular reflection in the real world is determined by the roughness of the surface. The smoother the surface, the greater the amount of light that is reflected. A mirror reflects (almost) all the light it receives.

The roughness of the surface is modeled through a texture. In the glTF format, this value is packed with the metalness and the occlusion values in a single texture to optimize resource use. We distinguish materials between conductors (or metallic) and dielectric (non-metallic) surfaces.

A metallic material has only a specular term, while a non-metallic material has both diffuse and specular terms. To model materials that have both metallic and non-metallic components, we use the metalness term to interpolate between the two.

An object made of wood will likely have a metalness of 0, plastic will have a mix of both metalness and roughness, and the body of a car will be dominated by the metallic component.

As we are modeling the real-world response of a material, we need a function that takes the view and light direction and returns the amount of light that is reflected. This function is called the bi-directional distribution function (BRDF).

We use the Trowbridge-Reitz/GGX distribution for the specular BRDF, and it is implemented as follows:

float NdotH = dot(N, H);
float alpha_squared = alpha * alpha;
float d_denom = ( NdotH * NdotH ) * ( alpha_squared - 1.0 )
    + 1.0;
float distribution = ( alpha_squared * heaviside( NdotH ) )
    / ( PI * d_denom * d_denom );
float NdotL = dot(N, L);
float NdotV = dot(N, V);
float HdotL = dot(H, L);
float HdotV = dot(H, V);
float visibility = ( heaviside( HdotL ) / ( abs( NdotL ) +
  sqrt( alpha_squared + ( 1.0 - alpha_squared ) *
  ( NdotL * NdotL ) ) ) ) * ( heaviside( HdotV ) /
  ( abs( NdotV ) + sqrt( alpha_squared +
  ( 1.0 - alpha_squared ) *
  ( NdotV * NdotV ) ) ) );
float specular_brdf = visibility * distribution;

First, we compute the distribution and visibility terms according to the formula presented in the glTF specification. Then, we multiply them to obtain the specular BRDF term.

There are other approximations that can be used, and we encourage you to experiment and replace ours with a different one!

We then compute the diffuse BDRF, as follows:

vec3 diffuse_brdf = (1 / PI) * base_colour.rgb;

We now introduce the Fresnel term. This determines the color of the reflection based on the viewing angle and the index of refraction of the material. Here is the implementation of the Schlick approximation, both for the metallic and dielectric components:

// f0 in the formula notation refers to the base colour
   here
vec3 conductor_fresnel = specular_brdf * ( base_colour.rgb
  + ( 1.0 - base_colour.rgb ) * pow( 1.0 - abs( HdotV ),
      5 ) );
// f0 in the formula notation refers to the value derived
   from ior = 1.5
float f0 = 0.04; // pow( ( 1 - ior ) / ( 1 + ior ), 2 )
float fr = f0 + ( 1 - f0 ) * pow(1 - abs( HdotV ), 5 );
vec3 fresnel_mix = mix( diffuse_brdf, vec3(
                        specular_brdf ), fr );

Here we compute the Fresnel term for both the conductor and the dielectric components according to the formula in the glTF specification.

Now that we have computed all the components of the model, we interpolate between them, based on the metalness of the material, as follows:

vec3 material_colour = mix( resnel_mix,
                            conductor_fresnel, metalness );

The occlusion term is not used as it only affects indirect light, which we haven’t implemented yet.

We realize this is a very quick introduction, and we skipped over a lot of the theory that makes these approximations work. However, it should provide a good starting point for further study.

We have added links to some excellent resources in the Further reading section if you’d like to experiment and modify our base implementation.

In the next section, we are going to introduce a debugging tool that we rely on whenever we have a non-trivial rendering issue. It has helped us many times while writing this book!