Skip to content

renderer: implement complete overBright in GLSL and high-precision frame-buffer #1050

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 2 commits into from
Mar 12, 2024

Conversation

illwieckz
Copy link
Member

@illwieckz illwieckz commented Feb 20, 2024

  • renderer: implement complete overBright in GLSL

We cannot overBright separate stage as stage are clamped within [0.0, 1.0] when blending multiple stages together, meaning the result of the multiplication of a separate light stage with a light factor will be clamped before blending it with the color map.

This implementation implements overBright in the camera shader, but to only apply this overBright to surfaces having received lights, surfaces that do not receive lights gets divided by the light factor in a way re-multiplying them cancels the division.

Stages and surfaces to be blended over other stages or surfaces are also divided to avoid multiplying multiple time the same surface, in order to have the final result being (m * a) + b instead of m * (a + b) wich would be
(m * a) + (m * b) and then sump up the factor, something we don't want to do.

The legacy code for overbrighting (but clamping) the light at BSP load is kept for when the feature is disabled, but rewritten to make the code better.

This feature can be enabled by disabling the r_mapClampOverBright cvar.

  • renderer: implement high precision rendering framebuffers

This is needed to avoid color-banding with the various division/multiplication round trips of the complete overBright implementation. This will also be needed for sRGB colors in the future too. It is enabled by default if a feature requires it.

This is optional and can be disabled with r_highPrecisionRendering, or automatically disabled if the hardware doesn't support the feature.

We cannot overBright separate stage as stage are clamped
within [0.0, 1.0] when blending multiple stages together,
meaning the result of the multiplication of a separate
light stage with a light factor will be clamped before
blending it with the color map.

This implementation implements overBright in the camera
shader, but to only apply this overBright to surfaces having
received lights, surfaces that do not receive lights gets
divided by the light factor in a way re-multiplying them
cancels the division.

Stages and surfaces to be blended over other stages or
surfaces are also divided to avoid multiplying multiple
time the same surface, in order to have the final result
being (m * a) + b instead of m * (a + b) wich would be
(m * a) + (m * b) and then sump up the factor, something
we don't want to do.

The legacy code for overbrighting (but clamping) the light at
BSP load is kept for when the feature is disabled, but rewritten
to make the code better.

This feature can be enabled by disabling the r_mapClampOverBright
cvar.
This is needed to avoid color-banding with the various
division/multiplication round trips of the complete overBright
implementation. This will also be needed for sRGB colors in the
future too. It is enabled by default if a feature requires it.

This is optional and can be disabled with r_highPrecisionRendering,
or automatically disabled if the hardware doesn't support the feature.
@illwieckz illwieckz force-pushed the illwieckz/complete-overbright branch from bd18597 to 3710c76 Compare February 20, 2024 15:16
@illwieckz
Copy link
Member Author

illwieckz commented Feb 20, 2024

Complete overBright in GLSL is currently disabled by default, the code is considered good enough to be enabled by users to evaluate it and report issues. This may be improved incrementally in the future but I haven't spot a single error myself yet.

Maps like terminus and area3 are not rendered properly (too bright) but that's not because of a bug in this code. This is because we render them with mapOverBrightBits feature being enabled but those maps were initially made for a game disabling mapOverBrightBits by default. This can be fixed by setting the "mapOverBrightBits" "0" key in the worldspawn entity of these maps, or if the clamped overBrightBits pleases the eye, keep the overBright but clamp it by setting the "mapClampOverBright" "0" key in the worldspawn entity.

@illwieckz
Copy link
Member Author

illwieckz commented Feb 20, 2024

Future sRGB code will require this complete overBright implementation and will also need high precision frame buffer for better results. All this code will be enabled by default when loading a map with sRGB, even if the r_mapClampOverBright cvar is enabled.

@illwieckz illwieckz added A-Renderer T-Feature-Request Proposed new feature labels Feb 20, 2024
@illwieckz illwieckz changed the title renderer: implement alternative complete overBright in GLSL and high-precision frame-buffer renderer: implement complete overBright in GLSL and high-precision frame-buffer Feb 20, 2024
@illwieckz illwieckz force-pushed the illwieckz/complete-overbright branch from 3710c76 to f2d2d78 Compare February 20, 2024 15:22
@illwieckz
Copy link
Member Author

