Notes for February 25 class -- Even More Ray Tracing


In the real world many materials, such as oil, water, plastic, glass and diamond, are transparent. A transparent material has an index of refraction which measures how much light slows down as it enters that medium. For example, the index of refraction of water is about 1.333, of glass is about 1.5. The index of refraction of diamond, the substance with the highest known index of refraction, is 2.42.

As in the diagram to the right, you can add refraction to your ray tracing by following Snell's law:

n1 / n2 = sin(θ2) / sin(θ1)
to determine how much the ray should bend as it enters or exits a transparent object.

Note that you will need to change your ray tracing model to incorporate refraction. In addition to your initial incoming ray, and any shadow or reflection rays, you also need to add a refraction ray, which starts just inside the surface, and continues inward.

Note that if you have ray traced to a sphere, and are now computing where the refracting ray will exit that sphere, you will want to compute the second root of the quadratic equation.

Then, after this refracting ray has exited out the back of the transparent sphere, you will want to compute how much it refracts on its way out, and then shoot a ray from there into the scene behind the sphere.

In general, you use the results of refraction by mixing the color it returns together with whatever surface color you have computed due to pure reflection or blinn/phong reflectance.


Ray / plane intersection

Plane P = (a,b,c,d) describes all points in 3D such that ax + by + cz + d = 0

Half-space (a,b,c,d) with the above bounding plane, describes all points in 3D such that ax + by + cz + d ≤ 0

We can shoot ray (V + tW) to this half space, by solving for t:

a (vx + t wx) + b (vy + t wy) + c (vz + t wz) + d = 0

t (a wx + b wy + c wz) = - (a vx + b vy + c vz + d)

t = - (a vx + b vy + c vz + d) / (a wx + b wy + c wz)

The unit length vector in the direction of (a,b,c) is the surface normal of the half plane. Knowing this, we can figure out whether the ray is going into the half space or out of it. There are three cases:
Ray is going into half space: (a wx + b wy + c wz) < 0
Ray is going out of half space: (a wx + b wy + c wz) > 0
Ray is parallel to half space: (a wx + b wy + c wz) = 0
The last case has two sub-cases:
Ray is completely inside half space: (a vx + b vy + c vz + d) ≤ 0
Ray is completely outside half space: (a vx + b vy + c vz + d) > 0

Ray tracing to a unit cube

Once you can ray trace to a half space, you can also ray trace to an intersection of half spaces. One such intersection is a unit cube, which is defined by the intersections of the six planes:
( 1, 0, 0,-1)           x ≥ -1
(-1, 0, 0,-1)           x ≤  1
( 0, 1, 0,-1)           y ≥ -1
( 0,-1, 0,-1)           y ≤  1
( 0, 0, 1,-1)           z ≥ -1
( 0, 0,-1,-1)           z ≤  1

The figure to the right shows the two dimensional analogous shape -- a square formed by the intersection of four half planes.

As always, you find the intersection by taking the maximum of all of the entering values of t and the minimum of all the exiting values of t. To compute how each half space contributes to thie intersection, consider each of the four possible cases:

If the ray enters a half space: the exiting value of t is positive infinity.

If the ray exits a half space: the entering value of t is 0.

If the ray is parallel to and entirely inside a half space: we can just ignore that half space.

If the ray is parallel to and entirely outside a half space: the ray misses the entire cube.


Ray tracing to an axis-aligned cylinder

The equation of an infinitely high vertical cylindrical volume of radius r, centered at (cx,cz) is:
(x - cx)2 + (z - cz)2 - r2 ≤ 0
So to ray trace to a vertical cylinder, we just need to ray trace to this shape intersected with bottom and top end caps, which can be described by the two respective equations:
y ≥ ymin

y ≤ ymax

These two equations can be rewritten as:

-y + ymin ≤ 0

 y -  ymax ≤ 0

Therefore they can be described by the respective sets of coefficients:

(0, -1, 0,  ymin)

(0,  1, 0, -ymax)

You can use a similar approach to form the equations of the three shapes to be intersected (an infinite tube and two end caps) for an x-axis or z-axis aligned cylinder.

History and algorithm for the noise function

In order to do interesting solid procedural textures, we are going to use the noise function.

We went through a brief history of and algorithmic overview of the noise function, by stepping through the on-line lecture notes at:

Noise is a coherent, space filling pseudo-random function defined over all values of (x,y,z), which returns values betweeo -1 and +1. When used in shaders, it is very useful for making more natural using textures.

The on-line lecture notes cover how it was first used in movies, gives a brief overview of how it is constructed, and gives some examples of how to make interesting textures using expressions containing noise.


Fractals, turbulence, marble

We then showed that there are a number of ways of using the noise function to make visually useful textual "idioms".

One of these is a fractal sum of noise functions:

