Home » Programming

Category Archives: Programming

Ray Tracer Part Four – Shading and illumination

gridcoin728x90_40b-99980e

Shading and Illumination

So far we can intersect a ray with a sphere, and calculate the normal at the intersect point, and in this article we will extend the raytracer to shade the sphere.  This will help give the illusion of depth to the sphere.

First it is necessary to check whether an intersectPoint is illuminated by a lightsource, or whether it is in shadow.  This is achieved by casting a ray from the intersect point towards the lightsource, and testing whether it intercepts any objects.  As soon as any object is intercepted, we know the intersectionPoint is in shade, and can stop testing. (note, although neither option is realistic, I have assumed a transparent object will not cast a shadow)

rayToLight = lightSourceCentre - intersectpoint;
lightSourceDistance = rayToLight.length();
rayToLight.normalise();

for(all non transparent objects in scene)
{
   if(currentObject.intersect(intersectPoint, rayToLight, lightSourceDistance, normal))
   {
      // ray hit an object CLOSER THAN LIGHTSOURCE
      // so point is in shadow
   }
}

This can be put in a method that returns true if a point is in shadow.  Be sure to ensure that the ray intersects at a distance less than the light source distance.

Diffuse Illumination

First we will calculate the diffuse lighting component.  This assumes that any light that hits the surface is scattered evenly in every direction.  So regardless of the viewing angle, the point will appear to be the same color.  The intensity of the light does however depend on the angle between the surface normal vector, and the vector towards the lightsource.  If the surface is perpendicular to the light source vector, the surface will receive maximum illumination (and if parallel, the surface will receive no illumination).  This diffuse lighting component can be calculated as follows:

intersectToLight = lightSourcePoint - intersectpoint;
intersectToLight.Normalise();
lDotNormal = surfaceNormal.Dot(intersectToLight);
lDotNormal = lDotNormal < 0 ? 0.0f : lDotNormal;
diffuseComponent = kd * surfaceColor * lightSourceColor * lDotNormal

‘kd’ is the diffuse coefficient, a value between 0 and 1.  Both the surfaceNormal vector and vector towards the lightSource must be normalised, and lDotNormal must be checked to ensure the value is greater than 0.  We know of course that if the dot product is negative then the surface is facing away from the lightsource, and is thus in shadow.

Phong lighting

The next component to calculate is the specular component, in this case Phong.  This gives an object the appearance of being shiny, such as plastic or metal. Again, the specular component is only calculated if the lDotNormal value is positive.

Phong

To begin with we know the vector L, N, and V (vector towards the eyePoint), and we want to find the vector R (R is the same as the reflection vector, which will be needed later). First, ensure L, N and V are all normalised.  Referring to the image above, the vector R can be calculated as (2*lDotN)*N – L.  Next take the dot product of R and V. this RdotV value is then raised to the power ‘n’.  This n value determines how concentrated the specular highlight will be. A large n value will lead to a smaller spot, a smaller n value will result in a broader spot.  We also have another value  to adjust, the specular constant ks. This should take a value between 0 and 1, and can be used to scale the intensity of the highlight.

Phong Component = ks*lightSourceColor*(RDotV^n)

This will give the specular highlight the same color as the light source color.  If the objects material is metallic, such as gold, the specular component (and reflections) tend to take on the color of the material.  Thus for metallic surfaces the Phong component becomes:

Phong Component = ks*lightSourceColor*surfaceColor*(RDotV^n)

Ambient Lighting

Ambient lighting is the addition of some lighting to every intersection point, even if in shadow.  Without this surfaces in shadow would appear completely black, which gives very unrealistic images.  Adding ambient lighting like this is far from realistic, but is simple to apply.  The way in which i have applied it is to calculate it for every light source in a scene, dependent on the lightsources colors (ka is an ambient constant, I use 0.1).

Ambient Component = ka * kd * lightSourceColor * surfaceColor

The total illumination for a point is the sum of all these components.

Total Illumination = Diffuse + Specular + Ambient

Modifications

An alternative to Phong illumination is Blinn-Phong.  Blinn-Phong has the advantage that it is faster to compute than Phong, with almost the same effect.

In the diffuse illumination formula, distance from the light source does not effect the intensity of illumination.  An object close to the light source, and one very far away will receive the same illumination (provided the angle between their normals and the lightSource ray are the same).  If you would like to account for this, simply divide the total illumination by (distanceToLightSource^2).

