hackification

Homepage

...rediscover the joy of coding

Experiments in Ray-Tracing, Part 4: Lighting

In my last post, I covered the absolute basics of ray-tracing, primary-ray-casting. I showed how to determine which object in the scene each pixel refers to, and hinted at surface colours and shadows. I'm now going to cover lighting in more detail. (Actually, thinking about it, I didn't go into any detail about how to determine the actual object the primary rays intersect - so that'll be another article).

There are many lighting equations used in ray-tracing, but I'm going to cover the most common one here, namely Phong shading. Phong shading works on a per-pixel basis, as opposed to (say) Gouraud shading, which is generally used to interpolate from a less accurate per-vertex basis.

Phong shading is made up of three components: ambient, diffuse, and specular.

(Source: Wikipedia)

Ambient Part

The ambient part is a constant that gives a fixed colour to parts of objects that are not directly illuminated. For certain kinds of scenes, this is a reasonable approximation, but for others, a fixed ambient component gives a very "flat" look to the final image. Calculating a better ambient term is beyond the scope of this article, but I'd like to cover it one day. For now, I'll use a constant ambient term. The ambient part is applied once per lighting calculation.

Diffuse Part

The diffuse part models the reflection from surfaces that have a "matt" finish and scatter light at all angles. The light emitted from a dull surface is determined by both the colour of the light, and of the colour of the surface. The diffuse part is controlled by a single scaling number. The diffuse equation is calculated for each light that shines on the object in question.
Specular Part

The specular part models the reflection from very shiny surfaces. Very polished surfaces reflect light strongly in one direction (the reflection against the surface), but hardly reflect at other angles. The reflected colour is only really determined by the colour of the light, not the colour of the surface. The specular part is controlled by two numbers: a scaling number, and a shininess factor which determines how "sharp" the points of bright reflected light will be. The specular equation is calculated for each light that shines on the object in question. In general, as the specular factor increases, the diffuse factor decreases, and vice-versa.

Attenuation

You may also want to attenuate (dim) the effect a light has on a surface, if the light is far away. (You may equally not want to - your mileage may vary). In the real world, light is attenuated by the inverse of the distance to the light squared, but in practise, you may want to tweak this a little. I calculate the attenuation of a light source as:
1 / (a + b*d + c*d*d)
Where d is the distance to the light, and a, b, and c are various contants.

Some Code

Rather than list the equations for the diffuse and specular parts, I've included them as C# code below. I've designed my materials to be nested, so that for example to produce the shiny balls in part one, I nest the materials as follows:
new Materials.PhongMaterial
(
  new Materials.ReflectiveMaterial
  (
    0.3, // Reflectivity - next article.
    new Materials.SingleColorMaterial
    ( new Base.Color( 1.0, 0.0, 0.0 ) ) // Red.
  ),
  0.8, 0.8, 100 // Diffuse factor, specular factor, shininess.
)
(This produces a shiny red surface material).

Here's the code. Hopefully you'll be able to figure out how it hooks into the rest of the ray-tracer through context.
public sealed class PhongMaterial : Material
{
  public PhongMaterial( Material material, double diffuseFactor, double specularFactor, double shininess )
  {
    _material = material;
    _diffuseFactor = diffuseFactor;
    _specularFactor = specularFactor;
    _shininess = shininess;
  }

public override Base.Color GetColor( Scene.Scene scene, Base.Intersection intersection, int depth ) { var color = _material.GetColor( scene, intersection, depth ); var ambient = scene.GetAmbientColor( color ); var diffuse = Base.Color.Black; var specular = Base.Color.Black;

var directlyVisibleLights = scene.GetDirectlyVisibleLights( intersection.Position, intersection.Primitive );

foreach( var light in directlyVisibleLights ) { var lightRay = light.GetRayToLight( intersection.Position ).Normalize(); var attenuation = light.GetAttenuation( intersection.Position );

var diffuseFactor = _diffuseFactor * ( Base.Vector.Dot( intersection.Normal, lightRay ) );

if( diffuseFactor > 0 ) { var thisDiffuse = color * light.Color * diffuseFactor;

thisDiffuse *= attenuation;

diffuse += thisDiffuse; }

var lightReflect = intersection.Normal * Base.Vector.Dot( intersection.Normal * 2, lightRay ) - lightRay; var cosFactor = Base.Vector.Dot( intersection.Normal, lightReflect );

if( cosFactor > 0 ) { var thisSpecular = light.Color * _specularFactor * Math.Pow( cosFactor, _shininess );

thisSpecular *= attenuation;

specular += thisSpecular; } }

return ambient + diffuse + specular; }

private Material _material; private double _diffuseFactor, _specularFactor, _shininess; }