-
Notifications
You must be signed in to change notification settings - Fork 61
Implement Lottes HDR->LDR tonemapper #1550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
With sliders:
|
All of the above screenshots were made with all the materials enabled, tri-linear filtering, bloom, heatHaze, and default overbright and tonemap settings unless specified otherwise. |
On some of the screenshots there are both light dark areas on screen: these could benefit from localised tonemapping. |
Code LGTM, for what it is worth. I'll be looking at maps for a while now. |
Someone said the |
I do get some flickering looking at light sources with tonemap-bloom-flickering.mp4It is more obvious with higher FPS and higher screen resolution. |
What's that?
I don't think that's anything new. Bloom is already like that, there is some kind of noise that varies rapidly with tiny viewpos changes. |
Some q3map2 setting. Sweet recompiled that map with the different settings. 100 is apparently the recommended value to start with. |
Yeah, it is because of blur itself. The tonemapping itself is stable if you're not changing the settings, i. e. the same input will always produce the same output. You can try changing |
That is how the outdoor area of this map is lit by q3map2. It uses this in the sky shader:
The fourth number (150) is the sun's light intensity. According to http://q3map2.robotrenegade.com/docs/shader_manual/q3map-global-directives.html, 100 is already a strong sun. I built two more versions of this map with weaker suns. This is an example of a rather badly lit map: the sun is far too strong in the original. Looking at the Tremulous screenshots the author provides, that might be due to some peculiarities of some Tremulous clients. It is remarkable that this branch can show the original map with reasonable brightness at all. But in some cases, changing the lighting in the map is the most natural thing to do. |
The blaster impact might be the best test case here, as it suffers badly from saturation with the current renderer. Blue at the edges but white at the center. The tone mapper seems to have some kind of bug with it though: sometimes there are yellow spots. EDIT: more obvious screenshot |
Oh, that's colours going over the HDR white point. I was going to add that note to the initial post, but forgot to. Try with |
Shouldn't we clamp it so it doesn't do that? Also is the HDR curve here designed for a linear color space or an sRGB one? |
The default value for
The mapping is from linear light space. I think #1543 should work as is with it, since the input of the camera effects shader would still be in linear space. |
We have to keep in mind that using such tonemap technique on a map with lighting compute not being in linearspace is like applying some instagram filter: it is useful to get pleasant results out of something bad, but this can't fix the compute being bad. It will really shine once the map starts to use proper light computation as a base. In the same way, we can use colorgrading to improve legacy maps, but it doesn't remove the fact the light computation is wrong to begin with. Here are some examples of wrongness still visible in screenshots shared by @VReaperV once tonemapping is used: Those are the screenshots with Those shadows aren't physical because they can't be that dark with so much light around. |
Aren't the lightmaps already linear? I'd say it's a map bug then. |
It's also not "like a filter", tonemapping is an essential part of an HDR pipeline. |
Lightmaps are linear (right) but multiplied with a non-linear texture (wrong). Then the result is displayed without being delinearized (wrong again). Then we have to take in account that the lightmap itself is computed without delinearizing the textures (already wrong). What we currently see in game is wrong three times. So even if you rule out the bogus lightmap×texture multiplication by applying the lightmap on a white texture, the game displays the linear lightmap as is instead of converting it to sRGB before displaying it. Actually people doing
I mean tweaking any option available to attempt to recover data from the result of a bogus computation is like using a filter, whatever the tool used. Here tweaking tonemapping options to recover light from a computation that was was buggy three time (doing a bogus computation on a bogus computation on a bogus computation) is a hack. Tonemapping can be abused to recover light data, like increasing gamma can be abused to recover light data, like colorgraging can be abused to recover light data, like bloom can be abused to recreate light data, but tonemapping is not purposed to receive this broken data as input to begin with. This is a bit like what we experienced with bloom, bloom was used to fake bright lights out of clamped data, this can be seen as bloom being abused to recover light values that were destroyed, but bloom was never meant to be fed with such bogus data to begin with. This tonemapping feature supposes the input is correct and is part of the HDR pipeline like you said, and can also be abused to “improve” bogus input data. Until we feed this tonemapping feature with correct data, we are still hacking wrong data. |
The shadow is just what is not lit. When I say this shadow is wrong, I mean what is wrong is that it is too dark. When light computations are wrong, either light is too bright or not enough bright, or shadow is to dark or not enough dark. Here what I show in those screenshot is that such contrast in such shadow cannot be real. Tone mapping can't fully fix that because the data in input is garbage.
Yes. q3map2 has been fixed by Xonotic, but the fix is not enabled by default because q3map2 is also used by legacy broken games that can't process correct data (like Unvanquished we can fix, and other legacy games we can't fix). The fix is done on two side: in the map compiler, and in the game renderer. I detailed what are the required fixes on both q3map2 and daemon side in this comment.
Yes, using tonemapping in all the maps we produced in the past 20 years is very welcome as it can help to improve the rendering of them, but it will always be like a workaround. We can't recompile every map (especially when we don't have any map source), so we need to workaround those maps. Right now using tonemapping on legacy and current maps is both providing tonemapping and a workaround as a lucky side-effect. But tonemapping will not be a workaround and just be a feature when we will use it on non-broken maps in the future. |
Well yeah, I'm just that it's not something we can fix in engine without completely doing lighting computations in it.
Time to do raytracing in engine lol. |
Yes! 😁️ Unfortunately legacy maps were dropping the light entity informations at build time. 😅️ |
Well... We can try to reconstruct them from the lightmaps that come with the map. |
32b968d
to
0713e50
Compare
I've decided to move adaptive exposure to its own pr in #1559, because it's quite large. |
This looks good other than the lack of clamping for out-of-range values. |
Oh, yes, forgot about that. |
Done now. |
Avoid weird-looking over-saturation.
1c3b7f3
to
1ce722c
Compare
(rebased) |
@slipher Does this count as lgtm from you? |
LGTM, that fixed the yellow lights with blaster |
Background
Ever since #1050, we've had some major changes to lighting thanks to fixing the bug that was affecting how strong the lights were on any map (see #1050 and related pr for the explanation).
#1050 added the option of using int16 render targets. This was done in order to avoid clamping shader output to the
[0.0, 1.0]
range before blending it. And then later #1406 changed it to an fp16 framebuffer to avoid colour clamping in multi-stage shaders. This all requiresr_highPrecisionRendering on
.One of the side-effects of the whole overbright fix is that some lights became brighter than what the 8-bit framebuffer could hold. Coincidentally, the change to 16-bit framebuffers - in particular the fp16 ones, because int framebuffers still get clamped to
[0.0, 1.0]
- is part of the solution to lights exceeding the[0.0, 1.0]
range - and part of a proper HDR implementation.However, doing only that is only good enough for operations that take place before presenting the final image to the screen: i. e. rendering all the map and entity surfaces etc, and blending them together. As the final step we were just doing




