Skip to content

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

Merged
merged 5 commits into from
Feb 23, 2025

Conversation

VReaperV
Copy link
Contributor

@VReaperV VReaperV commented Feb 11, 2025

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 requires r_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:
unvanquished_2025-02-11_211352_000
Which gets even worse when r_bloom is enabled:
unvanquished_2025-02-11_211357_000
Here are 2 more examples using the test-pbr map:
Bloom off:
unvanquished_2025-02-11_211945_000
Bloom on:
unvanquished_2025-02-11_211949_000
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:

y = x, x < 1.0
y = 1.0, x >= 1.0

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/):
image

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 as highlights.

Settings

The tonemapper currently provides 5 different settings that can be changed:
r_tonemap: enable/disable tonemapper
r_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 areas
r_tonemapContrast: again similar to a contrast setting on a camera - changes the contrast between light and dark areas
r_tonemapHighlightsCompressionSpeed: this is how fast the light will tend to approach 1.0 in the highlights area
r_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 higher
r_tonemapDarkAreaPointHDR: this is the HDR input cut-off for the dark area - any value below this will be processed as "toe"
r_tonemapDarkAreaPointLDR: this is the LDR output that corresponds to the previous value - the highest values in "toe" will be mapped to this
All 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:
image
r_tonemapHighlightsCompressionSpeed:
image
r_tonemapHDRMax (this is actually for shoulderClip and is computed in code, but it does depend on r_tonemapHDRMax):
image
r_tonemapDarkAreaPointHDR and r_tonemapDarkAreaPointLDR:
image

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:
unvanquished_2025-02-11_211402_000
And here's the difference with a slider (both with bloom on):
https://imgsli.com/MzQ4MjY5

Different tonemapper settings on the test-pbr map:
unvanquished_2025-02-11_212003_000
unvanquished_2025-02-11_212027_000
unvanquished_2025-02-11_212054_000
unvanquished_2025-02-11_212316_000

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:
unvanquished_2025-02-11_201832_000
r_overbrightDefaultClamp off; r_bloomBlur 1:
unvanquished_2025-02-11_201902_000
r_overbrightDefaultClamp off; r_bloomBlur 1; r_tonemap on (increased exposure):
unvanquished_2025-02-11_201922_000

With a slider:
https://imgsli.com/MzQ4Mjc1

@VReaperV VReaperV added T-Bug T-Improvement Improvement for an existing feature A-Renderer T-Feature-Request Proposed new feature labels Feb 11, 2025
@VReaperV
Copy link
Contributor Author

VReaperV commented Feb 11, 2025

More screenshots:
ptcs8:
r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on:
image

r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on:
image

edge-one:
r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 0.6:
image
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 0.2; r_gamma 2.2:
image
With sharper contrast and more range in dark areas:
image

hangar-28b:
r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 1.2:
image

