Skip to content

Always perform color correction and debanding on nonlinear sRGB values. #107782

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

allenwp
Copy link
Contributor

@allenwp allenwp commented Jun 20, 2025

Fixes #107730

Screenshots at 4x magnification (view in a dark environment/surround to see the detail):
image

Notes to reviewers:

  • The intent is for debanding to only and always be applied after any encoding transformation (linear/nonlinear) has been applied and as the final step before quantization to integer values. Please let me know if you can think of or discover a scenario where this may not be the case with this PR!
  • Please let me know if you can think of any other ways to improve performance
  • My first time working with a lot of this code, so I might be making bad assumptions
  • I haven't tested with XR and I'm not sure that debanding is always being applied correctly as the last step before quantization to integer values

My test project
color-correction-debanding-fixed.zip

@allenwp
Copy link
Contributor Author

allenwp commented Jun 20, 2025

@beicause You mentioned an idea about how to better address the color correction bug. Thanks for sharing this! Please feel free to share more details here or open a new PR that builds off this one with me as a co-contributor if you'd like.

@allenwp allenwp force-pushed the vulkan-nonlinear-color-correction-dithering branch from 4179398 to 5cd619b Compare June 20, 2025 20:22
@allenwp allenwp marked this pull request as ready for review June 20, 2025 20:44
@allenwp allenwp requested a review from a team as a code owner June 20, 2025 20:44
@allenwp allenwp marked this pull request as draft June 20, 2025 20:48
@beicause
Copy link
Contributor

Optimizes out one srgb->linear conversion by sampling sRGB texture:

