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