Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subpixel text positioning #1196

Merged
merged 2 commits into from
Jan 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ serialize = ["bevy_internal/serialize"]
wayland = ["bevy_internal/wayland"]
x11 = ["bevy_internal/x11"]

# enable rendering of font glyphs using subpixel accuracy
subpixel_glyph_atlas = ["bevy_internal/subpixel_glyph_atlas"]

[dependencies]
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}
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ serialize = ["bevy_input/serialize"]
wayland = ["bevy_winit/wayland"]
x11 = ["bevy_winit/x11"]

# enable rendering of font glyphs using subpixel accuracy
subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"]

[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.4.0" }
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_text/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ repository = "https://github.com/bevyengine/bevy"
license = "MIT"
keywords = ["bevy"]

[features]
subpixel_glyph_atlas = []

[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.4.0" }
Expand Down
54 changes: 47 additions & 7 deletions crates/bevy_text/src/font_atlas.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
use ab_glyph::GlyphId;
use ab_glyph::{GlyphId, Point};
use bevy_asset::{Assets, Handle};
use bevy_math::Vec2;
use bevy_render::texture::{Extent3d, Texture, TextureDimension, TextureFormat};
use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlas};
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 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>,
}

Expand All @@ -31,27 +62,36 @@ impl FontAtlas {
}
}

pub fn get_glyph_index(&self, glyph_id: GlyphId) -> Option<u32> {
self.glyph_to_atlas_index.get(&glyph_id).copied()
pub fn get_glyph_index(
&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 {
self.glyph_to_atlas_index.contains_key(&glyph_id)
pub fn has_glyph(&self, glyph_id: GlyphId, subpixel_offset: SubpixelOffset) -> bool {
self.glyph_to_atlas_index
.contains_key(&(glyph_id, subpixel_offset))
}

pub fn add_glyph(
&mut self,
textures: &mut Assets<Texture>,
texture_atlases: &mut Assets<TextureAtlas>,
glyph_id: GlyphId,
subpixel_offset: SubpixelOffset,
texture: &Texture,
) -> bool {
let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap();
if let Some(index) =
self.dynamic_texture_atlas_builder
.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
} else {
false
Expand Down
25 changes: 19 additions & 6 deletions crates/bevy_text/src/font_atlas_set.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{error::TextError, Font, FontAtlas};
use ab_glyph::{GlyphId, OutlinedGlyph};
use ab_glyph::{GlyphId, OutlinedGlyph, Point};
use bevy_asset::{Assets, Handle};
use bevy_core::FloatOrd;
use bevy_math::Vec2;
Expand Down Expand Up @@ -35,11 +35,13 @@ impl FontAtlasSet {
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
.get(&FloatOrd(font_size))
.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()))
})
}

Expand All @@ -51,6 +53,7 @@ impl FontAtlasSet {
) -> Result<GlyphAtlasInfo, TextError> {
let glyph = outlined_glyph.glyph();
let glyph_id = glyph.id;
let glyph_position = glyph.position;
let font_size = glyph.scale.y;
let font_atlases = self
.font_atlases
Expand All @@ -64,7 +67,13 @@ impl FontAtlasSet {
});
let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph);
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) {
font_atlases.push(FontAtlas::new(
Expand All @@ -76,19 +85,23 @@ impl FontAtlasSet {
textures,
texture_atlases,
glyph_id,
glyph_position.into(),
&glyph_texture,
) {
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(
&self,
font_size: f32,
glyph_id: GlyphId,
position: Point,
) -> Option<GlyphAtlasInfo> {
self.font_atlases
.get(&FloatOrd(font_size))
Expand All @@ -97,7 +110,7 @@ impl FontAtlasSet {
.iter()
.find_map(|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, texture_atlas)| GlyphAtlasInfo {
Expand Down
49 changes: 41 additions & 8 deletions crates/bevy_text/src/glyph_brush.rs
Original file line number Diff line number Diff line change
@@ -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_math::{Size, Vec2};
use bevy_render::prelude::Texture;
Expand Down Expand Up @@ -80,16 +80,16 @@ impl GlyphBrush {
font_id: _,
} = sg;
let glyph_id = glyph.id;
let base_x = glyph.position.x.floor();
glyph.position.x = 0.;
let glyph_position = glyph.position;
let adjust = GlyphPlacementAdjuster::new(&mut glyph);
if let Some(outlined_glyph) = font.font.outline_glyph(glyph) {
let bounds = outlined_glyph.px_bounds();
let handle_font_atlas: Handle<FontAtlasSet> = handle.as_weak();
let font_atlas_set = font_atlas_set_storage
.get_or_insert_with(handle_font_atlas, FontAtlasSet::default);

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)
.unwrap_or_else(|| {
font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph)
Expand All @@ -100,11 +100,9 @@ impl GlyphBrush {
let glyph_width = glyph_rect.width();
let glyph_height = glyph_rect.height();

let x = base_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 x = bounds.min.x + glyph_width / 2.0 - min_x;
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 {
position,
Expand All @@ -129,3 +127,38 @@ pub struct PositionedGlyph {
pub position: Vec2,
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
}
}
6 changes: 6 additions & 0 deletions docs/cargo_features.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,9 @@ Vorbis audio format support.
### wayland

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.