Sunday, February 17, 2019

Lazy person's tone mapping

In a physically-based renderer, your RGB values are not confined to [0,1] and your need to deal with that somehow.

The simplest thing is to clamp them to zero to one.   In my own C++ code:
inline vec3 vec3::clamp() {
    if (e[0] < real(0)) e[0] = 0;
    if (e[1] < real(0)) e[1] = 0;
    if (e[2] < real(0)) e[2] = 0;
    if (e[0] > real(1)) e[0] = 1;
    if (e[1] > real(1)) e[1] = 1;
    if (e[2] > real(1)) e[2] = 1;
    return *this;
 }


A more pleasing result can probably be had by applying a "tone mapping" algorithm.   The easiest is probably Eric Reinhard's "L/(1+L)" operator from the Equation 3 of this paper

Here is my implementation of it.   You still need to clamp because of highly saturated colors, and purists wont like my luminance formula (1/3.1/3.1/3) but never listen to purists :)

void reinhard_tone_map(real mid_grey = real(0.2)) {
// using even values for luminance.   This is more robust than standard NTSC luminance
// Reinhard tone mapper is to first map a value that we want to be "mid gray" to 0.2// And then we apply the L = 1/(1+L) formula that controls the values above 1.0 in a graceful manner.
    real scale = (real(0.2)/mid_grey);
    for (int i = 0; i < nx*ny; i++) {
        vec3 temp = scale*vdata[i];
        real L = real(1.0/3.0)*(temp[0] + temp[1] + temp[2]);
        real multiplier = ONE/(ONE + L);
        temp *= multiplier;
           temp.clamp();
           vdata[i] = temp;
        }

}

This will slightly darken the dark pixels and greatly darken the bright pixels.   Equation 4 in the Reinhard paper will give you more control.   The cool kids  have been using "filmic tone mapping" and it is the best tone mapping I have seen, but I have not implemented it (see title to this blog post)



1 comment:

Steve M said...

Definitely, definitely always use some sort of filmic, s-shaped tone mapping curve! These days I would recommend ACES. Otherwise, you will need to do massive color correction afterwards to get a reasonably pleasing rendering from linear HDR data, and that's not ideal from a precision point of view. Or, worse, you will be tuning your assets/lighting to look good under the bad tone curve, which is a fool's errand - you will not get the correct look in varying lighting conditions, because your inputs are no longer physically based (ex: dark albedos are too dark, etc). IMO, the only thing that's "physically based" is emulating what cameras do, which is to map linear HDR sensor data to an LDR screen or print using an S shaped response curve. This is the same for film and digital media, so it should work equally well for rendering.

The most basic test is that a properly captured linear HDR environment map of the real world should look pleasing when rendered through your engine, roughly what a jpeg photo or mpeg video of the same environment would look like as it comes out of the camera. Under Reinhard, it will look like washed out garbage.

Sadly, almost all software packages use a linear tone curve by default, to this day, which is an atrocity of global proportions!