diff --git a/egui_demo_lib/src/apps/color_test.rs b/egui_demo_lib/src/apps/color_test.rs index f7ea8fecc..a8c9c3ebe 100644 --- a/egui_demo_lib/src/apps/color_test.rs +++ b/egui_demo_lib/src/apps/color_test.rs @@ -39,9 +39,8 @@ impl epi::App for ColorTest { if frame.is_web() { ui.colored_label( RED, - "NOTE: The WebGL backend does NOT pass the color test." + "NOTE: The WebGL1 backend does NOT pass the color test. The WebGL2 backend does." ); - ui.small("This is because WebGL does not support a linear framebuffer blending (not even WebGL2!).\nMaybe when WebGL3 becomes mainstream in 2030 the web can finally get colors right?"); ui.separator(); } ScrollArea::auto_sized().show(ui, |ui| { diff --git a/egui_demo_lib/src/apps/demo/painting.rs b/egui_demo_lib/src/apps/demo/painting.rs index 2468b3858..2e4d20723 100644 --- a/egui_demo_lib/src/apps/demo/painting.rs +++ b/egui_demo_lib/src/apps/demo/painting.rs @@ -12,7 +12,7 @@ impl Default for Painting { fn default() -> Self { Self { lines: Default::default(), - stroke: Stroke::new(2.0, Color32::LIGHT_BLUE), // Thin strokes looks bad on web + stroke: Stroke::new(1.0, Color32::LIGHT_BLUE), } } } diff --git a/egui_web/CHANGELOG.md b/egui_web/CHANGELOG.md index 207af3dd2..dfe8f977e 100644 --- a/egui_web/CHANGELOG.md +++ b/egui_web/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to the `egui_web` integration will be noted in this file. ## Unreleased +### Fixed 🐛 +* Fix alpha blending for WebGL2 backend, now having identical results as egui_glium + ## 0.14.0 - 2021-08-24 diff --git a/egui_web/Cargo.toml b/egui_web/Cargo.toml index 5b2750692..7f44d612f 100644 --- a/egui_web/Cargo.toml +++ b/egui_web/Cargo.toml @@ -92,11 +92,13 @@ features = [ "TouchList", "WebGl2RenderingContext", "WebGlBuffer", + "WebGlFramebuffer", "WebGlProgram", "WebGlRenderingContext", "WebGlShader", "WebGlTexture", "WebGlUniformLocation", + "WebGlVertexArrayObject", "WheelEvent", "Window", ] diff --git a/egui_web/src/shader/fragment_100es.glsl b/egui_web/src/shader/fragment_100es.glsl index c5a3263f1..5b45edf2d 100644 --- a/egui_web/src/shader/fragment_100es.glsl +++ b/egui_web/src/shader/fragment_100es.glsl @@ -30,7 +30,7 @@ vec4 linear_from_srgba(vec4 srgba) { } void main() { - // We must decode the colors, since WebGL doesn't come with sRGBA textures: + // We must decode the colors, since WebGL1 doesn't come with sRGBA textures: vec4 texture_rgba = linear_from_srgba(texture2D(u_sampler, v_tc) * 255.0); /// Multiply vertex color with texture color (in linear space). diff --git a/egui_web/src/shader/fragment_300es.glsl b/egui_web/src/shader/fragment_300es.glsl deleted file mode 100644 index aeb90bfcf..000000000 --- a/egui_web/src/shader/fragment_300es.glsl +++ /dev/null @@ -1,45 +0,0 @@ -precision mediump float; -uniform sampler2D u_sampler; -varying vec4 v_rgba; -varying vec2 v_tc; - -// 0-255 sRGB from 0-1 linear -vec3 srgb_from_linear(vec3 rgb) { - bvec3 cutoff = lessThan(rgb, vec3(0.0031308)); - vec3 lower = rgb * vec3(3294.6); - vec3 higher = vec3(269.025) * pow(rgb, vec3(1.0 / 2.4)) - vec3(14.025); - return mix(higher, lower, vec3(cutoff)); -} - -// 0-255 sRGBA from 0-1 linear -vec4 srgba_from_linear(vec4 rgba) { - return vec4(srgb_from_linear(rgba.rgb), 255.0 * rgba.a); -} - -void main() { - // The texture is set up with `SRGB8_ALPHA8`, so no need to decode here! - vec4 texture_rgba = texture2D(u_sampler, v_tc); - - /// Multiply vertex color with texture color (in linear space). - gl_FragColor = v_rgba * texture_rgba; - - // WebGL doesn't support linear blending in the framebuffer, - // so we do a hack here where we change the premultiplied alpha - // to do the multiplication in gamma space instead: - - // Unmultiply alpha: - if (gl_FragColor.a > 0.0) { - gl_FragColor.rgb /= gl_FragColor.a; - } - - // Empiric tweak to make e.g. shadows look more like they should: - gl_FragColor.a *= sqrt(gl_FragColor.a); - - // To gamma: - gl_FragColor = srgba_from_linear(gl_FragColor) / 255.0; - - // Premultiply alpha, this time in gamma space: - if (gl_FragColor.a > 0.0) { - gl_FragColor.rgb *= gl_FragColor.a; - } -} diff --git a/egui_web/src/shader/main_fragment_300es.glsl b/egui_web/src/shader/main_fragment_300es.glsl new file mode 100644 index 000000000..4dc0eb63a --- /dev/null +++ b/egui_web/src/shader/main_fragment_300es.glsl @@ -0,0 +1,13 @@ +precision mediump float; +uniform sampler2D u_sampler; +varying vec4 v_rgba; +varying vec2 v_tc; + +void main() { + // The texture is set up with `SRGB8_ALPHA8`, so no need to decode here! + vec4 texture_rgba = texture2D(u_sampler, v_tc); + + // Multiply vertex color with texture color (in linear space). + // Linear color is written and blended in Framebuffer and converted to sRGB later + gl_FragColor = v_rgba * texture_rgba; +} diff --git a/egui_web/src/shader/vertex_300es.glsl b/egui_web/src/shader/main_vertex_300es.glsl similarity index 100% rename from egui_web/src/shader/vertex_300es.glsl rename to egui_web/src/shader/main_vertex_300es.glsl diff --git a/egui_web/src/shader/post_fragment_300es.glsl b/egui_web/src/shader/post_fragment_300es.glsl new file mode 100644 index 000000000..001f8a3f2 --- /dev/null +++ b/egui_web/src/shader/post_fragment_300es.glsl @@ -0,0 +1,22 @@ +precision mediump float; +uniform sampler2D u_sampler; +varying vec2 v_tc; + +// 0-255 sRGB from 0-1 linear +vec3 srgb_from_linear(vec3 rgb) { + bvec3 cutoff = lessThan(rgb, vec3(0.0031308)); + vec3 lower = rgb * vec3(3294.6); + vec3 higher = vec3(269.025) * pow(rgb, vec3(1.0 / 2.4)) - vec3(14.025); + return mix(higher, lower, vec3(cutoff)); +} + +// 0-255 sRGBA from 0-1 linear +vec4 srgba_from_linear(vec4 rgba) { + return vec4(srgb_from_linear(rgba.rgb), 255.0 * rgba.a); +} + +void main() { + gl_FragColor = texture2D(u_sampler, v_tc); + + gl_FragColor = srgba_from_linear(gl_FragColor) / 255.; +} diff --git a/egui_web/src/shader/post_vertex_300es.glsl b/egui_web/src/shader/post_vertex_300es.glsl new file mode 100644 index 000000000..37280bc1d --- /dev/null +++ b/egui_web/src/shader/post_vertex_300es.glsl @@ -0,0 +1,8 @@ +precision mediump float; +attribute vec2 a_pos; +varying vec2 v_tc; + +void main() { + gl_Position = vec4(a_pos * 2. - 1., 0.0, 1.0); + v_tc = a_pos; +} diff --git a/egui_web/src/webgl2.rs b/egui_web/src/webgl2.rs index f8c72262d..479a03ba5 100644 --- a/egui_web/src/webgl2.rs +++ b/egui_web/src/webgl2.rs @@ -3,7 +3,10 @@ use { js_sys::WebAssembly, wasm_bindgen::{prelude::*, JsCast}, - web_sys::{WebGl2RenderingContext, WebGlBuffer, WebGlProgram, WebGlShader, WebGlTexture}, + web_sys::{ + WebGl2RenderingContext, WebGlBuffer, WebGlFramebuffer, WebGlProgram, WebGlShader, + WebGlTexture, WebGlVertexArrayObject, + }, }; use egui::{ @@ -22,6 +25,7 @@ pub struct WebGl2Painter { pos_buffer: WebGlBuffer, tc_buffer: WebGlBuffer, color_buffer: WebGlBuffer, + post_process: PostProcess, egui_texture: WebGlTexture, egui_texture_version: Option, @@ -62,12 +66,12 @@ impl WebGl2Painter { let vert_shader = compile_shader( &gl, Gl::VERTEX_SHADER, - include_str!("shader/vertex_300es.glsl"), + include_str!("shader/main_vertex_300es.glsl"), )?; let frag_shader = compile_shader( &gl, Gl::FRAGMENT_SHADER, - include_str!("shader/fragment_300es.glsl"), + include_str!("shader/main_fragment_300es.glsl"), )?; let program = link_program(&gl, [vert_shader, frag_shader].iter())?; @@ -76,6 +80,9 @@ impl WebGl2Painter { let tc_buffer = gl.create_buffer().ok_or("failed to create tc_buffer")?; let color_buffer = gl.create_buffer().ok_or("failed to create color_buffer")?; + let post_process = + PostProcess::new(gl.clone(), canvas.width() as i32, canvas.height() as i32)?; + Ok(WebGl2Painter { canvas_id: canvas_id.to_owned(), canvas, @@ -85,6 +92,7 @@ impl WebGl2Painter { pos_buffer, tc_buffer, color_buffer, + post_process, egui_texture, egui_texture_version: None, user_textures: Default::default(), @@ -368,8 +376,7 @@ impl crate::Painter for WebGl2Painter { } let mut pixels: Vec = Vec::with_capacity(texture.pixels.len() * 4); - let font_gamma = 1.0 / 2.2; // HACK due to non-linear framebuffer blending. - for srgba in texture.srgba_pixels(font_gamma) { + for srgba in texture.srgba_pixels(1.0) { pixels.push(srgba.r()); pixels.push(srgba.g()); pixels.push(srgba.b()); @@ -429,6 +436,9 @@ impl crate::Painter for WebGl2Painter { let gl = &self.gl; + self.post_process + .begin(self.canvas.width() as i32, self.canvas.height() as i32)?; + gl.enable(Gl::SCISSOR_TEST); gl.disable(Gl::CULL_FACE); // egui is not strict about winding order. gl.enable(Gl::BLEND); @@ -485,8 +495,172 @@ impl crate::Painter for WebGl2Painter { )); } } + + self.post_process.end(); + + Ok(()) + } +} + +/// Uses a framebuffer to render everything in linear color space and convert it back to sRGB +/// in a separate "post processing" step +struct PostProcess { + gl: Gl, + pos_buffer: WebGlBuffer, + index_buffer: WebGlBuffer, + vao: WebGlVertexArrayObject, + texture: WebGlTexture, + texture_size: (i32, i32), + fbo: WebGlFramebuffer, + program: WebGlProgram, +} + +impl PostProcess { + fn new(gl: Gl, width: i32, height: i32) -> Result { + let fbo = gl + .create_framebuffer() + .ok_or("failed to create framebuffer")?; + gl.bind_framebuffer(Gl::FRAMEBUFFER, Some(&fbo)); + + let texture = gl.create_texture().unwrap(); + gl.bind_texture(Gl::TEXTURE_2D, Some(&texture)); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::NEAREST as i32); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::NEAREST as i32); + gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1); + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( + Gl::TEXTURE_2D, + 0, + Gl::SRGB8_ALPHA8 as i32, + width, + height, + 0, + Gl::RGBA, + Gl::UNSIGNED_BYTE, + None, + ) + .unwrap(); + gl.framebuffer_texture_2d( + Gl::FRAMEBUFFER, + Gl::COLOR_ATTACHMENT0, + Gl::TEXTURE_2D, + Some(&texture), + 0, + ); + + gl.bind_texture(Gl::TEXTURE_2D, None); + gl.bind_framebuffer(Gl::FRAMEBUFFER, None); + + let vert_shader = compile_shader( + &gl, + Gl::VERTEX_SHADER, + include_str!("shader/post_vertex_300es.glsl"), + )?; + let frag_shader = compile_shader( + &gl, + Gl::FRAGMENT_SHADER, + include_str!("shader/post_fragment_300es.glsl"), + )?; + let program = link_program(&gl, [vert_shader, frag_shader].iter())?; + + let vao = gl.create_vertex_array().ok_or("failed to create vao")?; + gl.bind_vertex_array(Some(&vao)); + + let positions = vec![0u8, 0, 1, 0, 0, 1, 1, 1]; + + let indices = vec![0u8, 1, 2, 1, 2, 3]; + + let pos_buffer = gl.create_buffer().ok_or("failed to create pos_buffer")?; + gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&pos_buffer)); + gl.buffer_data_with_u8_array(Gl::ARRAY_BUFFER, &positions, Gl::STATIC_DRAW); + + let a_pos_loc = gl.get_attrib_location(&program, "a_pos"); + assert!(a_pos_loc >= 0); + gl.vertex_attrib_pointer_with_i32(a_pos_loc as u32, 2, Gl::UNSIGNED_BYTE, false, 0, 0); + gl.enable_vertex_attrib_array(a_pos_loc as u32); + + gl.bind_buffer(Gl::ARRAY_BUFFER, None); + + let index_buffer = gl.create_buffer().ok_or("failed to create index_buffer")?; + gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&index_buffer)); + gl.buffer_data_with_u8_array(Gl::ELEMENT_ARRAY_BUFFER, &indices, Gl::STATIC_DRAW); + + gl.bind_vertex_array(None); + gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None); + + Ok(PostProcess { + gl, + pos_buffer, + index_buffer, + vao, + texture, + texture_size: (width, height), + fbo, + program, + }) + } + + fn begin(&mut self, width: i32, height: i32) -> Result<(), JsValue> { + let gl = &self.gl; + + if (width, height) != self.texture_size { + gl.bind_texture(Gl::TEXTURE_2D, Some(&self.texture)); + gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1); + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( + Gl::TEXTURE_2D, + 0, + Gl::SRGB8_ALPHA8 as i32, + width, + height, + 0, + Gl::RGBA, + Gl::UNSIGNED_BYTE, + None, + )?; + gl.bind_texture(Gl::TEXTURE_2D, None); + + self.texture_size = (width, height); + } + + gl.bind_framebuffer(Gl::FRAMEBUFFER, Some(&self.fbo)); + Ok(()) } + + fn end(&self) { + let gl = &self.gl; + + gl.bind_framebuffer(Gl::FRAMEBUFFER, None); + gl.disable(Gl::SCISSOR_TEST); + + gl.use_program(Some(&self.program)); + + gl.active_texture(Gl::TEXTURE0); + gl.bind_texture(Gl::TEXTURE_2D, Some(&self.texture)); + let u_sampler_loc = gl.get_uniform_location(&self.program, "u_sampler").unwrap(); + gl.uniform1i(Some(&u_sampler_loc), 0); + + gl.bind_vertex_array(Some(&self.vao)); + + gl.draw_elements_with_i32(Gl::TRIANGLES, 6, Gl::UNSIGNED_BYTE, 0); + + gl.bind_texture(Gl::TEXTURE_2D, None); + gl.bind_vertex_array(None); + gl.use_program(None); + } +} + +impl Drop for PostProcess { + fn drop(&mut self) { + let gl = &self.gl; + gl.delete_vertex_array(Some(&self.vao)); + gl.delete_buffer(Some(&self.pos_buffer)); + gl.delete_buffer(Some(&self.index_buffer)); + gl.delete_program(Some(&self.program)); + gl.delete_framebuffer(Some(&self.fbo)); + gl.delete_texture(Some(&self.texture)); + } } fn compile_shader(