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:
I(Incident vector): The normalized direction vector of the incoming light ray pointing toward the interface.N(Normal vector): The normalized surface normal vector pointing outward from the boundary surface.eta(\(\eta\)): The ratio of the refractive index of the original medium to that of the medium being entered (\(\eta = n_1 / n_2\)).
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:
- Ensure \(I\) and \(N\) are normalized. Unnormalized inputs distort the dot product \(N \cdot I\), producing an incorrect discriminant \(k\) and altering the refracted vector magnitude.
- When handling back-faces or rays exiting an object, invert the normal vector (\(N = -N\)) and invert the refraction ratio (\(\eta = n_2 / n_1\)) to maintain consistent boundary physics.