three_d_text_builder/
cache.rs

1// STD Dependencies -----------------------------------------------------------
2// ----------------------------------------------------------------------------
3use std::collections::{BTreeMap, HashMap};
4
5
6// External Dependencies ------------------------------------------------------
7// ----------------------------------------------------------------------------
8use fontdue::{
9    Font,
10    layout::Layout
11};
12use three_d::{
13    Vec2,
14    Srgba, Context,
15    CpuTexture, CpuMaterial,
16    TextureData, Wrapping
17};
18use rectangle_pack::{
19    GroupedRectsToPlace, RectToInsert, TargetBin, RectanglePackOk
20};
21
22
23// Internal Dependencies ------------------------------------------------------
24// ----------------------------------------------------------------------------
25use crate::TextRef;
26use crate::builder::TextBuilderSettings;
27use crate::geometry::material::TextMaterial;
28use crate::glyph::{CachedGlyph, PositionedGlyph};
29
30
31///
32/// Rasterizes and caches glyphs for a specific font / size combination.
33///
34/// # Implementation
35///
36/// Glyphs get rasterized on demand when first encountered and are packed into
37/// a CPU backed atlas texture, when the atlas can no longer fit any additional
38/// glyphs its size doubles until it reaches the maximum allowed size.
39///
40/// Once the maximum atlas size has been reached, no further glyphs will get
41/// rasterized any any characters that require them will be skipped when building
42/// the meshes.
43///
44/// When an atlas gets updated with new glyphs, its GPU texture is uploaded to
45/// the GPU as a new material for use with the text meshes.
46///
47/// > **Note:** Font sizes are rounded to the first decimal place.
48///
49/// > **Note:** Glyphs get only rasterized once, but may get copied multiple times
50/// > into the backing CPU texture in case their position in the atlas changes.
51///
52pub struct GlyphCache {
53    raster_size: f32,
54
55    atlas_is_exhausted: bool,
56    atlas_requires_update: bool,
57    atlas_fill_ratio: f32,
58    atlas_max_size: u32,
59    atlas_rects: GroupedRectsToPlace<char>,
60
61    glyph_padding: u32,
62    glyph_alpha_transform: Option<&'static dyn Fn(u8) -> u8>,
63    glyphs: HashMap<char, CachedGlyph>,
64
65    cpu_material: CpuMaterial,
66    gpu_material: Option<TextMaterial>
67}
68
69impl GlyphCache {
70    /// Returns `true` in case there are currently no glyphs in the cache
71    pub fn is_empty(&self) -> bool {
72        self.glyphs.is_empty()
73    }
74
75    /// Returnss the number of rasterized glyphs in the cache
76    pub fn len(&self) -> usize {
77        self.glyphs.len()
78    }
79
80    /// Returns the size of the CPU texture in use by the cache
81    pub fn size(&self) -> (u32, u32) {
82        let texture = self.cpu_material.albedo_texture.as_ref().expect("cpu material must always be present");
83        (texture.width, texture.height)
84    }
85
86    /// Returns how much of the CPU texture's area is currently used by glyphs
87    pub fn fill_ratio(&self) -> f32 {
88        self.atlas_fill_ratio
89    }
90
91    ///
92    /// Returns `true` when no further glyphs can be fit into the cache and the
93    /// maximum texture size as allowed by [``TextBuilderSettings``] has
94    /// already been reached
95    ///
96    pub fn is_exhausted(&self) -> bool {
97        self.atlas_is_exhausted
98    }
99
100    ///
101    /// Returns a reference to the cache's currently uploaded GPU material
102    ///
103    pub fn material(&self) -> Option<&TextMaterial> {
104        self.gpu_material.as_ref()
105    }
106
107    pub(crate) fn index(size: f32) -> u32 {
108        (size * 10.0) as u32
109    }
110
111    pub(crate) fn new(size: u32, settings: &TextBuilderSettings) -> Self {
112        let u = settings.cache_atlas_min_size;
113        let bytes = (u * u) as usize;
114        Self {
115            raster_size: size as f32 * 0.1,
116
117            atlas_is_exhausted: false,
118            atlas_requires_update: true,
119            atlas_fill_ratio: 0.0,
120            atlas_max_size: settings.cache_atlas_max_size,
121            atlas_rects: GroupedRectsToPlace::new(),
122
123            glyph_padding: settings.cache_atlas_glyph_padding,
124            glyph_alpha_transform: settings.glyph_alpha_transform,
125            glyphs: HashMap::with_capacity(32),
126
127            cpu_material: CpuMaterial {
128                albedo: Srgba::from([1.0, 1.0, 1.0, 1.0]),
129                albedo_texture: Some(CpuTexture {
130                    name: format!("text_renderer_cache_{size}"),
131                    data: TextureData::RU8(std::iter::repeat(0).take(bytes).collect()),
132                    width: u,
133                    height: u,
134                    min_filter: settings.texture_filter,
135                    mag_filter: settings.texture_filter,
136                    mip_map_filter: None,
137                    wrap_s: Wrapping::ClampToEdge,
138                    wrap_t: Wrapping::ClampToEdge
139                }),
140                ..Default::default()
141            },
142            gpu_material: None
143        }
144    }
145
146    pub(crate) fn get_glyph(&self, character: char) -> Option<&CachedGlyph> {
147        self.glyphs.get(&character)
148    }
149
150    pub(crate) fn layout_glyphs(
151        &mut self,
152        font: &Font,
153        layout: &mut Layout,
154        line_height: f32,
155        text: &TextRef
156
157    ) -> (Vec<PositionedGlyph>, Vec2) {
158        text.walk_glyphs(font, layout, line_height, |index, count, glyph, transform, color| {
159            // We only rasterize unknown glyphs when the atlas has not exhausted its maximum size
160            let is_known_glyph = self.glyphs.contains_key(&glyph.parent);
161
162            // Skip any whitespace...
163            if glyph.parent.is_whitespace() ||
164                // ...and don't create positions for unknown glyphs when exhausted
165                self.atlas_is_exhausted && !is_known_glyph {
166                None
167
168            // Only rasterize glyphs we don't have cached in the atlas yet
169            } else if !is_known_glyph {
170                let (metrics, pixels) = font.rasterize(glyph.parent, self.raster_size);
171                self.atlas_rects.push_rect(
172                    glyph.parent,
173                    None,
174                    RectToInsert::new(
175                        metrics.width as u32 + self.glyph_padding * 2,
176                        metrics.height as u32 + self.glyph_padding * 2,
177                        1
178                    )
179                );
180                self.glyphs.insert(glyph.parent, CachedGlyph::new(metrics, pixels));
181                self.atlas_requires_update = true;
182                Some(PositionedGlyph::new(
183                    glyph,
184                    transform.map(|t| t(index, count, glyph.parent)),
185                    color.map(|t| t(index, count, glyph.parent))
186                ))
187
188            // Return any glyphs that we have already cached
189            } else {
190                Some(PositionedGlyph::new(
191                    glyph,
192                    transform.map(|t| t(index, count, glyph.parent)),
193                    color.map(|t| t(index, count, glyph.parent))
194                ))
195            }
196        })
197    }
198
199    pub(crate) fn update_material(&mut self, context: &Context) -> TextMaterial {
200        // Re-pack our atlas texture if any new glyphs have been added
201        if self.atlas_requires_update {
202            self.atlas_requires_update = false;
203            match self.update_atlas() {
204                Some(()) => {
205                    self.gpu_material = None;
206                },
207                None => {
208                    self.atlas_is_exhausted = true;
209                }
210            }
211        }
212
213        // Upload a new material to the GPU if required
214        if self.gpu_material.is_none() {
215            self.gpu_material = TextMaterial::new(context, &self.cpu_material);
216        }
217
218        // Return a clone of the material for binding to models
219        self.gpu_material.as_ref().unwrap().clone()
220    }
221
222    fn update_atlas(&mut self) -> Option<()> {
223        // Try to repack all glyphs into the atlas,
224        // increasing its size if both necessary and possible
225        let texture = self.cpu_material.albedo_texture.as_ref().unwrap();
226        let (packing, required_size) = self.repack_atlas(texture.width)?;
227
228        // Update Texture
229        let texture = self.cpu_material.albedo_texture.as_mut().unwrap();
230        if let TextureData::RU8(ref mut texture_data) = texture.data {
231            // Resize texture if required (by extending with the required number of bytes)
232            if texture.width != required_size {
233                let current = (texture.width * texture.height) as usize;
234                let target = (required_size * required_size) as usize;
235                let missing = target - current;
236                texture_data.extend_from_slice(&vec![0; missing]);
237                texture.width = required_size;
238                texture.height = required_size;
239
240                // Invalidate all glyphs positions
241                for glyph in self.glyphs.values_mut() {
242                    glyph.invalidate();
243                }
244            }
245
246            // Write glyph alpha data into the texture
247            let mut filled_pixels = 0;
248            for (character, (_, location)) in packing.packed_locations() {
249                if let Some(glyph) = self.glyphs.get_mut(character) {
250                    filled_pixels += glyph.render_to_texture(
251                        texture_data,
252                        texture.width,
253                        location,
254                        self.glyph_padding,
255                        self.glyph_alpha_transform
256                    );
257                }
258            }
259            let available_pixels = texture.width * texture.height;
260            self.atlas_fill_ratio = if available_pixels > 0 {
261                1.0 / available_pixels as f32 * filled_pixels as f32
262
263            } else {
264                1.0
265            };
266        }
267        Some(())
268    }
269
270    fn repack_atlas(&mut self, mut current_size: u32) -> Option<(RectanglePackOk<char, i32>, u32)> {
271        loop {
272            let mut target_bins = BTreeMap::new();
273            target_bins.insert(0, TargetBin::new(current_size, current_size, 1));
274
275            // Try to pack the glyphs into the available atlas size
276            if let Ok(result) = rectangle_pack::pack_rects(
277                &self.atlas_rects,
278                &mut target_bins,
279                &rectangle_pack::volume_heuristic,
280                &rectangle_pack::contains_smallest_box
281            ) {
282                return Some((result, current_size));
283
284            // Re-try with another size
285            } else if current_size < self.atlas_max_size {
286                current_size *= 2;
287
288            // If we have already reached the maximum size but still cannot fit
289            // all the glyphs, we consider ourselves exhausted and will no
290            // longer accept / render any unknown glyphs that we encounter
291            } else {
292                return None;
293            }
294        };
295    }
296}
297