illwieckz commented Feb 20, 2024

This was tested with maps like:

  • test-portal-recursion (portals and mirrors).
  • atcshd (legacy materials, light known to be broken by clamped overBright)
  • atcszalpha (legacy materials, light known to be broken by clamped overBright)
  • metro (legacy materials, light styles)
  • pulse (legacy materials, light styles)
  • plat23 (state of the art materials)
  • Wolf:ET's oasis (legacy materials, global fog, mixed light map and vertex light)

No visual regressions and glitches were noticed.

@illwieckz illwieckz force-pushed the illwieckz/complete-overbright branch from f2d2d78 to d116d3d Compare February 20, 2024 15:31
Copy link
Member

@slipher slipher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as #1035 (review) - among lightmaps/light grids/vertex lighting which ones is the new algorithm expected to apply to?

@@ -77,6 +77,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
cvar_t *r_dynamicLightCastShadows;
cvar_t *r_precomputedLighting;
Cvar::Cvar<int> r_mapOverBrightBits("r_mapOverBrightBits", "default map light color shift", Cvar::NONE, 2);
Cvar::Cvar<bool> r_mapClampOverBright("r_mapClampOverBright", "clamp over bright of legacy maps (enable multiplied color clamping and normalization)", Cvar::NONE, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the word "map" in these cvar names? Seems backwards since these are only used when a value is NOT provided by the map. I suggest r_defaultOverbrightBits and r_defaultClampOverbright with the word "default" emphasizing that changes to their value might be ignored.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was originally two cvars, r_overBrightBits and r_mapOverBrightBits. The first one was configuring some hardware feature, and the engine was multiplying the lights with r_mapOverBrightBits and r_overBrightBits.

Let's imagine we had r_mapOverBrightBits set to 2, if there was no hardware overbright bits (r_overBrightBits 0), the lights were fully multiplied by 2, if there was 1 hardware overbright bit (r_overBrightBits 1), the lights were only multiplied by 1, and if there was 1 hardware overbright bits (r_overBrightBits 2), the lights were not multiplied at all.

The related rgbGen identity and rgbGen identityLighting were also related to this, as one was related to blending something with multiplied light or premultiplied light, meaning one of them had to cancel the hardware overbright multiplication, with rgbGen identity and rgbGen identityLighting being equal if there was no hardware overbright bit (nothing to compensate)

But since our engine doesn't implement hardware overbright bit, the only cvar left is r_mapOverBrightBits and rgbGen identity and rgbGen identityLighting are equal (nothing to compensate).

With that being said, one may claim that to be fully compatible with hardware overbrightbits, one may only have to multiply the end result. But the life is more complicated than that. This official Quake 3 documentation said on 6.3.1 RgbGen identityLighting chapter:

6.3.1 RgbGen identityLighting
Colors will be (1.0,1.0,1.0) if running without overbright bits (NT, linux, windowed modes), or (0.5, 0.5, 0.5) if running with overbright. Overbright allows a greater color range at the expense of a loss of precision. Additive and blended stages will get this by default.

According to this documentation, the last platform to support hardware overbright bits was Windows 98 when playing fullscreen. Also, it is said in other places that the feature was not compatible with LCD screens and then required a CRT screen.

It happens that some more recent may have provided the feature more recently, as mentionned here, but for most people the feature is lost since forever.

It means than since 20 years most mappers and developers were testing their maps and graphical effects without that feature, and may have implemented things without testing them with hardware overbright. And since we don't do hardware overbright, we have to face the ultimate limitation: the precision of the user display, which is 24-bit, 8 bit per color. One good example of thing we cannot multiply is the Wolf:ET global fog, the screen itself is clamping the multiplied global fog, and it's permitted to have very strong doubts developer expected that fog to be multiplied, it looks wrong even on non-clamped values.

Copy link
Member Author

@illwieckz illwieckz Feb 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The map prefix in r_mapClampOverBright is mainly cargo cult, but it means that option sets a value that can differ per map.

For r_mapOverBrightBits, this is the default per-map value for when the map doesn't set mapOverBrightBits key in worldspawn, so I did the same for r_mapClampOverBright, the default per-map value for when the map doesn't set mapClampOverBright.

Copy link
Member Author

@illwieckz illwieckz Mar 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can reword the explanation in the way mapOverBrightBits is a BSP worldspawn entity key, and the r_mapOverBrightBits cvar carries the engine default value for it when the BSP doesn't set it, hence the same name.

In the same way new r_mapClampOverBright engine cvar carries the default value for the new mapClampOverBright BSP worldspawn key. I can name those cvar and key as r_clampOverBright and clampOverBright if people prefer. It's fine.

But mapOverBrightBits and related cvar should be kept that way for legacy compatibility, even if the extra map prefix for a map key is superfluous.

Copy link
Member Author

@illwieckz illwieckz Mar 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can name those cvar and key as r_clampOverBright and clampOverBright if people prefer. It's fine.

At least for the engine cvar, the map prefix reminds this value can be overriden by the map itself, which is not bad and may be better. If the map sets the value, it will help people understand why tweaking the engine cvar does nothing.

@illwieckz
Copy link
Member Author

Same question as #1035 (review) - among lightmaps/light grids/vertex lighting which ones is the new algorithm expected to apply to?

All of them. There should be overbright anytime there is a precomputed light.

With that reverse implementation there is overbright cancelling anytime there is no precomputed light, in a way multiplying the final result reverts non-lit surface to their original colors, and multiplies lit surfaces to their overbright colors.

That just makes me think we may have to divide the dynamic lights, as those are expected to not be multiplied. The overbright light factor is for precomputed lights only.

@illwieckz
Copy link
Member Author

That just makes me think we may have to divide the dynamic lights, as those are expected to not be multiplied. The overbright light factor is for precomputed lights only.

Dynamic lights are now divided to cancel overBright on them.

@illwieckz
Copy link
Member Author

illwieckz commented Mar 2, 2024

So, what people can do (just to exclude bugs from other buggy features):

  • check that r_dynamicLight is 2
  • check that cg_shadows is either 0 or 1, not more

Then:

  • set r_mapClampOverBright to off

And play public games this way, and report which map break. This way we can decide to disable it by default or not.

I expect maps from Tremulous being all fine, but some maps built for Unvanquished having bugs. The thing is that mappers have tested against their maps with a broken lighting and then, mappers were not aware they introduced some bugs.

Example of maps I spotted:

map bug proposed fix
fortification outdoor surfaces are too bright set mapClampOverBright 1 in BSP worldspawn

The two maps ported from Urban Terror are also buggy, but that's expected: we set mapOverBrightBits 2 on them by default but the original game expected 0. It happens that mapOverBrightBits 2 is not bad with the terminus map when overbright is clamped, so we may do that instead.

map bug proposed fix
area3 outdoor surfaces are too bright set mapOverBrightBits 0 in BSP worldspawn
terminus many surfaces are too bright set mapClampOverBright 1 in BSP worldspawn

@illwieckz
Copy link
Member Author

illwieckz commented Mar 2, 2024

Actually freeway having “too bright windows“ may be due to something wrong: the glow maps are multiplied but we may not want to do this. Problem is, with collapsed stages, we know what is a glow map, but with legacy stages, especially if the engine fails to collapse them, we don't know what is a glow map.

I thought today of an alternate implementation for GLSL not-clamped overBrightBits: the reason why I multiply everything at the end but divide what should not be multiplied to cancel the multiplication is that framebuffers are clamped to 1, so if we multiply a legacy light stage, it will be clamped to 1 before blending the texture over it, so it's as bad as if we clamp the lightmap at load time.

But, what I thought today is that since the engine max overBrightBits is 3, we may divide all stages by 2^3, then multiply the final result by 2^3, then we would only multiply the lights, only the lights. Because the stage would always be divided before blending, and divided with a value greater or equal to the overBright multiplier, it will never be clamped. The good thing is that we always know what is a light.

@illwieckz illwieckz force-pushed the illwieckz/complete-overbright branch from 183d0ce to e76f648 Compare March 4, 2024 11:52
@illwieckz
Copy link
Member Author

illwieckz commented Mar 4, 2024

I cancelled overBright on collapsed glow maps.

It looks like I already cancelled on separate stages one, in fact they are simply detected as other blended surfaces that should not be overBright, without needing to know they are glow stages.

The remaining problem with freeway map seems to be that bloom is multiplied. There is no issue in freeway map anymore.

@illwieckz
Copy link
Member Author

Bloom overbright is now properly cancelled.

@illwieckz
Copy link
Member Author

Here is a combination of this branch and the ATCSHD remaster @RedSkyByte is working on:

unvanquished_2024-03-04_142403_000

unvanquished_2024-03-04_142436_000

unvanquished_2024-03-04_142444_000

unvanquished_2024-03-04_142827_000

@illwieckz illwieckz force-pushed the illwieckz/complete-overbright branch from 9d7c7dd to 5014f4e Compare March 4, 2024 15:31
@DolceTriade
Copy link
Contributor

Been running this for a bit, and I haven't noticed any regression. The only oddness I have seen is in the map Archao, but that oddness was present on master too (flickering textures at the spectator spawn point).

@illwieckz
Copy link
Member Author

illwieckz commented Mar 9, 2024

What do you think about renaming mapClampOverBright to legacyMapClampOverBright? The clamping is only a backward compatibility purposed for being compatible with bugs of legacy maps.

If you don't mind, I think I will merge this soon if no one object. Not having this is blocking the work on sRGB/linear colorspace:

The clamping will be automatically disabled on maps using the sRGB/linear colorspace in all cases, meaning all newly built maps will use that code.

After this is merged, there can be other improvements done:

  • Disable the legacy map overbright clamping by default, I tested many maps, and it's fine.
    In case we stumble upon a map requiring those bugs, we can repackage it with the key to enable the clamping.
  • I may evaluate some redesign of the feature in the future with some fresh ideas.
    We don't need to wait for such revamp to merge this, this implementation works.

@slipher
Copy link
Member

slipher commented Mar 10, 2024

I insist that we should use "default" in the cvar names lest users be surprised when the cvar fails to override the value set in the worldspawn. r_defaultMapOverbrightBits and r_default(Map)ClampOverbright. (I don't get why the word "map" should be included in the worldspawn key name since there is not any other kind of "clamp overbright" that it needs to be distinguished from.)

@illwieckz
Copy link
Member Author

r_defaultMapOverbrightBits

We better not want to rename r_mapOverBrightBits. This one was kind of public API for games to configure the engine.

@@ -27,6 +27,7 @@ uniform sampler2D u_MaterialMap;
uniform sampler2D u_GlowMap;

uniform float u_AlphaThreshold;
uniform float u_LightFactor;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could optimize it a bit by inverting (x -> 1/x) this uniform so that you can use multiplication instead of division. Also by setting it to 0 instead of 1 when there is no need for multiplications so then the operations can be skipped.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. I now do the division once at map load time instead, and replaced all the GLSL divisions by a multiplication.

It is assumed most GPUs do not know how to do jumps and then never skip a computation in a test but just discard the result if the test is false, so something *= a with a = 1 is better than if ( a != 0 ) { something *= a; } with a = 0 for when there is nothing to multiply, as we avoid the test instruction in all cases and we cannot avoid the multiplication in all cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is assumed most GPUs do not know how to do jumps and then never skip a computation in a test but just discard the result if the test is false

My concept is a bit different. I think they can jump, but all threads must execute the same code. With if (x) { foo(); } else { bar(); } if x is true at least once and false at least once, all threads have to execute both branches. But if x is false in all threads it can jump directly to bar().

I agree that x *= 1; may very well be faster than if ( false ) { x *= 1; }. But my contention is that if ( false ) { x *= 1; } is likely faster than if ( true ) { x *= 1; }. (Here by false, true and 1 I mean expressions derived from uniforms that always have the same value, rather than compile-time constants.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, maybe, but, that complexifies the code for the reader for a benefit that may be very minor. Maybe @VReaperV has an opinion on it?

Copy link
Contributor

@VReaperV VReaperV Mar 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here by false, true and 1 I mean expressions derived from uniforms that always have the same value

If it's based only on uniforms and constant expressions (which it is unless I'm missing something), then it will be statically uniform so there wouldn't be any divergence and the unused code path will likely be eliminated.

Also, keep in mind that a compiler might optimise something like if( condition > 0.0f ) { value *= condition } into value *= condition > 0.0f ? condition : 1.0f anyway.

At the end of the day though, trying to optimise a single vec3 multiplication that is done once per fragment is a waste of time. (If you really want to know which one will be faster you'd need to profile both and it would be hw/driver specific and you likely won't see any significant difference, and it would just be a giant waste of time)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the explanations! ❤️

@slipher
Copy link
Member

slipher commented Mar 10, 2024

Have you tested with different values of r_lightMode? I tried fullbright mode with clamping off and it looks really bad.

unvanquished-1

@illwieckz illwieckz force-pushed the illwieckz/complete-overbright branch from 5014f4e to 10fa968 Compare March 11, 2024 00:18
@illwieckz
Copy link
Member Author

Have you tested with different values of r_lightMode? I tried fullbright mode with clamping off and it looks really bad.

Ah right, the fix for it was lost in my multiple rewrites. This is now fixed.

@illwieckz illwieckz force-pushed the illwieckz/complete-overbright branch from 10fa968 to 332338b Compare March 11, 2024 00:37
@illwieckz
Copy link
Member Author

illwieckz commented Mar 11, 2024

For r_mapClampOverBright, I think we may just name it r_forceLegacyMapClampOverBright and disable the clamping by default. I can also name the map variable legacyMapClampOverBright, as it is not expected to be used by non-legacy maps and will be ignored if used in non-legacy map.

The map variable would tell the renderer to use the legacy code path as a workaround for maps that has may look better with the old buggy code because they were tested with the old buggy code, and the cvar would be usable by the user to test a map with the old buggy code to test the workaround before setting the variable in the map.

Actually, I now don't see good reasons to not disable clamping by default in all maps. Once people start to play with not-clamped overbright, any legacy map with clamped overbright looks broken, much more than the quirks that may be revealed by not clamping the lights.

We were not seeing the bugs because we never seen a map properly rendered before, but once we see a map being properly renderer, the eyes then see the bugs on almost every surface when clamping is enabled. We can't unseen those light bugs once seen, and they are everywhere.

Configuring a map to force the legacy code would require such map to be strongly broken, and for now the only maps that are that broken are maps from another game that used a game using different renderer default values.

Also a r_forceLegacyMapClampOverBright cvar would be useful when debugging regressions from Tremulous. For example it looks like our fog is wrong, so if someone wants to debug the fog and have a fair comparison with Tremulous, that one may want to work with a similarly buggy renderer to not be fooled by our fix. For example if a fog color was clamped, we should test the new implementation first with clamping on to test for visual parity, because testing for visual parity with clamping off on our end may lead us to re-implement clamping in fog code itself.

@illwieckz illwieckz force-pushed the illwieckz/complete-overbright branch 2 times, most recently from 54c12a6 to 649b94b Compare March 11, 2024 14:28
@illwieckz
Copy link
Member Author

So, the cvar to enable old clamped code base is r_forceLegacyMapOverBrightClamping, it overrides any map option. It is only meant to be a diagnostic tool to have a fair comparison when comparing renders with engines having old clamped code (be backward compatible on bugs when comparing engines).

The map related option is forceLegacyMapOverBrightClamping and can be used as a per-map option to tell the renderer to use that old code path as a workaround. I'm not sure this variable will be kept forever, we may remove it in the future if it turns out it is never needed.

The complete overbright is now enabled by default for all maps. One can't unseen the buggy clamping after having played on maps without buggy clamping.

The r_mapOverBrightBits cvar name is kept this way for backward compatibility with legacy game configs when loading third-party game assets when evaluating the engine for running third-party games or loading third-party game data.

I now consider this PR totally ready (after I squash some fixup commits).

Copy link
Member

@slipher slipher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@illwieckz illwieckz force-pushed the illwieckz/complete-overbright branch from 4cf1855 to 42bc94f Compare March 11, 2024 23:39
@sweet235
Copy link
Contributor

This branch is addictive. Since I discovered it a few days ago, I have spent hours just looking at maps.

@illwieckz illwieckz merged commit d2b9a68 into master Mar 12, 2024
@illwieckz illwieckz deleted the illwieckz/complete-overbright branch March 12, 2024 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants