screamyGuy
Ambient Occlusion
You need to install or enable Java to view this content. Get it from java.com
Figure 1. Left mousedrag to look around. Right drag to zoom. Middle drag or shift left drag to move the lamp. The light size slider will modify the softness of the lamp. The oversampling slider affects the number of camera rays while the light samples slider affects the quality of shadows. Prefer larger ratio of oversamples to light samples. The ambient light slider will affect the ratio of light power to each source: at zero, illumination will be entirely from the light source, at one, the scene will be lit entirely by ambient occlusion. Press the render key to render current view.

Source Explanation

These raytracers are surprisingly simple:

First, the code chops the screen into buckets and dumps them into a vector. Then it creates multiple threads (ideally one per CPU) that will simultaneously grab a bucket and render it. Once a thread finishes a bucket, it grabs the next until all the buckets are done. This allows the program to use as many CPUs as available.

To render a bucket, the thread simply iterates over each pixel and finds its incident light by calling the getColor() method.

      //if more buckets, get one
      while(buckets.size() > 0){    
      //get the next bucket from the queque    
      RenderBucket bucket = 
		(RenderBucket)buckets.elementAt((int)random(0,buckets.size()));
      buckets.remove(bucket);
      
       for (int i = bucket.minX; i < bucket.maxX; i++){
         for (int j= bucket.minY; j < bucket.maxY; j++){
           rendered.pixels[j*width + i] = getColor(i,j);
         }
       }
      }
  

The getColor() method gets the incident light for a pixel. First, it calls the getRay() method, which will generate a ray from the camera into the scene. It also jitters the sample over the dimension of a single pixel -- this will antialias the pixel. Specifically, if the pixel is subdivided between two triangles, the pixel color will be an average of the two shades. Note that oversampling is done as a square of the user set value.

  for (int i =0; i < overSamples.getVal()*overSamples.getVal(); i++){
  dx = random(-.5,.5);
  dy = random(-.5,.5);
  Tuple3f direction = cam.getRay(x + dx,y + dy);
  ...
  }
  

The ray is then cast into the scene. If it intersects anything, we need to evaluate the reflectance function to get a color. If not, we simply return the background color.

  //prepare the picking ray
  Ray pick = new Ray(cam.pos, direction);
  
  //intersect the camera ray with the scene
  tracer.accelerator.findNearest(pick);
  
  //if ray intersects anything, get the incident light
  if (pick.occluder != null){
  //get incident light
  }
  else{
  //background color
  }
  
To evaluate incident light from a point in the scene, we cast shadow rays towards the light sources. In this scene, we only have two: the lamp and ambient occlusion. Given a set number of light samples, we'll use simple stochastic sampling to ensure that the percentage of the total samples traced to each light is equivalent to the relative power as chosen by the user. This tends to increase variance for the less sampled light, but keeps the code and interface simpler.
      //pick whether to cast a light sample or an ambient occlusion sample
      if (random(0,1) > hemiPower.getVal()){
        ao = lightSize.getVal();
        //get a ray towards the light
		lmp.getSample(shadow,null);
      }
      //ambient occlusion
      else{
        ao = 1;
		//get the triangle normal -- we'll perturb it later
        shadow.direction = ((Triangle)pick.occluder).gNormal.getCopy();  
      }
  
In the case of sampling the light, we get a shadow ray by calling the getSample method of the lamp, which returns a vector pointing to the lamp. To simulate soft shadows, we perturb this ray. In the case of ambient occlusion, we assume the entire sphere surrounding the scene is a light source, so we generate rays at random surrounding the surface normal. Here, I perturb the direction by a uniform sphere. This not the statistically proper way to do this, but is very simple and produces acceptable results.
    shadow.direction.x += random(-ao,ao);
    shadow.direction.y += random(-ao,ao);
    shadow.direction.z += random(-ao,ao);
    shadow.direction.normalize();
    shadow.minDist = .01f;
  
Finally, the shadow ray is intersected with the scene. If it does not hit anything, we've found a path from the light to the camera eye, so we add evaluate the surface reflectance and add the result to the accumulator pixel. Here, I'm using a very quick diffuse approximation for light falloff -- a cosine.
    tracer.accelerator.findNearest(shadow);
    samples++;
    
    if (shadow.occluder == null){
      tritemp = (Triangle)pick.occluder;
      float k = abs(shadow.direction.dot(tritemp.gNormal));
      accum.plusEquals(tritemp.shader.fillColor.times(255*k));
    }
  
I'm not tonemapping in the proper sense. I've been tracking the total number of samples. As the last step, we divide the accumulated light by the number of samples to normalize it for display.
  accum.timesEquals(1.f/samples);
  return doColor(accum.x, accum.y,accum.z);
  
Interestingly, the color() method provided by Processing is not thread safe, so setting the pixel color in the PImage must be deferred to a synchronized method.

The raytracer here only performs one "bounce." If we had the time and processing power, any shadow rays that intersected other surface could have spawned a new set of shadow rays. This can become computationally cumbersome very quickly. There are methods to address this, but they are beyond the scope of this page.

Files

Related

Gallery
3D gallery environment with image rotation.
Shadow Baking
In scene texture baking in the presence of dynamic lighting.

by Matthew Kozak
All code GPL
All else... bananas?