fix(texture_atlas): added texel offset to ensure correct interpolation on the GPU side

This commit is contained in:
lisk77 2025-11-02 02:09:58 +01:00
parent 025d2b3a5f
commit 8831c46b4c

View file

@ -1,13 +1,10 @@
use crate::font::*;
use comet_log::*;
use image::{DynamicImage, GenericImage, GenericImageView};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use std::time::Instant;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat};
use comet_log::*;
use wgpu::{Device, FilterMode, TextureFormat, TextureUsages};
use crate::font::GlyphData;
use crate::Texture;
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct TextureRegion { pub struct TextureRegion {
u0: f32, u0: f32,
v0: f32, v0: f32,
@ -20,7 +17,16 @@ pub struct TextureRegion {
} }
impl TextureRegion { impl TextureRegion {
pub fn new(u0: f32, v0: f32, u1: f32, v1: f32, dimensions: (u32, u32), advance: f32, offset_x: f32, offset_y: f32) -> Self { pub fn new(
u0: f32,
v0: f32,
u1: f32,
v1: f32,
dimensions: (u32, u32),
advance: f32,
offset_x: f32,
offset_y: f32,
) -> Self {
Self { Self {
u0, u0,
v0, v0,
@ -29,7 +35,7 @@ impl TextureRegion {
advance, advance,
offset_x, offset_x,
offset_y, offset_y,
dimensions dimensions,
} }
} }
@ -66,7 +72,7 @@ impl TextureRegion {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct TextureAtlas { pub struct TextureAtlas {
atlas: DynamicImage, atlas: DynamicImage,
textures: HashMap<String, TextureRegion>, textures: HashMap<String, TextureRegion>,
@ -75,8 +81,8 @@ pub struct TextureAtlas {
impl TextureAtlas { impl TextureAtlas {
pub fn empty() -> Self { pub fn empty() -> Self {
Self { Self {
atlas: DynamicImage::new(1,1, image::ColorType::Rgb8), atlas: DynamicImage::new(1, 1, image::ColorType::Rgb8),
textures: HashMap::new() textures: HashMap::new(),
} }
} }
@ -122,15 +128,13 @@ impl TextureAtlas {
fn insert_texture_at(base: &mut DynamicImage, texture: &DynamicImage, x_pos: u32, y_pos: u32) { fn insert_texture_at(base: &mut DynamicImage, texture: &DynamicImage, x_pos: u32, y_pos: u32) {
for y in 0..texture.height() { for y in 0..texture.height() {
for x in 0..texture.width() { for x in 0..texture.width() {
let pixel = texture.get_pixel(x,y); let pixel = texture.get_pixel(x, y);
base.put_pixel(x + x_pos, y + y_pos, pixel); base.put_pixel(x + x_pos, y + y_pos, pixel);
} }
} }
} }
pub fn from_texture_paths( pub fn from_texture_paths(paths: Vec<String>) -> Self {
paths: Vec<String>,
) -> Self {
let mut textures: Vec<DynamicImage> = Vec::new(); let mut textures: Vec<DynamicImage> = Vec::new();
let mut regions: HashMap<String, TextureRegion> = HashMap::new(); let mut regions: HashMap<String, TextureRegion> = HashMap::new();
@ -143,14 +147,20 @@ impl TextureAtlas {
info!("Textures loaded!"); info!("Textures loaded!");
info!("Sorting textures by height..."); info!("Sorting textures by height...");
let mut texture_path_pairs: Vec<(&DynamicImage, &String)> = textures.iter().zip(paths.iter()).collect(); let mut texture_path_pairs: Vec<(&DynamicImage, &String)> =
textures.iter().zip(paths.iter()).collect();
texture_path_pairs.sort_by(|a, b| b.0.height().cmp(&a.0.height())); texture_path_pairs.sort_by(|a, b| b.0.height().cmp(&a.0.height()));
let (sorted_textures, sorted_paths): (Vec<&DynamicImage>, Vec<&String>) = texture_path_pairs.into_iter().unzip(); let (sorted_textures, sorted_paths): (Vec<&DynamicImage>, Vec<&String>) =
let sorted_textures: Vec<DynamicImage> = sorted_textures.into_iter().map(|t| t.clone()).collect(); texture_path_pairs.into_iter().unzip();
let sorted_textures: Vec<DynamicImage> =
sorted_textures.into_iter().map(|t| t.clone()).collect();
let sorted_paths: Vec<String> = sorted_paths.into_iter().map(|s| s.to_string()).collect(); let sorted_paths: Vec<String> = sorted_paths.into_iter().map(|s| s.to_string()).collect();
let (height, width) = (Self::calculate_atlas_height(&sorted_textures), Self::calculate_atlas_width(&sorted_textures)); let (height, width) = (
let mut base = DynamicImage::new_rgba8(width,height); Self::calculate_atlas_height(&sorted_textures),
Self::calculate_atlas_width(&sorted_textures),
);
let mut base = DynamicImage::new_rgba8(width, height);
let mut previous = sorted_textures.get(0).unwrap().height(); let mut previous = sorted_textures.get(0).unwrap().height();
let mut x_offset: u32 = 0; let mut x_offset: u32 = 0;
@ -166,16 +176,18 @@ impl TextureAtlas {
} }
Self::insert_texture_at(&mut base, &texture, x_offset, y_offset); Self::insert_texture_at(&mut base, &texture, x_offset, y_offset);
regions.insert(path.to_string(), TextureRegion::new( let texel_w = 0.5 / width as f32;
x_offset as f32 / width as f32, let texel_h = 0.5 / height as f32;
y_offset as f32 / height as f32,
(x_offset + texture.width()) as f32 / width as f32, let u0 = (x_offset as f32 + texel_w) / width as f32;
(y_offset + texture.height()) as f32 / height as f32, let v0 = (y_offset as f32 + texel_h) / height as f32;
texture.dimensions(), let u1 = ((x_offset + texture.width()) as f32 - texel_w) / width as f32;
0.0, let v1 = ((y_offset + texture.height()) as f32 - texel_h) / height as f32;
0.0,
0.0 regions.insert(
)); path.to_string(),
TextureRegion::new(u0, v0, u1, v1, texture.dimensions(), 0.0, 0.0, 0.0),
);
x_offset += texture.width(); x_offset += texture.width();
} }
@ -183,26 +195,29 @@ impl TextureAtlas {
TextureAtlas { TextureAtlas {
atlas: base, atlas: base,
textures: regions textures: regions,
} }
} }
pub fn from_textures( pub fn from_textures(names: Vec<String>, textures: Vec<DynamicImage>) -> Self {
names: Vec<String>,
textures: Vec<DynamicImage>,
) -> Self {
let mut regions: HashMap<String, TextureRegion> = HashMap::new(); let mut regions: HashMap<String, TextureRegion> = HashMap::new();
info!("Sorting textures by height..."); info!("Sorting textures by height...");
let mut texture_path_pairs: Vec<(&DynamicImage, &String)> = textures.iter().zip(names.iter()).collect(); let mut texture_path_pairs: Vec<(&DynamicImage, &String)> =
textures.iter().zip(names.iter()).collect();
texture_path_pairs.sort_by(|a, b| b.0.height().cmp(&a.0.height())); texture_path_pairs.sort_by(|a, b| b.0.height().cmp(&a.0.height()));
let (sorted_textures, sorted_paths): (Vec<&DynamicImage>, Vec<&String>) = texture_path_pairs.into_iter().unzip(); let (sorted_textures, sorted_paths): (Vec<&DynamicImage>, Vec<&String>) =
let sorted_textures: Vec<DynamicImage> = sorted_textures.into_iter().map(|t| t.clone()).collect(); texture_path_pairs.into_iter().unzip();
let sorted_textures: Vec<DynamicImage> =
sorted_textures.into_iter().map(|t| t.clone()).collect();
let sorted_paths: Vec<String> = sorted_paths.into_iter().map(|s| s.to_string()).collect(); let sorted_paths: Vec<String> = sorted_paths.into_iter().map(|s| s.to_string()).collect();
let (height, width) = (Self::calculate_atlas_height(&sorted_textures), Self::calculate_atlas_width(&sorted_textures)); let (height, width) = (
let mut base = DynamicImage::new_rgba8(width,height); Self::calculate_atlas_height(&sorted_textures),
Self::calculate_atlas_width(&sorted_textures),
);
let mut base = DynamicImage::new_rgba8(width, height);
let mut previous = sorted_textures.get(0).unwrap().height(); let mut previous = sorted_textures.get(0).unwrap().height();
let mut x_offset: u32 = 0; let mut x_offset: u32 = 0;
@ -218,7 +233,9 @@ impl TextureAtlas {
} }
Self::insert_texture_at(&mut base, &texture, x_offset, y_offset); Self::insert_texture_at(&mut base, &texture, x_offset, y_offset);
regions.insert(name.to_string(), TextureRegion::new( regions.insert(
name.to_string(),
TextureRegion::new(
x_offset as f32 / width as f32, x_offset as f32 / width as f32,
y_offset as f32 / height as f32, y_offset as f32 / height as f32,
(x_offset + texture.width()) as f32 / width as f32, (x_offset + texture.width()) as f32 / width as f32,
@ -226,8 +243,9 @@ impl TextureAtlas {
texture.dimensions(), texture.dimensions(),
0.0, 0.0,
0.0, 0.0,
0.0 0.0,
)); ),
);
x_offset += texture.width(); x_offset += texture.width();
} }
@ -235,7 +253,7 @@ impl TextureAtlas {
TextureAtlas { TextureAtlas {
atlas: base, atlas: base,
textures: regions textures: regions,
} }
} }
@ -243,10 +261,10 @@ impl TextureAtlas {
glyphs.sort_by(|a, b| b.render.height().cmp(&a.render.height())); glyphs.sort_by(|a, b| b.render.height().cmp(&a.render.height()));
let height = Self::calculate_atlas_height( let height = Self::calculate_atlas_height(
&glyphs.iter().map(|g| g.render.clone()).collect::<Vec<_>>() &glyphs.iter().map(|g| g.render.clone()).collect::<Vec<_>>(),
); );
let width = Self::calculate_atlas_width( let width = Self::calculate_atlas_width(
&glyphs.iter().map(|g| g.render.clone()).collect::<Vec<_>>() &glyphs.iter().map(|g| g.render.clone()).collect::<Vec<_>>(),
); );
let padding = (glyphs.len() * 3) as u32; let padding = (glyphs.len() * 3) as u32;
@ -257,7 +275,6 @@ impl TextureAtlas {
let mut x_offset: u32 = 0; let mut x_offset: u32 = 0;
let mut y_offset: u32 = 0; let mut y_offset: u32 = 0;
for g in glyphs.iter() { for g in glyphs.iter() {
let glyph_w = g.render.width(); let glyph_w = g.render.width();
let glyph_h = g.render.height(); let glyph_h = g.render.height();
@ -276,7 +293,10 @@ impl TextureAtlas {
let v1 = (y_offset + glyph_h) as f32 / height as f32; let v1 = (y_offset + glyph_h) as f32 / height as f32;
let region = TextureRegion::new( let region = TextureRegion::new(
u0, v0, u1, v1, u0,
v0,
u1,
v1,
(glyph_w, glyph_h), (glyph_w, glyph_h),
g.advance, g.advance,
g.offset_x, g.offset_x,
@ -294,6 +314,106 @@ impl TextureAtlas {
} }
} }
pub fn from_fonts(fonts: &Vec<Font>) -> Self {
if fonts.is_empty() {
return Self::empty();
}
let mut all_glyphs: Vec<(String, DynamicImage, TextureRegion)> = Vec::new();
let mut font_indices: Vec<usize> = (0..fonts.len()).collect();
font_indices.sort_by(|&a, &b| fonts[a].name().cmp(&fonts[b].name()));
for fi in font_indices {
let font = &fonts[fi];
let font_name = font.name();
let mut glyph_names: Vec<String> = font.glyphs().textures().keys().cloned().collect();
glyph_names.sort();
for glyph_name in glyph_names {
let region = font.glyphs().textures().get(&glyph_name).unwrap();
let (u0, v0, u1, v1) = (region.u0(), region.v0(), region.u1(), region.v1());
let (width, height) = region.dimensions();
let src_x = (u0 * font.glyphs().atlas().width() as f32) as u32;
let src_y = (v0 * font.glyphs().atlas().height() as f32) as u32;
let glyph_img = DynamicImage::ImageRgba8(
font.glyphs()
.atlas()
.view(src_x, src_y, width, height)
.to_image(),
);
let key = format!("{}::{}", font_name, glyph_name);
all_glyphs.push((key, glyph_img, region.clone()));
}
}
all_glyphs.sort_by(|a, b| {
let ha = a.1.height();
let hb = b.1.height();
match hb.cmp(&ha) {
std::cmp::Ordering::Equal => a.0.cmp(&b.0),
other => other,
}
});
let textures: Vec<DynamicImage> =
all_glyphs.iter().map(|(_, img, _)| img.clone()).collect();
let atlas_height = Self::calculate_atlas_height(&textures);
let atlas_width = Self::calculate_atlas_width(&textures);
let padding = (all_glyphs.len() * 3) as u32;
let mut base = DynamicImage::new_rgba8(atlas_width + padding, atlas_height);
let mut regions = HashMap::new();
let mut current_row_height = textures[0].height();
let mut x_offset: u32 = 0;
let mut y_offset: u32 = 0;
for (key, img, original_region) in all_glyphs {
let w = img.width();
let h = img.height();
if h != current_row_height {
y_offset += current_row_height + 3;
x_offset = 0;
current_row_height = h;
}
Self::insert_texture_at(&mut base, &img, x_offset, y_offset);
let u0 = x_offset as f32 / (atlas_width + padding) as f32;
let v0 = y_offset as f32 / atlas_height as f32;
let u1 = (x_offset + w) as f32 / (atlas_width + padding) as f32;
let v1 = (y_offset + h) as f32 / atlas_height as f32;
let region = TextureRegion::new(
u0,
v0,
u1,
v1,
(w, h),
original_region.advance(),
original_region.offset_x(),
original_region.offset_y(),
);
regions.insert(key, region);
x_offset += w + 3;
}
TextureAtlas {
atlas: base,
textures: regions,
}
}
pub fn atlas(&self) -> &DynamicImage { pub fn atlas(&self) -> &DynamicImage {
&self.atlas &self.atlas
} }