Skip to content
Fundamental · VectorsIntermediate

Reflect & refract

Bounce a direction off a surface, or bend it through one, given the surface normal and the index of refraction.

How it works

Light that hits a surface either bounces off or passes through, bending as it enters. GLSL has a built-in function for each: reflect and refract. Both take an incoming direction and the surface normal and return a new direction. Highlights, mirror and environment lookups, glass and water all start from them.

reflect

reflect(I, N) mirrors the incident direction I about the normal N. It is defined as:

vec3 r = I - 2.0 * dot(N, I) * N; // what reflect(I, N) returns

The dot product measures how much of I points into the surface; subtracting twice that part flips it, and the part along the surface stays. N must be normalized for the result to keep I’s length.

The usual mistake is the direction of I. It points toward the surface, along the travel of the light. The view vector most shaders carry (i.view on this site’s cards) points the other way, from the surface toward the camera, so negate it:

vec3 n = normalize(i.normal);
vec3 v = normalize(i.view);   // surface → camera
vec3 r = reflect(-v, n);      // the direction a mirror would show at this pixel

Feed r to an environment map or a sky function and you get a mirror. Compare the reflected light direction with the view direction and you get a Phong highlight, bright where the light bounces straight into the camera:

vec3 l = normalize(vec3(0.4, 1.0, 0.3));             // toward the light
float spec = pow(max(dot(reflect(-l, n), v), 0.0), 32.0); // higher exponent, smaller and sharper highlight

Blinn-Phong replaces the reflected vector with the half vector normalize(l + v) compared against n, which is cheaper and behaves better at grazing angles; the Specular highlights technique builds on both.

refract

When light crosses from one material into another, it changes speed and bends. Each material has an index of refraction (IOR): about 1.0 for air, 1.33 for water, 1.5 for common glass, 2.42 for diamond. Snell’s law relates the angles on both sides to the ratio of the two indices, and refract(I, N, eta) applies it:

  • I is the normalized incident direction, toward the surface, as with reflect.
  • N is the normalized normal on the side I comes from.
  • eta is the ratio of the indices, the one being left over the one being entered.
float eta = 1.0 / 1.5;           // air into glass
vec3 t = refract(-v, n, eta);    // the bent ray inside the glass

Getting out again reverses everything: the normal flips to face the ray, and eta becomes 1.5 / 1.0. Going from a higher index into a lower one, like glass into air, a ray at a shallow enough angle can’t leave at all and reflects back inside. This is total internal reflection, and refract signals it by returning a zero vector, which you should check before using the result:

vec3 t = refract(dir, -n, 1.5);  // glass into air
if (dot(t, t) == 0.0) {
    t = reflect(dir, -n);        // total internal reflection
}

Refraction on screen

A real-time shader rarely traces the refracted ray through the scene. The common approximation offsets the lookup into an image of what is behind the surface by the bent direction:

vec3 tv = mat3(uView) * t;                    // world → view, so x and y follow the screen
vec2 offset = tv.xy * 0.05;                   // strength in screen units, tuned by eye
vec3 behind = texture(uScene, screenUv + offset).rgb;

It is not physically exact, but the eye accepts it. Refracting each color channel with a slightly different eta separates them like a prism, a cheap dispersion effect. How much of the light reflects and how much refracts depends on the angle too; the Fresnel effect describes that split.

Further reading