Work Session 0
Physically Based Rendering (PBR)
Goal
This worksessiong should be built off of assignment 3. That would mean we are creating a deferred PBR rendering pipeline.
This worksession involves converting mathematical equations into a shader program for use in the graphics pipeline. You will be provided with equations, and your task is to express them in GLSL (OpenGL Shading Language) code. This exercise aims to develop your skills in translating equations into a fragment shader for visualization on the GPU.
Through this practical application, you’ll gain hands-on experience in shader programming, connecting mathematical formulations with visual outputs.
Requirements
There isn’t a list of traits that define what a “Physically Based Renderer” is compared to a regular lighting model is, but we will define some common traits that we will implement.
- Energy Conservation
- The energy going in must not exceed the energy coming out.
- Microfacet Model
- Surfaces are made of microscopic faces that perfectly reflect light.
- Fresnel Effect
- The lower the viewing angle on surface is the better the reflection is.
- Authored Materials
- Using textures gives us per-fragment control over how each specific surface point should react to light.
In your own research you may notice that not all tutorials follow these traits, yet they are still considered to be PBR. You may also find that not all resources use the same functions or models, this is normal.
Lighting Equation
The lighting equation, sometimes referred to as the “rendering equation”, is described to as follows:
Lo(x,V)=Le(x,V)+∫fr(x,L,V)Li(x,V)(L⋅N)dLSince we won’t be fully simulating all possible lights within a hemisphere. We’re going to simplify the equation to use a fixed number of lights, and I’ll give these variables names so we can more easily reference them later.
Lo(x,V)⏟outgoing=Le(x,V)⏟emitted+∑fr(x,Ln,V)⏟BDRFLi(x,Ln)⏟incoming(Ln⋅N)⏟LdotN {x=fragment position,V=view vector,Ln=current light vector,/* code spoilers */
// material properties
uniform float metallic;
uniform float roughness;
// cached dot products
float NdotH;
float NdotV;
float NdotL;
float VdotH;
float VdotN;
float LdotN;
// Rendering equation
// N: normal
// V: view vector
// L: light vector
// H: halfway vector
void main()
{
// normalize all vectors
vec3 N = normalize(vs_normal);
vec3 V = normalize(camera_position);
vec3 L = normalize(light.position);
vec3 H = normalize(V + L);
// pre-compute all dot products
NdotH = max(dot(N, H), 0.0);
NdotV = max(dot(N, V), 0.0);
NdotL = max(dot(N, L), 0.0);
VdotN = max(dot(V, N), 0.0);
VdotH = max(dot(V, H), 0.0);
LdotN = max(dot(L, N), 0.0);
// NOTE: these are just placeholders.
// It would be appropriate to turn them into functions.
vec3 emitted = vec3(0.0);
vec3 brdf = vec3(0.0);
vec3 incoming = vec3(0.0);
vec3 pbr = emited + (brdf * incoming * LdotN);
// NOTE: returning pbr as the final color isn't strictly necessary.
// At this point, or at any point really, you can styalize how you want.
FragColor = vec4(pbr, 1.0);
}
Bidirectional Reflective Distribution Function (BRDF)
The first variable (or function) were going to tackle is the BRDF. This variable is relatively straight forward as we simply add our diffuse and specular lighting. Notice that ( k_d + k_s = 1 ) this is for energy conservation, no extra light is being produced.
BRDF=kdflambert⏟diffuse+ksfcook−torrence⏟specular {ks=Fresnel,kd=1−ks,/* code spoilers */
vec3 BDRF()
{
// lambertian distribution
vec3 lambert = alb / PI;
// NOTE: these are just placeholders.
// It would be appropriate to turn them into functions.
vec3 schlickFresnel = vec3(1.0);
vec3 cookTorrance = vec3(1.0);
// ratio between reflection and refraction
// Ks + Kd = 1
vec3 Ks = schlickFresnel;
vec3 Kd = vec3(1.0) - Ks;
// diffuse + specular
vec3 diffuseBRDF = (Kd * lambert);
vec3 specularBRDF = (Ks * cookTorrance);
return diffuseBRDF + specularBRDF;
}
Fresnel-Schlick Function
The Fresnel-Schlick function approximates reflectance variation based on the viewing angle, increasing specular reflection at grazing angles for realistic material appearance.
Fschlick=F0+(1−F0)(1−(V⋅H))5 {F0=base reflectivity,V=view vector,H=half-way vector,Lambertian Distribution
flambert=albedoπCook-Torrence Function
The Cook-Torrance function simulates light reflection by considering surface roughness, light scattering, and view dependence, producing realistic highlights and material appearance in rendering.
fcook-torrence=DGF4(V⋅N)(Ln⋅N) {D=Normal Distribution Function,G=Geometry Shadowing Function,F=Fresnel Function,V=view vector,N=normal vector,Ln=current light vector,/* code spoilers */
// V: view vector
// H: half-way vector
// L: light vector
// N: normal
// F0: base reflectivity
// alpha: roughness
vec3 cookTorrence(vec3 VdotH, vec3 VdotN, vec3 LdotN, vec3 F0, float alpha)
{
// D: GGX / Throwbridge-Reitz Normal Distribution Function
// G: Schlick-Beckmann Geometry Shadowing Function
// F: Fresnel-Schlick Function
vec3 cookTorranceNumerator = D(alpha) * G(alpha) * F(F0, VdotH);
float cookTorranceDenominator = 4.0 * VdotN * LdotN;
// avoid divide by 0.
cookTorranceDenominator = max(cookTorranceDenominator, 0.000001);
return cookTorranceNumerator / cookTorranceDenominator;
}
GGX / Throwbridge-Reitz Normal Distribution Function
The GGX/Trowbridge-Reitz NDF models surface microfacet distribution, controlling highlight shape and roughness, making specular reflections appear more realistic in rendering.
α=roughness2 Dthrowbridge-reitz=α2π((N⋅H)2(α2−1)+1)2Schlick-Beckmann Geometry Shadowing Function
This approximates light occlusion on rough surfaces, reducing reflection based on viewing angle and surface roughness for realistic shading.
In this case ( x ) will represent a dot product.
Gschlick-beckmann=(N⋅X)(N⋅X)(1−k)+k {k=α2,X=V or LSmith Model
The Smith model calculates geometric shadowing and masking for microfacet shading, improving realism by accounting for how surface roughness affects light reflection and visibility.
Gsmith=G1(L,N)G1(V,N)