Hi!

Today we're going to cover the phong illumination model, depth checking and reflection. The phong illumination model is effectively a formula which describes the final colour of each pixel as a linear combination of a number of components:

- Ambient light
- Diffuse light
- Specular light
- More complex parts (reflections/refractions).

In our current pseudo code from part 2 we already have ambient lighting terms and diffuse lighting (i.e. our lambertian shading), so now we can look at adding specular lighting. A specular light is effectively a reflection of the light source in the object we're looking at. If the material is very reflective, it is a sharp, clear reflection. If the material is matte, the specular highlight will spread out further and blend into the diffuse shading.

To calculate the specular component, we need to yet again use a dot product to calculate the cosine of an angle between two vectors. In this case, we want to know the cosine of the angle between the reflection of the current eye-ray from the surface of the object and the vector from the surface point to the light source of interest (which we already have in the code from part 2, denoted light_vector). If the reflection vector and the vector to the light were going the same direction, we would expect the specular highlight to be large. This is why the cosine of the angle from the dot product is useful again - cos(0) = 1, so the closer the direction of the two vectors is, the bigger the specular component is.

Firstly, we'll need to calculate the reflection vector. To do this, we can use this handy formula:

Where R is our resultant reflection vector, N is the normal vector (which we derived in part 2), and V is the current ray direction from the "eye." There's a good derivation of this here. Now that we have the reflected vector, finding our specular component is as simple as performing a dot product, then using some math to calculate the component based on the shinyness of the surface. Here's the updated pseudocode:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
canvas = new texture; //loop through each pixel on the plane for(px = 0; px<plane_width; px++) for(py = 0; py<plane_height; py++){ //determine the ray direction vector ray_x = pixel_world_x - camera_x; ray_y = pixel_world_y - camera_y; ray_z = pixel_world_z - camera_z; //get the amplitude of this vector and normalize it amp = 1/sqrt(sqr(ray_x)+sqr(ray_y)+sqr(ray_z)); ray_x*=amp; ray_y*=amp; ray_z*=amp; for(every sphere){ //loop through all spheres in the scene if check_ray(sphere_x,sphere_y,sphere_z,ray_x,ray_y,ray_z,sphere_radius){ //calculate the sphere normal sphere_normal = normalize(hit_position - sphere_position); light = ambient_level; temp_light = 0 for(every light_source){ //loop through every light source //calculate the directional vector towards the light light_vector = normalize(light_source.position - hit_position); in_shadow = false; for(every sphere){ if (sphere != other_sphere) //don't let the current sphere check itself if check_ray(sphere_x,sphere_y,sphere_z,light_vector_x,light_vector_y,light_vector_z,sphere_radius){ in_shadow = true; break } } if (!in_shadow){ //only add contribution if it's not in a shadow temp_light += max(0, dot(sphere_normal, light_vector)); //specular component spec = dot(reflection, light_vector); temp_light += specular_intensity * spec^(shininess) } light.val += temp_light; } } } draw_pixel(canvas, make_colour(sphere_hue, sphere_saturation, light), px, py) } |

There we have it, specularity! Hopefully, if it's implemented correctly, it should look something like Figure 1. You can play about with the values, specular intensity should be from 0-1, and shininess is generally good from around 50-150.

Now's probably a good time to sort out some kind of depth checking. Objects appearing infront of eachother when they are not is not desirable, after all! The depth checking is relatively simple thankfully, the algorithm being as follows:

In a for loop, check_ray against each sphere. In each iteration, check if the distance to the collision point on this sphere is smaller than the last one, and then so on. When the loop is complete you're left with the collision point *nearest *to the screen, and you can then proceed as normal! This can be implemented something like:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
lowest_distance, tempcol = shoot_ray(ray_start, ray_dir, sphere_list, lights, depth) { lowest_distance = 10000000; first_object = null; foreach (sphere in sphere_list) { hit = ray_sphere(raystart - sphere.position, raydir, cs.radius); if (hit > 0.1 && hit < lowest_distance) //find closest intersection { lowest_distance = hit; first_object = s; } } if (first_object != null) //if the ray hit something { hit = lowest_distance; hitpos = raystart + ray_dir.X * hit; //find hit coords light_col = sphere_lighting(hitpos, raydir, sphere_list, sphere, lights, depth); hit_something = true; } } |

This can be called in our "plane pixel" loop instead of the "for (every sphere)" section, then our actual shading code can be moved into a sphere_lighting function, which returns the colour of our pixel. That's all there is to the depth checking, really.

Now, we've calculated a nice reflection vector for the specularity, so why not put it to further use with some pretty reflections? This can be a little tricky, as it is a recursive operation. What does this mean? Well, imagine two mirrors facing eachother. Light, or one of our rays, could theoretically get trapped between them forever, so to fix this we need to keep track of the "depth" of the ray and terminate it if it goes too high.

If your ray shooting functions have been reasonably well structured (unlike mine on my first attempt..), an implementation of the reflections is actually straightforward, like so:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
light_col = sphere_lighting(hit_position, sphere, depth){ //calculate the sphere normal sphere_normal = normalize(hit_position - sphere.position); light = ambient_level; temp_light = 0 for(every light_source){ //loop through every light source //calculate the directional vector towards the light light_vector = normalize(light_source.position - hit_position); in_shadow = false; for(every sphere){ if (sphere != other_sphere) //don't let the current sphere check itself if check_ray(sphere_x,sphere_y,sphere_z,light_vector_x,light_vector_y,light_vector_z,sphere_radius){ in_shadow = true; break } if (!in_shadow) //only add contribution if it's not in a shadow temp_light += max(0, dot(sphere_normal, light_vector)); //specular component spec = dot(reflection, light_vector); if (spec>0.85) temp_light *= 1 + (spec - 0.85)^2 } light.val += temp_light; //reflective part if (reflective > 0 && depth < max_reflection_depth){ //we'll shoot a new ray in the reflection vector direction with higher depth ref = shoot_ray(hitpos, reflection, sphere_list, lights, depth + 1); //we can now combine the diffuse colour with the colour from the reflection //we need to do this in RGB not HSV, so you have to transform temp_col = HSV_TO_RGB(light_col); temp_col.r = diffuse.r * (1.0f - reflective) + reflective * ref.r; temp_col.g = diffuse.g * (1.0f - reflective) + reflective * ref.g; temp_col.b = diffuse.b * (1.0f - reflective) + reflective * ref.b; //where reflective = 0 is not at all reflective, reflective = 1 is a mirror //finally return the blended colours in HSV light_col = RGB_TO_HSV(temp_col); } } |

So, firstly we calculate our standard phong illumination model. Then, if the sphere is reflective, we get the colour returned by a new ray sent in the reflection vector direction. This is the recursive part, as the shoot_ray script is calling itself - although the depth is increased with each call, and if it's above a maximum amount the recursion stops.

Eventually after all the recursion, our initial call of the script will receive the total colour (after all the reflections are added up), which is blended with the colour from the normal lighting, and that's that! Reflections really look great, as can be seen in Figure 2, which used a recursion depth of 3. Any higher than 3 is not usually worthwhile as the reflections get very small at that point, I usually stick with 2.

The next post will probably be on distributed ray tracing - multisample anti-aliasing, and depth-of-field.