Skip to content

Commit 3cc9eca

Browse files
AsmPrgmC3mankinskin
authored andcommitted
Fix alpha blending in WebGL2 backend (emilk#650)
Add a render-to-texture step with an sRGBA8 texture
1 parent 1f4aef6 commit 3cc9eca

11 files changed

+230
-54
lines changed

egui_demo_lib/src/apps/color_test.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,8 @@ impl epi::App for ColorTest {
3939
if frame.is_web() {
4040
ui.colored_label(
4141
RED,
42-
"NOTE: The WebGL backend does NOT pass the color test."
42+
"NOTE: The WebGL1 backend does NOT pass the color test. The WebGL2 backend does."
4343
);
44-
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?");
4544
ui.separator();
4645
}
4746
ScrollArea::auto_sized().show(ui, |ui| {

egui_demo_lib/src/apps/demo/painting.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ impl Default for Painting {
1212
fn default() -> Self {
1313
Self {
1414
lines: Default::default(),
15-
stroke: Stroke::new(2.0, Color32::LIGHT_BLUE), // Thin strokes looks bad on web
15+
stroke: Stroke::new(1.0, Color32::LIGHT_BLUE),
1616
}
1717
}
1818
}

egui_web/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ All notable changes to the `egui_web` integration will be noted in this file.
55

66
## Unreleased
77

8+
### Fixed 🐛
9+
* Fix alpha blending for WebGL2 backend, now having identical results as egui_glium
10+
811

912
## 0.14.0 - 2021-08-24
1013

egui_web/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,13 @@ features = [
9292
"TouchList",
9393
"WebGl2RenderingContext",
9494
"WebGlBuffer",
95+
"WebGlFramebuffer",
9596
"WebGlProgram",
9697
"WebGlRenderingContext",
9798
"WebGlShader",
9899
"WebGlTexture",
99100
"WebGlUniformLocation",
101+
"WebGlVertexArrayObject",
100102
"WheelEvent",
101103
"Window",
102104
]

egui_web/src/shader/fragment_100es.glsl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ vec4 linear_from_srgba(vec4 srgba) {
3030
}
3131

3232
void main() {
33-
// We must decode the colors, since WebGL doesn't come with sRGBA textures:
33+
// We must decode the colors, since WebGL1 doesn't come with sRGBA textures:
3434
vec4 texture_rgba = linear_from_srgba(texture2D(u_sampler, v_tc) * 255.0);
3535

3636
/// Multiply vertex color with texture color (in linear space).

egui_web/src/shader/fragment_300es.glsl

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
precision mediump float;
2+
uniform sampler2D u_sampler;
3+
varying vec4 v_rgba;
4+
varying vec2 v_tc;
5+
6+
void main() {
7+
// The texture is set up with `SRGB8_ALPHA8`, so no need to decode here!
8+
vec4 texture_rgba = texture2D(u_sampler, v_tc);
9+
10+
// Multiply vertex color with texture color (in linear space).
11+
// Linear color is written and blended in Framebuffer and converted to sRGB later
12+
gl_FragColor = v_rgba * texture_rgba;
13+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
precision mediump float;
2+
uniform sampler2D u_sampler;
3+
varying vec2 v_tc;
4+
5+
// 0-255 sRGB from 0-1 linear
6+
vec3 srgb_from_linear(vec3 rgb) {
7+
bvec3 cutoff = lessThan(rgb, vec3(0.0031308));
8+
vec3 lower = rgb * vec3(3294.6);
9+
vec3 higher = vec3(269.025) * pow(rgb, vec3(1.0 / 2.4)) - vec3(14.025);
10+
return mix(higher, lower, vec3(cutoff));
11+
}
12+
13+
// 0-255 sRGBA from 0-1 linear
14+
vec4 srgba_from_linear(vec4 rgba) {
15+
return vec4(srgb_from_linear(rgba.rgb), 255.0 * rgba.a);
16+
}
17+
18+
void main() {
19+
gl_FragColor = texture2D(u_sampler, v_tc);
20+
21+
gl_FragColor = srgba_from_linear(gl_FragColor) / 255.;
22+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
precision mediump float;
2+
attribute vec2 a_pos;
3+
varying vec2 v_tc;
4+
5+
void main() {
6+
gl_Position = vec4(a_pos * 2. - 1., 0.0, 1.0);
7+
v_tc = a_pos;
8+
}

egui_web/src/webgl2.rs

Lines changed: 179 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
use {
44
js_sys::WebAssembly,
55
wasm_bindgen::{prelude::*, JsCast},
6-
web_sys::{WebGl2RenderingContext, WebGlBuffer, WebGlProgram, WebGlShader, WebGlTexture},
6+
web_sys::{
7+
WebGl2RenderingContext, WebGlBuffer, WebGlFramebuffer, WebGlProgram, WebGlShader,
8+
WebGlTexture, WebGlVertexArrayObject,
9+
},
710
};
811

912
use egui::{
@@ -22,6 +25,7 @@ pub struct WebGl2Painter {
2225
pos_buffer: WebGlBuffer,
2326
tc_buffer: WebGlBuffer,
2427
color_buffer: WebGlBuffer,
28+
post_process: PostProcess,
2529

2630
egui_texture: WebGlTexture,
2731
egui_texture_version: Option<u64>,
@@ -62,12 +66,12 @@ impl WebGl2Painter {
6266
let vert_shader = compile_shader(
6367
&gl,
6468
Gl::VERTEX_SHADER,
65-
include_str!("shader/vertex_300es.glsl"),
69+
include_str!("shader/main_vertex_300es.glsl"),
6670
)?;
6771
let frag_shader = compile_shader(
6872
&gl,
6973
Gl::FRAGMENT_SHADER,
70-
include_str!("shader/fragment_300es.glsl"),
74+
include_str!("shader/main_fragment_300es.glsl"),
7175
)?;
7276

7377
let program = link_program(&gl, [vert_shader, frag_shader].iter())?;
@@ -76,6 +80,9 @@ impl WebGl2Painter {
7680
let tc_buffer = gl.create_buffer().ok_or("failed to create tc_buffer")?;
7781
let color_buffer = gl.create_buffer().ok_or("failed to create color_buffer")?;
7882

83+
let post_process =
84+
PostProcess::new(gl.clone(), canvas.width() as i32, canvas.height() as i32)?;
85+
7986
Ok(WebGl2Painter {
8087
canvas_id: canvas_id.to_owned(),
8188
canvas,
@@ -85,6 +92,7 @@ impl WebGl2Painter {
8592
pos_buffer,
8693
tc_buffer,
8794
color_buffer,
95+
post_process,
8896
egui_texture,
8997
egui_texture_version: None,
9098
user_textures: Default::default(),
@@ -368,8 +376,7 @@ impl crate::Painter for WebGl2Painter {
368376
}
369377

370378
let mut pixels: Vec<u8> = Vec::with_capacity(texture.pixels.len() * 4);
371-
let font_gamma = 1.0 / 2.2; // HACK due to non-linear framebuffer blending.
372-
for srgba in texture.srgba_pixels(font_gamma) {
379+
for srgba in texture.srgba_pixels(1.0) {
373380
pixels.push(srgba.r());
374381
pixels.push(srgba.g());
375382
pixels.push(srgba.b());
@@ -429,6 +436,9 @@ impl crate::Painter for WebGl2Painter {
429436

430437
let gl = &self.gl;
431438

439+
self.post_process
440+
.begin(self.canvas.width() as i32, self.canvas.height() as i32)?;
441+
432442
gl.enable(Gl::SCISSOR_TEST);
433443
gl.disable(Gl::CULL_FACE); // egui is not strict about winding order.
434444
gl.enable(Gl::BLEND);
@@ -485,8 +495,172 @@ impl crate::Painter for WebGl2Painter {
485495
));
486496
}
487497
}
498+
499+
self.post_process.end();
500+
501+
Ok(())
502+
}
503+
}
504+
505+
/// Uses a framebuffer to render everything in linear color space and convert it back to sRGB
506+
/// in a separate "post processing" step
507+
struct PostProcess {
508+
gl: Gl,
509+
pos_buffer: WebGlBuffer,
510+
index_buffer: WebGlBuffer,
511+
vao: WebGlVertexArrayObject,
512+
texture: WebGlTexture,
513+
texture_size: (i32, i32),
514+
fbo: WebGlFramebuffer,
515+
program: WebGlProgram,
516+
}
517+
518+
impl PostProcess {
519+
fn new(gl: Gl, width: i32, height: i32) -> Result<PostProcess, JsValue> {
520+
let fbo = gl
521+
.create_framebuffer()
522+
.ok_or("failed to create framebuffer")?;
523+
gl.bind_framebuffer(Gl::FRAMEBUFFER, Some(&fbo));
524+
525+
let texture = gl.create_texture().unwrap();
526+
gl.bind_texture(Gl::TEXTURE_2D, Some(&texture));
527+
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32);
528+
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32);
529+
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::NEAREST as i32);
530+
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::NEAREST as i32);
531+
gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1);
532+
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
533+
Gl::TEXTURE_2D,
534+
0,
535+
Gl::SRGB8_ALPHA8 as i32,
536+
width,
537+
height,
538+
0,
539+
Gl::RGBA,
540+
Gl::UNSIGNED_BYTE,
541+
None,
542+
)
543+
.unwrap();
544+
gl.framebuffer_texture_2d(
545+
Gl::FRAMEBUFFER,
546+
Gl::COLOR_ATTACHMENT0,
547+
Gl::TEXTURE_2D,
548+
Some(&texture),
549+
0,
550+
);
551+
552+
gl.bind_texture(Gl::TEXTURE_2D, None);
553+
gl.bind_framebuffer(Gl::FRAMEBUFFER, None);
554+
555+
let vert_shader = compile_shader(
556+
&gl,
557+
Gl::VERTEX_SHADER,
558+
include_str!("shader/post_vertex_300es.glsl"),
559+
)?;
560+
let frag_shader = compile_shader(
561+
&gl,
562+
Gl::FRAGMENT_SHADER,
563+
include_str!("shader/post_fragment_300es.glsl"),
564+
)?;
565+
let program = link_program(&gl, [vert_shader, frag_shader].iter())?;
566+
567+
let vao = gl.create_vertex_array().ok_or("failed to create vao")?;
568+
gl.bind_vertex_array(Some(&vao));
569+
570+
let positions = vec![0u8, 0, 1, 0, 0, 1, 1, 1];
571+
572+
let indices = vec![0u8, 1, 2, 1, 2, 3];
573+
574+
let pos_buffer = gl.create_buffer().ok_or("failed to create pos_buffer")?;
575+
gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&pos_buffer));
576+
gl.buffer_data_with_u8_array(Gl::ARRAY_BUFFER, &positions, Gl::STATIC_DRAW);
577+
578+
let a_pos_loc = gl.get_attrib_location(&program, "a_pos");
579+
assert!(a_pos_loc >= 0);
580+
gl.vertex_attrib_pointer_with_i32(a_pos_loc as u32, 2, Gl::UNSIGNED_BYTE, false, 0, 0);
581+
gl.enable_vertex_attrib_array(a_pos_loc as u32);
582+
583+
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
584+
585+
let index_buffer = gl.create_buffer().ok_or("failed to create index_buffer")?;
586+
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&index_buffer));
587+
gl.buffer_data_with_u8_array(Gl::ELEMENT_ARRAY_BUFFER, &indices, Gl::STATIC_DRAW);
588+
589+
gl.bind_vertex_array(None);
590+
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
591+
592+
Ok(PostProcess {
593+
gl,
594+
pos_buffer,
595+
index_buffer,
596+
vao,
597+
texture,
598+
texture_size: (width, height),
599+
fbo,
600+
program,
601+
})
602+
}
603+
604+
fn begin(&mut self, width: i32, height: i32) -> Result<(), JsValue> {
605+
let gl = &self.gl;
606+
607+
if (width, height) != self.texture_size {
608+
gl.bind_texture(Gl::TEXTURE_2D, Some(&self.texture));
609+
gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1);
610+
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
611+
Gl::TEXTURE_2D,
612+
0,
613+
Gl::SRGB8_ALPHA8 as i32,
614+
width,
615+
height,
616+
0,
617+
Gl::RGBA,
618+
Gl::UNSIGNED_BYTE,
619+
None,
620+
)?;
621+
gl.bind_texture(Gl::TEXTURE_2D, None);
622+
623+
self.texture_size = (width, height);
624+
}
625+
626+
gl.bind_framebuffer(Gl::FRAMEBUFFER, Some(&self.fbo));
627+
488628
Ok(())
489629
}
630+
631+
fn end(&self) {
632+
let gl = &self.gl;
633+
634+
gl.bind_framebuffer(Gl::FRAMEBUFFER, None);
635+
gl.disable(Gl::SCISSOR_TEST);
636+
637+
gl.use_program(Some(&self.program));
638+
639+
gl.active_texture(Gl::TEXTURE0);
640+
gl.bind_texture(Gl::TEXTURE_2D, Some(&self.texture));
641+
let u_sampler_loc = gl.get_uniform_location(&self.program, "u_sampler").unwrap();
642+
gl.uniform1i(Some(&u_sampler_loc), 0);
643+
644+
gl.bind_vertex_array(Some(&self.vao));
645+
646+
gl.draw_elements_with_i32(Gl::TRIANGLES, 6, Gl::UNSIGNED_BYTE, 0);
647+
648+
gl.bind_texture(Gl::TEXTURE_2D, None);
649+
gl.bind_vertex_array(None);
650+
gl.use_program(None);
651+
}
652+
}
653+
654+
impl Drop for PostProcess {
655+
fn drop(&mut self) {
656+
let gl = &self.gl;
657+
gl.delete_vertex_array(Some(&self.vao));
658+
gl.delete_buffer(Some(&self.pos_buffer));
659+
gl.delete_buffer(Some(&self.index_buffer));
660+
gl.delete_program(Some(&self.program));
661+
gl.delete_framebuffer(Some(&self.fbo));
662+
gl.delete_texture(Some(&self.texture));
663+
}
490664
}
491665

492666
fn compile_shader(

0 commit comments

Comments
 (0)