To get ready for the comming project, I am looking into an customized shading model for the base material - the one with a minimal implementation of PBR and without any unnecessary built-in features for a better rendering performance. After digging into the PBR theories as well as this amazing article spacific about PBR in Unreal Engine, I finally pieced up the puzzle of the basic shading model.
Theory Breakdown
Shading an object in rendering pipeline is basically generating colors on its surface area. In the language of physics, this is done by calculating the outgoing light energy (perceived by human as colors) of all the points on the surface of the object.
Light energy is described as Radiance with the letter $L$. (The definition of radiance is rather complex and will be noted later; it actually represents light energy differential on area and solid angle). The outgoing light contribution $L_o$ can be described as a function of a point position $p$ on the object surface and the direction $\omega_o$ of the outgoing light ray: $L_o(p,\omega_o)$.
To calculate this $L_o$ we have to integrated the incoming light energy $L_i$ (which is also a function of position and direction like $L_o$) on the hemesphere guided by surface normal at that point. $L_i$ is also weighted by the cosine of the incident angle (Lambert’s Law) which can be put as $n \cdot \omega_i$.
So the total lighting phenomenon can be described as:
\[L_o(p,\omega_o) = \int\limits_{\Omega} L_i(p,\omega_i) (n \cdot \omega_i) d\omega_i\]However, because different surface has different properties on influencing the light reaction (energy absorbing, reflection, refraction, subsurface scattering etc.) The integral is also weighted by another factor, which is the classic concept of BRDF (More explanation later).
In general, a typical BRDF consists a diffuse term and a specular term.
The diffuse part we are using Lambertian Diffuse BRDF
\[f_{Lambert} =\frac{c}{\pi}\]The specular part we are using Cook-Torrance Microfacet Specular BRDF
\[f_{cook-torrance} = \frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)}\]Then we also scale the diffuse term and specular term by $k_d$ and $k_s$, and put all together we get the final description of the lighting phenomenon:
\[L_o(p,\omega_o) = \int\limits_{\Omega} (k_d\frac{c}{\pi} + k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)}) L_i(p,\omega_i) (n \cdot \omega_i) d\omega_i\]This is called Cook-Torrance reflectance equation. Details explained as follows.
Implementation
Result:
Diffuse Term
Using Lambertian Diffuse BRDF
/**
* Standard Lambertian diffuse lighting.
*/
vec3 CalculateDiffuse(
in vec3 albedo)
{
return (albedo * ONE_OVER_PI);
}
Specular Term
Using Cook-Torrance Microfacet Specular BRDF
D - Normal distribution function (NDF)
We are using GGX/Trowbridge-Reitz NDF function. Roughness lies on the range [0.0, 1.0], with lower values producing a smoother, “glossier”, surface. Higher values produce a rougher surface with the specular lighting distributed over a larger surface area.
\[NDF_{GGX TR}(n, h, \alpha) = \frac{\alpha^2}{\pi((n \cdot h)^2 (\alpha^2 - 1) + 1)^2}\]where Here h is the halfway vector - half-way between the light (l) and view (v); $\alpha$ is adopted Disney’s reparameterization which equal to $Roughness^2$.
If we plot GGX/Trowbridge-Reitz NDF like this:
/**
* GGX/Trowbridge-Reitz NDF
*/
float CalculateNDF(
in vec3 surfNorm,
in vec3 halfVector,
in float roughness)
{
float a2 = (roughness * roughness);
float halfAngle = dot(surfNorm, halfVector);
return (a2 / (PI * pow((pow(halfAngle, 2.0) * (a2 - 1.0) + 1.0), 2.0)));
}
G - Microfacet geometric attenuation
We are using GGX/Schlick-Beckmann function and Smith’s method for analytical lighting scenario(TBD). The attenuation is modified by the roughness (input as $k$) and approximates the influence/amount of microfacets in the surface.
\[G_{SchlickGGX}(n, v, k) = \frac{n \cdot v} {(n \cdot v)(1 - k) + k }\]Note that for analytical lighting scenario,
\[k = \frac{({Roughness} + 1)^2}{8}\]/**
* GGX/Schlick-Beckmann microfacet geometric attenuation.
*/
float CalculateAttenuation(
in vec3 surfNorm,
in vec3 vector,
in float k)
{
float d = max(dot(surfNorm, vector), 0.0);
return (d / ((d * (1.0 - k)) + k));
}
/**
* GGX/Schlick-Beckmann attenuation for analytical light sources.
*/
float CalculateAttenuationAnalytical(
in vec3 surfNorm,
in vec3 toLight,
in vec3 toView,
in float roughness)
{
float k = pow((roughness + 1.0), 2.0) * 0.125;
// G(l) and G(v)
float lightAtten = CalculateAttenuation(surfNorm, toLight, k);
float viewAtten = CalculateAttenuation(surfNorm, toView, k);
// Smith
return (lightAtten * viewAtten);
}
For IBL (TBD)
F - Fresnel reflectivity
We are using Fresnel-Schlick approximation and Spherical Gaussian approximation. The Metallic parameter controls the fresnel incident value (fresnel0), more explanation later.
/**
* Calculates the Fresnel reflectivity.
*/
vec3 CalculateFresnel(
in vec3 surfNorm,
in vec3 toView,
in vec3 fresnel0)
{
float d = max(dot(surfNorm, toView), 0.0);
float p = ((-5.55473 * d) - 6.98316) * d;
// Fresnel-Schlick approximation
return fresnel0 + ((1.0 - fresnel0) * pow(1.0 - d, 5.0));
// modified by Spherical Gaussian approximation to replace the power, more efficient
return fresnel0 + ((1.0 - fresnel0) * pow(2.0, p));
}
Put together
/**
* Cook-Torrance BRDF for analytical light sources.
*/
vec3 CalculateSpecularAnalytical(
in vec3 surfNorm, // Surface normal
in vec3 toLight, // Normalized vector pointing to light source
in vec3 toView, // Normalized vector point to the view/camera
in vec3 fresnel0, // Fresnel incidence value
inout vec3 sfresnel, // Final fresnel value used a kS
in float roughness) // Roughness parameter (microfacet contribution)
{
vec3 halfVector = CalculateHalfVector(toLight, toView);
float ndf = CalculateNDF(surfNorm, halfVector, roughness);
float geoAtten = CalculateAttenuationAnalytical(surfNorm, toLight, toView, roughness);
sfresnel = CalculateFresnel(surfNorm, toView, fresnel0);
vec3 numerator = (sfresnel * ndf * geoAtten); // FDG
float denominator = 4.0 * dot(surfNorm, toLight) * dot(surfNorm, toView);
return (numerator / denominator);
}
For IBL (TBD)
Solve Reflectance Equation
Now we have to combine Diffuse Term + Specular Term.
/**
* Calculates the total light contribution for the analytical light source.
*/
vec3 CalculateLightingAnalytical(
in vec3 surfNorm,
in vec3 toLight,
in vec3 toView,
in vec3 albedo,
in float roughness)
{
vec3 fresnel0 = mix(vec3(0.04), albedo, Metallic);
vec3 ks = vec3(0.0);
vec3 kd = (1.0 - ks);
vec3 diffuse = CalculateDiffuse(albedo);
vec3 specular = CalculateSpecularAnalytical(surfNorm, toLight, toView, fresnel0, ks, roughness);
float angle = clamp(dot(surfNorm, toLight), 0.0, 1.0);
return ((kd * diffuse) + specular) * angle;
}
For IBL (TBD)
TBC - IBL/Unreal Material Implementation