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

Fix alpha blending in WebGL backends #650

Merged
merged 10 commits into from
Aug 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions egui_demo_lib/src/apps/color_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
AsmPrgmC3 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
2 changes: 1 addition & 1 deletion egui_demo_lib/src/apps/demo/painting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions egui_web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions egui_web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,13 @@ features = [
"TouchList",
"WebGl2RenderingContext",
"WebGlBuffer",
"WebGlFramebuffer",
"WebGlProgram",
"WebGlRenderingContext",
"WebGlShader",
"WebGlTexture",
"WebGlUniformLocation",
"WebGlVertexArrayObject",
"WheelEvent",
"Window",
]
2 changes: 1 addition & 1 deletion egui_web/src/shader/fragment_100es.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
45 changes: 0 additions & 45 deletions egui_web/src/shader/fragment_300es.glsl

This file was deleted.

13 changes: 13 additions & 0 deletions egui_web/src/shader/main_fragment_300es.glsl
Original file line number Diff line number Diff line change
@@ -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;
}
22 changes: 22 additions & 0 deletions egui_web/src/shader/post_fragment_300es.glsl
Original file line number Diff line number Diff line change
@@ -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.;
}
8 changes: 8 additions & 0 deletions egui_web/src/shader/post_vertex_300es.glsl
Original file line number Diff line number Diff line change
@@ -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;
}
184 changes: 179 additions & 5 deletions egui_web/src/webgl2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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<u64>,
Expand Down Expand Up @@ -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())?;
Expand All @@ -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,
Expand All @@ -85,6 +92,7 @@ impl WebGl2Painter {
pos_buffer,
tc_buffer,
color_buffer,
post_process,
egui_texture,
egui_texture_version: None,
user_textures: Default::default(),
Expand Down Expand Up @@ -368,8 +376,7 @@ impl crate::Painter for WebGl2Painter {
}

let mut pixels: Vec<u8> = 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());
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<PostProcess, JsValue> {
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(
Expand Down