Shadertoy Tutorial Part 12 - Fresnel and Rim Lighting

Published: Monday, April 19, 2021

Greetings, friends! Welcome to Part 12 of my Shadertoy tutorial series. In this tutorial, we'll learn how to add rim lighting around a sphere using fresnel reflection.

Initial Setup

We'll start with a basic ray marching template.

glsl
Copied! ⭐️
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;

float sdSphere(vec3 p, float r )
{
  vec3 offset = vec3(0, 0, -2);
  return length(p - offset) - r;
}

float sdScene(vec3 p) {
  return sdSphere(p, 1.);
}

float rayMarch(vec3 ro, vec3 rd) {
  float depth = MIN_DIST;

  for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
    vec3 p = ro + depth * rd;
    float d = sdScene(p);
    depth += d;
    if (d < PRECISION || depth > MAX_DIST) break;
  }

  return depth;
}

vec3 calcNormal(vec3 p) {
    vec2 e = vec2(1.0, -1.0) * 0.0005;
    return normalize(
      e.xyy * sdScene(p + e.xyy) +
      e.yyx * sdScene(p + e.yyx) +
      e.yxy * sdScene(p + e.yxy) +
      e.xxx * sdScene(p + e.xxx));
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec3 backgroundColor = vec3(0.1);
  vec3 col = vec3(0);

  vec3 ro = vec3(0, 0, 3);
  vec3 rd = normalize(vec3(uv, -1));

  float d = rayMarch(ro, rd);

  if (d > MAX_DIST) {
    col = backgroundColor;
  } else {
    vec3 p = ro + rd * d;
    vec3 normal = calcNormal(p);
    vec3 lightPosition = vec3(4, 4, 7);
    vec3 lightDirection = normalize(lightPosition - p);

    float diffuse = clamp(dot(normal, lightDirection), 0., 1.);
    vec3 diffuseColor = vec3(0, 0.6, 1);

    col = diffuse * diffuseColor;
  }

  fragColor = vec4(col, 1.0);
}

When you run this code, you should see a blue sphere with only diffuse (Lambertian) reflection.

Canvas with an almost black background and blue sphere in the center. The sphere is illuminated from the top-right, causing shadows on the bottom-left.

Fresnel Reflection

The Fresnel equations describe the reflection and transmission of light when it is incident on an interface between two different optical media. In simpler terms, this means that objects can be lit a bit differently when you look at them from grazing angles.

The term, optical media, refers to the type of material light passes through. Different materials tend to have different refractive indices which makes it appear that light is bending.

A ray of light being refracted in a plastic block.
Refraction by Wikipedia

Air is a type of medium. It typically has an index of refraction of about 1.000293. Materials such as diamonds have a high index of refraction. Diamond has an index of refraction of 2.417. A high index of refraction means light will appear to bend even more.

The Fresnel equations can get pretty complicated. For computer graphics, you will typically see people use the Schlick's approximation for approximating the Fresnel contribution of reflection.

Schlick's approximation.
Schlick's approximation by Wikipedia

The above equation calculates the Fresnel contribution to reflection, R where R0 is the reflection coefficient for light incoming parallel to the normal (typically when θ equals zero).

The value of cos θ is equal to the dot product between the surface normal and the direction the incident light is coming from. In our code, however, we'll use the ray direction, rd.

For the purposes of our examples, we will assume that the refractive index of air and the sphere are both equal to one. This will help simplify our calculations. This means that R0 is equal to zero.

text
Copied! ⭐️
n1 = 1
n2 = 1

R0 = ((n1 - n2)/(n1 + n2)) ^ 2
R0 = ((1 - 1)/(1 + 1)) ^ 2
R0 = 0

With R0 equal to zero, we can simplify the Fresnel reflection equation even more.

text
Copied! ⭐️
R = R0 + (1 - R0)(1 - cosθ)^5

Since R0 = 0,
R = (1 - cosθ)^5

In GLSL code, this can be written as:

glsl
Copied! ⭐️
float fresnel = pow(1. - dot(normal, -rd), 5.);

However, we clamp the values to make sure we keep the range between zero and one. We also use -rd. If you used positive rd, then you might not see the color only being applied to the rim of the sphere.

glsl
Copied! ⭐️
float fresnel = pow(clamp(1. - dot(normal, -rd), 0., 1.), 5.);

We can multiply this fresnel value by a color value, so we can apply a colored rim around our blue sphere. Below is the finished code:

glsl
Copied! ⭐️
const int MAX_MARCHING_STEPS = 255;
const float MIN_DIST = 0.0;
const float MAX_DIST = 100.0;
const float PRECISION = 0.001;

float sdSphere(vec3 p, float r )
{
  vec3 offset = vec3(0, 0, -2);
  return length(p - offset) - r;
}

float sdScene(vec3 p) {
  return sdSphere(p, 1.);
}

float rayMarch(vec3 ro, vec3 rd) {
  float depth = MIN_DIST;

  for (int i = 0; i < MAX_MARCHING_STEPS; i++) {
    vec3 p = ro + depth * rd;
    float d = sdScene(p);
    depth += d;
    if (d < PRECISION || depth > MAX_DIST) break;
  }

  return depth;
}

vec3 calcNormal(vec3 p) {
    vec2 e = vec2(1.0, -1.0) * 0.0005;
    return normalize(
      e.xyy * sdScene(p + e.xyy) +
      e.yyx * sdScene(p + e.yyx) +
      e.yxy * sdScene(p + e.yxy) +
      e.xxx * sdScene(p + e.xxx));
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
  vec3 backgroundColor = vec3(0.1);
  vec3 col = vec3(0);

  vec3 ro = vec3(0, 0, 3);
  vec3 rd = normalize(vec3(uv, -1));

  float d = rayMarch(ro, rd);

  if (d > MAX_DIST) {
    col = backgroundColor;
  } else {
    vec3 p = ro + rd * d;
    vec3 normal = calcNormal(p);
    vec3 lightPosition = vec3(4, 4, 7);
    vec3 lightDirection = normalize(lightPosition - p);

    float diffuse = clamp(dot(normal, lightDirection), 0., 1.);
    vec3 diffuseColor = vec3(0, 0.6, 1);

    float fresnel = pow(clamp(1. - dot(normal, -rd), 0., 1.), 5.);
    vec3 rimColor = vec3(1, 1, 1);

    col = diffuse * diffuseColor + fresnel * rimColor; // add the fresnel contribution
  }

  fragColor = vec4(col, 1.0);
}

If you run this code, you should see a thin white rim of our blue sphere. This simulates the effect of light hitting a grazing angle of our sphere.

Canvas with an almost black background and blue sphere in the center. The sphere is illuminated from the top-right, causing shadows on the bottom-left. With fresnel reflection, the sphere appears to have a white rim.

You can play around with the exponent and the rim color to get a "force field" like effect.

glsl
Copied! ⭐️
float fresnel = pow(clamp(1. - dot(normal, -rd), 0., 1.), 0.5);
vec3 rimColor = vec3(1, 0, 1);

col = diffuse * diffuseColor + fresnel * rimColor;

Canvas with an almost black background and blue sphere in the center. The sphere is illuminated from the top-right, causing shadows on the bottom-left. With fresnel reflection using an exponent of 0.5, the sphere appears to have a pink force field around it.

Conclusion

In this article we learned how to add rim lighting around objects by applying fresnel reflection. If you're dealing with objects that mimic glass or plastic, then adding fresnel can help make them a bit more realistic.

Resources