td:
r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on (the reactor is not glowing because it's a time-based effect that happened to blink out at that moment):
image

antares:
r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on:
image

plat23:
r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on:
image
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 1.6:
image

pulse:
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on (tonemap settings changed to give more details to dark areas):
image

amcs:
r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on:
image

sokolov:
r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 1.3; r_tonemapExposure 2.0:
image
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 1.6; r_tonemapExposure 2.6:
image

@VReaperV
Copy link
Contributor Author

atcs-x-retake with different tonemapper settings:
image
image
image

@VReaperV
Copy link
Contributor Author

forlorn:
r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on:
image

thunder:
r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on:
image

r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on:
image
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 1.3:
image

With sliders:
https://imgsli.com/MzQ4Mjgx/1/2
https://imgsli.com/MzQ4Mjgy/1/2

cerberus:
r_overbrightDefaultClamp on:
image
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on:
image

dark:
r_overbrightDefaultClamp off:
image
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 2.0:
image

@VReaperV
Copy link
Contributor Author

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.

@VReaperV
Copy link
Contributor Author

edge-one:
Sun strength 150 (default):
r_overbrightDefaultClamp off:
unvanquished_2025-02-11_234501_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 1.3; r_tonemapDarkAreaPointHDR 0.3; r_tonemapExposure 0.75; r_tonemapHighlightsCompressionSpeed 0.95:
unvanquished_2025-02-11_234149_000

Sun strength 100:
r_overbrightDefaultClamp off:
unvanquished_2025-02-11_234725_000
r_overbrightDefaultClamp off; r_tonemap on:
unvanquished_2025-02-11_234732_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 2.0; r_tonemapExposure 0.75:
unvanquished_2025-02-11_235224_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 2.0; r_tonemapDarkAreaPointHDR 0.3; r_tonemapExposure 0.75:
unvanquished_2025-02-11_235230_000

Sun strength 50:
r_overbrightDefaultClamp off:
unvanquished_2025-02-11_235438_000
r_overbrightDefaultClamp off; r_tonemap on:
unvanquished_2025-02-11_235444_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 1.6; r_tonemapDarkAreaPointHDR 0.25; r_tonemapDarkAreaPointLDR 0.23:
unvanquished_2025-02-11_235444_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 1.6; r_tonemapDarkAreaPointHDR 0.25; r_tonemapDarkAreaPointLDR 0.23; r_tonemapExposure 0.9:
unvanquished_2025-02-11_235850_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 2.0; r_tonemapDarkAreaPointHDR 0.25; r_tonemapDarkAreaPointLDR 0.23; r_tonemapExposure 0.9:
unvanquished_2025-02-11_235934_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 2.0; r_tonemapDarkAreaPointHDR 0.25; r_tonemapDarkAreaPointLDR 0.3; r_tonemapExposure 0.9:
unvanquished_2025-02-12_000107_000

@VReaperV
Copy link
Contributor Author

On some of the screenshots there are both light dark areas on screen: these could benefit from localised tonemapping.
See e. g: https://bartwronski.com/2016/08/29/localized-tonemapping/
We can also divide the screen into something like 4x3 areas and automaticlly set exposure based on the overall luminance (per component) for the area a pixel is in.

@VReaperV
Copy link
Contributor Author

mxl-school:
r_overbrightDefaultClamp on:
unvanquished_2025-02-12_011537_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_011457_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 1.3; r_tonemapDarkAreaPointHDR 0.1; r_tonemapDarkAreaPointLDR 0.2; r_tonemapHighlightsCompressionSpeed 0.9:
unvanquished_2025-02-12_011504_000

r_overbrightDefaultClamp on:
unvanquished_2025-02-12_012343_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_012315_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 0.75:
unvanquished_2025-02-12_012302_000

r_overbrightDefaultClamp on:
unvanquished_2025-02-12_012406_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_012502_000
r_overbrightDefaultClamp off; r_tonemap on:
unvanquished_2025-02-12_012510_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 0.4:
unvanquished_2025-02-12_012523_000

staratcs:
r_overbrightDefaultClamp on:
unvanquished_2025-02-12_012645_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_012656_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 2.0; r_tonemapDarkAreaPointHDR 0.15; r_tonemapExposure 1.2:
unvanquished_2025-02-12_012811_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 2.0; r_tonemapDarkAreaPointHDR 0.15; r_tonemapDarkAreaPointLDR 0.3; r_tonemapExposure 1.2:
unvanquished_2025-02-12_012815_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 2.0; r_tonemapDarkAreaPointHDR 0.15; r_tonemapDarkAreaPointLDR 0.3; r_tonemapExposure 0.75:
unvanquished_2025-02-12_013700_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 3.0; r_tonemapDarkAreaPointHDR 0.15; r_tonemapDarkAreaPointLDR 0.3; r_tonemapExposure 0.75:
unvanquished_2025-02-12_013851_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 3.0; r_tonemapDarkAreaPointHDR 0.3; r_tonemapDarkAreaPointLDR 0.5; r_tonemapExposure 0.75:
unvanquished_2025-02-12_014143_000

@VReaperV
Copy link
Contributor Author

tremship:
r_overbrightDefaultClamp on:
unvanquished_2025-02-12_014537_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_014516_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 2.0:
unvanquished_2025-02-12_014506_000

r_overbrightDefaultClamp on:
unvanquished_2025-02-12_020142_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_020108_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 2.0:
unvanquished_2025-02-12_020045_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 2.0; r_tonemapExposure 0.75:
unvanquished_2025-02-12_020100_000

r_overbrightDefaultClamp off:
unvanquished_2025-02-12_020400_000
r_overbrightDefaultClamp off; r_tonemap on:
unvanquished_2025-02-12_020525_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 0.75:
unvanquished_2025-02-12_020406_000

r_overbrightDefaultClamp off:
unvanquished_2025-02-12_020913_000
r_overbrightDefaultClamp off; r_tonemap on:
unvanquished_2025-02-12_020918_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 1.25:
unvanquished_2025-02-12_020934_000

@VReaperV
Copy link
Contributor Author

station15:
r_overbrightDefaultClamp on:
unvanquished_2025-02-12_021717_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_021413_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapDarkAreaPointLDR 0.3:
unvanquished_2025-02-12_021419_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapDarkAreaPointLDR 0.3; r_tonemapExposure 1.25:
unvanquished_2025-02-12_021523_000

r_overbrightDefaultClamp on:
unvanquished_2025-02-12_022251_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_022217_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapDarkAreaPointLDR 0.3; r_tonemapExposure 1.2:
unvanquished_2025-02-12_022223_000

r_overbrightDefaultClamp on:
unvanquished_2025-02-12_022456_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_022426_000
r_overbrightDefaultClamp off; r_tonemap on:
unvanquished_2025-02-12_022533_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapDarkAreaPointLDR 0.3; r_tonemapExposure 1.2:
unvanquished_2025-02-12_022429_000

r_overbrightDefaultClamp on:
unvanquished_2025-02-12_023114_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_023037_000
r_overbrightDefaultClamp off; r_tonemap on:
unvanquished_2025-02-12_023030_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 1.8; r_tonemapDarkAreaPointHDR 0.15; r_tonemapDarkAreaPointLDR 0.2:
unvanquished_2025-02-12_023320_000

r_overbrightDefaultClamp on:
unvanquished_2025-02-12_023632_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_023605_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapDarkAreaPointHDR 0.15; r_tonemapDarkAreaPointLDR 0.3; r_tonemapExposure 1.25:
unvanquished_2025-02-12_023557_000

r_overbrightDefaultClamp on:
unvanquished_2025-02-12_024135_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_024155_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapDarkAreaPointHDR 0.15; r_tonemapDarkAreaPointLDR 0.3; r_tonemapExposure 1.25:
unvanquished_2025-02-12_024202_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 1.3; r_tonemapDarkAreaPointHDR 0.12; r_tonemapDarkAreaPointLDR 0.3; r_tonemapHighlightsCompressionSpeed 0.95; r_tonemapExposure 1.25:
unvanquished_2025-02-12_024302_000

r_overbrightDefaultClamp on:
unvanquished_2025-02-12_024622_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_024554_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 1.2:
unvanquished_2025-02-12_024829_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapContrast 1.3; r_tonemapDarkAreaPointHDR 0.12; r_tonemapDarkAreaPointLDR 0.3; r_tonemapHighlightsCompressionSpeed 0.95; r_tonemapExposure 1.25:
unvanquished_2025-02-12_024559_000

r_overbrightDefaultClamp on:
unvanquished_2025-02-12_025023_000
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_024951_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapExposure 1.25:
unvanquished_2025-02-12_024939_000

@VReaperV
Copy link
Contributor Author

There's also a difference with dynamic lights, although it's harder to capture:
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_030429_000
r_overbrightDefaultClamp off; r_tonemap on:
unvanquished_2025-02-12_030435_000

r_overbrightDefaultClamp off:
unvanquished_2025-02-12_030911_000
r_overbrightDefaultClamp off; r_tonemap on:
unvanquished_2025-02-12_030914_000

@VReaperV
Copy link
Contributor Author

The super-bright light on hq-beta28:
r_overbrightDefaultClamp off:
unvanquished_2025-02-12_031114_000
r_overbrightDefaultClamp off; r_tonemap on; r_tonemapHDRMax 64; r_tonemapExposure 0.2:
unvanquished_2025-02-12_031105_000

While that light itself is considered a map bug and is fixed by #3184, it's still a good showcase of what the tonemapper can do.

@sweet235
Copy link
Contributor

sweet235 commented Feb 12, 2025

Code LGTM, for what it is worth.

I'll be looking at maps for a while now.

@sweet235
Copy link
Contributor

Someone said the r_tonemap on screenshots look best in call cases.

@sweet235
Copy link
Contributor

I do get some flickering looking at light sources with r_bloom 1, as shown in this video:

tonemap-bloom-flickering.mp4

It is more obvious with higher FPS and higher screen resolution.

@slipher
Copy link
Member

slipher commented Feb 12, 2025

Sun strength 150 (default):

What's that?

I do get some flickering looking at light sources with r_bloom 1

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.

@VReaperV
Copy link
Contributor Author

What's that?

Some q3map2 setting. Sweet recompiled that map with the different settings. 100 is apparently the recommended value to start with.

@VReaperV
Copy link
Contributor Author

I do get some flickering looking at light sources with r_bloom 1

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.

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 r_bloomBlur and r_bloomPasses to fix this shimmering effect.

@sweet235
Copy link
Contributor

sweet235 commented Feb 12, 2025

Sun strength 150 (default):

What's that?

That is how the outdoor area of this map is lit by q3map2. It uses this in the sky shader:

      q3map_sunExt 1.00 1.00 1.00 150 150 40 2 16

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.

@slipher
Copy link
Member

slipher commented Feb 12, 2025

There's also a difference with dynamic lights, although it's harder to capture

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.

unvanquished_2025-02-12_051845_000

EDIT: more obvious screenshot

unvanquished_2025-02-12_053205_000

@VReaperV
Copy link
Contributor Author

There's also a difference with dynamic lights, although it's harder to capture

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.

unvanquished_2025-02-12_051845_000

EDIT: more obvious screenshot

unvanquished_2025-02-12_053205_000

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 r_tonemapHDRMax set to something like 32 or 64.

@slipher
Copy link
Member

slipher commented Feb 12, 2025

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 r_tonemapHDRMax set to something like 32 or 64.

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?

@VReaperV
Copy link
Contributor Author

VReaperV commented Feb 12, 2025

Shouldn't we clamp it so it doesn't do that?

The default value for r_tonemapHDRMax might still need to be increased, since clamping is something we're trying to avoid. After that yeah, either clamp, or do automatic exposure over the whole screen, or pre-expose lights (like in https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/course-notes-moving-frostbite-to-pbr-v2.pdf, p. 90). We might also want to change the dynamic lights strength in assets, now that it's not clamped (though that would need to be done on the for-0.56.0/sync branch, due to the nuking of random dynamic light scaling).

Also is the HDR curve here designed for a linear color space or an sRGB one?

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.

@illwieckz
Copy link
Member

illwieckz commented Feb 12, 2025

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:

412201675-c8747f12-c044-4f9c-9393-192af03f198f-annotation

412151368-427286d2-af2f-4e4a-81a1-f20ace693801-annotation

Those are the screenshots with r_overbrightDefaultClamp off; r_tonemap on.

Those shadows aren't physical because they can't be that dark with so much light around.

@VReaperV
Copy link
Contributor Author

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:

412201675-c8747f12-c044-4f9c-9393-192af03f198f-annotation

412151368-427286d2-af2f-4e4a-81a1-f20ace693801-annotation

Those are the screenshots with r_overbrightDefaultClamp off; r_tonemap on.

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.

@VReaperV
Copy link
Contributor Author

It's also not "like a filter", tonemapping is an essential part of an HDR pipeline.

@illwieckz
Copy link
Member

illwieckz commented Feb 12, 2025

Aren't the lightmaps already linear? I'd say it's a map bug then.

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 r_gamma 2.2 are kinda fixing the lightmap rendering by delinearizing the lightmap (they somewhat convert the linear lightmap to sRGB), but they break textures even more as they delinearize textures that are already delinearized (they somewhat convert to SRGB again textures that are already in sRGB, hence the washed colors), and that can't fix the lightmap containing bogus data to begin with.

It's also not "like a filter", tonemapping is an essential part of an HDR pipeline.

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.

@VReaperV
Copy link
Contributor Author

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.

The shadow is clearly in the lightmap itself:
unvanquished_2025-02-12_222530_000

With r_gamma 2.2:
unvanquished_2025-02-12_223102_000

So whatever it is, it's an issue with q3map2 or map compilation parameters. I assume it's too few bounces or something to do with backlights?

Then we have to take in account that the lightmap itself is computed without delinearizing the textures (already wrong).

How would we fix this? Wouldn't this require fixing q3map2 and then recompiling all the maps to fix lighting on them? Well, that or lighting them in the engine completely instead.

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.

Sure, but it's only part of what it can do. It's very important to avoid bright lights being whitened out. The darker regions may go away with all the aforementioned fixes, but the bright lights will still be there.

@illwieckz
Copy link
Member

illwieckz commented Feb 12, 2025

The shadow is clearly in the lightmap itself.

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.

How would we fix this? Wouldn't this require fixing q3map2 and then recompiling all the maps to fix lighting on them? Well, that or lighting them in the engine completely instead.

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.

Sure, but it's only part of what it can do. It's very important to avoid bright lights being whitened out.

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.

@VReaperV
Copy link
Contributor Author

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.

Well yeah, I'm just that it's not something we can fix in engine without completely doing lighting computations in it.

We can't recompile every map (especially when we don't have any map source), so we need to workaround those maps.

