Skip to content

Commit

Permalink
Introduce dithering to reduce banding (emilk#4497)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jwagner authored Jul 8, 2024
1 parent fcd02bd commit b283b8a
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 18 deletions.
22 changes: 22 additions & 0 deletions crates/eframe/src/epi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,15 @@ pub struct NativeOptions {
/// The folder where `eframe` will store the app state. If not set, eframe will get the paths
/// from [directories].
pub persistence_path: Option<std::path::PathBuf>,

/// Controls whether to apply dithering to minimize banding artifacts.
///
/// Dithering assumes an sRGB output and thus will apply noise to any input value that lies between
/// two 8bit values after applying the sRGB OETF function, i.e. if it's not a whole 8bit value in "gamma space".
/// This means that only inputs from texture interpolation and vertex colors should be affected in practice.
///
/// Defaults to true.
pub dithering: bool,
}

#[cfg(not(target_arch = "wasm32"))]
Expand Down Expand Up @@ -429,6 +438,8 @@ impl Default for NativeOptions {
persist_window: true,

persistence_path: None,

dithering: true,
}
}
}
Expand Down Expand Up @@ -466,6 +477,15 @@ pub struct WebOptions {
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,

/// Controls whether to apply dithering to minimize banding artifacts.
///
/// Dithering assumes an sRGB output and thus will apply noise to any input value that lies between
/// two 8bit values after applying the sRGB OETF function, i.e. if it's not a whole 8bit value in "gamma space".
/// This means that only inputs from texture interpolation and vertex colors should be affected in practice.
///
/// Defaults to true.
pub dithering: bool,
}

#[cfg(target_arch = "wasm32")]
Expand All @@ -481,6 +501,8 @@ impl Default for WebOptions {

#[cfg(feature = "wgpu")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),

dithering: true,
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion crates/eframe/src/native/glow_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,12 @@ impl GlowWinitApp {
}))
};

let painter = egui_glow::Painter::new(gl, "", native_options.shader_version)?;
let painter = egui_glow::Painter::new(
gl,
"",
native_options.shader_version,
native_options.dithering,
)?;

Ok((glutin_window_context, painter))
}
Expand Down
1 change: 1 addition & 0 deletions crates/eframe/src/native/wgpu_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ impl WgpuWinitApp {
self.native_options.stencil_buffer,
),
self.native_options.viewport.transparent.unwrap_or(false),
self.native_options.dithering,
);

let window = Arc::new(window);
Expand Down
2 changes: 1 addition & 1 deletion crates/eframe/src/web/web_painter_glow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl WebPainterGlow {
#[allow(clippy::arc_with_non_send_sync)]
let gl = std::sync::Arc::new(gl);

let painter = egui_glow::Painter::new(gl, shader_prefix, None)
let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering)
.map_err(|err| format!("Error starting glow painter: {err}"))?;

Ok(Self { canvas, painter })
Expand Down
14 changes: 10 additions & 4 deletions crates/eframe/src/web/web_painter_wgpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,16 @@ impl WebPainterWgpu {

let depth_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);

let render_state =
RenderState::create(&options.wgpu_options, &instance, &surface, depth_format, 1)
.await
.map_err(|err| err.to_string())?;
let render_state = RenderState::create(
&options.wgpu_options,
&instance,
&surface,
depth_format,
1,
options.dithering,
)
.await
.map_err(|err| err.to_string())?;