Using point light sources casts very hard shadows, by using soft shadows the realism of images can be greatly improved.  In order to cast soft shadows we instead use area light sources, and cast many shadow test rays from each intersection point towards random points on the surface of the area light source.  The ratio of obstructed to unobstructed rays then determines how much illumination is applied to the surface point.

Ray Tracer Part Six – Depth Of Field

gridcoin728x90_40b-99980e

Depth Of Field

Depth of Field Camera

Adding depth of field capability to your ray tracer is well worth the effort, and you’ll be pleased t know that it is not all too difficult either. You will however need to wait significantly longer for your images to render, as every pixel now needs to send multiple rays in order reduce noise.

The illustration below gives a basic idea of how we can achieve this effect.  For simplicity lets assume 2 dimensions, and only 3 pixels need rendering.  The standard camera will cast one ray ( yellow, green, cyan) for each pixel, from the eye point through the associated view plane point.  In the case of DOF camera, multiple rays will be cast for each pixel.  This time however the view plane will be situated at the desired focal distance, and the eye point will be randomised around the eyepoint, to an extent dependent on the radius of the aperture.

DOF Camera

So implementing this in code should be fairly easy. The approach I will describe requires recalculating the same parameters as for the standard camera, but this time the view plane  is located at the focal distance.  Whenever the Focal distance is changed, these parameters will need to be recalculated.

The changes that need to be made to the standard camera code are as follows:

Calculate the focal distance, assuming the focus should be set to the lookat point.

  Focal Distance = Length(lookAtPoint – eyePoint)

If the focal distance is instead set at some other value, calculate a new point at the centre of the new view plane.

Calculate the new half width of the view plane.

  halfWidth = focalDistance *tan(fov/2)

So now the bottomLeft , and increment vectors will be calculated correctly using the same code as for the standard camera.

In order to randomise the eye point later, 2 vectors will also be precalculated, such that we can add them to the eye point.

  xApertureRadius = u.normalise()*apertureRadius
  yApertureRadius = v.normalise()*apertureRadius;

Now we need to change the code in the getRay method.

Random rays work well, so we can generate 2 random numbers,

One for the x variation around the eyePoint, and one for the y. I don’t believe the extra computation to ensure the new eye points lay inside a circular aperture make much difference, however you can decide.

  R1 = random value between -1 and 1
  R2 = random value between -1 and 1
  ViewPlanePoint = bottomLeft + x*xInc + y*yInc
  newRandomisedEyePoint = eyePoint+ R1*xApertureRadius + R2*yApertureRadius
  ray = (ViewPlanePoint - newRandomisedEyePoint).normalise()

Now everything required to generate an image with depth of field is complete.  Instead of firing just a single ray through each pixel, we now add something like the pseudo-code below.

  Color tempColor(0.0f,0.0f,0.0f);
  for(int i = 0; i < numDOFRaysPerPixel; i++){
      camera.getRay(x,y, castRay, eyePoint);  // where castRay and eyePoint are references and set by the getRay method
      tempColor.setBlack();
      traceRay(eyePoint, castRay , tempColor....);  //tempColor will be set to the resulting color for this ray
      displayBuffer.add(x, y, tempColor); // add all the DOF rays to the same screen pixel
  }
  displayBuffer.divide(x,y, numDOFRaysPerPixel);  // divide the pixel by the number of DOF rays cast

This will cast the same number of DOF rays for every pixel in the screen, which is not very efficient.  Consider these two situations.  First where there is an object situated on the focal plane (in the path of our ray), and secondly where the object is situated far behind the focal plane.  In the first case all our depth of field rays will converge to the same point, so casting a large number of rays is wasteful.  In the second case, many more rays will be required, as each ray will ‘pick up’ colours form points which are very far apart, and possibly intersecting different objects (refer to the figure above).  So instead of casting a fixed number of DOF rays per pixel an improvement can be made by continuing to cast rays until a certain error condition is met.  For example once the effect on the final colour of a pixel by additional DOF rays is less than a defined limit.


Ray Tracer Part Three – Adding a Sphere

gridcoin728x90_40b-99980e

Now lets create something to add to our scene.  One of the simplest objects we can add is a sphere, so lets get started with that first. We can define a sphere in our world as having a position, and a radius. But first lets create an abstract Object class, that all objects can derive from.

Object Class

Every object in our scene will share some common properties.  They will all have a color, and they will all have properties related to how they are illuminated.  They will also all need an intersect method.  In C++ the basic Object class would look something like the code below:

