1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 13:50:04 -04:00

Improve text redering and do all color operation in gamma space (#2071)

This commit is contained in:
Emil Ernerfeldt
2022-09-24 17:53:11 +02:00
committed by GitHub
parent 29fa63317e
commit 4ac1e28eae
36 changed files with 468 additions and 733 deletions

View File

@@ -15,7 +15,6 @@ pub mod painter;
pub use glow;
pub use painter::{CallbackFn, Painter};
mod misc_util;
mod post_process;
mod shader_version;
mod vao;

View File

@@ -12,7 +12,6 @@ use memoffset::offset_of;
use crate::check_for_gl_error;
use crate::misc_util::{compile_shader, link_program};
use crate::post_process::PostProcess;
use crate::shader_version::ShaderVersion;
use crate::vao;
@@ -52,10 +51,8 @@ pub struct Painter {
u_screen_size: glow::UniformLocation,
u_sampler: glow::UniformLocation,
is_webgl_1: bool,
is_embedded: bool,
vao: crate::vao::VertexArrayObject,
srgb_textures: bool,
post_process: Option<PostProcess>,
vbo: glow::Buffer,
element_array_buffer: glow::Buffer,
@@ -105,7 +102,6 @@ impl Painter {
/// * failed to create buffer
pub fn new(
gl: Arc<glow::Context>,
pp_fb_extent: Option<[i32; 2]>,
shader_prefix: &str,
shader_version: Option<ShaderVersion>,
) -> Result<Painter, String> {
@@ -115,8 +111,8 @@ impl Painter {
let max_texture_side = unsafe { gl.get_parameter_i32(glow::MAX_TEXTURE_SIZE) } as usize;
let shader_version = shader_version.unwrap_or_else(|| ShaderVersion::get(&gl));
let is_webgl_1 = shader_version == ShaderVersion::Es100;
let header = shader_version.version_declaration();
tracing::debug!("Shader header: {:?}.", header);
let shader_version_declaration = shader_version.version_declaration();
tracing::debug!("Shader header: {:?}.", shader_version_declaration);
let supported_extensions = gl.supported_extensions();
tracing::trace!("OpenGL extensions: {supported_extensions:?}");
@@ -125,44 +121,17 @@ impl Painter {
// EXT_sRGB, GL_ARB_framebuffer_sRGB, GL_EXT_sRGB, GL_EXT_texture_sRGB_decode, …
extension.contains("sRGB")
});
tracing::debug!("SRGB Support: {:?}", srgb_textures);
let (post_process, srgb_support_define) = if shader_version.is_embedded() {
// WebGL doesn't support linear framebuffer blending… but maybe we can emulate it with `PostProcess`?
if let Some(size) = pp_fb_extent {
tracing::debug!("WebGL with sRGB support. Turning on post processing for linear framebuffer blending.");
// install post process to correct sRGB color:
(
Some(unsafe { PostProcess::new(gl.clone(), shader_prefix, is_webgl_1, size)? }),
"#define SRGB_SUPPORTED",
)
} else {
tracing::warn!("WebGL or OpenGL ES detected but PostProcess disabled because dimension is None. Linear framebuffer blending will not be supported.");
(None, "")
}
} else {
if srgb_textures {
(None, "#define SRGB_SUPPORTED")
} else {
tracing::warn!("sRGB aware texture filtering not available");
(None, "")
}
};
tracing::debug!("SRGB texture Support: {:?}", srgb_textures);
unsafe {
let vert = compile_shader(
&gl,
glow::VERTEX_SHADER,
&format!(
"{}\n{}\n{}\n{}",
header,
"{}\n#define NEW_SHADER_INTERFACE {}\n{}\n{}",
shader_version_declaration,
shader_version.is_new_shader_interface() as i32,
shader_prefix,
if shader_version.is_new_shader_interface() {
"#define NEW_SHADER_INTERFACE\n"
} else {
""
},
VERT_SRC
),
)?;
@@ -170,15 +139,11 @@ impl Painter {
&gl,
glow::FRAGMENT_SHADER,
&format!(
"{}\n{}\n{}\n{}\n{}",
header,
"{}\n#define NEW_SHADER_INTERFACE {}\n#define SRGB_TEXTURES {}\n{}\n{}",
shader_version_declaration,
shader_version.is_new_shader_interface() as i32,
srgb_textures as i32,
shader_prefix,
srgb_support_define,
if shader_version.is_new_shader_interface() {
"#define NEW_SHADER_INTERFACE\n"
} else {
""
},
FRAG_SRC
),
)?;
@@ -236,10 +201,8 @@ impl Painter {
u_screen_size,
u_sampler,
is_webgl_1,
is_embedded: shader_version.is_embedded(),
vao,
srgb_textures,
post_process,
vbo,
element_array_buffer,
textures: Default::default(),
@@ -268,8 +231,11 @@ impl Painter {
/// So if in a [`egui::Shape::Callback`] you need to use an offscreen FBO, you should
/// then restore to this afterwards with
/// `gl.bind_framebuffer(glow::FRAMEBUFFER, painter.intermediate_fbo());`
#[allow(clippy::unused_self)]
pub fn intermediate_fbo(&self) -> Option<glow::Framebuffer> {
self.post_process.as_ref().map(|pp| pp.fbo())
// We don't currently ever render to an offscreen buffer,
// but we may want to start to in order to do anti-aliasing on web, for instance.
None
}
unsafe fn prepare_painting(
@@ -298,7 +264,7 @@ impl Painter {
);
if !cfg!(target_arch = "wasm32") {
self.gl.enable(glow::FRAMEBUFFER_SRGB);
self.gl.disable(glow::FRAMEBUFFER_SRGB);
check_for_gl_error!(&self.gl, "FRAMEBUFFER_SRGB");
}
@@ -372,17 +338,6 @@ impl Painter {
crate::profile_function!();
self.assert_not_destroyed();
if let Some(ref mut post_process) = self.post_process {
unsafe {
post_process.begin(screen_size_px[0] as i32, screen_size_px[1] as i32);
post_process.bind();
self.gl.disable(glow::SCISSOR_TEST);
self.gl
.viewport(0, 0, screen_size_px[0] as i32, screen_size_px[1] as i32);
// use the same clear-color as was set for the screen framebuffer.
self.gl.clear(glow::COLOR_BUFFER_BIT);
}
}
let size_in_pixels = unsafe { self.prepare_painting(screen_size_px, pixels_per_point) };
for egui::ClippedPrimitive {
@@ -435,12 +390,7 @@ impl Painter {
check_for_gl_error!(&self.gl, "callback");
// Restore state:
unsafe {
if let Some(ref mut post_process) = self.post_process {
post_process.bind();
}
self.prepare_painting(screen_size_px, pixels_per_point)
};
unsafe { self.prepare_painting(screen_size_px, pixels_per_point) };
}
}
}
@@ -450,10 +400,6 @@ impl Painter {
self.vao.unbind(&self.gl);
self.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
if let Some(ref post_process) = self.post_process {
post_process.end();
}
self.gl.disable(glow::SCISSOR_TEST);
check_for_gl_error!(&self.gl, "painting");
@@ -532,13 +478,8 @@ impl Painter {
"Mismatch between texture size and texel count"
);
let gamma = if self.is_embedded && self.post_process.is_none() {
1.0 / 2.2
} else {
1.0
};
let data: Vec<u8> = image
.srgba_pixels(gamma)
.srgba_pixels(None)
.flat_map(|a| a.to_array())
.collect();
@@ -684,9 +625,6 @@ impl Painter {
if !self.destroyed {
unsafe {
self.destroy_gl();
if let Some(ref post_process) = self.post_process {
post_process.destroy();
}
}
self.destroyed = true;
}

View File

@@ -1,284 +0,0 @@
#![allow(unsafe_code)]
use crate::check_for_gl_error;
use crate::misc_util::{compile_shader, link_program};
use crate::vao::BufferInfo;
use glow::HasContext as _;
/// Uses a framebuffer to render everything in linear color space and convert it back to `sRGB`
/// in a separate "post processing" step
pub(crate) struct PostProcess {
gl: std::sync::Arc<glow::Context>,
pos_buffer: glow::Buffer,
index_buffer: glow::Buffer,
vao: crate::vao::VertexArrayObject,
is_webgl_1: bool,
color_texture: glow::Texture,
depth_renderbuffer: Option<glow::Renderbuffer>,
texture_size: (i32, i32),
fbo: glow::Framebuffer,
program: glow::Program,
}
impl PostProcess {
pub(crate) unsafe fn new(
gl: std::sync::Arc<glow::Context>,
shader_prefix: &str,
is_webgl_1: bool,
[width, height]: [i32; 2],
) -> Result<PostProcess, String> {
gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1);
let fbo = gl.create_framebuffer()?;
gl.bind_framebuffer(glow::FRAMEBUFFER, Some(fbo));
// ----------------------------------------------
// Set up color tesxture:
let color_texture = gl.create_texture()?;
gl.bind_texture(glow::TEXTURE_2D, Some(color_texture));
gl.tex_parameter_i32(
glow::TEXTURE_2D,
glow::TEXTURE_WRAP_S,
glow::CLAMP_TO_EDGE as i32,
);
gl.tex_parameter_i32(
glow::TEXTURE_2D,
glow::TEXTURE_WRAP_T,
glow::CLAMP_TO_EDGE as i32,
);
gl.tex_parameter_i32(
glow::TEXTURE_2D,
glow::TEXTURE_MIN_FILTER,
glow::NEAREST as i32,
);
gl.tex_parameter_i32(
glow::TEXTURE_2D,
glow::TEXTURE_MAG_FILTER,
glow::NEAREST as i32,
);
let (internal_format, format) = if is_webgl_1 {
(glow::SRGB_ALPHA, glow::SRGB_ALPHA)
} else {
(glow::SRGB8_ALPHA8, glow::RGBA)
};
gl.tex_image_2d(
glow::TEXTURE_2D,
0,
internal_format as i32,
width,
height,
0,
format,
glow::UNSIGNED_BYTE,
None,
);
crate::check_for_gl_error_even_in_release!(&gl, "post process texture initialization");
gl.framebuffer_texture_2d(
glow::FRAMEBUFFER,
glow::COLOR_ATTACHMENT0,
glow::TEXTURE_2D,
Some(color_texture),
0,
);
gl.bind_texture(glow::TEXTURE_2D, None);
// ---------------------------------------------------------
// Depth buffer - we only need this when embedding 3D within egui using `egui::PaintCallback`.
// TODO(emilk): add a setting to enable/disable the depth buffer.
let with_depth_buffer = true;
let depth_renderbuffer = if with_depth_buffer {
let depth_renderbuffer = gl.create_renderbuffer()?;
gl.bind_renderbuffer(glow::RENDERBUFFER, Some(depth_renderbuffer));
gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_COMPONENT16, width, height);
gl.bind_renderbuffer(glow::RENDERBUFFER, None);
Some(depth_renderbuffer)
} else {
None
};
// ---------------------------------------------------------
gl.bind_framebuffer(glow::FRAMEBUFFER, None);
// ---------------------------------------------------------
let vert_shader = compile_shader(
&gl,
glow::VERTEX_SHADER,
&format!(
"{}\n{}",
shader_prefix,
include_str!("shader/post_vertex_100es.glsl")
),
)?;
let frag_shader = compile_shader(
&gl,
glow::FRAGMENT_SHADER,
&format!(
"{}\n{}",
shader_prefix,
include_str!("shader/post_fragment_100es.glsl")
),
)?;
let program = link_program(&gl, [vert_shader, frag_shader].iter())?;
let positions: Vec<f32> = vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0];
let indices: Vec<u8> = vec![0, 1, 2, 1, 2, 3];
let pos_buffer = gl.create_buffer()?;
gl.bind_buffer(glow::ARRAY_BUFFER, Some(pos_buffer));
gl.buffer_data_u8_slice(
glow::ARRAY_BUFFER,
bytemuck::cast_slice(&positions),
glow::STATIC_DRAW,
);
let a_pos_loc = gl
.get_attrib_location(program, "a_pos")
.ok_or_else(|| "failed to get location of a_pos".to_owned())?;
let vao = crate::vao::VertexArrayObject::new(
&gl,
pos_buffer,
vec![BufferInfo {
location: a_pos_loc,
vector_size: 2,
data_type: glow::FLOAT,
normalized: false,
stride: 0,
offset: 0,
}],
);
let index_buffer = gl.create_buffer()?;
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(index_buffer));
gl.buffer_data_u8_slice(glow::ELEMENT_ARRAY_BUFFER, &indices, glow::STATIC_DRAW);
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
crate::check_for_gl_error_even_in_release!(&gl, "post process initialization");
Ok(PostProcess {
gl,
pos_buffer,
index_buffer,
vao,
is_webgl_1,
color_texture,
depth_renderbuffer,
texture_size: (width, height),
fbo,
program,
})
}
/// What we render to.
pub(crate) fn fbo(&self) -> glow::Framebuffer {
self.fbo
}
pub(crate) unsafe fn begin(&mut self, width: i32, height: i32) {
if (width, height) != self.texture_size {
self.gl
.bind_texture(glow::TEXTURE_2D, Some(self.color_texture));
self.gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1);
let (internal_format, format) = if self.is_webgl_1 {
(glow::SRGB_ALPHA, glow::SRGB_ALPHA)
} else {
(glow::SRGB8_ALPHA8, glow::RGBA)
};
self.gl.tex_image_2d(
glow::TEXTURE_2D,
0,
internal_format as i32,
width,
height,
0,
format,
glow::UNSIGNED_BYTE,
None,
);
self.gl.bind_texture(glow::TEXTURE_2D, None);
if let Some(depth_renderbuffer) = self.depth_renderbuffer {
self.gl
.bind_renderbuffer(glow::RENDERBUFFER, Some(depth_renderbuffer));
self.gl.renderbuffer_storage(
glow::RENDERBUFFER,
glow::DEPTH_COMPONENT16,
width,
height,
);
self.gl.bind_renderbuffer(glow::RENDERBUFFER, None);
}
self.texture_size = (width, height);
}
check_for_gl_error!(&self.gl, "PostProcess::begin");
}
pub(crate) unsafe fn bind(&self) {
self.gl.bind_framebuffer(glow::FRAMEBUFFER, Some(self.fbo));
self.gl.framebuffer_texture_2d(
glow::FRAMEBUFFER,
glow::COLOR_ATTACHMENT0,
glow::TEXTURE_2D,
Some(self.color_texture),
0,
);
self.gl.framebuffer_renderbuffer(
glow::FRAMEBUFFER,
glow::DEPTH_ATTACHMENT,
glow::RENDERBUFFER,
self.depth_renderbuffer,
);
check_for_gl_error!(&self.gl, "PostProcess::bind");
}
pub(crate) unsafe fn end(&self) {
self.gl.bind_framebuffer(glow::FRAMEBUFFER, None);
self.gl.disable(glow::SCISSOR_TEST);
self.gl.use_program(Some(self.program));
self.gl.active_texture(glow::TEXTURE0);
self.gl
.bind_texture(glow::TEXTURE_2D, Some(self.color_texture));
let u_sampler_loc = self
.gl
.get_uniform_location(self.program, "u_sampler")
.unwrap();
self.gl.uniform_1_i32(Some(&u_sampler_loc), 0);
self.vao.bind(&self.gl);
self.gl
.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.index_buffer));
self.gl
.draw_elements(glow::TRIANGLES, 6, glow::UNSIGNED_BYTE, 0);
self.vao.unbind(&self.gl);
self.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
self.gl.bind_texture(glow::TEXTURE_2D, None);
self.gl.use_program(None);
check_for_gl_error!(&self.gl, "PostProcess::end");
}
pub(crate) unsafe fn destroy(&self) {
self.gl.delete_buffer(self.pos_buffer);
self.gl.delete_buffer(self.index_buffer);
self.gl.delete_program(self.program);
self.gl.delete_framebuffer(self.fbo);
self.gl.delete_texture(self.color_texture);
if let Some(depth_renderbuffer) = self.depth_renderbuffer {
self.gl.delete_renderbuffer(depth_renderbuffer);
}
}
}

View File

@@ -4,73 +4,38 @@
uniform sampler2D u_sampler;
#ifdef NEW_SHADER_INTERFACE
in vec4 v_rgba;
#if NEW_SHADER_INTERFACE
in vec4 v_rgba_in_gamma;
in vec2 v_tc;
out vec4 f_color;
// a dirty hack applied to support webGL2
#define gl_FragColor f_color
#define texture2D texture
#else
varying vec4 v_rgba;
varying vec4 v_rgba_in_gamma;
varying vec2 v_tc;
#endif
#ifdef SRGB_SUPPORTED
void main() {
// The texture sampler is sRGB aware, and OpenGL already expects linear rgba output
// so no need for any sRGB conversions here:
gl_FragColor = v_rgba * texture2D(u_sampler, v_tc);
}
// 0-1 sRGB gamma from 0-1 linear
vec3 srgb_gamma_from_linear(vec3 rgb) {
bvec3 cutoff = lessThan(rgb, vec3(0.0031308));
vec3 lower = rgb * vec3(12.92);
vec3 higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055);
return mix(higher, lower, vec3(cutoff));
}
// 0-1 sRGBA gamma from 0-1 linear
vec4 srgba_gamma_from_linear(vec4 rgba) {
return vec4(srgb_gamma_from_linear(rgba.rgb), rgba.a);
}
void main() {
#if SRGB_TEXTURES
vec4 texture_in_gamma = srgba_gamma_from_linear(texture2D(u_sampler, v_tc));
#else
// 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() {
// 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).
gl_FragColor = v_rgba * texture_rgba;
// WebGL1 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;
}
}
vec4 texture_in_gamma = texture2D(u_sampler, v_tc);
#endif
// We multiply the colors in gamma space, because that's the only way to get text to look right.
gl_FragColor = v_rgba_in_gamma * texture_in_gamma;
}

