The following improvements have been made to Marathon’s HDR implementation:
-
Fixed all gamma correction issues. I had significant trouble with this, but after correcting a couple of stupid mistakes I did with the first implementation, it was quite simple. You bring everything to linear space when rendering (most importantly when texturing sampling - EXT_texture_sRGB helps a lot here) and you go back to gamma space at the very end.
-
Improved the bloom quality in 3 ways:
a. All textures used for blooming are floating point now. With 8bit textures there was no way to conserve decent luminance values, especially from bright but tiny light sources. Simply moving to FP textures fixed this, with no significant loss in performance or memory (the bloom textures are small anyway).
b. A new blurring algorithm is now used. First a chain of bloom “mips” is generated (from 1/4x1/4 to 1/32x1/32 of the framebuffer dimensions) and then each one is gaussian blurred with the same number of samples. This has the effect of progressively fainting but also expanding the bloom. This way, even tiny bright spots can generate bloom on big parts of the screen and generally it looks much better.
One note here, the gaussian blur is separable and implemented as two passes (one horizontal and one vertical). But I wanted to avoid having two textures for each bloom level. So, I reused the initial bright-pass texture as a temporary buffer (e.g. 80x64 bloom => (horizontal blur) => 320x256 temp => (vertical vlur) => 80x64 bloom). The problem of course was that the vertical blur would grab samples from outside the current bloom level in the temp buffer. I fixed this by clamping the y texture coordinate to the appropriate point for each bloom level.
c. I figured out how to fix the color saturation mentioned by Markus on the second post of this thread. Code:
// Old bright-pass
color.rgb *= max(color.rgb - vec3(HIGHLIGHT_THRESHOLD), vec3(0.0));
// New bright-pass
float luminance = dot(color.rgb, vec3(0.30, 0.59, 0.11));
color.rgb *= max(luminance - HIGHLIGHT_THRESHOLD, 0.0) / HIGHLIGHT_THRESHOLD;
So, suppose we have a pixel (4.0, 2.0, 0.0) and we've set the threshold at 2.0. The first code would result in a bloom pixel (2.0, 0.0, 0.0) and the second (0.76, 0.38, 0.0). It's clear that, from the original yellowish, we get plain red with the old code, whereas the new code produces the correct yellowish color (the lower intensity is not a problem).
-
When downscaling the scene luminance to find the average, I now also calculate the minimum and maximum of the whole scene. These have the following uses:
a. ATI cards do not support filtering on FP textures, so I’m using 16bit fixed-point for the bloom textures on ATI. The maximum luminance is necessary for an efficient conversion from FP to fixed and back.
b. It is possible to automatically calculate the tone mapping parameters from the min, max and average luminances, in a way that produces good quality in a variety of situations. I haven’t tried this yet though.
Other than the HDR improvements, and if anyone noticed, the shadows in the videos are true area light shadows (especially in the 2nd, 32 jittered samples are used). The technique I’m trying is still problematic (produces excellent quality with great performance, but flickers a bit), so I’ll post details when (if) I perfect it.