class Object {
public:
    Color color;
    float kd; // diffuse coefficient
    float ks; // specular coefficient
    float pr; // reflectvity
    float n;  // phong shininess
    float IOR; // index of refraction
    Object(Color color, float kd, float pr, float ks, float n, float IOR, bool transparent){
        ..... set the attributes
    }
    virtual int intersect(Point &origin, Ray &ray, float &distance, Ray &normal) = 0;
}

In the intersect method, the ‘distance’ reference contains the closest intersect distance thus far, and if the object being tested is intersected closer than this, distance will be modified;

Sphere Class

The sphere class thus needs to provide an implementation of the intersect method.  The following formula can be used to determine if a ray intersects a sphere, and the distance of the hit.

A*(distance^2) +B*distance + C = 0;

where

A = castRay.Dot(castRay)
B = 2*(origin - sphereCentre).Dot(castRay)
C = (origin-castRay).Dot(origin-castRay) - radius^2

This is a quadratic equation, so the two solutions for the intersect distance can be obtained using the quadratic formula.

When we test a ray we want to consider 3 possible situations.

1) The ray misses the sphere – When the discriminant is less than zero. or the intersect distance is greater than that of an intersect test with another object

2) The ray hits the sphere from the outside – When the discriminant is greater than zero, and the two calculated intersect distances are positive.

3) The ray hits the sphere from the inside – When the discriminant is greater than zero, and the lower distance is negative.

A = ray.Dot(ray);
originToCentreRay =  originOfRay - centreOfSphere;
B = originToCentreRay.Dot(ray)*2;
C = originToCentreRay.Dot(originToCentreRay) - radius*radius;

discriminant = B*B -4*A*C;
if(discriminant > 0){
   discriminant = std::sqrtf(discriminant);
   float distance1 = (-B - discriminant)/(2*A);
   float distance2 = (-B + discriminant)/(2*A);
   if(distance2 > FLT_EPSILON) //allow for rounding
   {
      if(distance1 < FLT_EPSILON) // allow for rounding
      {
         if(distance2 < distance) 
         {
            distance = distance2;
            normal.x = 10000;
            return -1; // In Object

         }
      }
      else
      {
         if(distance1 < distance)
         {
            normal.x = 10000;
            distance = distance1;
            return 1;
         }
      }
   }
}// Ray Misses
return 0;

You will notice that first the discriminant is checked to ensure it is greater than zero. If the discriminat is negative the ray does not intercept the sphere.  If the lesser of the two intersections is negative and the greater distance is positive, then the origin is inside the sphere.  A value of -1 is returned here as it is useful for correcting the normal later.  It is also necesary to ensure that this intersection is closer than any previous object intersection.

The normal.x value has been set here to an arbitrarily large number, so that other code knows it needs to explicitly calculate the normal. Of course the normal vector could be calculated simply here, however I found a slight performance improvement by only calculating the normal once we know of the nearest object.

Normal vector

In order to calculate lighting, reflection and refraction rays, we need to know the normal vector at the point of intersection.  All objects will need this method, so lets add it to the Object abstract class.

virtual void getNormal(Point& intersectPoint, Ray& normal) = 0;

In the sphere class, this will look something like this

void getNormal(Point& intersectpoint, Ray& normal)
{
   normal = intersectPoint - sphereCentre;
   normal.Normalise();
}

That’s it, now we’re ready to move on to applying illumination and shading to the sphere.

 

Ray Tracer Part Two – Creating the Camera

gridcoin728x90_40b-99980e

The camera is responsible for creating a ray which passes from they eye point through the pixel we want to render.

The camera will be defined by an ‘eye point’, a ‘look at point’, and a ‘field of view’.  It is also necessary to define an ‘up vector’ for the camera, however this will be assumed to be always up in this case (0,1,0).

When a camera is constructed, several attributes will be set which will allow the easy generation of rays later. The first parameter we will calculate is the Point located at the bottom left of the view plane.  We will then calculate two vectors, one which will be added to the bottom left point for every increase in x on the view plane, and one which will be added to the bottom left point for every increase in y on the view plane.

Constructing the camera

Camera

Our constructor requirements,

Camera(Point eyePoint, Point lookAtPoint, float fov, unsigned int xResolution, unsigned int yResolution)

First calculate the “viewDirection” vector.

viewDirection = lookAtPoint - eyePoint;

Next calculate the V vector , here we will assume up = (0,1,0).

U = viewDirection x up

