bevy_text/
font_atlas.rs

1use bevy_asset::{Assets, Handle, RenderAssetUsages};
2use bevy_image::{prelude::*, ImageSampler, ToExtents};
3use bevy_math::{IVec2, UVec2};
4use bevy_platform::collections::HashMap;
5use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
6
7use crate::{FontSmoothing, GlyphAtlasInfo, GlyphAtlasLocation, TextError};
8
9/// Rasterized glyphs are cached, stored in, and retrieved from, a `FontAtlas`.
10///
11/// A `FontAtlas` contains one or more textures, each of which contains one or more glyphs packed into them.
12///
13/// A [`FontAtlasSet`](crate::FontAtlasSet) contains a `FontAtlas` for each font size in the same font face.
14///
15/// For the same font face and font size, a glyph will be rasterized differently for different subpixel offsets.
16/// In practice, ranges of subpixel offsets are grouped into subpixel bins to limit the number of rasterized glyphs,
17/// providing a trade-off between visual quality and performance.
18///
19/// A [`CacheKey`](cosmic_text::CacheKey) encodes all of the information of a subpixel-offset glyph and is used to
20/// find that glyphs raster in a [`TextureAtlas`] through its corresponding [`GlyphAtlasLocation`].
21pub struct FontAtlas {
22    /// Used to update the [`TextureAtlasLayout`].
23    pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder,
24    /// A mapping between subpixel-offset glyphs and their [`GlyphAtlasLocation`].
25    pub glyph_to_atlas_index: HashMap<cosmic_text::CacheKey, GlyphAtlasLocation>,
26    /// The handle to the [`TextureAtlasLayout`] that holds the rasterized glyphs.
27    pub texture_atlas: Handle<TextureAtlasLayout>,
28    /// The texture where this font atlas is located
29    pub texture: Handle<Image>,
30}
31
32impl FontAtlas {
33    /// Create a new [`FontAtlas`] with the given size, adding it to the appropriate asset collections.
34    pub fn new(
35        textures: &mut Assets<Image>,
36        texture_atlases_layout: &mut Assets<TextureAtlasLayout>,
37        size: UVec2,
38        font_smoothing: FontSmoothing,
39    ) -> FontAtlas {
40        let mut image = Image::new_fill(
41            size.to_extents(),
42            TextureDimension::D2,
43            &[0, 0, 0, 0],
44            TextureFormat::Rgba8UnormSrgb,
45            // Need to keep this image CPU persistent in order to add additional glyphs later on
46            RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
47        );
48        if font_smoothing == FontSmoothing::None {
49            image.sampler = ImageSampler::nearest();
50        }
51        let texture = textures.add(image);
52        let texture_atlas = texture_atlases_layout.add(TextureAtlasLayout::new_empty(size));
53        Self {
54            texture_atlas,
55            glyph_to_atlas_index: HashMap::default(),
56            dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1),
57            texture,
58        }
59    }
60
61    /// Get the [`GlyphAtlasLocation`] for a subpixel-offset glyph.
62    pub fn get_glyph_index(&self, cache_key: cosmic_text::CacheKey) -> Option<GlyphAtlasLocation> {
63        self.glyph_to_atlas_index.get(&cache_key).copied()
64    }
65
66    /// Checks if the given subpixel-offset glyph is contained in this [`FontAtlas`].
67    pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey) -> bool {
68        self.glyph_to_atlas_index.contains_key(&cache_key)
69    }
70
71    /// Add a glyph to the atlas, updating both its texture and layout.
72    ///
73    /// The glyph is represented by `glyph`, and its image content is `glyph_texture`.
74    /// This content is copied into the atlas texture, and the atlas layout is updated
75    /// to store the location of that glyph into the atlas.
76    ///
77    /// # Returns
78    ///
79    /// Returns `()` if the glyph is successfully added, or [`TextError::FailedToAddGlyph`] otherwise.
80    /// In that case, neither the atlas texture nor the atlas layout are
81    /// modified.
82    pub fn add_glyph(
83        &mut self,
84        textures: &mut Assets<Image>,
85        atlas_layouts: &mut Assets<TextureAtlasLayout>,
86        cache_key: cosmic_text::CacheKey,
87        texture: &Image,
88        offset: IVec2,
89    ) -> Result<(), TextError> {
90        let atlas_layout = atlas_layouts
91            .get_mut(&self.texture_atlas)
92            .ok_or(TextError::MissingAtlasLayout)?;
93        let atlas_texture = textures
94            .get_mut(&self.texture)
95            .ok_or(TextError::MissingAtlasTexture)?;
96
97        if let Ok(glyph_index) =
98            self.dynamic_texture_atlas_builder
99                .add_texture(atlas_layout, texture, atlas_texture)
100        {
101            self.glyph_to_atlas_index.insert(
102                cache_key,
103                GlyphAtlasLocation {
104                    glyph_index,
105                    offset,
106                },
107            );
108            Ok(())
109        } else {
110            Err(TextError::FailedToAddGlyph(cache_key.glyph_id))
111        }
112    }
113}
114
115impl core::fmt::Debug for FontAtlas {
116    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
117        f.debug_struct("FontAtlas")
118            .field("glyph_to_atlas_index", &self.glyph_to_atlas_index)
119            .field("texture_atlas", &self.texture_atlas)
120            .field("texture", &self.texture)
121            .field("dynamic_texture_atlas_builder", &"[...]")
122            .finish()
123    }
124}
125
126/// Adds the given subpixel-offset glyph to the given font atlases
127pub fn add_glyph_to_atlas(
128    font_atlases: &mut Vec<FontAtlas>,
129    texture_atlases: &mut Assets<TextureAtlasLayout>,
130    textures: &mut Assets<Image>,
131    font_system: &mut cosmic_text::FontSystem,
132    swash_cache: &mut cosmic_text::SwashCache,
133    layout_glyph: &cosmic_text::LayoutGlyph,
134    font_smoothing: FontSmoothing,
135) -> Result<GlyphAtlasInfo, TextError> {
136    let physical_glyph = layout_glyph.physical((0., 0.), 1.0);
137
138    let (glyph_texture, offset) =
139        get_outlined_glyph_texture(font_system, swash_cache, &physical_glyph, font_smoothing)?;
140    let mut add_char_to_font_atlas = |atlas: &mut FontAtlas| -> Result<(), TextError> {
141        atlas.add_glyph(
142            textures,
143            texture_atlases,
144            physical_glyph.cache_key,
145            &glyph_texture,
146            offset,
147        )
148    };
149    if !font_atlases
150        .iter_mut()
151        .any(|atlas| add_char_to_font_atlas(atlas).is_ok())
152    {
153        // Find the largest dimension of the glyph, either its width or its height
154        let glyph_max_size: u32 = glyph_texture
155            .texture_descriptor
156            .size
157            .height
158            .max(glyph_texture.width());
159        // Pick the higher of 512 or the smallest power of 2 greater than glyph_max_size
160        let containing = (1u32 << (32 - glyph_max_size.leading_zeros())).max(512);
161
162        let mut new_atlas = FontAtlas::new(
163            textures,
164            texture_atlases,
165            UVec2::splat(containing),
166            font_smoothing,
167        );
168
169        new_atlas.add_glyph(
170            textures,
171            texture_atlases,
172            physical_glyph.cache_key,
173            &glyph_texture,
174            offset,
175        )?;
176
177        font_atlases.push(new_atlas);
178    }
179
180    get_glyph_atlas_info(font_atlases, physical_glyph.cache_key)
181        .ok_or(TextError::InconsistentAtlasState)
182}
183
184/// Get the texture of the glyph as a rendered image, and its offset
185pub fn get_outlined_glyph_texture(
186    font_system: &mut cosmic_text::FontSystem,
187    swash_cache: &mut cosmic_text::SwashCache,
188    physical_glyph: &cosmic_text::PhysicalGlyph,
189    font_smoothing: FontSmoothing,
190) -> Result<(Image, IVec2), TextError> {
191    // NOTE: Ideally, we'd ask COSMIC Text to honor the font smoothing setting directly.
192    // However, since it currently doesn't support that, we render the glyph with antialiasing
193    // and apply a threshold to the alpha channel to simulate the effect.
194    //
195    // This has the side effect of making regular vector fonts look quite ugly when font smoothing
196    // is turned off, but for fonts that are specifically designed for pixel art, it works well.
197    //
198    // See: https://github.com/pop-os/cosmic-text/issues/279
199    let image = swash_cache
200        .get_image_uncached(font_system, physical_glyph.cache_key)
201        .ok_or(TextError::FailedToGetGlyphImage(physical_glyph.cache_key))?;
202
203    let cosmic_text::Placement {
204        left,
205        top,
206        width,
207        height,
208    } = image.placement;
209
210    let data = match image.content {
211        cosmic_text::SwashContent::Mask => {
212            if font_smoothing == FontSmoothing::None {
213                image
214                    .data
215                    .iter()
216                    // Apply a 50% threshold to the alpha channel
217                    .flat_map(|a| [255, 255, 255, if *a > 127 { 255 } else { 0 }])
218                    .collect()
219            } else {
220                image
221                    .data
222                    .iter()
223                    .flat_map(|a| [255, 255, 255, *a])
224                    .collect()
225            }
226        }
227        cosmic_text::SwashContent::Color => image.data,
228        cosmic_text::SwashContent::SubpixelMask => {
229            // TODO: implement
230            todo!()
231        }
232    };
233
234    Ok((
235        Image::new(
236            Extent3d {
237                width,
238                height,
239                depth_or_array_layers: 1,
240            },
241            TextureDimension::D2,
242            data,
243            TextureFormat::Rgba8UnormSrgb,
244            RenderAssetUsages::MAIN_WORLD,
245        ),
246        IVec2::new(left, top),
247    ))
248}
249
250/// Generates the [`GlyphAtlasInfo`] for the given subpixel-offset glyph.
251pub fn get_glyph_atlas_info(
252    font_atlases: &mut [FontAtlas],
253    cache_key: cosmic_text::CacheKey,
254) -> Option<GlyphAtlasInfo> {
255    font_atlases.iter().find_map(|atlas| {
256        atlas
257            .get_glyph_index(cache_key)
258            .map(|location| GlyphAtlasInfo {
259                location,
260                texture_atlas: atlas.texture_atlas.id(),
261                texture: atlas.texture.id(),
262            })
263    })
264}