mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 14:20:04 -04:00
Improve text redering and do all color operation in gamma space (#2071)
This commit is contained in:
@@ -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.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.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.vertex_gradients, "Vertex gradients");
|
||||
ui.checkbox(&mut self.texture_gradients, "Texture gradients");
|
||||
});
|
||||
|
||||
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);
|
||||
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);
|
||||
let g = Gradient::endpoints(left, right);
|
||||
|
||||
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);
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user