Notes for February 6 class -- Ray tracing, part 1

Ray tracing is a way to render a scene in object space, rather than image space.

The fundamental idea is to shoot a ray into the scene at every pixel. Whatever object in the scene this ray hits first, that is the object visible at that pixel.

We can define a ray using an origin point V and a unit-length direction vector W.

Any point on the ray can be described by:

V + t W       for t >= 0
Note that points where t < 0 are not part of the ray, because those points are behind the ray origin V, not in front of it.

In object space, we can describe the image plane itself as a square, with x==-1 along the left edge, x==+1 along the right edge, y==-1 along the bottom edge, and y==+1 along the top edge. The value of z is zero.

As it happens, that is exactly the range of values for vPos in the shader programs you are writing.

Since the observer is in front of the square, and our coordinate system follows the right hand rule, she will be located on a point along the z axis, in positive z.

Adapting the convention of photography, we use the variable f -- for focal length -- for the distance of the observer from the image plane. We say that observer is in location:

V = [0,0,f]
To form a ray through any pixel [x,y] of the image, we first take the different vector from that pixel to the observer:
Then we normalize that vector to get the ray direction:
W = normalize([x, y, f])
If we were to write the same thing as shader language code it would look like this:
vec3 W = normalize(vec3(vPos.x, vPos.y, f));
Any point along this ray can be described by:
[Vx + t Wx, Vy + t Wy, Vz + t Wz], where t >= 0
We can use our ray to "ray trace" to various objects in the scene. Suppose, for example, we want to ray trace to a sphere. To do this, we first need to come up with a good definition of a sphere shape. Since a sphere has a center location C and a radius r, we can define a sphere using the following four values:
Cx, Cy, Cz, r
To ray trace to this sphere, we need to find what point (if any) along the ray is on the surface of the sphere.

Points on the surface of the sphere are going to be those points that are a distance r from the center of the sphere.

Recall, from our definition of ● product, that the magnitude squared of any vector v is:

vx2 + vy2 + vz2
Using this fact, we can compute the distance squared from any point [x,y,z] to the sphere center by:
(x-Cx)2 + (y-Cy)2 + (z-Cz)2
So if a point [x,y,z] is on the sphere, it must be true that:
(x-Cx)2 + (y-Cy)2 + (z-Cz)2 = r2
Recall that any point along our ray can be described by:
[Vx + t Wx, Vy + t Wy, Vz + t Wz], where t >= 0
To make our math easier, let's shift coordinates, so that the sphere is at the origin [0,0,0].

To do this, we just need to replace V by

P = V - C
This substitution moves the sphere to the origin. Now our ray equation becomes:
[Px + t Wx, Py + t Wy, Pz + t Wz], where t >= 0
We can substitute this point into our equation for the sphere (which is now at the origin) to get:
(Px + t Wx)2 + (Py + t Wy)2 + (Pz + t Wz) = r2
Multiplying out the terms, we get:
t2 (Wx * Wx + Wy * Wy + Wz * Wz) +
t   (2 * (Wx * Px + Wy * Py + Wz * Pz) ) +
    (Px * Px + Py * Py + Pz * Pz - r2) = 0
Using our definition of dot product, we can rewrite this as:
(W ● W) t2 + 2 (W ● P) t + (P ● P - r2) = 0
But since W is unit length, (W ● W) is just 1. This further simplifies our equation to:
t2 + 2 (W ● P) t + (P ● P - r2) = 0
We can now solve the standard quadratic equation,
t = (-B +- sqrt(B2 - 4AC)) / 2A
where in our case:
A = 1
B = 2 (W ● P)
C = P ● P - r2
to get:
t = (-2(W ● P) ± sqrt(4(W ● P)2 - 4(P ● P-r2))) / 2
t = -(W●P) ± sqrt((W●P)2 - P●P + r2)       Equation 1
The above equation can have zero, one or two real solutions, depending on whether the expression inside the square root is negative, zero or positive, respectively.

Zero real solutions means that the ray has missed the sphere.

If there is a real solution but t is negative, that means the sphere is behind the ray.

Otherwise, one solution means the ray is just barely grazing the sphere, and two solutions means the ray is going into the sphere at one point and then exiting out at another point.

If you do find two positive roots, then you want the smaller of the two roots, because that is the one where the ray enters the sphere.

Once you find t, you can find the intersection point S on the sphere surface by plugging t into the ray equation:

S = V + t W
Then in order to do lighting on the sphere, you can find the normalized vector from the center of the sphere to this surface point:
N = normalize(S - C)
To summarize the algorithm:

  • vec3 V = vec3(0, 0, f);
  • vec3 W = normalize(vec3(vPos.x, vPos.y, -f));
  • vec3 P = V - C;
  • Solve the quadratic equation above (Equation 1) to compute t
  • If first root t exists and is positive:
    • vec3 S = V + t * W;
    • vec3 N = normalize(S - C);


Due before class on Tuesday Feb 13.

Below is a slightly cleaned up version of the code that I wrote at the end of class. Notice that it defines a vec3 variable N which gives the direction on a sphere. For pixels outside the sphere, N.z is set to -1.0.

For pixels within the sphere, N is used to do shading of the sphere.

Write a simple shader program that ray traces to every pixel, using the algorithm described above. You should be able to end up with a value for N which you can test with the code below.

uniform float uTime;
varying vec3 vPos;

vec3 sph(float x, float y) {
   float zz = 1. - x * x - y * y;
   float sign = step(0., zz);
   float z = mix(-1., sqrt(zz), sign);
   return vec3(x, y, z);

void main() {
   vec3 N = sph(2.*vPos.x, 2.*vPos.y);
   vec3 L = vec3(.5*sin(5.*uTime),1.,1.);
   float c = max(.1, dot(N, normalize(L)));
   c = step(0., N.z) * .5 * c;
   vec3 color = vec3(c, c*c, c*c*c);
   gl_FragColor = vec4(sqrt(color), 1.0);