Subpixel text positioning (#1196)

* cleanup unnecessary changes from PR #1171

* add feature to correctly render glyphs with sub-pixel positioning
This commit is contained in:
Nathan Jeffords 2021-01-03 12:39:11 -08:00 committed by GitHub
parent 8a330a889d
commit 60be99859a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 122 additions and 21 deletions

View File

@ -72,6 +72,9 @@ serialize = ["bevy_internal/serialize"]
wayland = ["bevy_internal/wayland"] wayland = ["bevy_internal/wayland"]
x11 = ["bevy_internal/x11"] x11 = ["bevy_internal/x11"]
# enable rendering of font glyphs using subpixel accuracy
subpixel_glyph_atlas = ["bevy_internal/subpixel_glyph_atlas"]
[dependencies] [dependencies]
bevy_dylib = {path = "crates/bevy_dylib", version = "0.4.0", default-features = false, optional = true} bevy_dylib = {path = "crates/bevy_dylib", version = "0.4.0", default-features = false, optional = true}
bevy_internal = {path = "crates/bevy_internal", version = "0.4.0", default-features = false} bevy_internal = {path = "crates/bevy_internal", version = "0.4.0", default-features = false}

View File

@ -38,6 +38,9 @@ serialize = ["bevy_input/serialize"]
wayland = ["bevy_winit/wayland"] wayland = ["bevy_winit/wayland"]
x11 = ["bevy_winit/x11"] x11 = ["bevy_winit/x11"]
# enable rendering of font glyphs using subpixel accuracy
subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"]
[dependencies] [dependencies]
# bevy # bevy
bevy_app = { path = "../bevy_app", version = "0.4.0" } bevy_app = { path = "../bevy_app", version = "0.4.0" }

View File

@ -12,6 +12,9 @@ repository = "https://github.com/bevyengine/bevy"
license = "MIT" license = "MIT"
keywords = ["bevy"] keywords = ["bevy"]
[features]
subpixel_glyph_atlas = []
[dependencies] [dependencies]
# bevy # bevy
bevy_app = { path = "../bevy_app", version = "0.4.0" } bevy_app = { path = "../bevy_app", version = "0.4.0" }

View File

@ -1,13 +1,44 @@
use ab_glyph::GlyphId; use ab_glyph::{GlyphId, Point};
use bevy_asset::{Assets, Handle}; use bevy_asset::{Assets, Handle};
use bevy_math::Vec2; use bevy_math::Vec2;
use bevy_render::texture::{Extent3d, Texture, TextureDimension, TextureFormat}; use bevy_render::texture::{Extent3d, Texture, TextureDimension, TextureFormat};
use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlas}; use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlas};
use bevy_utils::HashMap; use bevy_utils::HashMap;
#[cfg(feature = "subpixel_glyph_atlas")]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct SubpixelOffset {
x: u16,
y: u16,
}
#[cfg(feature = "subpixel_glyph_atlas")]
impl From<Point> for SubpixelOffset {
fn from(p: Point) -> Self {
fn f(v: f32) -> u16 {
((v % 1.) * (u16::MAX as f32)) as u16
}
Self {
x: f(p.x),
y: f(p.y),
}
}
}
#[cfg(not(feature = "subpixel_glyph_atlas"))]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct SubpixelOffset;
#[cfg(not(feature = "subpixel_glyph_atlas"))]
impl From<Point> for SubpixelOffset {
fn from(_: Point) -> Self {
Self
}
}
pub struct FontAtlas { pub struct FontAtlas {
pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder,
pub glyph_to_atlas_index: HashMap<GlyphId, u32>, pub glyph_to_atlas_index: HashMap<(GlyphId, SubpixelOffset), u32>,
pub texture_atlas: Handle<TextureAtlas>, pub texture_atlas: Handle<TextureAtlas>,
} }
@ -31,12 +62,19 @@ impl FontAtlas {
} }
} }
pub fn get_glyph_index(&self, glyph_id: GlyphId) -> Option<u32> { pub fn get_glyph_index(
self.glyph_to_atlas_index.get(&glyph_id).copied() &self,
glyph_id: GlyphId,
subpixel_offset: SubpixelOffset,
) -> Option<u32> {
self.glyph_to_atlas_index
.get(&(glyph_id, subpixel_offset))
.copied()
} }
pub fn has_glyph(&self, glyph_id: GlyphId) -> bool { pub fn has_glyph(&self, glyph_id: GlyphId, subpixel_offset: SubpixelOffset) -> bool {
self.glyph_to_atlas_index.contains_key(&glyph_id) self.glyph_to_atlas_index
.contains_key(&(glyph_id, subpixel_offset))
} }
pub fn add_glyph( pub fn add_glyph(
@ -44,6 +82,7 @@ impl FontAtlas {
textures: &mut Assets<Texture>, textures: &mut Assets<Texture>,
texture_atlases: &mut Assets<TextureAtlas>, texture_atlases: &mut Assets<TextureAtlas>,
glyph_id: GlyphId, glyph_id: GlyphId,
subpixel_offset: SubpixelOffset,
texture: &Texture, texture: &Texture,
) -> bool { ) -> bool {
let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap();
@ -51,7 +90,8 @@ impl FontAtlas {
self.dynamic_texture_atlas_builder self.dynamic_texture_atlas_builder
.add_texture(texture_atlas, textures, texture) .add_texture(texture_atlas, textures, texture)
{ {
self.glyph_to_atlas_index.insert(glyph_id, index); self.glyph_to_atlas_index
.insert((glyph_id, subpixel_offset), index);
true true
} else { } else {
false false

View File

@ -1,5 +1,5 @@
use crate::{error::TextError, Font, FontAtlas}; use crate::{error::TextError, Font, FontAtlas};
use ab_glyph::{GlyphId, OutlinedGlyph}; use ab_glyph::{GlyphId, OutlinedGlyph, Point};
use bevy_asset::{Assets, Handle}; use bevy_asset::{Assets, Handle};
use bevy_core::FloatOrd; use bevy_core::FloatOrd;
use bevy_math::Vec2; use bevy_math::Vec2;
@ -35,11 +35,13 @@ impl FontAtlasSet {
self.font_atlases.iter() self.font_atlases.iter()
} }
pub fn has_glyph(&self, glyph_id: GlyphId, font_size: f32) -> bool { pub fn has_glyph(&self, glyph_id: GlyphId, glyph_position: Point, font_size: f32) -> bool {
self.font_atlases self.font_atlases
.get(&FloatOrd(font_size)) .get(&FloatOrd(font_size))
.map_or(false, |font_atlas| { .map_or(false, |font_atlas| {
font_atlas.iter().any(|atlas| atlas.has_glyph(glyph_id)) font_atlas
.iter()
.any(|atlas| atlas.has_glyph(glyph_id, glyph_position.into()))
}) })
} }
@ -51,6 +53,7 @@ impl FontAtlasSet {
) -> Result<GlyphAtlasInfo, TextError> { ) -> Result<GlyphAtlasInfo, TextError> {
let glyph = outlined_glyph.glyph(); let glyph = outlined_glyph.glyph();
let glyph_id = glyph.id; let glyph_id = glyph.id;
let glyph_position = glyph.position;
let font_size = glyph.scale.y; let font_size = glyph.scale.y;
let font_atlases = self let font_atlases = self
.font_atlases .font_atlases
@ -64,7 +67,13 @@ impl FontAtlasSet {
}); });
let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph); let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph);
let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool { let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool {
atlas.add_glyph(textures, texture_atlases, glyph_id, &glyph_texture) atlas.add_glyph(
textures,
texture_atlases,
glyph_id,
glyph_position.into(),
&glyph_texture,
)
}; };
if !font_atlases.iter_mut().any(add_char_to_font_atlas) { if !font_atlases.iter_mut().any(add_char_to_font_atlas) {
font_atlases.push(FontAtlas::new( font_atlases.push(FontAtlas::new(
@ -76,19 +85,23 @@ impl FontAtlasSet {
textures, textures,
texture_atlases, texture_atlases,
glyph_id, glyph_id,
glyph_position.into(),
&glyph_texture, &glyph_texture,
) { ) {
return Err(TextError::FailedToAddGlyph(glyph_id)); return Err(TextError::FailedToAddGlyph(glyph_id));
} }
} }
Ok(self.get_glyph_atlas_info(font_size, glyph_id).unwrap()) Ok(self
.get_glyph_atlas_info(font_size, glyph_id, glyph_position)
.unwrap())
} }
pub fn get_glyph_atlas_info( pub fn get_glyph_atlas_info(
&self, &self,
font_size: f32, font_size: f32,
glyph_id: GlyphId, glyph_id: GlyphId,
position: Point,
) -> Option<GlyphAtlasInfo> { ) -> Option<GlyphAtlasInfo> {
self.font_atlases self.font_atlases
.get(&FloatOrd(font_size)) .get(&FloatOrd(font_size))
@ -97,7 +110,7 @@ impl FontAtlasSet {
.iter() .iter()
.find_map(|atlas| { .find_map(|atlas| {
atlas atlas
.get_glyph_index(glyph_id) .get_glyph_index(glyph_id, position.into())
.map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak())) .map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak()))
}) })
.map(|(glyph_index, texture_atlas)| GlyphAtlasInfo { .map(|(glyph_index, texture_atlas)| GlyphAtlasInfo {

View File

@ -1,4 +1,4 @@
use ab_glyph::{Font as _, FontArc, ScaleFont as _}; use ab_glyph::{Font as _, FontArc, Glyph, ScaleFont as _};
use bevy_asset::{Assets, Handle}; use bevy_asset::{Assets, Handle};
use bevy_math::{Size, Vec2}; use bevy_math::{Size, Vec2};
use bevy_render::prelude::Texture; use bevy_render::prelude::Texture;
@ -80,8 +80,8 @@ impl GlyphBrush {
font_id: _, font_id: _,
} = sg; } = sg;
let glyph_id = glyph.id; let glyph_id = glyph.id;
let base_x = glyph.position.x.floor(); let glyph_position = glyph.position;
glyph.position.x = 0.; let adjust = GlyphPlacementAdjuster::new(&mut glyph);
if let Some(outlined_glyph) = font.font.outline_glyph(glyph) { if let Some(outlined_glyph) = font.font.outline_glyph(glyph) {
let bounds = outlined_glyph.px_bounds(); let bounds = outlined_glyph.px_bounds();
let handle_font_atlas: Handle<FontAtlasSet> = handle.as_weak(); let handle_font_atlas: Handle<FontAtlasSet> = handle.as_weak();
@ -89,7 +89,7 @@ impl GlyphBrush {
.get_or_insert_with(handle_font_atlas, FontAtlasSet::default); .get_or_insert_with(handle_font_atlas, FontAtlasSet::default);
let atlas_info = font_atlas_set let atlas_info = font_atlas_set
.get_glyph_atlas_info(font_size, glyph_id) .get_glyph_atlas_info(font_size, glyph_id, glyph_position)
.map(Ok) .map(Ok)
.unwrap_or_else(|| { .unwrap_or_else(|| {
font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph) font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph)
@ -100,11 +100,9 @@ impl GlyphBrush {
let glyph_width = glyph_rect.width(); let glyph_width = glyph_rect.width();
let glyph_height = glyph_rect.height(); let glyph_height = glyph_rect.height();
let x = base_x + bounds.min.x + glyph_width / 2.0 - min_x; let x = bounds.min.x + glyph_width / 2.0 - min_x;
// the 0.5 accounts for odd-numbered heights (bump up by 1 pixel)
// max_y = text block height, and up is negative (whereas for transform, up is positive)
let y = max_y - bounds.max.y + glyph_height / 2.0; let y = max_y - bounds.max.y + glyph_height / 2.0;
let position = Vec2::new(x, y); let position = adjust.position(Vec2::new(x, y));
positioned_glyphs.push(PositionedGlyph { positioned_glyphs.push(PositionedGlyph {
position, position,
@ -129,3 +127,38 @@ pub struct PositionedGlyph {
pub position: Vec2, pub position: Vec2,
pub atlas_info: GlyphAtlasInfo, pub atlas_info: GlyphAtlasInfo,
} }
#[cfg(feature = "subpixel_glyph_atlas")]
struct GlyphPlacementAdjuster;
#[cfg(feature = "subpixel_glyph_atlas")]
impl GlyphPlacementAdjuster {
#[inline(always)]
pub fn new(_: &mut Glyph) -> Self {
Self
}
#[inline(always)]
pub fn position(&self, p: Vec2) -> Vec2 {
p
}
}
#[cfg(not(feature = "subpixel_glyph_atlas"))]
struct GlyphPlacementAdjuster(f32);
#[cfg(not(feature = "subpixel_glyph_atlas"))]
impl GlyphPlacementAdjuster {
#[inline(always)]
pub fn new(glyph: &mut Glyph) -> Self {
let v = glyph.position.x.round();
glyph.position.x = 0.;
glyph.position.y = glyph.position.y.ceil();
Self(v)
}
#[inline(always)]
pub fn position(&self, v: Vec2) -> Vec2 {
Vec2::new(self.0, 0.) + v
}
}

View File

@ -71,3 +71,9 @@ Vorbis audio format support.
### wayland ### wayland
Enable this to use Wayland display server protocol other than X11. Enable this to use Wayland display server protocol other than X11.
### subpixel_glyph_atlas
Enable this to cache glyphs using subpixel accuracy. This increases texture
memory usage as each position requires a separate sprite in the glyph atlas, but
provide more accurate character spacing.