Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_text/src/font_atlas.rs
9427 views
1
use bevy_asset::{Assets, Handle, RenderAssetUsages};
2
use bevy_image::{prelude::*, ImageSampler, ToExtents};
3
use bevy_math::{IVec2, UVec2};
4
use bevy_platform::collections::HashMap;
5
use swash::scale::Scaler;
6
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
7
8
use crate::{FontSmoothing, GlyphAtlasInfo, GlyphAtlasLocation, TextError};
9
10
/// Key identifying a glyph
11
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
12
pub struct GlyphCacheKey {
13
/// Id used to look up the glyph
14
pub glyph_id: u16,
15
}
16
17
/// Rasterized glyphs are cached, stored in, and retrieved from, a `FontAtlas`.
18
///
19
/// A `FontAtlas` contains one or more textures, each of which contains one or more glyphs packed into them.
20
///
21
/// A [`FontAtlasSet`](crate::FontAtlasSet) contains a `FontAtlas` for each font size in the same font face.
22
///
23
/// For the same font face and font size, a glyph will be rasterized differently for different subpixel offsets.
24
/// In practice, ranges of subpixel offsets are grouped into subpixel bins to limit the number of rasterized glyphs,
25
/// providing a trade-off between visual quality and performance.
26
///
27
/// A [`GlyphCacheKey`] encodes all of the information of a subpixel-offset glyph and is used to
28
/// find that glyphs raster in a [`TextureAtlas`] through its corresponding [`GlyphAtlasLocation`].
29
pub struct FontAtlas {
30
/// Used to update the [`TextureAtlasLayout`].
31
pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder,
32
/// A mapping between subpixel-offset glyphs and their [`GlyphAtlasLocation`].
33
pub glyph_to_atlas_index: HashMap<GlyphCacheKey, GlyphAtlasLocation>,
34
/// The handle to the [`TextureAtlasLayout`] that holds the rasterized glyphs.
35
pub texture_atlas: Handle<TextureAtlasLayout>,
36
/// The texture where this font atlas is located
37
pub texture: Handle<Image>,
38
}
39
40
impl FontAtlas {
41
/// Create a new [`FontAtlas`] with the given size, adding it to the appropriate asset collections.
42
pub fn new(
43
textures: &mut Assets<Image>,
44
texture_atlases_layout: &mut Assets<TextureAtlasLayout>,
45
size: UVec2,
46
font_smoothing: FontSmoothing,
47
) -> FontAtlas {
48
let mut image = Image::new_fill(
49
size.to_extents(),
50
TextureDimension::D2,
51
&[0, 0, 0, 0],
52
TextureFormat::Rgba8UnormSrgb,
53
// Need to keep this image CPU persistent in order to add additional glyphs later on
54
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
55
);
56
if font_smoothing == FontSmoothing::None {
57
image.sampler = ImageSampler::nearest();
58
}
59
let texture = textures.add(image);
60
let texture_atlas = texture_atlases_layout.add(TextureAtlasLayout::new_empty(size));
61
Self {
62
texture_atlas,
63
glyph_to_atlas_index: HashMap::default(),
64
dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 1),
65
texture,
66
}
67
}
68
69
/// Get the [`GlyphAtlasLocation`] for a subpixel-offset glyph.
70
pub fn get_glyph_index(&self, cache_key: GlyphCacheKey) -> Option<GlyphAtlasLocation> {
71
self.glyph_to_atlas_index.get(&cache_key).copied()
72
}
73
74
/// Checks if the given subpixel-offset glyph is contained in this [`FontAtlas`].
75
pub fn has_glyph(&self, cache_key: GlyphCacheKey) -> bool {
76
self.glyph_to_atlas_index.contains_key(&cache_key)
77
}
78
79
/// Add a glyph to the atlas, updating both its texture and layout.
80
///
81
/// The glyph is represented by `glyph`, and its image content is `glyph_texture`.
82
/// This content is copied into the atlas texture, and the atlas layout is updated
83
/// to store the location of that glyph into the atlas.
84
///
85
/// # Returns
86
///
87
/// Returns `()` if the glyph is successfully added, or [`TextError::FailedToAddGlyph`] otherwise.
88
/// In that case, neither the atlas texture nor the atlas layout are
89
/// modified.
90
pub fn add_glyph(
91
&mut self,
92
textures: &mut Assets<Image>,
93
atlas_layouts: &mut Assets<TextureAtlasLayout>,
94
key: GlyphCacheKey,
95
texture: &Image,
96
offset: IVec2,
97
) -> Result<(), TextError> {
98
let mut atlas_layout = atlas_layouts
99
.get_mut(&self.texture_atlas)
100
.ok_or(TextError::MissingAtlasLayout)?;
101
let mut atlas_texture = textures
102
.get_mut(&self.texture)
103
.ok_or(TextError::MissingAtlasTexture)?;
104
105
if let Ok(glyph_index) = self.dynamic_texture_atlas_builder.add_texture(
106
&mut atlas_layout,
107
texture,
108
&mut atlas_texture,
109
) {
110
self.glyph_to_atlas_index.insert(
111
key,
112
GlyphAtlasLocation {
113
glyph_index,
114
offset,
115
},
116
);
117
Ok(())
118
} else {
119
Err(TextError::FailedToAddGlyph(key.glyph_id))
120
}
121
}
122
}
123
124
impl core::fmt::Debug for FontAtlas {
125
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
126
f.debug_struct("FontAtlas")
127
.field("glyph_to_atlas_index", &self.glyph_to_atlas_index)
128
.field("texture_atlas", &self.texture_atlas)
129
.field("texture", &self.texture)
130
.field("dynamic_texture_atlas_builder", &"[...]")
131
.finish()
132
}
133
}
134
135
/// Adds the given subpixel-offset glyph to the given font atlases
136
pub fn add_glyph_to_atlas(
137
font_atlases: &mut Vec<FontAtlas>,
138
texture_atlases: &mut Assets<TextureAtlasLayout>,
139
textures: &mut Assets<Image>,
140
scaler: &mut Scaler,
141
font_smoothing: FontSmoothing,
142
glyph_id: u16,
143
) -> Result<GlyphAtlasInfo, TextError> {
144
let (glyph_texture, offset) = get_outlined_glyph_texture(scaler, glyph_id, font_smoothing)?;
145
let mut add_char_to_font_atlas = |atlas: &mut FontAtlas| -> Result<(), TextError> {
146
atlas.add_glyph(
147
textures,
148
texture_atlases,
149
GlyphCacheKey { glyph_id },
150
&glyph_texture,
151
offset,
152
)
153
};
154
if !font_atlases
155
.iter_mut()
156
.any(|atlas| add_char_to_font_atlas(atlas).is_ok())
157
{
158
// Find the largest dimension of the glyph, either its width or its height
159
let glyph_max_size: u32 = glyph_texture
160
.texture_descriptor
161
.size
162
.height
163
.max(glyph_texture.width());
164
// Pick the higher of 512 or the smallest power of 2 greater than glyph_max_size
165
let containing = (1u32 << (32 - glyph_max_size.leading_zeros())).max(512);
166
167
let mut new_atlas = FontAtlas::new(
168
textures,
169
texture_atlases,
170
UVec2::splat(containing),
171
font_smoothing,
172
);
173
174
new_atlas.add_glyph(
175
textures,
176
texture_atlases,
177
GlyphCacheKey { glyph_id },
178
&glyph_texture,
179
offset,
180
)?;
181
182
font_atlases.push(new_atlas);
183
}
184
185
get_glyph_atlas_info(font_atlases, GlyphCacheKey { glyph_id })
186
.ok_or(TextError::InconsistentAtlasState)
187
}
188
189
/// Get the texture of the glyph as a rendered image, and its offset
190
#[expect(
191
clippy::identity_op,
192
reason = "Alignment improves clarity during RGBA operations."
193
)]
194
pub fn get_outlined_glyph_texture(
195
scaler: &mut Scaler,
196
glyph_id: u16,
197
font_smoothing: FontSmoothing,
198
) -> Result<(Image, IVec2), TextError> {
199
let image = swash::scale::Render::new(&[
200
swash::scale::Source::ColorOutline(0),
201
swash::scale::Source::ColorBitmap(swash::scale::StrikeWith::BestFit),
202
swash::scale::Source::Outline,
203
])
204
.format(swash::zeno::Format::Alpha)
205
.render(scaler, glyph_id)
206
.ok_or(TextError::FailedToGetGlyphImage(glyph_id))?;
207
208
let left = image.placement.left;
209
let top = image.placement.top;
210
let width = image.placement.width;
211
let height = image.placement.height;
212
213
let px = (width * height) as usize;
214
let mut rgba = vec![0u8; px * 4];
215
match font_smoothing {
216
FontSmoothing::AntiAliased => {
217
for i in 0..px {
218
let a = image.data[i];
219
rgba[i * 4 + 0] = 255; // R
220
rgba[i * 4 + 1] = 255; // G
221
rgba[i * 4 + 2] = 255; // B
222
rgba[i * 4 + 3] = a; // A from swash
223
}
224
}
225
FontSmoothing::None => {
226
for i in 0..px {
227
let a = image.data[i];
228
rgba[i * 4 + 0] = 255; // R
229
rgba[i * 4 + 1] = 255; // G
230
rgba[i * 4 + 2] = 255; // B
231
rgba[i * 4 + 3] = if 127 < a { 255 } else { 0 }; // A from swash
232
}
233
}
234
}
235
236
Ok((
237
Image::new(
238
Extent3d {
239
width,
240
height,
241
depth_or_array_layers: 1,
242
},
243
TextureDimension::D2,
244
rgba,
245
TextureFormat::Rgba8UnormSrgb,
246
RenderAssetUsages::MAIN_WORLD,
247
),
248
IVec2::new(left, top),
249
))
250
}
251
252
/// Generates the [`GlyphAtlasInfo`] for the given subpixel-offset glyph.
253
pub fn get_glyph_atlas_info(
254
font_atlases: &mut [FontAtlas],
255
cache_key: GlyphCacheKey,
256
) -> Option<GlyphAtlasInfo> {
257
font_atlases.iter().find_map(|atlas| {
258
atlas
259
.get_glyph_index(cache_key)
260
.map(|location| GlyphAtlasInfo {
261
location,
262
texture_atlas: atlas.texture_atlas.id(),
263
texture: atlas.texture.id(),
264
})
265
})
266
}
267
268