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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 468 additions and 733 deletions

View file

@ -6,6 +6,10 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
## Unreleased
* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)).
* ⚠️ BREAKING: egui now expects integrations to do all color blending in gamma space ([#2071](https://github.com/emilk/egui/pull/2071)).
### Fixed 🐛
* Improved text rendering ([#2071](https://github.com/emilk/egui/pull/2071)).
## 0.19.0 - 2022-08-20

1
Cargo.lock generated
View file

@ -1304,6 +1304,7 @@ dependencies = [
"document-features",
"egui",
"egui-winit",
"egui_demo_lib",
"glium",
"image",
]

View file

@ -338,7 +338,7 @@ mod glow_integration {
.with_hardware_acceleration(hardware_acceleration)
.with_depth_buffer(native_options.depth_buffer)
.with_multisampling(native_options.multisampling)
.with_srgb(true)
.with_srgb(false)
.with_stencil_buffer(native_options.stencil_buffer)
.with_vsync(native_options.vsync)
.build_windowed(window_builder, event_loop)
@ -365,7 +365,7 @@ mod glow_integration {
let gl = Arc::new(gl);
let painter =
egui_glow::Painter::new(gl.clone(), None, "", self.native_options.shader_version)
egui_glow::Painter::new(gl.clone(), "", self.native_options.shader_version)
.unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error));
let system_theme = self.native_options.system_theme();

View file

@ -22,8 +22,7 @@ impl WebPainter {
let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options)?;
let gl = std::sync::Arc::new(gl);
let dimension = [canvas.width() as i32, canvas.height() as i32];
let painter = egui_glow::Painter::new(gl, Some(dimension), shader_prefix, None)
let painter = egui_glow::Painter::new(gl, shader_prefix, None)
.map_err(|error| format!("Error starting glow painter: {}", error))?;
Ok(Self {

View file

@ -3,10 +3,10 @@ All notable changes to the `egui-wgpu` integration will be noted in this file.
## Unreleased
* Rename `RenderPass` to `Renderer`
* Rename `RenderPass::execute` to `RenderPass::render`
* Rename `RenderPass::execute_with_renderpass` to `Renderer::render_onto_renderpass`
* Reexport `Renderer`
* Renamed `RenderPass` to `Renderer`.
* Renamed `RenderPass::execute` to `RenderPass::render`.
* Renamed `RenderPass::execute_with_renderpass` to `Renderer::render_onto_renderpass`.
* Reexported `Renderer`.
## 0.19.0 - 2022-08-20

View file

@ -2,7 +2,7 @@
struct VertexOutput {
@location(0) tex_coord: vec2<f32>,
@location(1) color: vec4<f32>,
@location(1) color: vec4<f32>, // gamma 0-1
@builtin(position) position: vec4<f32>,
};
@ -14,22 +14,35 @@ struct Locals {
};
@group(0) @binding(0) var<uniform> r_locals: Locals;
// 0-1 from 0-255
fn linear_from_srgb(srgb: vec3<f32>) -> vec3<f32> {
let cutoff = srgb < vec3<f32>(10.31475);
let lower = srgb / vec3<f32>(3294.6);
let higher = pow((srgb + vec3<f32>(14.025)) / vec3<f32>(269.025), vec3<f32>(2.4));
// 0-1 linear from 0-1 sRGB gamma
fn linear_from_gamma_rgb(srgb: vec3<f32>) -> vec3<f32> {
let cutoff = srgb < vec3<f32>(0.04045);
let lower = srgb / vec3<f32>(12.92);
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
return select(higher, lower, cutoff);
}
// [u8; 4] SRGB as u32 -> [r, g, b, a]
// 0-1 sRGB gamma from 0-1 linear
fn gamma_from_linear_rgb(rgb: vec3<f32>) -> vec3<f32> {
let cutoff = rgb < vec3<f32>(0.0031308);
let lower = rgb * vec3<f32>(12.92);
let higher = vec3<f32>(1.055) * pow(rgb, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
return select(higher, lower, cutoff);
}
// 0-1 sRGBA gamma from 0-1 linear
fn gamma_from_linear_rgba(linear_rgba: vec4<f32>) -> vec4<f32> {
return vec4<f32>(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a);
}
// [u8; 4] SRGB as u32 -> [r, g, b, a] in 0.-1
fn unpack_color(color: u32) -> vec4<f32> {
return vec4<f32>(
f32(color & 255u),
f32((color >> 8u) & 255u),
f32((color >> 16u) & 255u),
f32((color >> 24u) & 255u),
);
) / 255.0;
}
fn position_from_screen(screen_pos: vec2<f32>) -> vec4<f32> {
@ -49,22 +62,7 @@ fn vs_main(
) -> VertexOutput {
var out: VertexOutput;
out.tex_coord = a_tex_coord;
let color = unpack_color(a_color);
out.color = vec4<f32>(linear_from_srgb(color.rgb), color.a / 255.0);
out.position = position_from_screen(a_pos);
return out;
}
@vertex
fn vs_conv_main(
@location(0) a_pos: vec2<f32>,
@location(1) a_tex_coord: vec2<f32>,
@location(2) a_color: u32,
) -> VertexOutput {
var out: VertexOutput;
out.tex_coord = a_tex_coord;
let color = unpack_color(a_color);
out.color = vec4<f32>(color.rgba / 255.0);
out.color = unpack_color(a_color);
out.position = position_from_screen(a_pos);
return out;
}
@ -75,6 +73,19 @@ fn vs_conv_main(
@group(1) @binding(1) var r_tex_sampler: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return in.color * textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
let out_color_gamma = in.color * tex_gamma;
return vec4<f32>(linear_from_gamma_rgb(out_color_gamma.rgb), out_color_gamma.a);
}
@fragment
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
let out_color_gamma = in.color * tex_gamma;
return out_color_gamma;
}

View file

@ -140,7 +140,8 @@ pub struct Renderer {
impl Renderer {
/// Creates a renderer for a egui UI.
///
/// If the format passed is not a *Srgb format, the shader will automatically convert to `sRGB` colors in the shader.
/// `output_format` should preferably be [`wgpu::TextureFormat::Rgba8Unorm`] or
/// [`wgpu::TextureFormat::Bgra8Unorm`], i.e. in gamma-space.
pub fn new(
device: &wgpu::Device,
output_format: wgpu::TextureFormat,
@ -235,11 +236,7 @@ impl Renderer {
label: Some("egui_pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
entry_point: if output_format.describe().srgb {
"vs_main"
} else {
"vs_conv_main"
},
entry_point: "vs_main",
module: &module,
buffers: &[wgpu::VertexBufferLayout {
array_stride: 5 * 4,
@ -268,7 +265,12 @@ impl Renderer {
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: "fs_main",
entry_point: if output_format.describe().srgb {
tracing::warn!("Detected a linear (sRGBA aware) framebuffer {:?}. egui prefers Rgba8Unorm or Bgra8Unorm", output_format);
"fs_main_linear_framebuffer"
} else {
"fs_main_gamma_framebuffer" // this is what we prefer
},
targets: &[Some(wgpu::ColorTargetState {
format: output_format,
blend: Some(wgpu::BlendState {
@ -518,7 +520,7 @@ impl Renderer {
image.pixels.len(),
"Mismatch between texture size and texel count"
);
Cow::Owned(image.srgba_pixels(1.0).collect::<Vec<_>>())
Cow::Owned(image.srgba_pixels(None).collect::<Vec<_>>())
}
};
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
@ -564,7 +566,7 @@ impl Renderer {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
format: wgpu::TextureFormat::Rgba8UnormSrgb, // TODO(emilk): handle WebGL1 where this is not always supported!
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
});
let filter = match image_delta.filter {

View file

@ -117,7 +117,9 @@ impl<'a> Painter<'a> {
if self.render_state.is_none() {
let adapter = self.adapter.as_ref().unwrap();
let swapchain_format = surface.get_supported_formats(adapter)[0];
let swapchain_format =
select_framebuffer_format(&surface.get_supported_formats(adapter));
let rs = pollster::block_on(self.init_render_state(adapter, swapchain_format));
self.render_state = Some(rs);
@ -322,3 +324,15 @@ impl<'a> Painter<'a> {
// TODO(emilk): something here?
}
}
fn select_framebuffer_format(formats: &[wgpu::TextureFormat]) -> wgpu::TextureFormat {
for &format in formats {
if matches!(
format,
wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
) {
return format;
}
}
formats[0] // take the first
}

View file

@ -1,7 +1,8 @@
use egui::{color::*, widgets::color_picker::show_color, TextureFilter, *};
use std::collections::HashMap;
const GRADIENT_SIZE: Vec2 = vec2(256.0, 24.0);
use egui::{color::*, widgets::color_picker::show_color, TextureFilter, *};
const GRADIENT_SIZE: Vec2 = vec2(256.0, 18.0);
const BLACK: Color32 = Color32::BLACK;
const GREEN: Color32 = Color32::GREEN;
@ -16,7 +17,6 @@ pub struct ColorTest {
tex_mngr: TextureManager,
vertex_gradients: bool,
texture_gradients: bool,
srgb: bool,
}
impl Default for ColorTest {
@ -25,7 +25,6 @@ impl Default for ColorTest {
tex_mngr: Default::default(),
vertex_gradients: true,
texture_gradients: true,
srgb: false,
}
}
}
@ -38,12 +37,17 @@ impl ColorTest {
ui.add(crate::egui_github_link_file!());
});
ui.label("This is made to test that the egui painter backend is set up correctly, so that all colors are interpolated and blended in linear space with premultiplied alpha.");
ui.label("If everything is set up correctly, all groups of gradients will look uniform");
ui.horizontal_wrapped(|ui|{
ui.label("This is made to test that the egui painter backend is set up correctly.");
ui.add(egui::Label::new("").sense(egui::Sense::click()))
.on_hover_text("The texture sampling should be sRGB-aware, and everyt other color operation should be done in gamma-space (sRGB). All colors should use pre-multiplied alpha");
});
ui.label("If the rendering is done right, all groups of gradients will look uniform.");
ui.horizontal(|ui| {
ui.checkbox(&mut self.vertex_gradients, "Vertex gradients");
ui.checkbox(&mut self.texture_gradients, "Texture gradients");
ui.checkbox(&mut self.srgb, "Show naive sRGBA horror");
});
ui.heading("sRGB color test");
ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500");
@ -56,12 +60,13 @@ impl ColorTest {
ui.separator();
ui.label("Test that vertex color times texture color is done in linear space:");
ui.label("Test that vertex color times texture color is done in gamma space:");
ui.scope(|ui| {
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
let tex_color = Rgba::from_rgb(1.0, 0.25, 0.25);
let vertex_color = Rgba::from_rgb(0.5, 0.75, 0.75);
let tex_color = Color32::from_rgb(64, 128, 255);
let vertex_color = Color32::from_rgb(128, 196, 196);
let ground_truth = mul_color_gamma(tex_color, vertex_color);
ui.horizontal(|ui| {
let color_size = ui.spacing().interact_size;
@ -72,13 +77,13 @@ impl ColorTest {
ui.label(" vertex color =");
});
{
let g = Gradient::one_color(Color32::from(tex_color * vertex_color));
let g = Gradient::one_color(ground_truth);
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g);
}
ui.horizontal(|ui| {
let g = Gradient::one_color(Color32::from(tex_color));
let g = Gradient::one_color(tex_color);
let tex = self.tex_mngr.get(ui.ctx(), &g);
let texel_offset = 0.5 / (g.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
@ -93,31 +98,25 @@ impl ColorTest {
// TODO(emilk): test color multiplication (image tint),
// to make sure vertex and texture color multiplication is done in linear space.
self.show_gradients(ui, WHITE, (RED, GREEN));
if self.srgb {
ui.label("Notice the darkening in the center of the naive sRGB interpolation.");
}
ui.separator();
ui.label("Gamma interpolation:");
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Gamma);
ui.separator();
self.show_gradients(ui, RED, (TRANSPARENT, GREEN));
self.show_gradients(ui, RED, (TRANSPARENT, GREEN), Interpolation::Gamma);
ui.separator();
self.show_gradients(ui, WHITE, (TRANSPARENT, GREEN));
if self.srgb {
ui.label(
"Notice how the linear blend stays green while the naive sRGBA interpolation looks gray in the middle.",
);
}
self.show_gradients(ui, WHITE, (TRANSPARENT, GREEN), Interpolation::Gamma);
ui.separator();
self.show_gradients(ui, BLACK, (BLACK, WHITE));
self.show_gradients(ui, BLACK, (BLACK, WHITE), Interpolation::Gamma);
ui.separator();
self.show_gradients(ui, WHITE, (BLACK, TRANSPARENT));
self.show_gradients(ui, WHITE, (BLACK, TRANSPARENT), Interpolation::Gamma);
ui.separator();
self.show_gradients(ui, BLACK, (TRANSPARENT, WHITE));
self.show_gradients(ui, BLACK, (TRANSPARENT, WHITE), Interpolation::Gamma);
ui.separator();
ui.label("Additive blending: add more and more blue to the red background:");
@ -125,18 +124,40 @@ impl ColorTest {
ui,
RED,
(TRANSPARENT, Color32::from_rgb_additive(0, 0, 255)),
Interpolation::Gamma,
);
ui.separator();
ui.label("Linear interpolation (texture sampling):");
self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Linear);
ui.separator();
pixel_test(ui);
ui.separator();
ui.label("Testing text rendering:");
text_on_bg(ui, Color32::from_gray(200), Color32::from_gray(230)); // gray on gray
text_on_bg(ui, Color32::from_gray(140), Color32::from_gray(28)); // dark mode normal text
// Matches Mac Font book (useful for testing):
text_on_bg(ui, Color32::from_gray(39), Color32::from_gray(255));
text_on_bg(ui, Color32::from_gray(220), Color32::from_gray(30));
ui.separator();
blending_and_feathering_test(ui);
}
fn show_gradients(&mut self, ui: &mut Ui, bg_fill: Color32, (left, right): (Color32, Color32)) {
fn show_gradients(
&mut self,
ui: &mut Ui,
bg_fill: Color32,
(left, right): (Color32, Color32),
interpolation: Interpolation,
) {
let is_opaque = left.is_opaque() && right.is_opaque();
ui.horizontal(|ui| {
@ -154,11 +175,12 @@ impl ColorTest {
ui.scope(|ui| {
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
if is_opaque {
let g = Gradient::ground_truth_linear_gradient(left, right);
let g = Gradient::ground_truth_gradient(left, right, interpolation);
self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g);
self.tex_gradient(ui, "Ground Truth (CPU gradient) - texture", bg_fill, &g);
} else {
let g = Gradient::ground_truth_linear_gradient(left, right).with_bg_fill(bg_fill);
let g = Gradient::ground_truth_gradient(left, right, interpolation)
.with_bg_fill(bg_fill);
self.vertex_gradient(
ui,
"Ground Truth (CPU gradient, CPU blending) - vertices",
@ -171,30 +193,27 @@ impl ColorTest {
bg_fill,
&g,
);
let g = Gradient::ground_truth_linear_gradient(left, right);
let g = Gradient::ground_truth_gradient(left, right, interpolation);
self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g);
self.tex_gradient(ui, "CPU gradient, GPU blending - texture", bg_fill, &g);
}
let g = Gradient::texture_gradient(left, right);
let g = Gradient::endpoints(left, right);
match interpolation {
Interpolation::Linear => {
// texture sampler is sRGBA aware, and should therefore be linear
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
}
Interpolation::Gamma => {
// vertex shader uses gamma
self.vertex_gradient(
ui,
"Triangle mesh of width 2 (test vertex decode and interpolation)",
bg_fill,
&g,
);
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
if self.srgb {
let g =
Gradient::ground_truth_bad_srgba_gradient(left, right).with_bg_fill(bg_fill);
self.vertex_gradient(
ui,
"Triangle mesh with naive sRGBA interpolation (WRONG)",
bg_fill,
&g,
);
self.tex_gradient(ui, "Naive sRGBA interpolation (WRONG)", bg_fill, &g);
}
}
});
}
@ -258,6 +277,12 @@ fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Respon
response
}
#[derive(Clone, Copy)]
enum Interpolation {
Linear,
Gamma,
}
#[derive(Clone, Hash, PartialEq, Eq)]
struct Gradient(pub Vec<Color32>);
@ -266,10 +291,21 @@ impl Gradient {
Self(vec![srgba, srgba])
}
pub fn texture_gradient(left: Color32, right: Color32) -> Self {
pub fn endpoints(left: Color32, right: Color32) -> Self {
Self(vec![left, right])
}
pub fn ground_truth_gradient(
left: Color32,
right: Color32,
interpolation: Interpolation,
) -> Self {
match interpolation {
Interpolation::Linear => Self::ground_truth_linear_gradient(left, right),
Interpolation::Gamma => Self::ground_truth_gamma_gradient(left, right),
}
}
pub fn ground_truth_linear_gradient(left: Color32, right: Color32) -> Self {
let left = Rgba::from(left);
let right = Rgba::from(right);
@ -285,33 +321,32 @@ impl Gradient {
)
}
/// This is how a bad person blends `sRGBA`
pub fn ground_truth_bad_srgba_gradient(left: Color32, right: Color32) -> Self {
pub fn ground_truth_gamma_gradient(left: Color32, right: Color32) -> Self {
let n = 255;
Self(
(0..=n)
.map(|i| {
let t = i as f32 / n as f32;
Color32::from_rgba_premultiplied(
lerp((left[0] as f32)..=(right[0] as f32), t).round() as u8, // Don't ever do this please!
lerp((left[1] as f32)..=(right[1] as f32), t).round() as u8, // Don't ever do this please!
lerp((left[2] as f32)..=(right[2] as f32), t).round() as u8, // Don't ever do this please!
lerp((left[3] as f32)..=(right[3] as f32), t).round() as u8, // Don't ever do this please!
)
lerp_color_gamma(left, right, t)
})
.collect(),
)
}
/// Do premultiplied alpha-aware blending of the gradient on top of the fill color
/// in gamma-space.
pub fn with_bg_fill(self, bg: Color32) -> Self {
let bg = Rgba::from(bg);
Self(
self.0
.into_iter()
.map(|fg| {
let fg = Rgba::from(fg);
Color32::from(bg * (1.0 - fg.a()) + fg)
let a = fg.a() as f32 / 255.0;
Color32::from_rgba_premultiplied(
(bg[0] as f32 * (1.0 - a) + fg[0] as f32).round() as u8,
(bg[1] as f32 * (1.0 - a) + fg[1] as f32).round() as u8,
(bg[2] as f32 * (1.0 - a) + fg[2] as f32).round() as u8,
(bg[3] as f32 * (1.0 - a) + fg[3] as f32).round() as u8,
)
})
.collect(),
)
@ -380,8 +415,6 @@ fn pixel_test(ui: &mut Ui) {
}
fn blending_and_feathering_test(ui: &mut Ui) {
ui.label("Some fine lines for testing anti-aliasing and blending:");
let size = vec2(512.0, 512.0);
let (response, painter) = ui.allocate_painter(size, Sense::hover());
let rect = response.rect;
@ -397,32 +430,67 @@ fn blending_and_feathering_test(ui: &mut Ui) {
paint_fine_lines_and_text(&painter, bottom_half, Color32::BLACK);
}
fn text_on_bg(ui: &mut egui::Ui, fg: Color32, bg: Color32) {
assert!(fg.is_opaque());
assert!(bg.is_opaque());
ui.horizontal(|ui| {
ui.label(
RichText::from("▣ The quick brown fox jumps over the lazy dog and runs away.")
.background_color(bg)
.color(fg),
);
ui.label(format!(
"({} {} {}) on ({} {} {})",
fg.r(),
fg.g(),
fg.b(),
bg.r(),
bg.g(),
bg.b(),
));
});
}
fn paint_fine_lines_and_text(painter: &egui::Painter, mut rect: Rect, color: Color32) {
{
let mut x = 0.0;
let mut y = 0.0;
for opacity in [1.00, 0.50, 0.25, 0.10, 0.05, 0.02, 0.01, 0.00] {
painter.text(
rect.center_top() + vec2(0.0, x),
rect.center_top() + vec2(0.0, y),
Align2::LEFT_TOP,
format!("{:.0}% white", 100.0 * opacity),
FontId::proportional(14.0),
Color32::WHITE.linear_multiply(opacity),
);
painter.text(
rect.center_top() + vec2(80.0, x),
rect.center_top() + vec2(80.0, y),
Align2::LEFT_TOP,
format!("{:.0}% gray", 100.0 * opacity),
FontId::proportional(14.0),
Color32::GRAY.linear_multiply(opacity),
);
painter.text(
rect.center_top() + vec2(160.0, x),
rect.center_top() + vec2(160.0, y),
Align2::LEFT_TOP,
format!("{:.0}% black", 100.0 * opacity),
FontId::proportional(14.0),
Color32::BLACK.linear_multiply(opacity),
);
x += 20.0;
y += 20.0;
}
for font_size in [6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 14.0] {
painter.text(
rect.center_top() + vec2(0.0, y),
Align2::LEFT_TOP,
format!(
"{font_size}px - The quick brown fox jumps over the lazy dog and runs away."
),
FontId::proportional(font_size),
color,
);
y += font_size + 1.0;
}
}
@ -472,3 +540,21 @@ fn paint_fine_lines_and_text(painter: &egui::Painter, mut rect: Rect, color: Col
mesh.add_triangle(1, 2, 3);
painter.add(mesh);
}
fn mul_color_gamma(left: Color32, right: Color32) -> Color32 {
Color32::from_rgba_premultiplied(
(left.r() as f32 * right.r() as f32 / 255.0).round() as u8,
(left.g() as f32 * right.g() as f32 / 255.0).round() as u8,
(left.b() as f32 * right.b() as f32 / 255.0).round() as u8,
(left.a() as f32 * right.a() as f32 / 255.0).round() as u8,
)
}
fn lerp_color_gamma(left: Color32, right: Color32, t: f32) -> Color32 {
Color32::from_rgba_premultiplied(
lerp((left[0] as f32)..=(right[0] as f32), t).round() as u8,
lerp((left[1] as f32)..=(right[1] as f32), t).round() as u8,
lerp((left[2] as f32)..=(right[2] as f32), t).round() as u8,
lerp((left[3] as f32)..=(right[3] as f32), t).round() as u8,
)
}

View file

@ -54,4 +54,5 @@ document-features = { version = "0.2", optional = true }
[dev-dependencies]
egui_demo_lib = { version = "0.19.0", path = "../egui_demo_lib", default-features = false }
image = { version = "0.24", default-features = false, features = ["png"] }

View file

@ -115,7 +115,7 @@ fn create_display(event_loop: &glutin::event_loop::EventLoop<()>) -> glium::Disp
let context_builder = glutin::ContextBuilder::new()
.with_depth_buffer(0)
.with_srgb(true)
.with_srgb(false)
.with_stencil_buffer(0)
.with_vsync(true);

View file

@ -10,6 +10,8 @@ fn main() {
let mut egui_glium = egui_glium::EguiGlium::new(&display, &event_loop);
let mut color_test = egui_demo_lib::ColorTest::default();
event_loop.run(move |event, _, control_flow| {
let mut redraw = || {
let mut quit = false;
@ -21,6 +23,12 @@ fn main() {
quit = true;
}
});
egui::CentralPanel::default().show(egui_ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
color_test.ui(ui);
});
});
});
*control_flow = if quit {
@ -93,7 +101,7 @@ fn create_display(event_loop: &glutin::event_loop::EventLoop<()>) -> glium::Disp
let context_builder = glutin::ContextBuilder::new()
.with_depth_buffer(0)
.with_srgb(true)
.with_srgb(false)
.with_stencil_buffer(0)
.with_vsync(true);

View file

@ -8,7 +8,6 @@ use {
glium::{
implement_vertex,
index::PrimitiveType,
program,
texture::{self, srgb_texture2d::SrgbTexture2d},
uniform,
uniforms::{MagnifySamplerFilter, SamplerWrapFunction},
@ -26,31 +25,77 @@ pub struct Painter {
next_native_tex_id: u64,
}
fn create_program(
facade: &dyn glium::backend::Facade,
vertex_shader: &str,
fragment_shader: &str,
) -> glium::program::Program {
let input = glium::program::ProgramCreationInput::SourceCode {
vertex_shader,
tessellation_control_shader: None,
tessellation_evaluation_shader: None,
geometry_shader: None,
fragment_shader,
transform_feedback_varyings: None,
outputs_srgb: true,
uses_point_size: false,
};
glium::program::Program::new(facade, input)
.unwrap_or_else(|err| panic!("Failed to compile shader: {}", err))
}
impl Painter {
pub fn new(facade: &dyn glium::backend::Facade) -> Painter {
use glium::CapabilitiesSource as _;
let max_texture_side = facade.get_capabilities().max_texture_size as _;
let program = program! {
let program = if facade
.get_context()
.is_glsl_version_supported(&glium::Version(glium::Api::Gl, 1, 4))
{
eprintln!("Using GL 1.4");
create_program(
facade,
120 => {
vertex: include_str!("shader/vertex_120.glsl"),
fragment: include_str!("shader/fragment_120.glsl"),
},
140 => {
vertex: include_str!("shader/vertex_140.glsl"),
fragment: include_str!("shader/fragment_140.glsl"),
},
100 es => {
vertex: include_str!("shader/vertex_100es.glsl"),
fragment: include_str!("shader/fragment_100es.glsl"),
},
300 es => {
vertex: include_str!("shader/vertex_300es.glsl"),
fragment: include_str!("shader/fragment_300es.glsl"),
},
}
.expect("Failed to compile shader");
include_str!("shader/vertex_140.glsl"),
include_str!("shader/fragment_140.glsl"),
)
} else if facade
.get_context()
.is_glsl_version_supported(&glium::Version(glium::Api::Gl, 1, 2))
{
eprintln!("Using GL 1.2");
create_program(
facade,
include_str!("shader/vertex_120.glsl"),
include_str!("shader/fragment_120.glsl"),
)
} else if facade
.get_context()
.is_glsl_version_supported(&glium::Version(glium::Api::GlEs, 3, 0))
{
eprintln!("Using GL ES 3.0");
create_program(
facade,
include_str!("shader/vertex_300es.glsl"),
include_str!("shader/fragment_300es.glsl"),
)
} else if facade
.get_context()
.is_glsl_version_supported(&glium::Version(glium::Api::GlEs, 1, 0))
{
eprintln!("Using GL ES 1.0");
create_program(
facade,
include_str!("shader/vertex_100es.glsl"),
include_str!("shader/fragment_100es.glsl"),
)
} else {
panic!(
"Failed to find a compatible shader for OpenGL version {:?}",
facade.get_version()
)
};
Painter {
max_texture_side,
@ -236,13 +281,10 @@ impl Painter {
);
image.pixels.iter().map(|color| color.to_tuple()).collect()
}
egui::ImageData::Font(image) => {
let gamma = 1.0;
image
.srgba_pixels(gamma)
egui::ImageData::Font(image) => image
.srgba_pixels(None)
.map(|color| color.to_tuple())
.collect()
}
.collect(),
};
let glium_image = glium::texture::RawImage2d {
data: std::borrow::Cow::Owned(pixels),

View file

@ -2,7 +2,7 @@
precision mediump float;
uniform sampler2D u_sampler;
varying vec4 v_rgba;
varying vec4 v_rgba_gamma; // 0-1 gamma sRGBA
varying vec2 v_tc;
// 0-255 sRGB from 0-1 linear
@ -30,16 +30,9 @@ vec4 linear_from_srgba(vec4 srgba) {
}
void main() {
// We must decode the colors, since WebGL doesn't come with sRGBA textures:
vec4 texture_rgba = linear_from_srgba(texture2D(u_sampler, v_tc) * 255.0);
// WebGL doesn't come with sRGBA textures:
vec4 texture_in_gamma = texture2D(u_sampler, v_tc);
/// Multiply vertex color with texture color (in linear space).
gl_FragColor = v_rgba * texture_rgba;
// We must gamma-encode again since WebGL doesn't support linear blending in the framebuffer.
gl_FragColor = srgba_from_linear(v_rgba * texture_rgba) / 255.0;
// WebGL doesn't support linear blending in the framebuffer,
// so we apply this hack to at least get a bit closer to the desired blending:
gl_FragColor.a = pow(gl_FragColor.a, 1.6); // Empiric nonsense
// Multiply vertex color with texture color (in gamma space).
gl_FragColor = v_rgba_gamma * texture_in_gamma;
}

View file

@ -1,11 +1,31 @@
#version 120
uniform sampler2D u_sampler;
varying vec4 v_rgba;
varying vec4 v_rgba_gamma; // 0-1 gamma sRGBA
varying vec2 v_tc;
void main() {
// The texture sampler is sRGB aware, and glium already expects linear rgba output
// so no need for any sRGB conversions here:
gl_FragColor = v_rgba * texture2D(u_sampler, 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);
}
// 0-1 gamma from 0-1 linear
vec4 gamma_from_linear_rgba(vec4 linear_rgba) {
return vec4(srgb_from_linear(linear_rgba.rgb) / 255.0, linear_rgba.a);
}
void main() {
// The texture is set up with `SRGB8_ALPHA8`
vec4 texture_in_gamma = gamma_from_linear_rgba(texture2D(u_sampler, v_tc));
// Multiply vertex color with texture color (in gamma space).
gl_FragColor = v_rgba_gamma * texture_in_gamma;
}

View file

@ -1,12 +1,32 @@
#version 140
uniform sampler2D u_sampler;
in vec4 v_rgba;
in vec4 v_rgba_gamma;
in vec2 v_tc;
out vec4 f_color;
void main() {
// The texture sampler is sRGB aware, and glium already expects linear rgba output
// so no need for any sRGB conversions here:
f_color = v_rgba * texture(u_sampler, 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);
}
// 0-1 gamma from 0-1 linear
vec4 gamma_from_linear_rgba(vec4 linear_rgba) {
return vec4(srgb_from_linear(linear_rgba.rgb) / 255.0, linear_rgba.a);
}
void main() {
// The texture is set up with `SRGB8_ALPHA8`
vec4 texture_in_gamma = gamma_from_linear_rgba(texture(u_sampler, v_tc));
// Multiply vertex color with texture color (in gamma space).
f_color = v_rgba_gamma * texture_in_gamma;
}

View file

@ -2,7 +2,7 @@
precision mediump float;
uniform sampler2D u_sampler;
varying vec4 v_rgba;
varying vec4 v_rgba_gamma; // 0-1 gamma sRGBA
varying vec2 v_tc;
// 0-255 sRGB from 0-1 linear
@ -13,21 +13,20 @@ vec3 srgb_from_linear(vec3 rgb) {
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;
// We must gamma-encode again since WebGL doesn't support linear blending in the framebuffer.
gl_FragColor = srgba_from_linear(v_rgba * texture_rgba) / 255.0;
// WebGL doesn't support linear blending in the framebuffer,
// so we apply this hack to at least get a bit closer to the desired blending:
gl_FragColor.a = pow(gl_FragColor.a, 1.6); // Empiric nonsense
// 0-1 gamma from 0-1 linear
vec4 gamma_from_linear_rgba(vec4 linear_rgba) {
return vec4(srgb_from_linear(linear_rgba.rgb) / 255.0, linear_rgba.a);
}
void main() {
// The texture is set up with `SRGB8_ALPHA8`
vec4 texture_in_gamma = gamma_from_linear_rgba(texture2D(u_sampler, v_tc));
// Multiply vertex color with texture color (in gamma space).
gl_FragColor = v_rgba_gamma * texture_in_gamma;
}

View file

@ -5,28 +5,15 @@ uniform vec2 u_screen_size;
attribute vec2 a_pos;
attribute vec2 a_tc;
attribute vec4 a_srgba;
varying vec4 v_rgba;
varying vec4 v_rgba_gamma; // 0-1 gamma sRGBA
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);
// egui encodes vertex colors in gamma spaces, so we must decode the colors here:
v_rgba = linear_from_srgba(a_srgba);
v_rgba_gamma = a_srgba / 255.0;
v_tc = a_tc;
}

View file

@ -4,28 +4,15 @@ uniform vec2 u_screen_size;
attribute vec2 a_pos;
attribute vec4 a_srgba; // 0-255 sRGB
attribute vec2 a_tc;
varying vec4 v_rgba;
varying vec4 v_rgba_gamma; // 0-1 gamma sRGBA
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);
// egui encodes vertex colors in gamma spaces, so we must decode the colors here:
v_rgba = linear_from_srgba(a_srgba);
v_rgba_gamma = a_srgba / 255.0;
v_tc = a_tc;
}

View file

@ -4,28 +4,15 @@ uniform vec2 u_screen_size;
in vec2 a_pos;
in vec4 a_srgba; // 0-255 sRGB
in vec2 a_tc;
out vec4 v_rgba;
out vec4 v_rgba_gamma;
out 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, 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 spaces, so we must decode the colors here:
v_rgba = linear_from_srgba(a_srgba);
v_rgba_gamma = a_srgba / 255.0;
v_tc = a_tc;
}

View file

@ -5,28 +5,15 @@ uniform vec2 u_screen_size;
attribute vec2 a_pos;
attribute vec2 a_tc;
attribute vec4 a_srgba;
varying vec4 v_rgba;
varying vec4 v_rgba_gamma; // 0-1 gamma sRGBA
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);
// egui encodes vertex colors in gamma spaces, so we must decode the colors here:
v_rgba = linear_from_srgba(a_srgba);
v_rgba_gamma = a_srgba / 255.0;
v_tc = a_tc;
}

View file

@ -6,6 +6,7 @@ All notable changes to the `egui_glow` integration will be noted in this file.
* Allow empty textures.
* Added `shader_version` variable on `EguiGlow::new` for easier cross compilling on different OpenGL | ES targets ([#1993](https://github.com/emilk/egui/pull/1993)).
## 0.19.0 - 2022-08-20
* MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)).
* `EguiGlow::new` now takes an `EventLoopWindowTarget<E>` instead of a `winit::Window` ([#1634](https://github.com/emilk/egui/pull/1634)).

View file

@ -116,7 +116,7 @@ fn create_display(
let gl_window = unsafe {
glutin::ContextBuilder::new()
.with_depth_buffer(0)
.with_srgb(true)
.with_srgb(false)
.with_stencil_buffer(0)
.with_vsync(true)
.build_windowed(window_builder, event_loop)

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);
}
#else
// 0-255 sRGB from 0-1 linear
vec3 srgb_from_linear(vec3 rgb) {
// 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(3294.6);
vec3 higher = vec3(269.025) * pow(rgb, vec3(1.0 / 2.4)) - vec3(14.025);
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));
}
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);
// 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() {
// 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;
}
}
#if SRGB_TEXTURES
vec4 texture_in_gamma = srgba_gamma_from_linear(texture2D(u_sampler, v_tc));
#else
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);
})

View file

@ -4,6 +4,7 @@ All notable changes to the epaint crate will be documented in this file.
## Unreleased
* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)).
* ⚠️ BREAKING: epaint now expects integrations to do all color blending in gamma space ([#2071](https://github.com/emilk/egui/pull/2071)).
## 0.19.0 - 2022-08-20

View file

@ -326,7 +326,7 @@ impl Rgba {
/// Transparent white
#[inline(always)]
pub fn from_white_alpha(a: f32) -> Self {
crate::epaint_assert!(0.0 <= a && a <= 1.0);
crate::epaint_assert!(0.0 <= a && a <= 1.0, "a: {}", a);
Self([a, a, a, a])
}

View file

@ -195,17 +195,19 @@ impl FontImage {
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
///
/// `gamma` should normally be set to 1.0.
/// If you are having problems with text looking skinny and pixelated, try
/// setting a lower gamma, e.g. `0.5`.
pub fn srgba_pixels(&'_ self, gamma: f32) -> impl ExactSizeIterator<Item = Color32> + '_ {
/// `gamma` should normally be set to `None`.
///
/// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`.
pub fn srgba_pixels(
&'_ self,
gamma: Option<f32>,
) -> impl ExactSizeIterator<Item = Color32> + '_ {
let gamma = gamma.unwrap_or(0.55); // TODO(emilk): this default coverage gamma is a magic constant, chosen by eye. I don't even know why we need it.
self.pixels.iter().map(move |coverage| {
// This is arbitrarily chosen to make text look as good as possible.
// In particular, it looks good with gamma=1 and the default eframe backend,
// which uses linear blending.
// See https://github.com/emilk/egui/issues/1410
let a = fast_round(coverage.powf(gamma / 2.2) * 255.0);
Color32::from_rgba_premultiplied(a, a, a, a) // this makes no sense, but works
let alpha = coverage.powf(gamma);
// We want to multiply with `vec4(alpha)` in the fragment shader:
let a = fast_round(alpha * 255.0);
Color32::from_rgba_premultiplied(a, a, a, a)
})
}

View file

@ -14,7 +14,7 @@ pub struct Shadow {
}
impl Shadow {
/// Tooltips, menus, …
/// Tooltips, menus, …, for dark mode.
pub fn small_dark() -> Self {
Self {
extrusion: 16.0,
@ -22,15 +22,15 @@ impl Shadow {
}
}
/// Tooltips, menus, …
/// Tooltips, menus, …, for light mode.
pub fn small_light() -> Self {
Self {
extrusion: 16.0,
color: Color32::from_black_alpha(32),
color: Color32::from_black_alpha(20),
}
}
/// Subtle and nice on dark backgrounds
/// Used for widnows in dark mode.
pub fn big_dark() -> Self {
Self {
extrusion: 32.0,
@ -38,11 +38,11 @@ impl Shadow {
}
}
/// Subtle and nice on white backgrounds
/// Used for widnows in light mode.
pub fn big_light() -> Self {
Self {
extrusion: 32.0,
color: Color32::from_black_alpha(40),
color: Color32::from_black_alpha(16),
}
}

View file

@ -18,6 +18,7 @@ fn main() {
struct MyApp {
image: RetainedImage,
tint: egui::Color32,
}
impl Default for MyApp {
@ -28,6 +29,7 @@ impl Default for MyApp {
include_bytes!("rust-logo-256x256.png"),
)
.unwrap(),
tint: egui::Color32::from_rgb(255, 0, 255),
}
}
}
@ -38,12 +40,22 @@ impl eframe::App for MyApp {
ui.heading("This is an image:");
self.image.show(ui);
ui.heading("This is a rotated image:");
ui.heading("This is a rotated image with a tint:");
ui.add(
egui::Image::new(self.image.texture_id(ctx), self.image.size_vec2())
.rotate(45.0_f32.to_radians(), egui::Vec2::splat(0.5)),
.rotate(45.0_f32.to_radians(), egui::Vec2::splat(0.5))
.tint(self.tint),
);
ui.horizontal(|ui| {
ui.label("Tint:");
egui::color_picker::color_edit_button_srgba(
ui,
&mut self.tint,
egui::color_picker::Alpha::BlendOrAdditive,
);
});
ui.heading("This is an image you can click:");
ui.add(egui::ImageButton::new(
self.image.texture_id(ctx),