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:
parent
8a330a889d
commit
60be99859a
@ -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}
|
||||||
|
@ -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" }
|
||||||
|
@ -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" }
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user