I am working on a path tracing renderer, and I want to produce an sRGB image out of the HDR image buffer I get at the end of my rendering algorithm. Up until now I have worked just by clamping values, which of course works fine only for a limited range of light conditions.
Now, reading around (I started here, then read some of the links given in that post) I got that one way to go is (starting in linear RGB space):
- Calculate the exposure
- Scale all the values in the image by the exposure, channel by channel
- Apply some tonemapping function
- Gamma correct.
The preferred way to calculate the exposure seems to be as the inverse of "the luminance value that will saturate the sensor", computed from some average luminance value of the image; the latter in turn is obtained by computing a histogram of the log2's of all pixel's luma, ignoring some percentage of values (e.g. 50% of the darkest pixels and 5% of the brightest), and computing the average of the remaining values.
Another way I found was in this answer here, where the scalar factor seems to be based only on the pixel value, rather than the whole scene. EDIT: Here, it seems to me that the steps 2 and 3 above are replaced by a different procedure:
- The color gets
mix
ed to its luminance by a factor of $\left(\frac{m-0.18}{m}\right)^{20}$, where $m$ is the maximum value in the RGB channels - Tonemapping is applied only to $m'$ = ($m$ after the above
mix
) - The color channels are multiplied by a factor equal to $f(m')/m'$, where $f$ is the tonemapping function.
What's the theory behind the second approach, and does it relate in any way to exposure?
Is it normal that if I apply the first method, I usually (regardless of the percentage thresholds I use) get images that are way darker than if I apply the second (although maybe a bit more realistic), or is this a smell that I'm getting something wrong in my implementation of the first method? (Additionally, the images I get by rendering my test scenes with Cycles are as bright as the images I get from my renderer using the second method, thus not nearly as dark as the ones I get from the first method.)
Here are some examples: for reference, this is a test scene I'm using, rendered with Cycles, containing two emissive squares each of emission respectively 2, 10, 100 and 1000
Here is the same scene, with the same factors, rendered with my renderer, applying this tonemapping function directly to each color channel, after exposure-correcting (histogram-based, clamped at 50% for dark and 5% for light)
Here are the tests without exposure correction (same tonemapping):
Here, I am using the answer's method to deal with the alternative "scaling" (with the same tonemapping function I used for the results above).
The exposure-corrected gives clearly wrong results, as the 10, 100 and 1000 values all give pretty much the same output, the non-exposure-corrected one has some weird saturation values but it's perceptively the closest to Cycles' illumination levels; the answer's method flattens the background too much, and feels a bit darker than it should, at least compared to Cycles' output.
At this point I'd guess that I'm applying the exposure correction wrongly. A simplified version of my code is the following:
constexpr float a = 2.51;
constexpr float b = 0.03;
constexpr float c = 2.43;
constexpr float d = 0.59;
constexpr float e = 0.14;
float expsr = this.exposure();
// the image buffer contains RGB values (no alpha) in linear space
for (size_t j = 0; j < this.image_buffer.size(); ++j)
{
image_buffer[j] *= expsr;
image_buffer[j] = (image_buffer[j] * (a * image_buffer[j] + b)) / (image_buffer[j] * (c * image_buffer[j] + d) + e);
}
And afterwards I gamma-correct. Am I supposed to use the exposure value or the tone mapping function differently?