GLSL Schlick Fresnel Approximation Implementation

This article provides a direct guide to implementing Christophe Schlick's Fresnel approximation in GLSL (OpenGL Shading Language). It breaks down the underlying mathematical formula, explains the geometric vectors used in Physically Based Rendering (PBR), and provides production-ready shader functions for both scalar and wavelength-dependent (RGB) reflectance calculations.

The Mathematical Formula

The Fresnel effect describes how the reflectivity of a surface increases at grazing viewing angles. Schlick's approximation models this behavior with significantly lower computational cost than the full electrodynamic Fresnel equations.

The standard equation is:

\[F(\theta) = F_0 + (1 - F_0)(1 - \cos\theta)^5\]

Where:

  • \(F_0\) is the surface reflectance at normal incidence (looking directly at the surface, \(0^\circ\) angle).
  • \(\cos\theta\) is the cosine of the angle between the surface normal (or microfacet half-vector) and the viewing direction.
  • \(F(\theta)\) is the resulting reflectance factor between \(0.0\) and \(1.0\).

In microfacet specular BRDFs (such as Cook-Torrance), \(\cos\theta\) is calculated as the dot product between the view vector (\(\mathbf{V}\)) and the half-vector (\(\mathbf{H}\)): \(\cos\theta = \mathbf{V} \cdot \mathbf{H}\). For simple non-microfacet approximations, the dot product between the surface normal (\(\mathbf{N}\)) and view direction (\(\mathbf{V}\)) is commonly used: \(\cos\theta = \mathbf{N} \cdot \mathbf{V}\).


GLSL Implementation

Because real-time PBR workflows deal with colored metals (conductors), \(F_0\) is typically represented as an RGB vector (vec3). Dielectrics (non-metals) use a monochromatic value (typically around 0.04), while metals use their base albedo color for \(F_0\).

1. Standard Vector Implementation (vec3)

vec3 fresnelSchlick(float cosTheta, vec3 F0) {
    return F0 + (vec3(1.0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}

2. Scalar Implementation (float)

For basic shaders that do not require colored specular highlights:

float fresnelSchlickScalar(float cosTheta, float F0) {
    return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}

Integration into a Shader

To use the function inside a fragment shader:

  1. Calculate the View and Half Vectors: Ensure vectors are normalized.
  2. Clamp the Dot Product: Ensure that \(\cos\theta\) does not become negative, which can cause graphical artifacts.
// Normalized vectors
vec3 N = normalize(vNormal);
vec3 V = normalize(uCameraPosition - vWorldPosition);
vec3 L = normalize(uLightPosition - vWorldPosition);
vec3 H = normalize(V + L);

// Cosine of the angle (clamped to prevent negative values)
float cosTheta = max(dot(H, V), 0.0);

// Base reflectivity: 0.04 for dielectrics, tinted albedo for conductors
vec3 F0 = mix(vec3(0.04), albedo, metallic);

// Calculate Fresnel factor
vec3 F = fresnelSchlick(cosTheta, F0);

Roughness Modification (Image-Based Lighting)

When calculating Fresnel for ambient Image-Based Lighting (IBL), high surface roughness should prevent sharp, mirror-like specular reflections at grazing angles. Sébastien Lagarde proposed this common adjustment:

vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
    return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}