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)



No comments: