Ray Tracing 006: Surface Normals


Surface Normals

The sphere I added in the last episode was rendered as a flat red circle. In the following step I’m going to add a surface normal to shade the sphere.

Surface normals are used for several purposes in graphics, primarily for lighting. A surface normal is a vector that is perpendicular to the surface at a given point. This vector can either point outward or inward. They usually point outward by convention, but inward pointing normals can be useful, for example, to distinguish between solid spheres and spherical bubbles.

For a sphere the surface normal is in the direction from the center through the hitpoint, minus the center.

image/svg+xml

This scene has no lights yet, but we can visualize the normals with a color map, see also Normal mapping. Assume N is a unit length vector so that each component is between -1 and 1. Map each component to the range from 0 to 1, and then map x,y,z to r,g,b.

// surfacenormal-example.cpp
float hit_sphere(const vec3& center, float radius, const ray& r) {
  // See notes, solving the quadratic formula where p(t)=A+tB meets the sphere x^2+y^2+z^2=R^2
  vec3 oc = r.origin() - center;
  float a = dot(r.direction(), r.direction());
  float b = 2.0 * dot(oc, r.direction());
  float c = dot(oc, oc) - radius * radius;
  float discriminant = b*b - 4*a*c;

  if (discriminant < 0) return -1.0; // square root of negative numbers not defined here, no roots = ray doesn't hit sphere
  else return (-b - sqrt(discriminant)) / (2.0*a); // return smallest t (closest hitpoint)
}

The hit_sphere() function is no longer just returning whether or not the sphere was hit by the ray. It returns the hitpoint needed to compute the normal, that is, the parameter t where \( p(t)=A+tB \) intersects with the sphere. The return value changes from bool to a float.

If the ray misses the sphere (discriminant < 0) we return -1. This value for t won’t affect the visible area because it lies behind the camera.

If the ray hits the sphere, we’ll return the smallest t, the closest hitpoint.

The color() function needs to be updated to calculate and visualize the normal:


vec3 color(const ray& r) {
  // get closest t intersecting with the sphere with center at (0,0,-1) and radius 0.5
  float t = hit_sphere(vec3(0.0,0.0,-1.0), 0.5, r);

  // if t > 0 we have hit the sphere
  // if t < 0 no roots, no intersection with sphere
  if (t > 0.0) {
    // N = (P - C) converted into a unit-vector with components between -1 and 1
    vec3 N = unit_vector(r.point_at_parameter(t) - vec3(0.0,0.0,-1.0));

    // map components to 0-1 and use these as r, g, b values
    return 0.5 * vec3(N.x() + 1.0, N.y() + 1.0, N.z() + 1.0);
  }

  // ray didn't hit the sphere, get the pixel position the ray is passing through (u,v) as unit-vector -1 to 1 
  vec3 unit_direction = unit_vector(r.direction());
  // map this to 0-1
  t = 0.5 * (unit_direction.y() + 1.0);
  // linear interpolation from blue to white
  return (1.0 - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}

Javascript and Canvas

Here’s also said Normal color map rendered with Javascript:

Please see my repository at github.com/celeph/ray-tracing for the complete code. For more details, check out Peter Shirley’s excellent book that inspired this episode: “Ray Tracing in One Weekend” and his original book and code repo.

Credits and Further Reading

Next Steps