fractal(p) = i ( noise(2i p) / 2i )
where p is a point in space (px, py, pz )

Another useful function, which simulates the visual appearance of the onset of turbulence by creating discontinuities in gradient at all scales, is:

turbulence(p) = i ( abs(noise(2i p)) / 2i )
Note that the only difference between fractal(p) and turbulence(p) is that turbulence uses the absolute value of noise, thereby creating discontinuities in gradient where the value of the noise function crosses zero.

Turbulence can be used in all sorts of textures. For example, to make the marble vase on the right, I used the turbulence function to create a phase shift in a stripe pattern made using a cosine function. Described as a GLSL this would be:

   float t = 0.4 * (turbulence(vec3(x * 1.5, y * 1.5, 10.)) + 1.8);
   float s = pow(0.5 + 0.5 * cos(7. * x + 6. * t), 0.1);
   gl_FragColor = vec4(s, s*s, s*s*s, 1.);


Fragment shader for the noise function

If you want to try out noise in your own textures, like we did in class, you can include this code in your fragment shader:
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
vec3 fade(vec3 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); }

float noise(vec3 P) {
   vec3 i0 = mod289(floor(P)), i1 = mod289(i0 + vec3(1.0));
   vec3 f0 = fract(P), f1 = f0 - vec3(1.0), f = fade(f0);
   vec4 ix = vec4(i0.x, i1.x, i0.x, i1.x), iy = vec4(i0.yy, i1.yy);
   vec4 iz0 = i0.zzzz, iz1 = i1.zzzz;
   vec4 ixy = permute(permute(ix) + iy), ixy0 = permute(ixy + iz0), ixy1 = permute(ixy + iz1);
   vec4 gx0 = ixy0 * (1.0 / 7.0), gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5;
   vec4 gx1 = ixy1 * (1.0 / 7.0), gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5;
   gx0 = fract(gx0); gx1 = fract(gx1);
   vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0), sz0 = step(gz0, vec4(0.0));
   vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1), sz1 = step(gz1, vec4(0.0));
   gx0 -= sz0 * (step(0.0, gx0) - 0.5); gy0 -= sz0 * (step(0.0, gy0) - 0.5);
   gx1 -= sz1 * (step(0.0, gx1) - 0.5); gy1 -= sz1 * (step(0.0, gy1) - 0.5);
   vec3 g0 = vec3(gx0.x,gy0.x,gz0.x), g1 = vec3(gx0.y,gy0.y,gz0.y),
        g2 = vec3(gx0.z,gy0.z,gz0.z), g3 = vec3(gx0.w,gy0.w,gz0.w),
        g4 = vec3(gx1.x,gy1.x,gz1.x), g5 = vec3(gx1.y,gy1.y,gz1.y),
        g6 = vec3(gx1.z,gy1.z,gz1.z), g7 = vec3(gx1.w,gy1.w,gz1.w);
   vec4 norm0 = taylorInvSqrt(vec4(dot(g0,g0), dot(g2,g2), dot(g1,g1), dot(g3,g3)));
   vec4 norm1 = taylorInvSqrt(vec4(dot(g4,g4), dot(g6,g6), dot(g5,g5), dot(g7,g7)));
   g0 *= norm0.x; g2 *= norm0.y; g1 *= norm0.z; g3 *= norm0.w;
   g4 *= norm1.x; g6 *= norm1.y; g5 *= norm1.z; g7 *= norm1.w;
   vec4 nz = mix(vec4(dot(g0, vec3(f0.x, f0.y, f0.z)), dot(g1, vec3(f1.x, f0.y, f0.z)),
                      dot(g2, vec3(f0.x, f1.y, f0.z)), dot(g3, vec3(f1.x, f1.y, f0.z))),
                 vec4(dot(g4, vec3(f0.x, f0.y, f1.z)), dot(g5, vec3(f1.x, f0.y, f1.z)),
                      dot(g6, vec3(f0.x, f1.y, f1.z)), dot(g7, vec3(f1.x, f1.y, f1.z))), f.z);
   return 2.2 * mix(mix(nz.x,nz.z,f.y), mix(nz.y,nz.w,f.y), f.x);

Homework (due before class on Wednesday Mar 4)

This week I'm just going to give you an array of possible things to work on. If you are feeling very ambitious you can attempt all of them, but it's also just fine to focus on just two or three of them, diving deep to produce something wonderful:
  • Implement refraction.
  • Generalize cube to boxes of arbitrary center and extent.
  • Figure out equations for other polyhedra such as prism, pyramid, tetrahedron, octahedron, dodecahedron and icosahedron.
  • Generalize to non-axis aligned cylinders.
  • Make solid texture using noise. Use it to vary different aspects of surface reflectance.

As always, make things that are cool and fun, and try to create something interactive (using uCursor) and/or animated (using uTime).