clamp( color, 0.0f, 1.0f )
in the shader, which obviously creates some pretty bad effects for bright lights:Which gets even worse when
r_bloom
is enabled:Here are 2 more examples using the
test-pbr
map:Bloom off:
Bloom on:
We get some very whitened out areas, in which we lose details due to that.
HDR->LDR conversion
The basic cause of this is that monitors generally display colours only in the
[0.0, 1.0]
range - i. e. the Low Dynamic Range (LDR). The range dynamicity here refers to how far away the highest value can be from the lowest one. As a result of this, on non-HDR monitors the input will first be converted to LDR - in OpenGL this is always done when rendering to the default framebuffer (i. e. one that's provided by the OS), and consists of clamping the values to the[0.0, 1.0]
range. It's the same thing as we were doing manually and, of course, does not produce good results if the input colours can actually go over that range.So the default - linear - mapping is basically just:
The other problem is that we lose details in dark regions too, because they're always mapped to some low range.
Tonemapping operators
In order to solve this, different HDR->LDR mappings exist. They, of course, cannot fully capture all the details across the whole range - but they sure make the end result look way better than either clamping the colours or doing everything in LDR.
There are many different tonemapping operators, and here's also a comparison of a few them displayed as the HDR->LDR mapping curves that someone made on shadertoy:
https://www.shadertoy.com/view/llXyWr
All of them have different trade-offs, because, again, there's no 1:1 HDR->LDR mapping, and we could probably try different algorithms for months on end, which I'm not inclined to do, so I just chose one - Lottes tonemapper (https://gpuopen.com/wp-content/uploads/2016/03/GdcVdrLottes.pdf), because it's easy to implement and gives good results.
I won't go into details about how the formula is derived and why, because there are a lot of them, but they all can be found in the above paper. However, like with most tonemapping operators (a notable exception is the Reinhard tonemapper, which just does

color /= color + 1.0
- this results in a very bleak output and still a lack of details in dark areas; some variations of this tonemapper exist, but they're not as good as other tonemappers), the mapping looks like this (image from https://filmicworlds.com/blog/filmic-tonemapping-with-piecewise-power-curves/):The curve mainly has the form
a * x ^ b
. The "toe" section is the dark area, where colours are mapped to exponentially brighter ones. "Linear" section, if it exists, just does a 1:1 mapping. While "shoulder" decreases the brightness of colours in that area - this is what fixes the problem of lights going over the[0.0, 1.0]
range, and just improves the look of it in general - these are also referred to ashighlights
.Settings
The tonemapper currently provides 5 different settings that can be changed:
r_tonemap
: enable/disable tonemapperr_tonemapExposure
: similar to an exposure setting on a camera - all colours will be premultiplied by it; this is the first thing to try to increase brightness in dark areasr_tonemapContrast
: again similar to a contrast setting on a camera - changes the contrast between light and dark areasr_tonemapHighlightsCompressionSpeed
: this is how fast the light will tend to approach1.0
in the highlights arear_tonemapHDRMax
: this is the "white point" for HDR - how far up a light value can go. Setting this too high will dim the output a bit, but setting it lower than the maximum colour value in input will result. If you see yellow spots appearing in lights, then this value should be set higherr_tonemapDarkAreaPointHDR
: this is theHDR
input cut-off for the dark area - any value below this will be processed as "toe"r_tonemapDarkAreaPointLDR
: this is theLDR
output that corresponds to the previous value - the highest values in "toe" will be mapped to thisAll of these settings can be changed at runtime, restarts or anything like that are not required at all.
(images from the paper with this tonemapper linked above)




r_tonemapContrast
:r_tonemapHighlightsCompressionSpeed
:r_tonemapHDRMax
(this is actually forshoulderClip
and is computed in code, but it does depend onr_tonemapHDRMax
):r_tonemapDarkAreaPointHDR
andr_tonemapDarkAreaPointLDR
:I've also put together this graph where different inputs can be changed to produce a different HDR->LDR mapping:
https://www.desmos.com/calculator/4jujpbdntp
Output with tonemapper enabled
Let's revisit the areas on previous screenshots, where the lights became too bright, but this time with the tonemapper enabled:

And here's the difference with a slider (both with bloom on):
https://imgsli.com/MzQ4MjY5
Different tonemapper settings on the




test-pbr
map:With a slider:
https://imgsli.com/MzQ4Mjcw
Different tonemapper settings:
https://imgsli.com/MzQ4Mjcy
https://imgsli.com/MzQ4Mjc0
By mapping the HDR values to LDR properly, we are able to keep details in highly-lit areas and add more details to areas that were previously dark:



r_overbrightDefaultClamp on; r_bloomBlur 1
:r_overbrightDefaultClamp off; r_bloomBlur 1
:r_overbrightDefaultClamp off; r_bloomBlur 1; r_tonemap on
(increased exposure):With a slider:
https://imgsli.com/MzQ4Mjc1