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 onewhile(buckets.size() > 0){//get the next bucket from the quequeRenderBucket 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 rayRay pick = new Ray(cam.pos, direction);//intersect the camera ray with the scenetracer.accelerator.findNearest(pick);//if ray intersects anything, get the incident lightif (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 sampleif (random(0,1) > hemiPower.getVal()){ao = lightSize.getVal();//get a ray towards the lightlmp.getSample(shadow,null);}//ambient occlusionelse{ao = 1;//get the triangle normal — we'll perturb it latershadow.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.