Fix alpha blending in WebGL2 backend (#650)
Add a render-to-texture step with an sRGBA8 texture
This commit is contained in:
parent
a256337856
commit
31a1882997
11 changed files with 230 additions and 54 deletions
|
@ -39,9 +39,8 @@ impl epi::App for ColorTest {
|
||||||
if frame.is_web() {
|
if frame.is_web() {
|
||||||
ui.colored_label(
|
ui.colored_label(
|
||||||
RED,
|
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();
|
ui.separator();
|
||||||
}
|
}
|
||||||
ScrollArea::auto_sized().show(ui, |ui| {
|
ScrollArea::auto_sized().show(ui, |ui| {
|
||||||
|
|
|
@ -12,7 +12,7 @@ impl Default for Painting {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
lines: Default::default(),
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,9 @@ All notable changes to the `egui_web` integration will be noted in this file.
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Fixed 🐛
|
||||||
|
* Fix alpha blending for WebGL2 backend, now having identical results as egui_glium
|
||||||
|
|
||||||
|
|
||||||
## 0.14.0 - 2021-08-24
|
## 0.14.0 - 2021-08-24
|
||||||
|
|
||||||
|
|
|
@ -92,11 +92,13 @@ features = [
|
||||||
"TouchList",
|
"TouchList",
|
||||||
"WebGl2RenderingContext",
|
"WebGl2RenderingContext",
|
||||||
"WebGlBuffer",
|
"WebGlBuffer",
|
||||||
|
"WebGlFramebuffer",
|
||||||
"WebGlProgram",
|
"WebGlProgram",
|
||||||
"WebGlRenderingContext",
|
"WebGlRenderingContext",
|
||||||
"WebGlShader",
|
"WebGlShader",
|
||||||
"WebGlTexture",
|
"WebGlTexture",
|
||||||
"WebGlUniformLocation",
|
"WebGlUniformLocation",
|
||||||
|
"WebGlVertexArrayObject",
|
||||||
"WheelEvent",
|
"WheelEvent",
|
||||||
"Window",
|
"Window",
|
||||||
]
|
]
|
||||||
|
|
|
@ -30,7 +30,7 @@ vec4 linear_from_srgba(vec4 srgba) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
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);
|
vec4 texture_rgba = linear_from_srgba(texture2D(u_sampler, v_tc) * 255.0);
|
||||||
|
|
||||||
/// Multiply vertex color with texture color (in linear space).
|
/// Multiply vertex color with texture color (in linear space).
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
13
egui_web/src/shader/main_fragment_300es.glsl
Normal file
13
egui_web/src/shader/main_fragment_300es.glsl
Normal file
|
@ -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
egui_web/src/shader/post_fragment_300es.glsl
Normal file
22
egui_web/src/shader/post_fragment_300es.glsl
Normal file
|
@ -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
egui_web/src/shader/post_vertex_300es.glsl
Normal file
8
egui_web/src/shader/post_vertex_300es.glsl
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -3,7 +3,10 @@
|
||||||
use {
|
use {
|
||||||
js_sys::WebAssembly,
|
js_sys::WebAssembly,
|
||||||
wasm_bindgen::{prelude::*, JsCast},
|
wasm_bindgen::{prelude::*, JsCast},
|
||||||
web_sys::{WebGl2RenderingContext, WebGlBuffer, WebGlProgram, WebGlShader, WebGlTexture},
|
web_sys::{
|
||||||
|
WebGl2RenderingContext, WebGlBuffer, WebGlFramebuffer, WebGlProgram, WebGlShader,
|
||||||
|
WebGlTexture, WebGlVertexArrayObject,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use egui::{
|
use egui::{
|
||||||
|
@ -22,6 +25,7 @@ pub struct WebGl2Painter {
|
||||||
pos_buffer: WebGlBuffer,
|
pos_buffer: WebGlBuffer,
|
||||||
tc_buffer: WebGlBuffer,
|
tc_buffer: WebGlBuffer,
|
||||||
color_buffer: WebGlBuffer,
|
color_buffer: WebGlBuffer,
|
||||||
|
post_process: PostProcess,
|
||||||
|
|
||||||
egui_texture: WebGlTexture,
|
egui_texture: WebGlTexture,
|
||||||
egui_texture_version: Option<u64>,
|
egui_texture_version: Option<u64>,
|
||||||
|
@ -62,12 +66,12 @@ impl WebGl2Painter {
|
||||||
let vert_shader = compile_shader(
|
let vert_shader = compile_shader(
|
||||||
&gl,
|
&gl,
|
||||||
Gl::VERTEX_SHADER,
|
Gl::VERTEX_SHADER,
|
||||||
include_str!("shader/vertex_300es.glsl"),
|
include_str!("shader/main_vertex_300es.glsl"),
|
||||||
)?;
|
)?;
|
||||||
let frag_shader = compile_shader(
|
let frag_shader = compile_shader(
|
||||||
&gl,
|
&gl,
|
||||||
Gl::FRAGMENT_SHADER,
|
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())?;
|
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 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 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 {
|
Ok(WebGl2Painter {
|
||||||
canvas_id: canvas_id.to_owned(),
|
canvas_id: canvas_id.to_owned(),
|
||||||
canvas,
|
canvas,
|
||||||
|
@ -85,6 +92,7 @@ impl WebGl2Painter {
|
||||||
pos_buffer,
|
pos_buffer,
|
||||||
tc_buffer,
|
tc_buffer,
|
||||||
color_buffer,
|
color_buffer,
|
||||||
|
post_process,
|
||||||
egui_texture,
|
egui_texture,
|
||||||
egui_texture_version: None,
|
egui_texture_version: None,
|
||||||
user_textures: Default::default(),
|
user_textures: Default::default(),
|
||||||
|
@ -368,8 +376,7 @@ impl crate::Painter for WebGl2Painter {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pixels: Vec<u8> = Vec::with_capacity(texture.pixels.len() * 4);
|
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(1.0) {
|
||||||
for srgba in texture.srgba_pixels(font_gamma) {
|
|
||||||
pixels.push(srgba.r());
|
pixels.push(srgba.r());
|
||||||
pixels.push(srgba.g());
|
pixels.push(srgba.g());
|
||||||
pixels.push(srgba.b());
|
pixels.push(srgba.b());
|
||||||
|
@ -429,6 +436,9 @@ impl crate::Painter for WebGl2Painter {
|
||||||
|
|
||||||
let gl = &self.gl;
|
let gl = &self.gl;
|
||||||
|
|
||||||
|
self.post_process
|
||||||
|
.begin(self.canvas.width() as i32, self.canvas.height() as i32)?;
|
||||||
|
|
||||||
gl.enable(Gl::SCISSOR_TEST);
|
gl.enable(Gl::SCISSOR_TEST);
|
||||||
gl.disable(Gl::CULL_FACE); // egui is not strict about winding order.
|
gl.disable(Gl::CULL_FACE); // egui is not strict about winding order.
|
||||||
gl.enable(Gl::BLEND);
|
gl.enable(Gl::BLEND);
|
||||||
|
@ -485,10 +495,174 @@ impl crate::Painter for WebGl2Painter {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.post_process.end();
|
||||||
|
|
||||||
Ok(())
|
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(
|
fn compile_shader(
|
||||||
gl: &WebGl2RenderingContext,
|
gl: &WebGl2RenderingContext,
|
||||||
shader_type: u32,
|
shader_type: u32,
|
||||||
|
|
Loading…
Reference in a new issue