-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
crates/egui-wgpu/src/egui.wgsl
Outdated
@@ -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); |
There was a problem hiding this comment.
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
crates/egui-wgpu/src/egui.wgsl
Outdated
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); |
There was a problem hiding this comment.
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? 🤔
There was a problem hiding this comment.
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:
egui/crates/egui-wgpu/src/renderer.rs
Line 319 in ff7a383
"fs_main_linear_framebuffer" |
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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
crates/egui-wgpu/src/egui.wgsl
Outdated
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); |
There was a problem hiding this comment.
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:
egui/crates/egui-wgpu/src/renderer.rs
Line 319 in ff7a383
"fs_main_linear_framebuffer" |
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.
I think a runtime feature flag is likely superior, as it is simpler, and lets the same user use Checking a bool in the shader should be fine |
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. |
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 |
I got a first draft of configurable dithering to work but I'm running out of time for today. I changed to state of the PR back to draft for now. |
I updated and cleaned up the code and will mark the PR as 'Ready for review' again. Testing performedI tested the demo app using both wgpu and glow, with dithering enabled and disabled, on two different systems (Linux and Mac), as well as on the web using Chrome and Firefox. The dithering works as expected in all cases. While the results from wgpu and glow are not identical, they are very close. There were some minute differences with dithering disabled as well. So I guess producing identical output between different backends isn't a requirement (and likely not a reasonable goal across systems anyways). In addition to the demo app, I tested the proposed changes with one of my own applications. Potential Optimizationsegui_glow::Painter::new, RenderState::create and Renderer::new, now all take quite a few arguments. If desired I can change them to take a struct (with a default) instead. The implementation of the condition is different for wgpu and glow. The glow implementation makes use of the glsl preprocessor using a define and if, similar to the existing code for SRGB_TEXTURES. This should result in zero shading cost when dithering is disabled. The wgpu implementation uses a uniform instead. At least as far as I know wgsl doesn't have a preprocessor and I wasn't to keen to introduce some code to process the shader source. Instead it's using a uniform and a condition in the shader as suggested by @emilk. I'm not sure about the optimizations applied in wgpu, let alone it's various backends but this is likely very cheap (coherent branching) but not zero cost in either case. Lastly it would be possible to make the amount if dithering configurable, for instance for using egui in an application with a bit depth lower than 8 bit. I don't think this is a very common use case though. |
there's no preprocessor, but this is a great usecase for a pipeline-overridable constant! :) There's an example in the wgpu 0.20.0 release notes. |
That sounds like a great solution. I'll need to do a bit more digging to figure out what it does in practice. If it's ok, I'll do a follow up on this PR once it is merged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm except docs!
Sorry for the delayed response. Life happened. I rebased the branch and applied the suggestions. I'm not sure if the more detailed descriptions should maybe go into the lower level crates (egui_glow, egui-wgpu) rather than eframe since users of those crates are more likely to care about the specifics. Another consideration is api compatibility. I guess we don't want to promise that dithering true will always dither to 8 bit. So maybe Both are minor concerns though so I'd be happy to get the change merged as is. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm!
Still torn about having it opt-out instead of opt-in, but I wrote earlier to leave it be, so let's just do that (:
now that wgpu 0.20 is in we could use pipeline constants, but that constant buffer value won't hurt anyone 🤷
Re api compatibility: I think for the renderers to handle hdr output correctly some other stuff needs to happen so not too concerned, whoever looks into that can also do something about that. Worst case they have to break the variable name to make it more obvious |
This PR introduces dithering in the egui_glow and egui_wgpu backends to reduce banding artifacts. It's based on the approach mentioned in emilk#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](https://github.com/emilk/egui/assets/293536/75782b83-9023-4cb2-99f7-a24e15fdefcc) Subtle dithering as commited. ![Screenshot from 2024-05-14 19-13-40](https://github.com/emilk/egui/assets/293536/eb904698-a6ec-494a-952b-447e9a49bfda) Closes emilk#4493
This PR introduces dithering in the egui_glow and egui_wgpu backends to reduce banding artifacts. It's based on the approach mentioned in emilk#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](https://github.com/emilk/egui/assets/293536/75782b83-9023-4cb2-99f7-a24e15fdefcc) Subtle dithering as commited. ![Screenshot from 2024-05-14 19-13-40](https://github.com/emilk/egui/assets/293536/eb904698-a6ec-494a-952b-447e9a49bfda) Closes emilk#4493
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:
Subtle dithering as commited.
Closes #4493