274 lines
8.5 KiB
Rust
274 lines
8.5 KiB
Rust
use std::ptr;
|
|
|
|
use crossfont::{BitmapBuffer, RasterizedGlyph};
|
|
|
|
use crate::gl;
|
|
use crate::gl::types::*;
|
|
|
|
use super::Glyph;
|
|
|
|
/// Size of the Atlas.
|
|
pub const ATLAS_SIZE: i32 = 1024;
|
|
|
|
/// Manages a single texture atlas.
|
|
///
|
|
/// The strategy for filling an atlas looks roughly like this:
|
|
///
|
|
/// ```text
|
|
/// (width, height)
|
|
/// ┌─────┬─────┬─────┬─────┬─────┐
|
|
/// │ 10 │ │ │ │ │ <- Empty spaces; can be filled while
|
|
/// │ │ │ │ │ │ glyph_height < height - row_baseline
|
|
/// ├─────┼─────┼─────┼─────┼─────┤
|
|
/// │ 5 │ 6 │ 7 │ 8 │ 9 │
|
|
/// │ │ │ │ │ │
|
|
/// ├─────┼─────┼─────┼─────┴─────┤ <- Row height is tallest glyph in row; this is
|
|
/// │ 1 │ 2 │ 3 │ 4 │ used as the baseline for the following row.
|
|
/// │ │ │ │ │ <- Row considered full when next glyph doesn't
|
|
/// └─────┴─────┴─────┴───────────┘ fit in the row.
|
|
/// (0, 0) x->
|
|
/// ```
|
|
#[derive(Debug)]
|
|
pub struct Atlas {
|
|
/// Texture id for this atlas.
|
|
id: GLuint,
|
|
|
|
/// Width of atlas.
|
|
width: i32,
|
|
|
|
/// Height of atlas.
|
|
height: i32,
|
|
|
|
/// Left-most free pixel in a row.
|
|
///
|
|
/// This is called the extent because it is the upper bound of used pixels
|
|
/// in a row.
|
|
row_extent: i32,
|
|
|
|
/// Baseline for glyphs in the current row.
|
|
row_baseline: i32,
|
|
|
|
/// Tallest glyph in current row.
|
|
///
|
|
/// This is used as the advance when end of row is reached.
|
|
row_tallest: i32,
|
|
}
|
|
|
|
/// Error that can happen when inserting a texture to the Atlas.
|
|
pub enum AtlasInsertError {
|
|
/// Texture atlas is full.
|
|
Full,
|
|
|
|
/// The glyph cannot fit within a single texture.
|
|
GlyphTooLarge,
|
|
}
|
|
|
|
impl Atlas {
|
|
pub fn new(size: i32) -> Self {
|
|
let mut id: GLuint = 0;
|
|
unsafe {
|
|
gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1);
|
|
gl::GenTextures(1, &mut id);
|
|
gl::BindTexture(gl::TEXTURE_2D, id);
|
|
// Use RGBA texture for both normal and emoji glyphs, since it has no performance
|
|
// impact.
|
|
gl::TexImage2D(
|
|
gl::TEXTURE_2D,
|
|
0,
|
|
gl::RGBA as i32,
|
|
size,
|
|
size,
|
|
0,
|
|
gl::RGBA,
|
|
gl::UNSIGNED_BYTE,
|
|
ptr::null(),
|
|
);
|
|
|
|
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
|
|
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
|
|
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32);
|
|
gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as i32);
|
|
|
|
gl::BindTexture(gl::TEXTURE_2D, 0);
|
|
}
|
|
|
|
Self { id, width: size, height: size, row_extent: 0, row_baseline: 0, row_tallest: 0 }
|
|
}
|
|
|
|
pub fn clear(&mut self) {
|
|
self.row_extent = 0;
|
|
self.row_baseline = 0;
|
|
self.row_tallest = 0;
|
|
}
|
|
|
|
/// Insert a RasterizedGlyph into the texture atlas.
|
|
pub fn insert(
|
|
&mut self,
|
|
glyph: &RasterizedGlyph,
|
|
active_tex: &mut u32,
|
|
) -> Result<Glyph, AtlasInsertError> {
|
|
if glyph.width > self.width || glyph.height > self.height {
|
|
return Err(AtlasInsertError::GlyphTooLarge);
|
|
}
|
|
|
|
// If there's not enough room in current row, go onto next one.
|
|
if !self.room_in_row(glyph) {
|
|
self.advance_row()?;
|
|
}
|
|
|
|
// If there's still not room, there's nothing that can be done here..
|
|
if !self.room_in_row(glyph) {
|
|
return Err(AtlasInsertError::Full);
|
|
}
|
|
|
|
// There appears to be room; load the glyph.
|
|
Ok(self.insert_inner(glyph, active_tex))
|
|
}
|
|
|
|
/// Insert the glyph without checking for room.
|
|
///
|
|
/// Internal function for use once atlas has been checked for space. GL
|
|
/// errors could still occur at this point if we were checking for them;
|
|
/// hence, the Result.
|
|
fn insert_inner(&mut self, glyph: &RasterizedGlyph, active_tex: &mut u32) -> Glyph {
|
|
let offset_y = self.row_baseline;
|
|
let offset_x = self.row_extent;
|
|
let height = glyph.height as i32;
|
|
let width = glyph.width as i32;
|
|
let multicolor;
|
|
|
|
unsafe {
|
|
gl::BindTexture(gl::TEXTURE_2D, self.id);
|
|
|
|
// Load data into OpenGL.
|
|
let (format, buffer) = match &glyph.buffer {
|
|
BitmapBuffer::Rgb(buffer) => {
|
|
multicolor = false;
|
|
(gl::RGB, buffer)
|
|
},
|
|
BitmapBuffer::Rgba(buffer) => {
|
|
multicolor = true;
|
|
(gl::RGBA, buffer)
|
|
},
|
|
};
|
|
|
|
gl::TexSubImage2D(
|
|
gl::TEXTURE_2D,
|
|
0,
|
|
offset_x,
|
|
offset_y,
|
|
width,
|
|
height,
|
|
format,
|
|
gl::UNSIGNED_BYTE,
|
|
buffer.as_ptr() as *const _,
|
|
);
|
|
|
|
gl::BindTexture(gl::TEXTURE_2D, 0);
|
|
*active_tex = 0;
|
|
}
|
|
|
|
// Update Atlas state.
|
|
self.row_extent = offset_x + width;
|
|
if height > self.row_tallest {
|
|
self.row_tallest = height;
|
|
}
|
|
|
|
// Generate UV coordinates.
|
|
let uv_bot = offset_y as f32 / self.height as f32;
|
|
let uv_left = offset_x as f32 / self.width as f32;
|
|
let uv_height = height as f32 / self.height as f32;
|
|
let uv_width = width as f32 / self.width as f32;
|
|
|
|
Glyph {
|
|
tex_id: self.id,
|
|
multicolor,
|
|
top: glyph.top as i16,
|
|
left: glyph.left as i16,
|
|
width: width as i16,
|
|
height: height as i16,
|
|
uv_bot,
|
|
uv_left,
|
|
uv_width,
|
|
uv_height,
|
|
}
|
|
}
|
|
|
|
/// Check if there's room in the current row for given glyph.
|
|
pub fn room_in_row(&self, raw: &RasterizedGlyph) -> bool {
|
|
let next_extent = self.row_extent + raw.width as i32;
|
|
let enough_width = next_extent <= self.width;
|
|
let enough_height = (raw.height as i32) < (self.height - self.row_baseline);
|
|
|
|
enough_width && enough_height
|
|
}
|
|
|
|
/// Mark current row as finished and prepare to insert into the next row.
|
|
pub fn advance_row(&mut self) -> Result<(), AtlasInsertError> {
|
|
let advance_to = self.row_baseline + self.row_tallest;
|
|
if self.height - advance_to <= 0 {
|
|
return Err(AtlasInsertError::Full);
|
|
}
|
|
|
|
self.row_baseline = advance_to;
|
|
self.row_extent = 0;
|
|
self.row_tallest = 0;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Load a glyph into a texture atlas.
|
|
///
|
|
/// If the current atlas is full, a new one will be created.
|
|
#[inline]
|
|
pub fn load_glyph(
|
|
active_tex: &mut GLuint,
|
|
atlas: &mut Vec<Atlas>,
|
|
current_atlas: &mut usize,
|
|
rasterized: &RasterizedGlyph,
|
|
) -> Glyph {
|
|
// At least one atlas is guaranteed to be in the `self.atlas` list; thus
|
|
// the unwrap.
|
|
match atlas[*current_atlas].insert(rasterized, active_tex) {
|
|
Ok(glyph) => glyph,
|
|
Err(AtlasInsertError::Full) => {
|
|
*current_atlas += 1;
|
|
if *current_atlas == atlas.len() {
|
|
let new = Atlas::new(ATLAS_SIZE);
|
|
*active_tex = 0; // Atlas::new binds a texture. Ugh this is sloppy.
|
|
atlas.push(new);
|
|
}
|
|
Atlas::load_glyph(active_tex, atlas, current_atlas, rasterized)
|
|
},
|
|
Err(AtlasInsertError::GlyphTooLarge) => Glyph {
|
|
tex_id: atlas[*current_atlas].id,
|
|
multicolor: false,
|
|
top: 0,
|
|
left: 0,
|
|
width: 0,
|
|
height: 0,
|
|
uv_bot: 0.,
|
|
uv_left: 0.,
|
|
uv_width: 0.,
|
|
uv_height: 0.,
|
|
},
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn clear_atlas(atlas: &mut Vec<Atlas>, current_atlas: &mut usize) {
|
|
for atlas in atlas.iter_mut() {
|
|
atlas.clear();
|
|
}
|
|
*current_atlas = 0;
|
|
}
|
|
}
|
|
|
|
impl Drop for Atlas {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
gl::DeleteTextures(1, &self.id);
|
|
}
|
|
}
|
|
}
|