Skip to content
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

Introduce dithering to reduce banding #4497

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

Conversation

jwagner
Copy link

@jwagner jwagner commented May 14, 2024

This PR introduces dithering in the egui_glow and egui_wgpu backends to reduce banding artifacts.

It's based on the approach mentioned in #4493 with the small difference that the amount of noise is scaled down slightly to avoid dithering colors that can be represented exactly. This keeps flat surfaces clean.

Exaggerated dithering to show what is happening:
Screenshot from 2024-05-14 19-09-48

Subtle dithering as commited.
Screenshot from 2024-05-14 19-13-40

Closes #4493

Copy link
Contributor

@murl-digital murl-digital left a comment

Choose a reason for hiding this comment

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

LGTM, I tested this with the baseview windowing backend, with is glow based. Taking a screenshot of a gradient and zooming all the way in shows me the dithering's there:

image

@@ -87,5 +114,6 @@ fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
let out_color_gamma = in.color * tex_gamma;
return out_color_gamma;
let out_color_dithered = vec4<f32>(dither_interleaved(out_color_gamma.rgb, 256.0, in.position * r_locals.pixels_per_point), out_color_gamma.a);
Copy link
Owner

Choose a reason for hiding this comment

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

Should have the same comments as the code above, and probably a linebreak

let out_color_linear = linear_from_gamma_rgb(out_color_gamma.rgb);
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.
let out_color_dithered = dither_interleaved(out_color_linear, 256.0, in.position);
Copy link
Owner

Choose a reason for hiding this comment

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

Are you sure we should apply it to the linear color and not the gamma color? 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree, this should be done on the gamma color. It's important to know the context of when this fragment shader is used:

"fs_main_linear_framebuffer"
it is enabled in the case of using an srgb converting target. That means whatever float value we emit here will be upon write converted to an srgb value. I.e. as-if the output of this method was wrapped in another call of gamma_from_linear_rgba.

Therefore, we have to make sure that all the meaningful value changes happen on the gamma color.

Note that it would be an entirely different story if we're outputing anything that isn't ending up on a wgpu::Surface that is targeting a srgb interpreting monitor/window. Meaning that all of this breaks down for HDR.

Copy link
Author

Choose a reason for hiding this comment

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

I assumed that the output buffer was linear in that case dithering the linear color is what is wanted. If it's converted back to srgb before storage, it isn't optimal. I'll need to double check that.

Copy link
Author

Choose a reason for hiding this comment

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

Just saw the comment be @Wumpf bellow, so you are right, this is off.

crates/egui-wgpu/src/renderer.rs Show resolved Hide resolved
@emilk emilk requested a review from Wumpf May 27, 2024 14:28
Copy link
Collaborator

@Wumpf Wumpf left a comment

Choose a reason for hiding this comment

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

great addition, however I think there should be an easy way to disable dithering:

  • this is srgb dithering and doesn't make sense if someone wants to use an HDR target (e.g. WebGPU can also support P3 instead of srgb)
  • there may be corner cases where a user doesn't actually want to dither everything drawn with egui indiscriminately as the added noise may destroy otherwise sharp corners

let out_color_linear = linear_from_gamma_rgb(out_color_gamma.rgb);
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.
let out_color_dithered = dither_interleaved(out_color_linear, 256.0, in.position);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree, this should be done on the gamma color. It's important to know the context of when this fragment shader is used:

"fs_main_linear_framebuffer"
it is enabled in the case of using an srgb converting target. That means whatever float value we emit here will be upon write converted to an srgb value. I.e. as-if the output of this method was wrapped in another call of gamma_from_linear_rgba.

Therefore, we have to make sure that all the meaningful value changes happen on the gamma color.

Note that it would be an entirely different story if we're outputing anything that isn't ending up on a wgpu::Surface that is targeting a srgb interpreting monitor/window. Meaning that all of this breaks down for HDR.

@@ -87,5 +114,6 @@ fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
let out_color_gamma = in.color * tex_gamma;
return out_color_gamma;
let out_color_dithered = vec4<f32>(dither_interleaved(out_color_gamma.rgb, 256.0, in.position * r_locals.pixels_per_point), out_color_gamma.a);
Copy link
Collaborator

@Wumpf Wumpf May 27, 2024

Choose a reason for hiding this comment

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

I don't get what * r_locals.pixels_per_point is about here. in.position is @builtin(position) which makes this raw pixel coordinates ranging from 0 to target size, see https://www.w3.org/TR/WGSL/#built-in-values-position. So scaling it with eguis points->pixel factor doesn't make any sense since we're starting with pixels anyways.

you also didn't apply this to either glsl or the fs_main_linear_framebuffer version. What's the intention here?

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for spotting this @Wumpf. I think I got confused between the input vertex coordinates and the position input to the fragment shader. Should have tested this better. Sorry for the oversight.

@@ -132,9 +132,10 @@ impl ScreenDescriptor {
#[repr(C)]
struct UniformBuffer {
screen_size_in_points: [f32; 2],
pixels_per_point: f32,
Copy link
Collaborator

Choose a reason for hiding this comment

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

as per other comment, I don't see why this is needed

@jwagner
Copy link
Author

jwagner commented May 27, 2024

@Wumpf @emilk for disabling dithering, would a feature on the crate do the job or should this be a runtime option?

@emilk
Copy link
Owner

emilk commented May 27, 2024

I think a runtime feature flag is likely superior, as it is simpler, and lets the same user use egui_wgpu with different options depending on their render target, which may depend on the adapter capabilities.

Checking a bool in the shader should be fine

@jwagner
Copy link
Author

jwagner commented May 27, 2024

I'll do a revision of the PR adding an option and fixing the coordinates in wgpu. I'm not certain where to add it yet. Adding it to native / web options in eframe, and then an implementation specific option in the specific backend (WgpuConfiguration for wgpu , EguiGlow::new for glow) seems about right.

If you'd like it to be added another way, just let me know.

@murl-digital
Copy link
Contributor

Adding it to native / web options in eframe, and then an implementation specific option in the specific backend (WgpuConfiguration for wgpu , EguiGlow::new for glow) seems about right.

That works for me, and that's probably the best way to do it, unless you want to mess with feature flags to reduce a tiny amount of runtime overhead

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

Successfully merging this pull request may close these issues.

Dithering in Fragment Shader
4 participants