From d49aec407960eaa1e3b61d88eb65277965698e34 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 11 Sep 2020 08:56:47 +0200 Subject: [PATCH] Add user texture support to egui_glium and egui_web backends --- egui/src/app.rs | 9 ++ egui_glium/src/backend.rs | 24 +++- egui_glium/src/painter.rs | 115 ++++++++++++---- egui_web/src/backend.rs | 13 +- egui_web/src/webgl.rs | 274 ++++++++++++++++++++++++++------------ 5 files changed, 321 insertions(+), 114 deletions(-) diff --git a/egui/src/app.rs b/egui/src/app.rs index ae95a576..2d01250a 100644 --- a/egui/src/app.rs +++ b/egui/src/app.rs @@ -5,6 +5,8 @@ //! Egui can be used as a library, but you can also use it as a framework to write apps in. //! This module defined the `App` trait that can be implemented and used with the `egui_web` and `egui_glium` crates. +// TODO: move egui/src/app.rs to own crate, e.g. egui_framework ? + use crate::Ui; /// Implement this trait to write apps that can be compiled both natively using the [`egui_glium`](https://crates.io/crates/egui_glium) crate, @@ -54,6 +56,13 @@ pub trait Backend { /// Signal the backend that we'd like to exit the app now. /// This does nothing for web apps. fn quit(&mut self) {} + + /// Allocate a user texture (EXPERIMENTAL!) + fn new_texture_srgba_premultiplied( + &mut self, + size: (usize, usize), + pixels: &[crate::Srgba], + ) -> crate::TextureId; } /// A place where you can store custom data in a way that persists when you restart the app. diff --git a/egui_glium/src/backend.rs b/egui_glium/src/backend.rs index 45d9a2ed..d93783ed 100644 --- a/egui_glium/src/backend.rs +++ b/egui_glium/src/backend.rs @@ -5,7 +5,10 @@ use crate::{ *, }; -pub use egui::app::{App, Backend, RunMode, Storage}; +pub use egui::{ + app::{App, Backend, RunMode, Storage}, + Srgba, +}; const EGUI_MEMORY_KEY: &str = "egui"; const WINDOW_KEY: &str = "window"; @@ -14,14 +17,16 @@ pub struct GliumBackend { frame_times: egui::MovementTracker, quit: bool, run_mode: RunMode, + painter: Painter, } impl GliumBackend { - pub fn new(run_mode: RunMode) -> Self { + pub fn new(run_mode: RunMode, painter: Painter) -> Self { Self { frame_times: egui::MovementTracker::new(1000, 1.0), quit: false, run_mode, + painter, } } } @@ -46,6 +51,14 @@ impl Backend for GliumBackend { fn quit(&mut self) { self.quit = true; } + + fn new_texture_srgba_premultiplied( + &mut self, + size: (usize, usize), + pixels: &[Srgba], + ) -> egui::TextureId { + self.painter.new_user_texture(size, pixels) + } } /// Run an egui app @@ -81,12 +94,11 @@ pub fn run( let mut ctx = egui::Context::new(); *ctx.memory() = egui::app::get_value(&storage, EGUI_MEMORY_KEY).unwrap_or_default(); - let mut painter = Painter::new(&display); let mut raw_input = make_raw_input(&display); // used to keep track of time for animations let start_time = Instant::now(); - let mut runner = GliumBackend::new(run_mode); + let mut runner = GliumBackend::new(run_mode, Painter::new(&display)); let mut clipboard = init_clipboard(); event_loop.run(move |event, _, control_flow| { @@ -105,7 +117,9 @@ pub fn run( let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32; runner.frame_times.add(raw_input.time, frame_time); - painter.paint_jobs(&display, paint_jobs, &ctx.texture()); + runner + .painter + .paint_jobs(&display, paint_jobs, &ctx.texture()); if runner.quit { *control_flow = glutin::event_loop::ControlFlow::Exit diff --git a/egui_glium/src/painter.rs b/egui_glium/src/painter.rs index 2901e593..691065f2 100644 --- a/egui_glium/src/painter.rs +++ b/egui_glium/src/painter.rs @@ -4,18 +4,35 @@ use { egui::{ math::clamp, paint::{PaintJobs, Triangles}, - Rect, + Rect, Srgba, }, glium::{ - implement_vertex, index::PrimitiveType, program, texture, uniform, - uniforms::SamplerWrapFunction, Frame, Surface, + implement_vertex, + index::PrimitiveType, + program, + texture::{self, srgb_texture2d::SrgbTexture2d}, + uniform, + uniforms::SamplerWrapFunction, + Frame, Surface, }, }; pub struct Painter { program: glium::Program, - texture: texture::texture2d::Texture2d, - current_texture_version: Option, + egui_texture: SrgbTexture2d, + egui_texture_version: Option, + + user_textures: Vec, +} + +#[derive(Default)] +struct UserTexture { + /// Pending upload (will be emptied later). + /// This is the format glium likes. + pixels: Vec>, + + /// Lazily uploaded + texture: Option, } impl Painter { @@ -63,7 +80,7 @@ impl Painter { void main() { // glium expects linear rgba - f_color = v_rgba * texture(u_sampler, v_tc).r; + f_color = v_rgba * texture(u_sampler, v_tc); } " }, @@ -109,7 +126,7 @@ impl Painter { void main() { // glium expects linear rgba - gl_FragColor = v_rgba * texture2D(u_sampler, v_tc).r; + gl_FragColor = v_rgba * texture2D(u_sampler, v_tc); } ", }, @@ -155,7 +172,7 @@ impl Painter { void main() { // glium expects linear rgba - gl_FragColor = v_rgba * texture2D(u_sampler, v_tc).r; + gl_FragColor = v_rgba * texture2D(u_sampler, v_tc); } ", }, @@ -163,34 +180,69 @@ impl Painter { .unwrap(); let pixels = vec![vec![255u8, 0u8], vec![0u8, 255u8]]; - let format = texture::UncompressedFloatFormat::U8; + let format = texture::SrgbFormat::U8U8U8U8; let mipmaps = texture::MipmapsOption::NoMipmap; - let texture = - texture::texture2d::Texture2d::with_format(facade, pixels, format, mipmaps).unwrap(); + let egui_texture = SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap(); Painter { program, - texture, - current_texture_version: None, + egui_texture, + egui_texture_version: None, + user_textures: Default::default(), } } - fn upload_texture(&mut self, facade: &dyn glium::backend::Facade, texture: &egui::Texture) { - if self.current_texture_version == Some(texture.version) { + pub fn new_user_texture(&mut self, size: (usize, usize), pixels: &[Srgba]) -> egui::TextureId { + assert_eq!(size.0 * size.1, pixels.len()); + + let pixels: Vec> = pixels + .chunks(size.0 as usize) + .map(|row| row.iter().map(|srgba| srgba.to_tuple()).collect()) + .collect(); + + let id = egui::TextureId::User(self.user_textures.len() as u64); + self.user_textures.push(UserTexture { + pixels, + texture: None, + }); + id + } + + fn upload_egui_texture( + &mut self, + facade: &dyn glium::backend::Facade, + texture: &egui::Texture, + ) { + if self.egui_texture_version == Some(texture.version) { return; // No change } - let pixels: Vec> = texture + let pixels: Vec> = texture .pixels .chunks(texture.width as usize) - .map(|row| row.to_vec()) + .map(|row| { + row.iter() + .map(|&a| Srgba::white_alpha(a).to_tuple()) + .collect() + }) .collect(); - let format = texture::UncompressedFloatFormat::U8; + let format = texture::SrgbFormat::U8U8U8U8; let mipmaps = texture::MipmapsOption::NoMipmap; - self.texture = - texture::texture2d::Texture2d::with_format(facade, pixels, format, mipmaps).unwrap(); - self.current_texture_version = Some(texture.version); + self.egui_texture = SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap(); + self.egui_texture_version = Some(texture.version); + } + + fn upload_user_textures(&mut self, facade: &dyn glium::backend::Facade) { + for user_texture in &mut self.user_textures { + if user_texture.texture.is_none() { + let pixels = std::mem::take(&mut user_texture.pixels); + let format = texture::SrgbFormat::U8U8U8U8; + let mipmaps = texture::MipmapsOption::NoMipmap; + user_texture.texture = + Some(SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap()); + } + } } pub fn paint_jobs( @@ -199,7 +251,8 @@ impl Painter { jobs: PaintJobs, texture: &egui::Texture, ) { - self.upload_texture(display, texture); + self.upload_egui_texture(display, texture); + self.upload_user_textures(display); let mut target = display.draw(); target.clear_color(0.0, 0.0, 0.0, 0.0); @@ -209,6 +262,18 @@ impl Painter { target.finish().unwrap(); } + fn get_texture(&self, texture_id: egui::TextureId) -> &SrgbTexture2d { + match texture_id { + egui::TextureId::Egui => &self.egui_texture, + egui::TextureId::User(id) => { + let id = id as usize; + assert!(id < self.user_textures.len()); + let texture = self.user_textures[id].texture.as_ref(); + texture.expect("Should have been uploaded") + } + } + } + #[inline(never)] // Easier profiling fn paint_job( &mut self, @@ -234,7 +299,7 @@ impl Painter { .map(|v| Vertex { a_pos: [v.pos.x, v.pos.y], a_tc: [v.uv.x, v.uv.y], - a_srgba: v.color.0, + a_srgba: v.color.to_array(), }) .collect(); @@ -251,9 +316,11 @@ impl Painter { let width_points = width_pixels as f32 / pixels_per_point; let height_points = height_pixels as f32 / pixels_per_point; + let texture = self.get_texture(triangles.texture_id); + let uniforms = uniform! { u_screen_size: [width_points, height_points], - u_sampler: self.texture.sampled().wrap_function(SamplerWrapFunction::Clamp), + u_sampler: texture.sampled().wrap_function(SamplerWrapFunction::Clamp), }; // Egui outputs colors with premultiplied alpha: diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 68c97461..44ef32cb 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -1,6 +1,9 @@ use crate::*; -pub use egui::app::{App, Backend, RunMode, WebInfo}; +pub use egui::{ + app::{App, Backend, RunMode, WebInfo}, + Srgba, +}; // ---------------------------------------------------------------------------- @@ -102,6 +105,14 @@ impl Backend for WebBackend { fn fps(&self) -> f32 { 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() } + + fn new_texture_srgba_premultiplied( + &mut self, + size: (usize, usize), + pixels: &[Srgba], + ) -> egui::TextureId { + self.painter.new_user_texture(size, pixels) + } } // ---------------------------------------------------------------------------- diff --git a/egui_web/src/webgl.rs b/egui_web/src/webgl.rs index 26183be4..b17bcc9a 100644 --- a/egui_web/src/webgl.rs +++ b/egui_web/src/webgl.rs @@ -12,18 +12,99 @@ use egui::{ type Gl = WebGlRenderingContext; +const VERTEX_SHADER_SOURCE: &str = r#" + precision mediump float; + uniform vec2 u_screen_size; + attribute vec2 a_pos; + attribute vec2 a_tc; + attribute vec4 a_srgba; + varying vec4 v_rgba; + varying vec2 v_tc; + + // 0-1 linear from 0-255 sRGB + vec3 linear_from_srgb(vec3 srgb) { + bvec3 cutoff = lessThan(srgb, vec3(10.31475)); + vec3 lower = srgb / vec3(3294.6); + vec3 higher = pow((srgb + vec3(14.025)) / vec3(269.025), vec3(2.4)); + return mix(higher, lower, vec3(cutoff)); + } + + vec4 linear_from_srgba(vec4 srgba) { + return vec4(linear_from_srgb(srgba.rgb), srgba.a / 255.0); + } + + void main() { + gl_Position = vec4( + 2.0 * a_pos.x / u_screen_size.x - 1.0, + 1.0 - 2.0 * a_pos.y / u_screen_size.y, + 0.0, + 1.0); + v_rgba = linear_from_srgba(a_srgba); + v_tc = a_tc; + } +"#; + +const FRAGMENT_SHADER_SOURCE: &str = r#" + 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)); + } + + vec4 srgba_from_linear(vec4 rgba) { + return vec4(srgb_from_linear(rgba.rgb), 255.0 * rgba.a); + } + + // 0-1 linear from 0-255 sRGB + vec3 linear_from_srgb(vec3 srgb) { + bvec3 cutoff = lessThan(srgb, vec3(10.31475)); + vec3 lower = srgb / vec3(3294.6); + vec3 higher = pow((srgb + vec3(14.025)) / vec3(269.025), vec3(2.4)); + return mix(higher, lower, vec3(cutoff)); + } + + vec4 linear_from_srgba(vec4 srgba) { + return vec4(linear_from_srgb(srgba.rgb), srgba.a / 255.0); + } + + void main() { + vec4 texture_rgba = linear_from_srgba(texture2D(u_sampler, v_tc) * 255.0); + gl_FragColor = srgba_from_linear(v_rgba * texture_rgba) / 255.0; + } +"#; + pub struct Painter { canvas_id: String, canvas: web_sys::HtmlCanvasElement, gl: WebGlRenderingContext, - texture: WebGlTexture, program: WebGlProgram, index_buffer: WebGlBuffer, pos_buffer: WebGlBuffer, tc_buffer: WebGlBuffer, color_buffer: WebGlBuffer, - tex_size: (u16, u16), - current_texture_version: Option, + + egui_texture: WebGlTexture, + egui_texture_version: Option, + + user_textures: Vec, +} + +#[derive(Default)] +struct UserTexture { + size: (usize, usize), + + /// Pending upload (will be emptied later). + pixels: Vec, + + /// Lazily uploaded + texture: Option, } impl Painter { @@ -48,77 +129,15 @@ impl Painter { // -------------------------------------------------------------------- - let gl_texture = gl.create_texture().unwrap(); - gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); + let egui_texture = gl.create_texture().unwrap(); + gl.bind_texture(Gl::TEXTURE_2D, Some(&egui_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::LINEAR as i32); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::LINEAR as i32); - // -------------------------------------------------------------------- - - let vert_shader = compile_shader( - &gl, - Gl::VERTEX_SHADER, - r#" - precision mediump float; - uniform vec2 u_screen_size; - attribute vec2 a_pos; - attribute vec2 a_tc; - attribute vec4 a_srgba; - varying vec4 v_rgba; - varying vec2 v_tc; - - // 0-1 linear from 0-255 sRGB - vec3 linear_from_srgb(vec3 srgb) { - bvec3 cutoff = lessThan(srgb, vec3(10.31475)); - vec3 lower = srgb / vec3(3294.6); - vec3 higher = pow((srgb + vec3(14.025)) / vec3(269.025), vec3(2.4)); - return mix(higher, lower, vec3(cutoff)); - } - - vec4 linear_from_srgba(vec4 srgba) { - return vec4(linear_from_srgb(srgba.rgb), srgba.a / 255.0); - } - - void main() { - gl_Position = vec4( - 2.0 * a_pos.x / u_screen_size.x - 1.0, - 1.0 - 2.0 * a_pos.y / u_screen_size.y, - 0.0, - 1.0); - v_rgba = linear_from_srgba(a_srgba); - v_tc = a_tc; - } - "#, - )?; - - let frag_shader = compile_shader( - &gl, - Gl::FRAGMENT_SHADER, - r#" - 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)); - } - - vec4 srgba_from_linear(vec4 rgba) { - return vec4(srgb_from_linear(rgba.rgb), 255.0 * rgba.a); - } - - void main() { - gl_FragColor = srgba_from_linear(v_rgba * texture2D(u_sampler, v_tc).a) / 255.0; - } - "#, - )?; + let vert_shader = compile_shader(&gl, Gl::VERTEX_SHADER, VERTEX_SHADER_SOURCE)?; + let frag_shader = compile_shader(&gl, Gl::FRAGMENT_SHADER, FRAGMENT_SHADER_SOURCE)?; let program = link_program(&gl, [vert_shader, frag_shader].iter())?; let index_buffer = gl.create_buffer().ok_or("failed to create index_buffer")?; @@ -130,14 +149,14 @@ impl Painter { canvas_id: canvas_id.to_owned(), canvas, gl, - texture: gl_texture, program, index_buffer, pos_buffer, tc_buffer, color_buffer, - tex_size: (0, 0), - current_texture_version: None, + egui_texture, + egui_texture_version: None, + user_textures: Default::default(), }) } @@ -146,18 +165,52 @@ impl Painter { &self.canvas_id } - fn upload_texture(&mut self, texture: &Texture) { - if self.current_texture_version == Some(texture.version) { + pub fn new_user_texture( + &mut self, + size: (usize, usize), + srgba_pixels: &[Srgba], + ) -> egui::TextureId { + assert_eq!(size.0 * size.1, srgba_pixels.len()); + + let mut pixels: Vec = Vec::with_capacity(srgba_pixels.len() * 4); + for srgba in srgba_pixels { + pixels.push(srgba.r()); + pixels.push(srgba.g()); + pixels.push(srgba.b()); + pixels.push(srgba.a()); + } + + let id = egui::TextureId::User(self.user_textures.len() as u64); + self.user_textures.push(UserTexture { + size, + pixels, + texture: None, + }); + id + } + + fn upload_egui_texture(&mut self, texture: &Texture) { + if self.egui_texture_version == Some(texture.version) { return; // No change } - let gl = &self.gl; - gl.bind_texture(Gl::TEXTURE_2D, Some(&self.texture)); + let mut pixels: Vec = Vec::with_capacity(texture.pixels.len() * 4); + for &alpha in &texture.pixels { + let srgba = Srgba::white_alpha(alpha); + pixels.push(srgba.r()); + pixels.push(srgba.g()); + pixels.push(srgba.b()); + pixels.push(srgba.a()); + } + let gl = &self.gl; + gl.bind_texture(Gl::TEXTURE_2D, Some(&self.egui_texture)); + + // TODO: https://developer.mozilla.org/en-US/docs/Web/API/EXT_sRGB let level = 0; - let internal_format = Gl::ALPHA; + let internal_format = Gl::RGBA; let border = 0; - let src_format = Gl::ALPHA; + let src_format = Gl::RGBA; let src_type = Gl::UNSIGNED_BYTE; gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( Gl::TEXTURE_2D, @@ -168,22 +221,74 @@ impl Painter { border, src_format, src_type, - Some(&texture.pixels), + Some(&pixels), ) .unwrap(); - self.tex_size = (texture.width as u16, texture.height as u16); - self.current_texture_version = Some(texture.version); + self.egui_texture_version = Some(texture.version); + } + + fn upload_user_textures(&mut self) { + let gl = &self.gl; + + for user_texture in &mut self.user_textures { + if user_texture.texture.is_none() { + let pixels = std::mem::take(&mut user_texture.pixels); + + let gl_texture = gl.create_texture().unwrap(); + gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_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::LINEAR as i32); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::LINEAR as i32); + + gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); + + // TODO: https://developer.mozilla.org/en-US/docs/Web/API/EXT_sRGB + let level = 0; + let internal_format = Gl::RGBA; + let border = 0; + let src_format = Gl::RGBA; + let src_type = Gl::UNSIGNED_BYTE; + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( + Gl::TEXTURE_2D, + level, + internal_format as i32, + user_texture.size.0 as i32, + user_texture.size.1 as i32, + border, + src_format, + src_type, + Some(&pixels), + ) + .unwrap(); + + user_texture.texture = Some(gl_texture); + } + } + } + + fn get_texture(&self, texture_id: egui::TextureId) -> &WebGlTexture { + match texture_id { + egui::TextureId::Egui => &self.egui_texture, + egui::TextureId::User(id) => { + let id = id as usize; + assert!(id < self.user_textures.len()); + let texture = self.user_textures[id].texture.as_ref(); + texture.expect("Should have been uploaded") + } + } } pub fn paint_jobs( &mut self, bg_color: Srgba, jobs: PaintJobs, - texture: &Texture, + egui_texture: &Texture, pixels_per_point: f32, ) -> Result<(), JsValue> { - self.upload_texture(texture); + self.upload_egui_texture(egui_texture); + self.upload_user_textures(); let gl = &self.gl; @@ -192,7 +297,6 @@ impl Painter { gl.blend_func(Gl::ONE, Gl::ONE_MINUS_SRC_ALPHA); // premultiplied alpha gl.use_program(Some(&self.program)); gl.active_texture(Gl::TEXTURE0); - gl.bind_texture(Gl::TEXTURE_2D, Some(&self.texture)); let u_screen_size_loc = gl .get_uniform_location(&self.program, "u_screen_size") @@ -224,6 +328,8 @@ impl Painter { gl.clear(Gl::COLOR_BUFFER_BIT); for (clip_rect, triangles) in jobs { + gl.bind_texture(Gl::TEXTURE_2D, Some(self.get_texture(triangles.texture_id))); + let clip_min_x = pixels_per_point * clip_rect.min.x; let clip_min_y = pixels_per_point * clip_rect.min.y; let clip_max_x = pixels_per_point * clip_rect.max.x;