mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 15:14:50 +00:00
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:
parent
749f3d7430
commit
0b0ef583b6
3 changed files with 30 additions and 12 deletions
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue