# OpenGL HDR tonemapping/exposure values

## Recommended Posts

Posted (edited)

So, i'm still on my quest to unterstanding the intricacies of HDR and implementing this into my engine. Currently i'm at the step to implementing tonemapping. I stumbled upon this blogposts:

and tried to implement some of those mentioned tonemapping methods into my postprocessing shader.

The issue is that none of them creates the same results as shown in the blogpost which definitely has to do with the initial range in which the values are stored in the HDR buffer. For simplicity sake i store the values between 0 and 1 in the HDR buffer (ambient light is 0.3, directional light is 0.7)

This is the tonemapping code:

vec3 Uncharted2Tonemap(vec3 x)
{
float A = 0.15;
float B = 0.50;
float C = 0.10;
float D = 0.20;
float E = 0.02;
float F = 0.30;

return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
}

This is without the uncharted tonemapping:

Spoiler

This is with the uncharted tonemapping:

Spoiler

Which makes the image a lot darker.

The shader code looks like this:

void main()
{

vec3 color =  texture2D(texture_diffuse, vTexcoord).rgb;

color = Uncharted2Tonemap(color);

//gamma correction (use only if not done in tonemapping code)
color = gammaCorrection(color);

outputF = vec4(color,1.0f);
}

Now, from my understanding is that tonemapping should bring the range down from HDR to 0-1.