Now recalculate the up vector, (if the camera is tilted, the cameras up vector will not be (0,1,0), right?)

V  = U x viewDirection

Be sure to normalise the U and V vectors at this point

Obtaining the bottom left point on the viewplane is now easy.

viewPlaneHalfWidth= tan(fieldOfView/2)
aspectRatio = yResolution/xResolution
viewPlaneHalfHeight = aspectRatio*viewPlaneHalfWidth

Camera2
viewPlaneBottomLeftPoint = lookatPoint- V*viewPlaneHalfHeight - U*viewPlaneHalfWidth

So now we need the increment vectors, which will be added to the bottomLeftViewPlanePoint to give our viewplane coordiantes for each pixel.

xIncVector = (U*2*halfWidth)/xResolution;
yIncVector = (V*2*halfHeight)/yResolution;

Getting a ray

Now we want a method that can return a ray that passes from the eye point through any pixel on the viewplane.  This can be easily obtained by calculating the view plane point, and then subtracting eye point. x and y are the viewplane coordinates of the pixel we want to render, numbering from the bottom left.

viewPlanePoint = viewPlaneBottomLeftPoint + x*xIncVector + y*yIncVector
castRay = viewPlanePoint - eyePoint;

Conclusion

This is by know means the only way to create a camera, but is fairly intuitive to get started. It is also easily modified at a later date to allow the calculation of anti-aliasing rays.
Next – Adding a Sphere

Ray Tracer Part One – What is Raytracing?

gridcoin728x90_40b-99980e

The following image was generated by a raytracer I have written in C++.  I originally wrote this as a programming assignment for a course at Canterbury University, COSC363 – Computer Graphics.  The program can be downloaded from this page.

RT1

This render illustrates many of the features currently implemented.  Supported object types are spheres, cones, boxes, and surfaces, these objects can be made reflective or refractive, and have textures applied to add realism. Area light sources allow soft shadows to be cast.  Surfaces can be bump mapped, giving the impression of an irregular surface (or water surface with refraction) , or even used to create a complex 3D surface.  A depth of field camera can also be used, to bring objects at a certain distance into focus, while leaving others blurry.  An environment map can be used to surround the scene, and fill in that dead space, and makes reflections look much more realistic. A KD-tree is used to speed up the raytracing process when a large number of objects is added.  Photon Mapping is also supported, allowing caustic lighting with refractive objects.

The basic idea

Raytracing Concept

Raytracing generates an image by tracing the path of light from an eye point, through each pixel of an image, and coloring each pixel depending on the objects the ray intercepts in it’s path.  When a ray hits an object, the color contribution of that hit is calculated (depending on material properties, and lighting…) and if the material is reflective or refractive, additional rays are calculated and traced until a certain end condition is met.  When a ray hits a reflective object a reflective ray is generated.  When a ray hits a transparent object two rays must be generated; a reflective ray and a transmission ray.

The basic raytracing algorithm

Probably the easiest way to implement  a raytracer is by using recursion.  The following pseudocode is not supposed to be in anyway complete or concise, but is rather intended to provide a simple introduction to the idea.

Render(){
   for each pixel in image {
      viewRayOrigin = eyePoint;
      viewRay       = currentPixelLocation - eyePoint;
      maxRecursiveDepth = 7; // or more
      traceRay(viewRayOrigin, viewRay, currentPixelColor, maxRecursiveDepth)
   }
}

traceRay(origin, viewRay, pixelColor, depth){
  if(depth <= 0) // our end condition
     return;

  nearestObject = getNearestIntersection(origin, viewRay, interceptDistance);   

  interceptPoint = origin + viewRay*interceptDistance;
  pixelColor += calculateLighting(nearestObject, interceptPoint);

  if(nearestObject is transparent){
     transmissionRay = getTransmissionRay(....);
     traceRay(interceptPoint, transmissionRay, pixelColor, depth-1);
  }

  if(nearestObject is reflective or is transparent){
    reflectiveRay = getReflectiveRay(...);
    traceRay(interceptPoint, reflectiveRay, pixelColor,depth-1);
  }
}

Now obviously many details have been left out from this, and as you develop your code you will undoubtedly encounter undesired visual artifacts at some point.

If you are new to computer graphics or ray tracing, some of these terms may be new to you, but it is my intention to introduce these topics in an easy to digest manner, and provide an explanation of how these features can be implemeted so hopefully you too can create a working ray tracer.  Ray tracing is very computationally expensive, so consider this when deciding which language to use.

Next – Creating the Camera