Notes for September 16 class -- Ray tracing to spheres and Phong shading

 

 

RAY TRACING TO SPHERES
 

(1) Forming a ray for each pixel

Ray tracing is a very powerful rendering technique. The fundamental idea is to shoot a "ray" into the scene at every pixel of the image. Effectively, we are tracing light backwards from a pinhole camera into the scene.

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 the parametric equation:

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

We can describe the image plane itself as a square floating in space. We can position the image plane in our virtual world so that x==-1 along its left edge, x==+1 along its right edge, y==-1 along ite bottom edge, and y==+1 along its top edge. We set the z value of the image plane to zero.

As it happens, that is exactly the range of values for vPos in the shader programs you have been implementing. :-)

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

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

V = [0,0,fl]
To form a ray through any pixel [x,y] of the image, we first take the difference vector from the observer's location to that pixel:
[x,y,-fl] = [x,y,0] - [0,0,fl]
Then we normalize that vector to get the ray direction:
W = normalize([x, y, -fl])
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, -fl));
Any point along this ray can be described by:
[Vx + t Wx, Vy + t Wy, Vz + t Wz], where t > 0

 

(2) Finding the distance along the ray to a sphere

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.

Recall that the surface of a sphere with center at C and radius r consists of all points P such that:

(Px - Cx)2 + (Py-Cy)2 + (Pz-Cz)2 = r2
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
In a shader, we can store that information in a single vec4. For example, to describe a sphere centered at [0,0,0] with radius 0.5:
vec4 S = vec4(0., 0., 0., .5);
To ray trace to a 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.

The magnitude squared of any vector v can be obtained by just taking a dot product of that vector with itself:

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

V' = V - C
This substitution moves the sphere to the origin. Now our ray equation becomes:
[V'x + t Wx, V'y + t Wy, V'z + t Wz], where t >= 0
We can substitute this point into our equation for the sphere (which is now at the origin) to get:
(V'x + t Wx)2 + (V'y + t Wy)2 + (V'z + t Wz)2 = r2
Multiplying out the terms, we get:
t2 (Wx * Wx + Wy * Wy + Wz * Wz) +
t   (2 * (Wx * V'x + Wy * V'y + Wz * V'z) ) +
    (V'x * V'x + V'y * V'y + V'z * V'z - r2) = 0
Using our definition of dot product, we can rewrite this as:
(W ● W) t2 + 2 (W ● V') t + (V' ● V' - r2) = 0
But since W is unit length, (W ● W) is just 1. This further simplifies our equation to:
t2 + 2 (W ● V') t + (V' ● V' - 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 ● V')
C = V' ● V' - r2
to get:
t = (-2(W ● V') ± sqrt(4(W ● V')2 - 4(V' ● V'-r2))) / 2
or:
t = -(W●V') ± sqrt((W●V')2 - V'●V' + r2)

 

(3) Finding the surface point and surface normal

The above equation can have zero, one or two real solutions, depending on whether the discriminant (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:

t = -(W●V') - sqrt((W●V')2 - V'●V' + r2)       Equation 1

If your ray hits more than one sphere, then you need to render the one that is nearest. This will be the one with the smallest positive value of t.

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

P = 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(P - C)

 

To summarize the algorithm:

  • vec3 N, P;
  • vec3 V = vec3(0, 0, fl);
  • vec3 W = normalize(vec3(vPos.x, vPos.y, -fl));
  • float tMin = 1000.;
  • Solve the quadratic equation above (Equation 1) to compute t
  • If t > 0 and t < tMin:
    • P = V + t * W;
    • N = normalize(P - C);
    • tMin = t;
As I mentioned in class, you might find it convenient to implement a function specifically to find the distance along a ray to a sphere. If the ray misses the sphere, your function can just return -1. The function declaration might look something like this:
float raySphere(vec3 V, vec3 W, vec4 S) {
   ...
}
where V and W specify the ray, and S specifies the center and radius of the sphere.
PHONG SHADING

The first really interesting model for surface reflection was developed by Bui-Tong Phong in 1973. Before that, computer graphics surfaces were rendered using only diffuse lambert reflection. Phong's was the first model that accounted for specular highlights.

The Phong model begins by defining a reflection vector R, which is a reflection of the direction to the light source L about the surface normal N.

As we showed in class, and as you can see from the diagram on the right, it is given by:

R = 2 (N • L) N - L
 
Once R has been defined, then the Phong model approximates the specular component of surface reflectance as:
Srgb max(0, E • R)p )
where Srgb is the color of specular reflection, p is a specular power, and E is the direction to the eye (in our case, E = -W, the reverse of the ray direction). The larger the specular power p, the "shinier" the surface will appear.

We can have more than one light. To get the complete Phong reflectance, we sum over the lights in the scene:

Argb + i lightColori ( Drgb max(0, N • Li) + Srgb max(0, E • R) p )
where Argb, Drgb and Srgb are the ambient, diffuse and specular color, respectively, and p is the specular power.
 
We can add shadows by modifying the Phong shading step. As we iterate through each light source, we first check whether the point is in shadow from any other sphere in the scene:
   function isInShadow(P, L): // is point P in shadow from light L?
      for all shapes S[i]
         if raySphere(P, L, S[i]) > 0.001
            return true;
      return false;
If you discover that your surface point P is in shadow from any given light source L[j], then don't add Diffuse or Specular component for that light source.

Homework

Due Monday September 23 before the start of class

Implement a ray tracer to spheres. Put at least two spheres into your scene and at least two light sources, and implement shadows. Position the spheres so that you can show that shadows are working.

We've made some improvements to the interactive shading editor system, so you will find it easier to start with the newer version, which you can download here: hw2.zip

As we mentioned in class, we have not yet covered how to get information about your scene (shapes, lights, material colors, etc) from the Javascript program running on your CPU to the GLSL shader running on your GPU.

For now you can just set the values of your spheres and lights right inside your main() function. For example, the following code will produce something similar to -- but not exactly the same as -- the image to the right.

...

const int NS = 2; // Number of spheres in the scene
const int NL = 2; // Number of light sources in the scene

// Declarations of arrays for spheres, lights and phong shading:

vec3 Ldir[NL], Lcol[NL], Ambient[NS], Diffuse[NS];
vec4 Sphere[NS], Specular[NS];

...

void main() {

    Ldir[0] = normalize(vec3(1.,1.,.5));
    Lcol[0] = vec3(1.,1.,1.);

    Ldir[1] = normalize(vec3(-1.,0.,-2.));
    Lcol[1] = vec3(.1,.07,.05);

    Sphere[0]   = vec4(.2,0.,0.,.3);
    Ambient[0]  = vec3(0.,.1,.1);
    Diffuse[0]  = vec3(0.,.5,.5);
    Specular[0] = vec4(0.,1.,1.,10.); // 4th value is specular power

    Sphere[1]   = vec4(-.6,.4,-.1,.1);
    Ambient[1]  = vec3(.1,.1,0.);
    Diffuse[1]  = vec3(.5,.5,0.);
    Specular[1] = vec4(1.,1.,1.,20.); // 4th value is specular power

    ...

}