But the output of the tonemapping function heavily depends on the initial range of the values in the HDR buffer. (You can't expect to set the sun intensity the first time to 10 and the second time to 1000 and excpect the same result if you feed that into the tonemapper.) So i suppose that this also depends on the exposure which i have to implement?

To check this i plotted the tonemapping curve:

Spoiler

You can see that the curve goes only up to around to a value of 0.21 (while being fed a value of 1) and then basically flattens out. (which would explain why the image got darker.)

My guestion is: In what range should the values in the HDR buffer be which then get tonemapped? Do i have to bring them down to a range of 0-1 by multiplying with the exposure?

For example, if i increase the values of the light by 10 (directional light would be 7 and ambient light 3) then i would need to divide HDR values by 10 in order to get a value range of 0-1 which then could be fed into the tonemapping curve. Is that correct?

Edited by Lewa

##### Share on other sites

Hi! Your function Uncharted2Tonemap converges to 1, but very slowly.

Try this function, called Reinhard operator: ldr(hdr) = hdr / (hdr + a), where a = 1.0 for the start. Try plotting it, you'll like it.

This maps low values to similar values (behaviour "similar" to gamma correction/sRGB for a=0.2, for example) and very high values closer to 1.0 (LDR white).

From your first screen-shot, it appears that the scene is pretty dark. If it wasn't, you'd have fully-blown white (white-ish) areas with values >1.0, which you don't seem to have. Therefore, after you tonemap it, it's dark. That's correct behaviour. Your input data doesn't seem to be.

The input HDR value must be linear, not sRGB, be sure to have it like that. Your render target will be either R16G16B16A16_FLOAT or R11G11B10_FLOAT (don't know OpenGL equivalents by heart). You should do all your light calculations in linear space. When sampling your textures, the texture sampling functions should return linear values. You can try by de-gamma-correcting your input values (pow(hdr, 2.2)) to see if you aren't off.

The output back buffer, however, will be expected to be sRGB (I expect), so depending on what your system is doing at the end you might need to do it yourself, so add this simple approximation (or not, experiment): finalLDR = pow(ldr, 1.0/2.2) (gamma correction).

Also, with a Reinhard tonemapper, but also that you posted, there isn't a very big difference between a value HDR 10.0 and HDR 100.0:

Reinhard with a=1.0: 10 / (10 + 1) = 0.909, 100 / (100 + 1) = 0.990

Uncharted2(10) = 0.706, Uncharted2(100) = 0.904

Remember you're trying to fit a big range (0..1000 nits) to a small LDR range (0..1 with 8-bit precision).

##### Share on other sites
Posted (edited)

I use a FP16 RGBA buffer to store the HDR values. (they are also in linear space.) And the image is gamma corrected.

The image appears a bit dark because the textures have a max brightness value of 0.8 and the directional light a value of 1. (thus, even if the dot product between the lightnormal and the trianglenormals is 1, at max you will get a value of 0.8)

That's also one of the issues which i haven't figured out yet. I'm using a PBR rendering pipeline and while researching online i always stumble upon the suggestion that in PBR one should use "real world values" to light the scene but it's never explained/shown how this should look like. (No reference values to take note of.)

For example, setting the light values to 7 (directiional light) and to 3 (ambient light), meaning the max value in the HDR FP16 buffer can never exceed 10, the image looks like this:

Without unchartedtonemap:

Spoiler

(Obviously mostly white because i'm mapping values >1 to the screen.)

With uncharted tonemap:

Spoiler

So if that's the correct behaviour, how can i get a "normal looking" image? What HDR range is required (in the FP16 buffer) in order to get correct results after tonemapping?

/Edit:

Quote

Uncharted2(10) = 0.706, Uncharted2(100) = 0.904

IMHO there is a big difference between a value of 0.7 and 0.9 which then gets displayed to the screen.

So, does this tonemapper excpect you to have values of >100 in order to "properly" map between the displays 0-1 range?

Edited by Lewa

##### Share on other sites
Posted (edited)

How can values around well under 10 be so washed out with the curve I've presented? 10/(10+1) = 0.91 = not quite 1.0

The value in the colour buffer could ideally be the radiance [W⋅sr−1⋅m−2], from the rendering equation for Lo(x, w) = Le(x,w) + Li(x,w) where x is the point on a surface visible through your pixel, w is the direction to it, Le is the radiance emitted from that point in your direction and Li is the radiance reflected from the scene (from all directions towards our point x and exiting in our direction). In our simpler cases, there will be no GI, Le=0 (no emission) and Li = radiance coming from all light sources.

Radiance is "radiant flux [W] 'divided' by solid angle [sr−1], area [m−2] and dot(N,L) [dimension-less]", radiant flux is in Watt and that's what I guess the powers of the lights should be in.

I hope somebody will correct me or make it easier to comprehend.

Edited by pcmaster

##### Share on other sites
Posted (edited)
36 minutes ago, pcmaster said:

How can values around well under 10 be so washed out with the curve I've presented? 10/(10+1) = 0.91 = not quite 1.0

That's how your proposed tonemapper looks like:

vec3 reinhardTone(vec3 color){
vec3 hdrColor = color;
// reinhard tone mapping
vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
return mapped;
}

And the image:

Spoiler

The light values are between 0 and 10. (7 for directional, 3 for ambient.)

Yes, it brings down the range of the values from 0-X to 0-1 but it doesn't look good at all.

That's why i wonder if the values of the lights and the sun have to be in a specific range (by adjusting exposure?) in order to work properly and create images like this:

Spoiler

Even in the screenshots from the blogpost of frictional games they don't look either too bright or too dark:

Spoiler

(Given that the image without any kind of tonemapping isn't overexposed i suppose they used values from 0-1 for the lightsources like i have before (instead of 0-10 or 0-100, etc...), but this doesn't explain why the uncharted tonemapper results in a more natural image in this case compared to my darkened image in the first post.)

That's how i apply the tonemaps:

vec3 reinhardTone(vec3 color){
vec3 hdrColor = color;
// reinhard tone mapping
vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
return mapped;
}
vec3 gammaCorrection(vec3 color){
// gamma correction
color = pow(color, vec3(1.0/2.2));
return color;
}
vec3 Uncharted2Tonemap(vec3 x)
{
float A = 0.15;
float B = 0.50;
float C = 0.10;
float D = 0.20;
float E = 0.02;
float F = 0.30;

return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
}

void main()
{
vec3 color =  texture2D(texture_diffuse, vTexcoord).rgb;//this texture is a
//FP16RGBA framebuffer which stores values from 0-10

color = reinhardTone(color);
//color = Uncharted2Tonemap(color);

//gamma correction (use only if not done in tonemapping code)
color = gammaCorrection(color);

outputF = vec4(color,1.0f);
}

Edited by Lewa

##### Share on other sites

I do so

Texture2D<float4> TexHDR : register(t0);

static const float3 LUM_FACTOR = float3(0.299, 0.587, 0.114);

float3 ToneMapping(float3 HDRColor) {
float LScale = dot(HDRColor, LUM_FACTOR);
LScale *= MiddleGrey / 0.01;
LScale = (LScale + LScale * LScale / LumWhiteSqr) / (1.0 + LScale);
return HDRColor * LScale;
}


##### Share on other sites

A major reason why tonemapping isn't working for you is you're not actually feeding it an image with a high dynamic range. You're using lighting values that are more appropriate to older, non-HDR lighting models.

Which is to say, your light:ambient ratio is way too low. Your light source is barely more than twice as bright as the ambient lighting, whereas in the real world (and thus, the lighting conditions HDR is attempting to simulate), that ratio is easily 100:1 or higher, sometimes even in excess of 1000:1.

Remember, the entire point of tonemapping is to take an image with a high dynamic range and map that range such that it's displayable on a device with a low dynamic range without the bright parts clipping to pure white or the dark parts clipping to pure black. If you tonemap an image that already has a normal dynamic range, the result is just going to be washed out.

##### Share on other sites
Posted (edited)
48 minutes ago, Anthony Serrano said:

A major reason why tonemapping isn't working for you is you're not actually feeding it an image with a high dynamic range. You're using lighting values that are more appropriate to older, non-HDR lighting models.

Which is to say, your light:ambient ratio is way too low. Your light source is barely more than twice as bright as the ambient lighting, whereas in the real world (and thus, the lighting conditions HDR is attempting to simulate), that ratio is easily 100:1 or higher, sometimes even in excess of 1000:1.

Remember, the entire point of tonemapping is to take an image with a high dynamic range and map that range such that it's displayable on a device with a low dynamic range without the bright parts clipping to pure white or the dark parts clipping to pure black. If you tonemap an image that already has a normal dynamic range, the result is just going to be washed out.

So, is there a reference in what range my lights/values should be?

I also tried setting the sun value to 1000 and the ambient light to 1 while applying the uncharted tonemapper:

Spoiler

I think that i get something fundamentally wrong here.

Code again:

void main()
{
vec3 color =  texture2D(texture_diffuse, vTexcoord).rgb;//floating point values from 0 - 1000

//tonemap
color = Uncharted2Tonemap(color);

color = gammaCorrection(color);

outputF = vec4(color,1.0f);
}

Tonemappers map the HDR range to LDR. But what i don't quite get how this can properly work if they don't "know" the range of your max brightness RGB value in the first place. (they only take the RGB values of the specific pixel in your FP buffer as input.).

The range has to be important if you want to realize (for example) an S-curve in your tonemapper (like in the ACES filmic tonemapper). And that can only happen if you A) pass the range into the tonemapper (if you have an arbitary range of brightness) or B) the tonemapping algorithm assumes that your values are in a specific "correct" range in the first place.

Edited by Lewa

##### Share on other sites
Posted (edited)

you are missing a fundamental detail!... as others said, the point of tone mapping is to bring down your color values from high range to the displayable low range, but... that high range changes every frame! so you must first compute it to generate an exposure value, usually by getting an average luminance of the current frame... just think about it, if the exposure was the same for every frame with a fixed and constant range, you could just scale all your values in a precomputation step to ensure decent results in the traditional low range... which is basically what was done intuitively before

Edited by Jihodg

##### Share on other sites
13 hours ago, Anthony Serrano said:

A major reason why tonemapping isn't working for you is you're not actually feeding it an image with a high dynamic range. You're using lighting values that are more appropriate to older, non-HDR lighting models.

Which is to say, your light:ambient ratio is way too low. Your light source is barely more than twice as bright as the ambient lighting, whereas in the real world (and thus, the lighting conditions HDR is attempting to simulate), that ratio is easily 100:1 or higher, sometimes even in excess of 1000:1.

Remember, the entire point of tonemapping is to take an image with a high dynamic range and map that range such that it's displayable on a device with a low dynamic range without the bright parts clipping to pure white or the dark parts clipping to pure black. If you tonemap an image that already has a normal dynamic range, the result is just going to be washed out.

Yes. I have to count the general lighting of the scenes and divide into this co-factor. I've just picked it up

## Create an account

Register a new account

1. 1
Rutin
41
2. 2
3. 3
4. 4
5. 5

• 18
• 20
• 14
• 14
• 9
• ### Forum Statistics

• Total Topics
633379
• Total Posts
3011570
• ### Who's Online (See full list)

There are no registered users currently online

×