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
factor<sup>tm</sup>.

### before 
<img width="300" alt="list-bad-text"
src="https://github.com/bevyengine/bevy/assets/27962798/482b384d-8743-4bae-9a65-468ff1b4c301">

### after
<img width="300" alt="good_list_text"
src="https://github.com/bevyengine/bevy/assets/27962798/34323b0a-f714-47ba-9728-a59804987bc8">
 
---

## 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.
This commit is contained in:
ickshonpe 2023-11-14 13:44:25 +00:00 committed by Carter Anderson
parent 63828621e8
commit 0483b69a79
3 changed files with 30 additions and 12 deletions

View file

@ -20,11 +20,15 @@ impl Font {
pub fn get_outlined_glyph_texture(outlined_glyph: OutlinedGlyph) -> Image { pub fn get_outlined_glyph_texture(outlined_glyph: OutlinedGlyph) -> Image {
let bounds = outlined_glyph.px_bounds(); let bounds = outlined_glyph.px_bounds();
let width = bounds.width() as usize; // Increase the length of the glyph texture by 2-pixels on each axis to make space
let height = bounds.height() as usize; // 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]; let mut alpha = vec![0.0; width * height];
outlined_glyph.draw(|x, y, v| { 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 // TODO: make this texture grayscale

View file

@ -65,7 +65,7 @@ impl FontAtlas {
Self { Self {
texture_atlas: texture_atlases.add(texture_atlas), texture_atlas: texture_atlases.add(texture_atlas),
glyph_to_atlas_index: HashMap::default(), glyph_to_atlas_index: HashMap::default(),
dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1), dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 0),
} }
} }

View file

@ -21,6 +21,7 @@ use crate::{
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle}; use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle};
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_math::Vec3Swizzles;
use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec4Swizzles}; use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec4Swizzles};
use bevy_render::{ use bevy_render::{
camera::Camera, camera::Camera,
@ -618,13 +619,14 @@ pub fn extract_text_uinodes(
>, >,
) { ) {
// TODO: Support window-independent UI scale: https://github.com/bevyengine/bevy/issues/5621 // 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 for (uinode, global_transform, text, text_layout_info, view_visibility, clip) in
uinode_query.iter() uinode_query.iter()
@ -633,8 +635,20 @@ pub fn extract_text_uinodes(
if !view_visibility.get() || uinode.size().x == 0. || uinode.size().y == 0. { if !view_visibility.get() || uinode.size().x == 0. || uinode.size().y == 0. {
continue; 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 color = Color::WHITE;
let mut current_section = usize::MAX; let mut current_section = usize::MAX;