From 0b0ef583b61e2efae67f89067252f4083463bae7 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 14 Nov 2023 13:44:25 +0000 Subject: [PATCH] Improved Text Rendering (#10537) # Objective The quality of Bevy's text rendering can vary wildly depending on the font, font size, pixel alignment and scale factor. But this situation can be improved dramatically with some small adjustments. ## Solution * Text node positions are rounded to the nearest physical pixel before rendering. * Each glyph texture has a 1-pixel wide transparent border added along its edges. This means font atlases will use more memory because of the extra pixel of padding for each glyph but it's more than worth it I think (although glyph size is increased by 2 pixels on both axes, the net increase is 1 pixel as the font texture atlas's padding has been removed). ## Results Screenshots are from the 'ui' example with a scale factor of 1.5. Things can get much uglier with the right font and worst scale factortm. ### before list-bad-text ### after good_list_text --- ## Changelog * Font texture atlases are no longer padded. * Each glyph texture has a 1-pixel wide padding added along its edges. * Text node positions are rounded to the nearest physical pixel before rendering. --- crates/bevy_text/src/font.rs | 10 +++++++--- crates/bevy_text/src/font_atlas.rs | 2 +- crates/bevy_ui/src/render/mod.rs | 30 ++++++++++++++++++++++-------- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index e968e37feb..1b91430932 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -20,11 +20,15 @@ impl Font { pub fn get_outlined_glyph_texture(outlined_glyph: OutlinedGlyph) -> Image { let bounds = outlined_glyph.px_bounds(); - let width = bounds.width() as usize; - let height = bounds.height() as usize; + // Increase the length of the glyph texture by 2-pixels on each axis to make space + // for a pixel wide transparent border along its edges. + let width = bounds.width() as usize + 2; + let height = bounds.height() as usize + 2; let mut alpha = vec![0.0; width * height]; outlined_glyph.draw(|x, y, v| { - alpha[y as usize * width + x as usize] = v; + // Displace the glyph by 1 pixel on each axis so that it is drawn in the center of the texture. + // This leaves a pixel wide transparent border around the glyph. + alpha[(y + 1) as usize * width + x as usize + 1] = v; }); // TODO: make this texture grayscale diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index be1903c121..cdf9e09588 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -65,7 +65,7 @@ impl FontAtlas { Self { texture_atlas: texture_atlases.add(texture_atlas), glyph_to_atlas_index: HashMap::default(), - dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1), + dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 0), } } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index b86f6544e6..97fb405e63 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -21,6 +21,7 @@ use crate::{ use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle}; use bevy_ecs::prelude::*; +use bevy_math::Vec3Swizzles; use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec4Swizzles}; use bevy_render::{ camera::Camera, @@ -623,13 +624,14 @@ pub fn extract_text_uinodes( >, ) { // TODO: Support window-independent UI scale: https://github.com/bevyengine/bevy/issues/5621 - let scale_factor = windows - .get_single() - .map(|window| window.resolution.scale_factor()) - .unwrap_or(1.0) - * ui_scale.0; - let inverse_scale_factor = (scale_factor as f32).recip(); + let scale_factor = (windows + .get_single() + .map(|window| window.scale_factor()) + .unwrap_or(1.) + * ui_scale.0) as f32; + + let inverse_scale_factor = scale_factor.recip(); for (uinode, global_transform, text, text_layout_info, view_visibility, clip) in uinode_query.iter() @@ -638,8 +640,20 @@ pub fn extract_text_uinodes( if !view_visibility.get() || uinode.size().x == 0. || uinode.size().y == 0. { continue; } - let transform = global_transform.compute_matrix() - * Mat4::from_translation(-0.5 * uinode.size().extend(0.)); + + let mut affine = global_transform.affine(); + + // Align the text to the nearest physical pixel: + // * Translate by minus the text node's half-size + // (The transform translates to the center of the node but the text coordinates are relative to the node's top left corner) + // * Multiply the logical coordinates by the scale factor to get its position in physical coordinates + // * Round the physical position to the nearest physical pixel + // * Multiply by the rounded physical position by the inverse scale factor to return to logical coordinates + let logical_top_left = affine.translation.xy() - 0.5 * uinode.size(); + let physical_nearest_pixel = (logical_top_left * scale_factor).round(); + let logical_top_left_nearest_pixel = physical_nearest_pixel * inverse_scale_factor; + affine.translation = logical_top_left_nearest_pixel.extend(0.).into(); + let transform = Mat4::from(affine); let mut color = Color::WHITE; let mut current_section = usize::MAX;