let surface_configuration = wgpu::SurfaceConfiguration {
format: render_state.target_format,
Expand Down
46 changes: 42 additions & 4 deletions crates/egui-wgpu/src/egui.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,35 @@ struct VertexOutput {

struct Locals {
screen_size: vec2<f32>,
dithering: u32, // 1 if dithering is enabled, 0 otherwise
// Uniform buffers need to be at least 16 bytes in WebGL.
// See https://github.com/gfx-rs/wgpu/issues/2072
_padding: vec2<u32>,
_padding: u32,
};
@group(0) @binding(0) var<uniform> r_locals: Locals;


// -----------------------------------------------
// Adapted from
// https://www.shadertoy.com/view/llVGzG
// Originally presented in:
// Jimenez 2014, "Next Generation Post-Processing in Call of Duty"
//
// A good overview can be found in
// https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence/
// via https://github.com/rerun-io/rerun/
fn interleaved_gradient_noise(n: vec2<f32>) -> f32 {
let f = 0.06711056 * n.x + 0.00583715 * n.y;
return fract(52.9829189 * fract(f));
}

fn dither_interleaved(rgb: vec3<f32>, levels: f32, frag_coord: vec4<f32>) -> vec3<f32> {
var noise = interleaved_gradient_noise(frag_coord.xy);
// scale down the noise slightly to ensure flat colors aren't getting dithered
noise = (noise - 0.5) * 0.95;
return rgb + noise / (levels - 1.0);
}

// 0-1 linear from 0-1 sRGB gamma
fn linear_from_gamma_rgb(srgb: vec3<f32>) -> vec3<f32> {
let cutoff = srgb < vec3<f32>(0.04045);
Expand Down Expand Up @@ -77,15 +100,30 @@ fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
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 vec4<f32>(linear_from_gamma_rgb(out_color_gamma.rgb), out_color_gamma.a);
var out_color_gamma = in.color * tex_gamma;
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.
// Note that dithering is performed on the gamma encoded values,
// because this function is used together with a srgb converting target.
if r_locals.dithering == 1 {
let out_color_gamma_rgb = dither_interleaved(out_color_gamma.rgb, 256.0, in.position);
out_color_gamma = vec4<f32>(out_color_gamma_rgb, out_color_gamma.a);
}
let out_color_linear = linear_from_gamma_rgb(out_color_gamma.rgb);
return vec4<f32>(out_color_linear, out_color_gamma.a);
}

@fragment
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
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;
var out_color_gamma = in.color * tex_gamma;
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.
if r_locals.dithering == 1 {
let out_color_gamma_rgb = dither_interleaved(out_color_gamma.rgb, 256.0, in.position);
out_color_gamma = vec4<f32>(out_color_gamma_rgb, out_color_gamma.a);
}
return out_color_gamma;
}
9 changes: 8 additions & 1 deletion crates/egui-wgpu/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ impl RenderState {
surface: &wgpu::Surface<'static>,
depth_format: Option<wgpu::TextureFormat>,
msaa_samples: u32,
dithering: bool,
) -> Result<Self, WgpuError> {
crate::profile_scope!("RenderState::create"); // async yield give bad names using `profile_function`

Expand Down Expand Up @@ -164,7 +165,13 @@ impl RenderState {
.await?
};

let renderer = Renderer::new(&device, target_format, depth_format, msaa_samples);
let renderer = Renderer::new(
&device,
target_format,
depth_format,
msaa_samples,
dithering,
);

Ok(Self {
adapter: Arc::new(adapter),
Expand Down
15 changes: 12 additions & 3 deletions crates/egui-wgpu/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,16 @@ impl ScreenDescriptor {
#[repr(C)]
struct UniformBuffer {
screen_size_in_points: [f32; 2],
dithering: u32,
// Uniform buffers need to be at least 16 bytes in WebGL.
// See https://github.com/gfx-rs/wgpu/issues/2072
_padding: [u32; 2],
_padding: u32,
}

impl PartialEq for UniformBuffer {
fn eq(&self, other: &Self) -> bool {
self.screen_size_in_points == other.screen_size_in_points
&& self.dithering == other.dithering
}
}

Expand Down Expand Up @@ -169,6 +171,8 @@ pub struct Renderer {
next_user_texture_id: u64,
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,

dithering: bool,

/// Storage for resources shared with all invocations of [`CallbackTrait`]'s methods.
///
/// See also [`CallbackTrait`].
Expand All @@ -185,6 +189,7 @@ impl Renderer {
output_color_format: wgpu::TextureFormat,
output_depth_format: Option<wgpu::TextureFormat>,
msaa_samples: u32,
dithering: bool,
) -> Self {
crate::profile_function!();

Expand All @@ -201,6 +206,7 @@ impl Renderer {
label: Some("egui_uniform_buffer"),
contents: bytemuck::cast_slice(&[UniformBuffer {
screen_size_in_points: [0.0, 0.0],
dithering: u32::from(dithering),
_padding: Default::default(),
}]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
Expand All @@ -212,7 +218,7 @@ impl Renderer {
label: Some("egui_uniform_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(std::mem::size_of::<UniformBuffer>() as _),
Expand Down Expand Up @@ -364,13 +370,15 @@ impl Renderer {
// Buffers on wgpu are zero initialized, so this is indeed its current state!
previous_uniform_buffer_content: UniformBuffer {
screen_size_in_points: [0.0, 0.0],
_padding: [0, 0],
dithering: 0,
_padding: 0,
},
uniform_bind_group,
texture_bind_group_layout,
textures: HashMap::default(),
next_user_texture_id: 0,
samplers: HashMap::default(),
dithering,
callback_resources: CallbackResources::default(),
}
}
Expand Down Expand Up @@ -781,6 +789,7 @@ impl Renderer {

let uniform_buffer_content = UniformBuffer {
screen_size_in_points,
dithering: u32::from(self.dithering),
_padding: Default::default(),
};
if uniform_buffer_content != self.previous_uniform_buffer_content {
Expand Down
4 changes: 4 additions & 0 deletions crates/egui-wgpu/src/winit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub struct Painter {
configuration: WgpuConfiguration,
msaa_samples: u32,
support_transparent_backbuffer: bool,
dithering: bool,
depth_format: Option<wgpu::TextureFormat>,
screen_capture_state: Option<CaptureState>,

Expand Down Expand Up @@ -113,6 +114,7 @@ impl Painter {
msaa_samples: u32,
depth_format: Option<wgpu::TextureFormat>,
support_transparent_backbuffer: bool,
dithering: bool,
) -> Self {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: configuration.supported_backends,
Expand All @@ -123,6 +125,7 @@ impl Painter {
configuration,
msaa_samples,
support_transparent_backbuffer,
dithering,
depth_format,
screen_capture_state: None,

Expand Down Expand Up @@ -264,6 +267,7 @@ impl Painter {
&surface,
self.depth_format,
self.msaa_samples,
self.dithering,
)
.await?;
self.render_state.get_or_insert(render_state)
Expand Down
2 changes: 1 addition & 1 deletion crates/egui_glow/examples/pure_glow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ fn main() {
let (gl_window, gl) = create_display(&event_loop);
let gl = std::sync::Arc::new(gl);

let mut egui_glow = egui_glow::EguiGlow::new(&event_loop, gl.clone(), None, None);
let mut egui_glow = egui_glow::EguiGlow::new(&event_loop, gl.clone(), None, None, true);

let event_loop_proxy = egui::mutex::Mutex::new(event_loop.create_proxy());
egui_glow
Expand Down
4 changes: 3 additions & 1 deletion crates/egui_glow/src/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ impl Painter {
gl: Arc<glow::Context>,
shader_prefix: &str,
shader_version: Option<ShaderVersion>,
dithering: bool,
) -> Result<Self, PainterError> {
crate::profile_function!();
crate::check_for_gl_error_even_in_release!(&gl, "before Painter::new");
Expand Down Expand Up @@ -197,9 +198,10 @@ impl Painter {
&gl,
glow::FRAGMENT_SHADER,
&format!(
"{}\n#define NEW_SHADER_INTERFACE {}\n#define SRGB_TEXTURES {}\n{}\n{}",
"{}\n#define NEW_SHADER_INTERFACE {}\n#define DITHERING {}\n#define SRGB_TEXTURES {}\n{}\n{}",
shader_version_declaration,
shader_version.is_new_shader_interface() as i32,
dithering as i32,
srgb_textures as i32,
shader_prefix,
FRAG_SRC
Expand Down
30 changes: 29 additions & 1 deletion crates/egui_glow/src/shader/fragment.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,27 @@ uniform sampler2D u_sampler;
varying vec2 v_tc;
#endif

// -----------------------------------------------
// Adapted from
// https://www.shadertoy.com/view/llVGzG
// Originally presented in:
// Jimenez 2014, "Next Generation Post-Processing in Call of Duty"
//
// A good overview can be found in
// https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence/
// via https://github.com/rerun-io/rerun/
float interleaved_gradient_noise(vec2 n) {
float f = 0.06711056 * n.x + 0.00583715 * n.y;
return fract(52.9829189 * fract(f));
}

vec3 dither_interleaved(vec3 rgb, float levels) {
float noise = interleaved_gradient_noise(gl_FragCoord.xy);
// scale down the noise slightly to ensure flat colors aren't getting dithered
noise = (noise - 0.5) * 0.95;
return rgb + noise / (levels - 1.0);
}

// 0-1 sRGB gamma from 0-1 linear
vec3 srgb_gamma_from_linear(vec3 rgb) {
bvec3 cutoff = lessThan(rgb, vec3(0.0031308));
Expand All @@ -37,5 +58,12 @@ void main() {
#endif

// We multiply the colors in gamma space, because that's the only way to get text to look right.
gl_FragColor = v_rgba_in_gamma * texture_in_gamma;
vec4 frag_color_gamma = v_rgba_in_gamma * texture_in_gamma;

// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.
#if DITHERING
frag_color_gamma.rgb = dither_interleaved(frag_color_gamma.rgb, 256.);
#endif
gl_FragColor = frag_color_gamma;
}
3 changes: 2 additions & 1 deletion crates/egui_glow/src/winit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ impl EguiGlow {
gl: std::sync::Arc<glow::Context>,
shader_version: Option<ShaderVersion>,
native_pixels_per_point: Option<f32>,
dithering: bool,
) -> Self {
let painter = crate::Painter::new(gl, "", shader_version)
let painter = crate::Painter::new(gl, "", shader_version, dithering)
.map_err(|err| {
log::error!("error occurred in initializing painter:\n{err}");
})
Expand Down

0 comments on commit b283b8a

Please sign in to comment.