I am trying to write my own path tracer in JavaScript, but I have a problem with the implementation of russian roulette. First, let me describe how my path tracer works.
I have a function called traceRay where I am looking for ray-objects intersections and doing some shading. Shading is divided into direct and indirect illumination. I have a separate function that does Importance Sampling only and it is being called inside traceRay. This function gets Monte Carlo samples count and max recursion depth as parameters, generates sample rays and calls traceRay again (if max recursion depth is not exceeded).
RayTracer.indirectLighting = function(scene, camera, intersectionPoint, normal, options)
{
var prob = 0.5;
var oneOverProb = 1/prob;
if(Math.random() < prob) return new Vector(0,0,0);
var diffuse = new Vector(0, 0, 0);
var specular = new Vector(0, 0, 0);
if(options.MC_SAMPLES > 0)// && options.MC_DEPTH > 0)
{
var basis = createCoordinationSystem(normal);
var sampleVector, sampleTransformed, mcRay;
for(var i = 0; i < options.MC_SAMPLES; ++i)
{
sampleVector = getCosineWeightedSample(Math.random(), Math.random());
sampleTransformed = new Vector(sampleVector.x * basis.tangent.x + sampleVector.y * normal.x + sampleVector.z * basis.bitangent.x,
sampleVector.x * basis.tangent.y + sampleVector.y * normal.y + sampleVector.z * basis.bitangent.y,
sampleVector.x * basis.tangent.z + sampleVector.y * normal.z + sampleVector.z * basis.bitangent.z);
mcRay = new Ray(intersectionPoint.clone().add(sampleTransformed.clone().mul(0.000001)), sampleTransformed);
diffuse.add(RayTracer.traceRay(mcRay, scene, camera, {
MC_DEPTH: options.MC_DEPTH - 1,
MC_SAMPLES: options.MC_SAMPLES,
AREA_LIGHT_SAMPLES: options.AREA_LIGHT_SAMPLES
}));
}
diffuse.mul(1 / options.MC_SAMPLES);
}
return Vector.add(diffuse, specular).mul(oneOverProb);
}
RayTracer.traceRay = function(ray, scene, camera, options)
{
var color = new Vector(0, 0, 0);
//Depth test - finding closest object
var minT = 1e12, currentT;
var object = null;
var objectI;
for(var i = 0; i < scene.objects.length; ++i)
{
currentT = scene.objects[i].intersects(ray);
if(currentT > 0 && currentT < minT)
{
objectI = i;
minT = currentT;
object = scene.objects[i];
}
}
var emissiveObjects = [];
for(var i = 0; i < scene.objects.length; ++i)
{
if(i == objectI) continue;
else if(scene.objects[i].isFinite && scene.objects[i].material.emittance.lengthSq() > 0.3)
emissiveObjects.push(scene.objects[i]);
}
if(object == null) return new Vector(0, 0, 0);
var indirectLighting = new Vector(0, 0, 0);
var directLighting = new Vector(0, 0, 0);
var intersectionPoint = ray.getPoint(minT);
var normal = object.getNormalAt(intersectionPoint);
var toEye = camera.position.clone().sub(intersectionPoint).normalize();
var albedo = object.material.color.clone().mul(object.material.diffuseFactor);
//Direct illumination
for(var n = 0; n < scene.lights.length; ++n)
{
//Searching for objects occluding light
var shadowRay = scene.lights[n].getShadowRay(intersectionPoint);
var inShadow = RayTracer.visabilityTest(shadowRay, scene.objects,
scene.lights[n].occlusionTestLimit(shadowRay)).occlusion;
if(!inShadow)
{
var lighting = scene.lights[n].getLightingInfo(intersectionPoint, normal, toEye);
directLighting.add(scene.lights[n].color.clone().mul(
lighting.diffuse * lighting.attenuation * scene.lights[n].intensity / Math.PI))
}
}
color.add(Vector.componetesMul(object.material.color, scene.ambient));
directLighting.add(RayTracer.sampleAreaLights(scene, emissiveObjects, intersectionPoint, normal, options.AREA_LIGHT_SAMPLES));
indirectLighting = RayTracer.indirectLighting(scene, camera, intersectionPoint, normal, options);
color.add(object.material.emittance.clone().clamp(0, 1));
color.add(Vector.componetesMul(directLighting.add(indirectLighting), albedo));
return color
};
The problem is that when I remove recursion depth test and add russian roulette to my indirect illumination function I get "Maximum call stack size exceeded" error. Same problem is when I implement roulette in traceRay function. But when I use only one Monte Carlo sample, everything works fine.
My russian roulette algorithm works as follows: at the beginning of a function, I am checking if a pseudo-random number is less then some constant threshold. If so, I am returning black color. In other case, function does what it would do without roulette, but the returned value is divided by the threshold.
So, the question is: how do I implement Russian Roulette properly?