mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 21:30:03 -04:00
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
130 lines
4.6 KiB
WebGPU Shading Language
130 lines
4.6 KiB
WebGPU Shading Language
// Vertex shader bindings
|
|
|
|
struct VertexOutput {
|
|
@location(0) tex_coord: vec2<f32>,
|
|
@location(1) color: vec4<f32>, // gamma 0-1
|
|
@builtin(position) position: vec4<f32>,
|
|
};
|
|
|
|
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: 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);
|
|
let lower = srgb / vec3<f32>(12.92);
|
|
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
|
|
return select(higher, lower, cutoff);
|
|
}
|
|
|
|
// 0-1 sRGB gamma from 0-1 linear
|
|
fn gamma_from_linear_rgb(rgb: vec3<f32>) -> vec3<f32> {
|
|
let cutoff = rgb < vec3<f32>(0.0031308);
|
|
let lower = rgb * vec3<f32>(12.92);
|
|
let higher = vec3<f32>(1.055) * pow(rgb, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
|
|
return select(higher, lower, cutoff);
|
|
}
|
|
|
|
// 0-1 sRGBA gamma from 0-1 linear
|
|
fn gamma_from_linear_rgba(linear_rgba: vec4<f32>) -> vec4<f32> {
|
|
return vec4<f32>(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a);
|
|
}
|
|
|
|
// [u8; 4] SRGB as u32 -> [r, g, b, a] in 0.-1
|
|
fn unpack_color(color: u32) -> vec4<f32> {
|
|
return vec4<f32>(
|
|
f32(color & 255u),
|
|
f32((color >> 8u) & 255u),
|
|
f32((color >> 16u) & 255u),
|
|
f32((color >> 24u) & 255u),
|
|
) / 255.0;
|
|
}
|
|
|
|
fn position_from_screen(screen_pos: vec2<f32>) -> vec4<f32> {
|
|
return vec4<f32>(
|
|
2.0 * screen_pos.x / r_locals.screen_size.x - 1.0,
|
|
1.0 - 2.0 * screen_pos.y / r_locals.screen_size.y,
|
|
0.0,
|
|
1.0,
|
|
);
|
|
}
|
|
|
|
@vertex
|
|
fn vs_main(
|
|
@location(0) a_pos: vec2<f32>,
|
|
@location(1) a_tex_coord: vec2<f32>,
|
|
@location(2) a_color: u32,
|
|
) -> VertexOutput {
|
|
var out: VertexOutput;
|
|
out.tex_coord = a_tex_coord;
|
|
out.color = unpack_color(a_color);
|
|
out.position = position_from_screen(a_pos);
|
|
return out;
|
|
}
|
|
|
|
// Fragment shader bindings
|
|
|
|
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
|
|
@group(1) @binding(1) var r_tex_sampler: sampler;
|
|
|
|
@fragment
|
|
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);
|
|
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);
|
|
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;
|
|
}
|