epaint/
texture_atlas.rs

1use emath::{remap_clamp, Rect};
2
3use crate::{FontImage, ImageDelta};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6struct Rectu {
7    /// inclusive
8    min_x: usize,
9
10    /// inclusive
11    min_y: usize,
12
13    /// exclusive
14    max_x: usize,
15
16    /// exclusive
17    max_y: usize,
18}
19
20impl Rectu {
21    const NOTHING: Self = Self {
22        min_x: usize::MAX,
23        min_y: usize::MAX,
24        max_x: 0,
25        max_y: 0,
26    };
27    const EVERYTHING: Self = Self {
28        min_x: 0,
29        min_y: 0,
30        max_x: usize::MAX,
31        max_y: usize::MAX,
32    };
33}
34
35#[derive(Copy, Clone, Debug)]
36struct PrerasterizedDisc {
37    r: f32,
38    uv: Rectu,
39}
40
41/// A pre-rasterized disc (filled circle), somewhere in the texture atlas.
42#[derive(Copy, Clone, Debug)]
43pub struct PreparedDisc {
44    /// The radius of this disc in texels.
45    pub r: f32,
46
47    /// Width in texels.
48    pub w: f32,
49
50    /// Where in the texture atlas the disc is.
51    /// Normalized in 0-1 range.
52    pub uv: Rect,
53}
54
55/// Contains font data in an atlas, where each character occupied a small rectangle.
56///
57/// More characters can be added, possibly expanding the texture.
58#[derive(Clone)]
59pub struct TextureAtlas {
60    image: FontImage,
61
62    /// What part of the image that is dirty
63    dirty: Rectu,
64
65    /// Used for when allocating new rectangles.
66    cursor: (usize, usize),
67
68    row_height: usize,
69
70    /// Set when someone requested more space than was available.
71    overflowed: bool,
72
73    /// pre-rasterized discs of radii `2^i`, where `i` is the index.
74    discs: Vec<PrerasterizedDisc>,
75}
76
77impl TextureAtlas {
78    pub fn new(size: [usize; 2]) -> Self {
79        assert!(size[0] >= 1024, "Tiny texture atlas");
80        let mut atlas = Self {
81            image: FontImage::new(size),
82            dirty: Rectu::EVERYTHING,
83            cursor: (0, 0),
84            row_height: 0,
85            overflowed: false,
86            discs: vec![], // will be filled in below
87        };
88
89        // Make the top left pixel fully white for `WHITE_UV`, i.e. painting something with solid color:
90        let (pos, image) = atlas.allocate((1, 1));
91        assert_eq!(pos, (0, 0));
92        image[pos] = 1.0;
93
94        // Allocate a series of anti-aliased discs used to render small filled circles:
95        // TODO(emilk): these circles can be packed A LOT better.
96        // In fact, the whole texture atlas could be packed a lot better.
97        // for r in [1, 2, 4, 8, 16, 32, 64] {
98        //     let w = 2 * r + 3;
99        //     let hw = w as i32 / 2;
100        const LARGEST_CIRCLE_RADIUS: f32 = 8.0; // keep small so that the initial texture atlas is small
101        for i in 0.. {
102            let r = 2.0_f32.powf(i as f32 / 2.0 - 1.0);
103            if r > LARGEST_CIRCLE_RADIUS {
104                break;
105            }
106            let hw = (r + 0.5).ceil() as i32;
107            let w = (2 * hw + 1) as usize;
108            let ((x, y), image) = atlas.allocate((w, w));
109            for dx in -hw..=hw {
110                for dy in -hw..=hw {
111                    let distance_to_center = ((dx * dx + dy * dy) as f32).sqrt();
112                    let coverage =
113                        remap_clamp(distance_to_center, (r - 0.5)..=(r + 0.5), 1.0..=0.0);
114                    image[((x as i32 + hw + dx) as usize, (y as i32 + hw + dy) as usize)] =
115                        coverage;
116                }
117            }
118            atlas.discs.push(PrerasterizedDisc {
119                r,
120                uv: Rectu {
121                    min_x: x,
122                    min_y: y,
123                    max_x: x + w,
124                    max_y: y + w,
125                },
126            });
127        }
128
129        atlas
130    }
131
132    pub fn size(&self) -> [usize; 2] {
133        self.image.size
134    }
135
136    /// Returns the locations and sizes of pre-rasterized discs (filled circles) in this atlas.
137    pub fn prepared_discs(&self) -> Vec<PreparedDisc> {
138        let size = self.size();
139        let inv_w = 1.0 / size[0] as f32;
140        let inv_h = 1.0 / size[1] as f32;
141        self.discs
142            .iter()
143            .map(|disc| {
144                let r = disc.r;
145                let Rectu {
146                    min_x,
147                    min_y,
148                    max_x,
149                    max_y,
150                } = disc.uv;
151                let w = max_x - min_x;
152                let uv = Rect::from_min_max(
153                    emath::pos2(min_x as f32 * inv_w, min_y as f32 * inv_h),
154                    emath::pos2(max_x as f32 * inv_w, max_y as f32 * inv_h),
155                );
156                PreparedDisc { r, w: w as f32, uv }
157            })
158            .collect()
159    }
160
161    fn max_height(&self) -> usize {
162        // the initial width is set to the max size
163        self.image.height().max(self.image.width())
164    }
165
166    /// When this get high, it might be time to clear and start over!
167    pub fn fill_ratio(&self) -> f32 {
168        if self.overflowed {
169            1.0
170        } else {
171            (self.cursor.1 + self.row_height) as f32 / self.max_height() as f32
172        }
173    }
174
175    /// The texture options suitable for a font texture
176    #[inline]
177    pub fn texture_options() -> crate::textures::TextureOptions {
178        crate::textures::TextureOptions::LINEAR
179    }
180
181    /// The full font atlas image.
182    #[inline]
183    pub fn image(&self) -> &FontImage {
184        &self.image
185    }
186
187    /// Call to get the change to the image since last call.
188    pub fn take_delta(&mut self) -> Option<ImageDelta> {
189        let texture_options = Self::texture_options();
190
191        let dirty = std::mem::replace(&mut self.dirty, Rectu::NOTHING);
192        if dirty == Rectu::NOTHING {
193            None
194        } else if dirty == Rectu::EVERYTHING {
195            Some(ImageDelta::full(self.image.clone(), texture_options))
196        } else {
197            let pos = [dirty.min_x, dirty.min_y];
198            let size = [dirty.max_x - dirty.min_x, dirty.max_y - dirty.min_y];
199            let region = self.image.region(pos, size);
200            Some(ImageDelta::partial(pos, region, texture_options))
201        }
202    }
203
204    /// Returns the coordinates of where the rect ended up,
205    /// and invalidates the region.
206    pub fn allocate(&mut self, (w, h): (usize, usize)) -> ((usize, usize), &mut FontImage) {
207        /// On some low-precision GPUs (my old iPad) characters get muddled up
208        /// if we don't add some empty pixels between the characters.
209        /// On modern high-precision GPUs this is not needed.
210        const PADDING: usize = 1;
211
212        assert!(
213            w <= self.image.width(),
214            "Tried to allocate a {} wide glyph in a {} wide texture atlas",
215            w,
216            self.image.width()
217        );
218        if self.cursor.0 + w > self.image.width() {
219            // New row:
220            self.cursor.0 = 0;
221            self.cursor.1 += self.row_height + PADDING;
222            self.row_height = 0;
223        }
224
225        self.row_height = self.row_height.max(h);
226
227        let required_height = self.cursor.1 + self.row_height;
228
229        if required_height > self.max_height() {
230            // This is a bad place to be - we need to start reusing space :/
231
232            #[cfg(feature = "log")]
233            log::warn!("epaint texture atlas overflowed!");
234
235            self.cursor = (0, self.image.height() / 3); // Restart a bit down - the top of the atlas has too many important things in it
236            self.overflowed = true; // this will signal the user that we need to recreate the texture atlas next frame.
237        } else if resize_to_min_height(&mut self.image, required_height) {
238            self.dirty = Rectu::EVERYTHING;
239        }
240
241        let pos = self.cursor;
242        self.cursor.0 += w + PADDING;
243
244        self.dirty.min_x = self.dirty.min_x.min(pos.0);
245        self.dirty.min_y = self.dirty.min_y.min(pos.1);
246        self.dirty.max_x = self.dirty.max_x.max(pos.0 + w);
247        self.dirty.max_y = self.dirty.max_y.max(pos.1 + h);
248
249        (pos, &mut self.image)
250    }
251}
252
253fn resize_to_min_height(image: &mut FontImage, required_height: usize) -> bool {
254    while required_height >= image.height() {
255        image.size[1] *= 2; // double the height
256    }
257
258    if image.width() * image.height() > image.pixels.len() {
259        image.pixels.resize(image.width() * image.height(), 0.0);
260        true
261    } else {
262        false
263    }
264}