Skip to content

Commit

Permalink
Improved Text Rendering (#10537)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
ickshonpe authored and cart committed Nov 30, 2023
1 parent 6382862 commit 0483b69
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 11 deletions.
10 changes: 7 additions & 3 deletions crates/bevy_text/src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_text/src/font_atlas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down
28 changes: 21 additions & 7 deletions crates/bevy_ui/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -618,13 +619,14 @@ pub fn extract_text_uinodes(
>,
) {
// TODO: Support window-independent UI scale: https://github.com/bevyengine/bevy/issues/5621
let scale_factor = windows

let scale_factor = (windows
.get_single()
.map(|window| window.resolution.scale_factor())
.unwrap_or(1.0)
* ui_scale.0;
.map(|window| window.scale_factor())
.unwrap_or(1.)
* ui_scale.0) as f32;

let inverse_scale_factor = (scale_factor as f32).recip();
let inverse_scale_factor = scale_factor.recip();

for (uinode, global_transform, text, text_layout_info, view_visibility, clip) in
uinode_query.iter()
Expand All @@ -633,8 +635,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;
Expand Down

0 comments on commit 0483b69

Please sign in to comment.