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<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<w; x++)   // Loop cols 
        for (int sy=0, i=(h-y-1)*w+x; sy<2; sy++)     // 2x2 subpixel rows 
            for (int sx=0; sx<2; sx++, r=Vec()){        // 2x2 subpixel cols 
                for (int s=0; s<samps; s++){ 
                    double r1=2*erand48(Xi), dx=r1<1 ? sqrt(r1)-1: 1-sqrt(2-r1); 
                    double r2=2*erand48(Xi), dy=r2<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; 

2 Answers 2


The clamping is done for the otherwise-jagged edge along the bright light. For pixels with the edge of the light in them, the radiance in the direction away from the center of the light is a very strong step function from (12,12,12) down to roughly (0.5,0.5,0.5) as you move across the pixel. This edge is smoothly anti-aliased as long as your pixel values can go up to (12,12,12), but once you clamp the pixels to (1,1,1) the outline of the light edge becomes very jagged (pixelated).

Clamping is technically adding bias but it's a limitation of the file format and display output.

One possible solution that wouldn't introduce bias would be tone-mapping or underexposing the image, such as dividing the radiance values by 12, but that would change the look of the image.

Clamping prior to averaging (and then averaging) the subpixels is effectively the same as down-sampling the clamped subpixel image with a box filter. The subpixels are unbiased (before clamping), but the pixels written out are down-sampled from them after clamping.

As for down-sampling, I don't know if it's biasing but it's the same operation one would do to shrink an image to fit a smaller display, for example. It typically makes images look better but smaller.

You could down-sample before clamping but it would look worse. But either way you will have to clamp when writing the file to PPM format unless you get all the radiance values below 1.

  • $\begingroup$ down-sampling the clamped subpixel image with a box filter is a clever observation explaining the intent behind both the subpixel raster and clamping before averaging. $\endgroup$
    – Matthias
    Aug 23, 2018 at 18:31
  • $\begingroup$ I don't know if it's biasing, it still remains a bias. You will converge to different radiance images with and without (i.e. pure path tracing) this clamping, since clamping is a non-linear operation. $\endgroup$
    – Matthias
    Aug 23, 2018 at 18:33
  • $\begingroup$ I was referring to down-sampling. Down-sampling is optional, clamping is not. $\endgroup$
    – Kevin
    Aug 24, 2018 at 20:05
  • $\begingroup$ There is still a final clamping in the toInt function. $\endgroup$
    – Matthias
    Aug 24, 2018 at 20:39

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.Image with fireflies

There are three ways to remove a firefly.

  1. 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.
  2. 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.
  3. 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.

  • $\begingroup$ There will, however, be a final clamping on the unbound pixel radiances in the toInt method. Though the average of 0.1, 0.4, 8.0, 0.8, 0.6; 1.98 finally clamps to 1.0, whereas the average of 0.1, 0.4, 1.0 (clamped separately), 0.8, 0.6; 0.58 finally clamps to itself and is thus more aggressive in this case to avoid fireflies at much lower sample rates. So this indeed works as a back at the cost of bias to remove fireflies due to the high variance at low sample counts. $\endgroup$
    – Matthias
    Aug 19, 2018 at 6:27
  • $\begingroup$ It reminds me of a related Twitter discussion originated from the "ray tracing tip of the day" by Morgan McGuire. $\endgroup$
    – Matthias
    Aug 19, 2018 at 6:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.