How Does the GLSL refract Function Work?

The built-in refract function in GLSL computes the transmission direction of a light ray passing through an interface between two media of differing optical densities. Based on Snell’s Law, it takes an incident vector, a surface normal, and the ratio of refractive indices to determine the refracted vector. This article breaks down the underlying mathematical derivation, the vector projection steps, and how the function handles physical phenomena such as total internal reflection.

Function Signature and Core Parameters

In GLSL, the refract function operates on float scalar and vector types (vec2, vec3, vec4):

genType refract(genType I, genType N, float eta);

The function expects three parameters:

Both I and N must be pre-normalized unit vectors to produce physically accurate results.

Mathematical Derivation from Snell's Law

Snell’s Law governs refraction at a planar boundary:

\[n_1 \sin(\theta_1) = n_2 \sin(\theta_2) \implies \sin(\theta_2) = \eta \sin(\theta_1)\]

Where \(\theta_1\) is the angle of incidence between \(-I\) and \(N\), and \(\theta_2\) is the angle of refraction between the transmitted vector \(R\) and \(-N\).

To derive the vector form of the transmitted ray \(R\), decompose \(R\) into components parallel and perpendicular to the normal \(N\):

\[R = R_{\perp} + R_{\parallel}\]

Using the geometric projection of the incident vector \(I\):

\[R_{\perp} = \eta \left(I - N(N \cdot I)\right)\]

The magnitude of the normal component \(R_{\parallel}\) is governed by \(\cos(\theta_2)\):

\[R_{\parallel} = -N \sqrt{1 - \vert{}R_{\perp}\vert{}^2}\]

Using the identity \(\sin^2(\theta_1) = 1 - (N \cdot I)^2\), the term under the square root simplifies into the discriminant \(k\):

\[k = 1.0 - \eta^2 \left(1.0 - (N \cdot I)^2\right)\]

Combining the components yields the complete GLSL refraction equation:

\[R = \eta I - \left(\eta (N \cdot I) + \sqrt{k}\right) N\]

Handling Total Internal Reflection

Total internal reflection (TIR) occurs when light travels from an optically denser medium to a rarer medium (\(n_1 > n_2\), meaning \(\eta > 1\)) at an angle exceeding the critical angle \(\theta_c\). Under these conditions:

\[\eta^2 \left(1.0 - (N \cdot I)^2\right) > 1.0\]

This causes the discriminant \(k\) to drop below zero (\(k < 0.0\)). Because a real square root cannot be evaluated for negative numbers, no transmitted ray can physically exist.

GLSL handles this scenario deterministically: whenever \(k < 0.0\), the function returns a zero vector (genType(0.0)). Shader implementations typically check for this zero vector to branch into a reflection calculation or sample an environment map accordingly.

Reference Implementation

The behavior of refract can be expressed in GLSL code as follows:

vec3 computeRefraction(vec3 I, vec3 N, float eta) {
    float dotNI = dot(N, I);
    float k = 1.0 - eta * eta * (1.0 - dotNI * dotNI);
    
    if (k < 0.0) {
        return vec3(0.0);
    } else {
        return eta * I - (eta * dotNI + sqrt(k)) * N;
    }
}

Performance and Numerical Considerations

Hardware graphics processing units execute this formula using fused multiply-add operations to evaluate \(k\) and the resulting vector with minimal arithmetic overhead. To ensure visual stability and prevent artifacts: