smallpt's early clamping Kevin Beason's smallpt estimates the pixel radiance by accumulating $2\times2$ subpixel radiance estimates using the following expression:

c[i] = c[i] + Vec(clamp(r.x),clamp(r.y),clamp(r.z))*.25;


Here, c[i] represents the radiance estimate of the pixel with flattened index i, r represents the radiance estimate of one of its four subpixels. As one notices a clamp operation is used to clamp the radiance estimate of each subpixel separately to the $[0,1]^3$ range before accumulation. But this seems wrong from an unbiased path tracing perspective, since this effectively introduces bias to the radiance estimate of each pixel. So I wonder if I am missing something (e.g., clever trick at the cost of bias), given that Kevin Beason's smallpt is already around and known for nearly ten years?

Apparently, Kevin Beason also added a presentation by David Cline explaining every line. The aforementioned expression is explained as follows:


  Add the gamma-corrected subpixel color estimate to the Pixel color
  c[i].


This seems, however, completely unrelated since no gamma correction is involved yet at this stage in the code. And since exponential functions are non-linear transformations. (Obviously clamp does not involve any exponential functions at all. ;-) )

Reference code:

for (int y=0; y&lt;h; y++){                       // Loop over image rows 
    fprintf(stderr,"\rRendering (%d spp) %5.2f%%",samps*4,100.*y/(h-1)); 
    for (unsigned short x=0, Xi[3]={0,0,y*y*y}; x&lt;w; x++)   // Loop cols 
        for (int sy=0, i=(h-y-1)*w+x; sy&lt;2; sy++)     // 2x2 subpixel rows 
            for (int sx=0; sx&lt;2; sx++, r=Vec()){        // 2x2 subpixel cols 
                for (int s=0; s&lt;samps; s++){ 
                    double r1=2*erand48(Xi), dx=r1&lt;1 ? sqrt(r1)-1: 1-sqrt(2-r1); 
                    double r2=2*erand48(Xi), dy=r2&lt;1 ? sqrt(r2)-1: 1-sqrt(2-r2); 
                    Vec d = cx*( ( (sx+.5 + dx)/2 + x)/w - .5) + 
                            cy*( ( (sy+.5 + dy)/2 + y)/h - .5) + cam.d; 
                    r = r + radiance(Ray(cam.o+d*140,d.norm()),0,Xi)*(1./samps); 
                } // Camera rays are pushed ^^^^^ forward to start in interior 
                c[i] = c[i] + Vec(clamp(r.x),clamp(r.y),clamp(r.z))*.25; 
            } 
}
This seems, however, completely unrelated since no gamma correction is involved yet at this stage in the code.


The values are indeed not gamma corrected. Earlier in the presentation (On page 32) you have the toInt function which does the gamma correction. This function is called when the image is written to a file.

Then the clamping itself.
If we have a list of 5 samples.

0.1; 0.4; 0.2; 0.8; 0.6


It averages to 0.42. However, if one samples becomes very bright.

0.1; 0.4; 8.0; 0.8; 0.6


The average would become 1.98. That is a lot brighter. Because we take random samples, it could be that these two averages are neighboring pixels. This would cause a few pixels in the entire image to be much brighter than their neighbors. This is called a firefly. These fireflies are often not wanted, as they stick out in the image and make it look a lot more noisy. Therefore we want to try and remove these fireflies.

An example image of this can be found in blender's cycles documentation here. The bright pixels are fireflies and are unwanted.

There are three ways to remove a firefly.


Add more samples. By adding more samples, you get a truer image and that one bright sample would end up with less influence. This does however take longer to render.
Some sort of denoising algorithm. The algorithm could find these fireflies and remove them. However, it will almost always change other parts of the image which is often unwanted, and there is also no room for it in smallpt.
Discarding the bright sample or similar. This bright sample is the culprit of the problem and we could always decide to just ignore it. However, that bright sample is there for a reason and removing it would most of the times not be the greatest idea (it also makes things more complicated). Clamping however would still allow us to somewhat discard the bright sample without actually discarding it and is very simple and easy to implement. We just make it not have such a big influence on the image.


Clamping is often the choice for most render engines. If you would look at any commercial render engine, or even hobby path tracers, they allow you to clamp the samples. Normally you clamp the individual samples instead of the sub-pixels and more specifically you clamp indirect samples with its own clamping value, but they have about the same effect.

This clamping does change the image as it makes some areas or all darker since it removes energy (by clamping the color values) from the scene. This would become unbiased, yet it is often done (and almost always available in unbiased render engines) simply because it produces visually nicer images, because there are no fireflies and fireflies are generally worse than noise. Often you would not notice any artifacts when clamping, other than the removed fireflies.

In the case of smallpt, clamping it between 0.0 and 1.0 seems to work well for that one scene. However, I do recommend to just tinker with the code and render two images out for yourself, one with clamping and one without.