View File

@@ -1,26 +0,0 @@
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.0;
#ifdef APPLY_BRIGHTENING_GAMMA
gl_FragColor = vec4(pow(gl_FragColor.rgb, vec3(1.0/2.2)), gl_FragColor.a);
#endif
}

View File

@@ -1,8 +0,0 @@
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;
}

View File

@@ -1,4 +1,4 @@
#ifdef NEW_SHADER_INTERFACE
#if NEW_SHADER_INTERFACE
#define I in
#define O out
#define V(x) x
@@ -16,28 +16,15 @@ uniform vec2 u_screen_size;
I vec2 a_pos;
I vec4 a_srgba; // 0-255 sRGB
I vec2 a_tc;
O vec4 v_rgba;
O vec4 v_rgba_in_gamma;
O 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, V(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);
// egui encodes vertex colors in gamma space, so we must decode the colors here:
v_rgba = linear_from_srgba(a_srgba);
v_rgba_in_gamma = a_srgba / 255.0;
v_tc = a_tc;
}

View File

@@ -20,7 +20,7 @@ impl EguiGlow {
gl: std::sync::Arc<glow::Context>,
shader_version: Option<ShaderVersion>,
) -> Self {
let painter = crate::Painter::new(gl, None, "", shader_version)
let painter = crate::Painter::new(gl, "", shader_version)
.map_err(|error| {
tracing::error!("error occurred in initializing painter:\n{}", error);
})