diff --git a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp
index 2a037d241e..4d30f625ef 100644
--- a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp
+++ b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp
@@ -665,6 +665,7 @@ void RendererSceneRenderRD::_render_buffers_post_process_and_tonemap(const Rende
 		tonemap.use_color_correction = false;
 		tonemap.use_1d_color_correction = false;
 		tonemap.color_correction_texture = texture_storage->texture_rd_get_default(RendererRD::TextureStorage::DEFAULT_RD_TEXTURE_3D_WHITE);
+		tonemap.convert_to_srgb = !texture_storage->render_target_is_using_hdr(render_target);
 
 		if (can_use_effects && p_render_data->environment.is_valid()) {
 			tonemap.use_bcs = environment_get_adjustments_enabled(p_render_data->environment);
@@ -674,15 +675,13 @@ void RendererSceneRenderRD::_render_buffers_post_process_and_tonemap(const Rende
 			if (environment_get_adjustments_enabled(p_render_data->environment) && environment_get_color_correction(p_render_data->environment).is_valid()) {
 				tonemap.use_color_correction = true;
 				tonemap.use_1d_color_correction = environment_get_use_1d_color_correction(p_render_data->environment);
-				tonemap.color_correction_texture = texture_storage->texture_get_rd_texture(environment_get_color_correction(p_render_data->environment));
+				tonemap.color_correction_texture = texture_storage->texture_get_rd_texture(environment_get_color_correction(p_render_data->environment), !tonemap.convert_to_srgb);
 			}
 		}
 
 		tonemap.luminance_multiplier = _render_buffers_get_luminance_multiplier();
 		tonemap.view_count = rb->get_view_count();
 
-		tonemap.convert_to_srgb = !texture_storage->render_target_is_using_hdr(render_target);
-
 		RID dest_fb;
 		if (spatial_upscaler != nullptr || use_smaa) {
 			// If we use a spatial upscaler to upscale or SMAA to antialias we need to write our result into an intermediate buffer.
diff --git a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl
index c4ae4cb8b0..84c4a0fca8 100644
--- a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl
+++ b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl
@@ -911,9 +911,6 @@ void main() {
 			color.rgb = linear_to_srgb(color.rgb);
 		}
 		color.rgb = apply_color_correction(color.rgb);
-		if (!convert_to_srgb) {
-			color.rgb = srgb_to_linear(color.rgb);
-		}
 	}
 
 	if (bool(params.flags & FLAG_USE_DEBANDING) && convert_to_srgb) {

@allenwp allenwp force-pushed the vulkan-nonlinear-color-correction-dithering branch from 5cd619b to 6bd11e0 Compare June 20, 2025 22:57
@allenwp
Copy link
Contributor Author

allenwp commented Jun 20, 2025

Optimizes out one srgb->linear conversion by sampling sRGB texture:
...

This seems to work well -- very clever! Thanks @beicause!

@AThousandShips AThousandShips added this to the 4.5 milestone Jun 22, 2025
@allenwp allenwp force-pushed the vulkan-nonlinear-color-correction-dithering branch 2 times, most recently from 9985913 to 160c349 Compare June 23, 2025 22:01
@allenwp allenwp marked this pull request as ready for review June 23, 2025 22:51
@allenwp allenwp requested a review from a team as a code owner June 23, 2025 22:51
@allenwp
Copy link
Contributor Author

allenwp commented Jun 23, 2025

I've finished work on this, so it's now ready for review. (Sorry about the mistaken "ready for review" I made earlier!)

@beicause
Copy link
Contributor

Debanding is a per-viewport setting. I don't know why it should change to a RendererCompositor setting here.

I think debanding has nothing to do with color space. It can be performed in linear space as long as it's done before writing the results to a lower-precision buffer. See also #87350.

@allenwp
Copy link
Contributor Author

allenwp commented Jun 24, 2025

Hmm, I hadn’t thought about how debanding is exposed to scripting as a property of Viewport. The current state of this PR makes that property do nothing when HDR 2D is enabled and also provides no method for changing the setting at runtime.

I’m not sure exactly what to do about this, so let me know if you have thoughts on how to handle this scenario.

Debanding is a per-viewport setting. I don't know why it should change to a RendererCompositor setting here.

I think debanding has nothing to do with color space. It can be performed in linear space as long as it's done before writing the results to a lower-precision buffer. See also #87350.

I will take a look into #87350 and post on that PR if it exhibits the same issues as seen in #107730. The specific dither function that is used for debanding is designed to be applied to nonlinear encoded float values of the final encoding directly before quantization to an 8 bit buffer. Details about this and the consequences of applying it to an encoding that is different than the one used immediately before quantization (i.e. linear encoding) is described in #107730.

The specific dithering function that Godot uses cannot be used on linear values. If you want to use a debanding technique that works on linear encoded values, you need to use a different function such as the one presented by Timothy Lottes in his GDC talk (See part 3: transfer functions and quantization).

@beicause
Copy link
Contributor

#87350 uses the same debanding algorithm with the viewport debanding. But sky shader uses a different one:

#ifdef USE_DEBANDING
// https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare
vec3 interleaved_gradient_noise(vec2 pos) {
const vec3 magic = vec3(0.06711056f, 0.00583715f, 52.9829189f);
float res = fract(magic.z * fract(dot(pos, magic.xy))) * 2.0 - 1.0;
return vec3(res, -res, res) / 255.0;
}
#endif

I'm not sure if these algorithms works in linear space (dividing by 255.0 seems a bit magical), but it's worth taking a look.

@allenwp
Copy link
Contributor Author

allenwp commented Jun 24, 2025

#87350 uses the same debanding algorithm with the viewport debanding. But sky shader uses a different one:

#ifdef USE_DEBANDING
// https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare
vec3 interleaved_gradient_noise(vec2 pos) {
const vec3 magic = vec3(0.06711056f, 0.00583715f, 52.9829189f);
float res = fract(magic.z * fract(dot(pos, magic.xy))) * 2.0 - 1.0;
return vec3(res, -res, res) / 255.0;
}
#endif

I'm not sure if these algorithms works in linear space (dividing by 255.0 seems a bit magical), but it's worth taking a look.

There's actually a PDF linked from the Godot docs that talks about what floating point encoding your dithering should be applied in with these sorts of dithering effects: https://loopit.dk/banding_in_games.pdf (see page 35 onward where it states "convert first, dither afterwards… dither in whatever space the color will be quantized in."). I think it's easier to just read the dithering code to understand that it needs to be applied to nonlinear encoded values, but if anyone wants some "literature" on the subject, there it is.

I will eventually look into the sky shader's debanding, but I noticed that the dithering is scaled by a luminance_multiplier, which may help reduce the problems associated with applying the dither to a linear encoding (maybe???). So this is probably best addressed in a different issue with a different PR that explores it in more depth.

I also now realize there is quite a bit in the docs that needs updating with this PR as well. I'll continue to update it to address all these cases.

@allenwp allenwp force-pushed the vulkan-nonlinear-color-correction-dithering branch from 160c349 to b193647 Compare July 7, 2025 20:36
@allenwp allenwp requested review from a team as code owners July 7, 2025 20:36
@allenwp allenwp force-pushed the vulkan-nonlinear-color-correction-dithering branch 2 times, most recently from 0825eed to f1d6012 Compare July 8, 2025 15:29
@allenwp
Copy link
Contributor Author

allenwp commented Jul 8, 2025

Debanding is a per-viewport setting. I don't know why it should change to a RendererCompositor setting here.

I've fixed this so that debanding is still a property of the viewport and also always applied as the last step before 8-bit quantization.

Just need to figure out how to add this to XR now...

Edit: Hmm. It seems it would be better to store use_debanding in the RenderTarget alongside of use_hdr...

@allenwp allenwp force-pushed the vulkan-nonlinear-color-correction-dithering branch from f1d6012 to a159151 Compare July 8, 2025 18:11
@allenwp
Copy link
Contributor Author

allenwp commented Jul 8, 2025

OK, I've moved use_debanding to be a part of RenderTarget, just like use_hdr... so... I think this means the debanding will be correctly applied in XR scenarios as well(?) Please let me know if you can find any places where debanding may not be correctly applied or where other transformations may happen between debanding and quantization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Color correction and debanding function incorrectly when HDR 2D is enabled
3 participants