Time to do raytracing in engine lol.

@illwieckz
Copy link
Member

illwieckz commented Feb 12, 2025

Time to do raytracing in engine lol.

Yes! 😁️

Unfortunately legacy maps were dropping the light entity informations at build time. 😅️

@VReaperV
Copy link
Contributor Author

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.

@VReaperV
Copy link
Contributor Author

I've decided to move adaptive exposure to its own pr in #1559, because it's quite large.

@slipher
Copy link
Member

slipher commented Feb 22, 2025

This looks good other than the lack of clamping for out-of-range values.

@VReaperV
Copy link
Contributor Author

Oh, yes, forgot about that.

@VReaperV
Copy link
Contributor Author

This looks good other than the lack of clamping for out-of-range values.

Done now.

@VReaperV
Copy link
Contributor Author

(rebased)

@VReaperV
Copy link
Contributor Author

This looks good other than the lack of clamping for out-of-range values.

@slipher Does this count as lgtm from you?

@slipher
Copy link
Member

slipher commented Feb 23, 2025

LGTM, that fixed the yellow lights with blaster

@VReaperV VReaperV merged commit c608d2f into DaemonEngine:master Feb 23, 2025
9 checks passed
@VReaperV VReaperV deleted the adaptive-lighting branch February 23, 2025 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Renderer T-Bug T-Feature-Request Proposed new feature T-Improvement Improvement for an existing feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants