feat(texture_atlas): reworked the texture atlas creation

This commit is contained in:
lisk77 2025-11-04 01:27:40 +01:00
parent 676b1dcb18
commit bdf33f2714
5 changed files with 274 additions and 263 deletions

View file

@ -1,12 +1,12 @@
use crate::renderer::Renderer;
use crate::{ use crate::{
camera::CameraManager, camera::CameraManager,
render_context::RenderContext, render_context::RenderContext,
render_pass::{universal_clear_execute, universal_load_execute, RenderPass}, render_pass::{universal_clear_execute, universal_load_execute, RenderPass},
renderer::Renderer,
}; };
use comet_colors::Color; use comet_colors::Color;
use comet_ecs::{Component, Render, Render2D, Transform2D}; use comet_ecs::{Component, Render, Render2D, Transform2D};
use comet_log::{debug, error, info}; use comet_log::*;
use comet_resources::{ use comet_resources::{
font::Font, graphic_resource_manager::GraphicResourceManager, texture_atlas::*, Texture, Vertex, font::Font, graphic_resource_manager::GraphicResourceManager, texture_atlas::*, Texture, Vertex,
}; };
@ -541,13 +541,28 @@ impl<'a> Renderer2D<'a> {
} }
fn get_glyph_region(&self, glyph: char, font: String) -> &TextureRegion { fn get_glyph_region(&self, glyph: char, font: String) -> &TextureRegion {
let font_atlas = self let key = format!("{}::{}", font, glyph);
.resource_manager
.fonts() match self.resource_manager.font_atlas().textures().get(&key) {
.iter() Some(region) => region,
.find(|f| f.name() == font) None => {
.unwrap(); warn!(
font_atlas.get_glyph(glyph).unwrap() "Missing glyph for character '{}' in font '{}', using fallback.",
glyph, font
);
let fallback_key = format!("{}:: ", font);
self.resource_manager
.font_atlas()
.textures()
.get(&fallback_key)
.unwrap_or_else(|| {
panic!(
"No fallback glyph available (space also missing) for font '{}'",
font
)
})
}
}
} }
pub fn add_text_to_buffers( pub fn add_text_to_buffers(

View file

@ -5,11 +5,12 @@ edition = "2021"
[dependencies] [dependencies]
comet_log = { path = "../comet_log" } comet_log = { path = "../comet_log" }
wgpu = { version = "22.0", features = ["spirv"] } wgpu = { version = "22.0", features = ["glsl", "wgsl", "naga-ir"] }
anyhow = "1.0" anyhow = "1.0"
bytemuck = { version = "1.16", features = [ "derive" ] } bytemuck = { version = "1.16", features = [ "derive" ] }
ab_glyph = "0.2.29" ab_glyph = "0.2.29"
chrono = "0.4.40" chrono = "0.4.40"
rect_packer = "0.2.1"
[dependencies.image] [dependencies.image]
version = "0.24" version = "0.24"

View file

@ -6,10 +6,7 @@ use crate::{
Texture, Texture,
}; };
use comet_log::info; use comet_log::info;
use wgpu::{ use wgpu::{naga::ShaderStage, Device, Queue, ShaderModule};
naga::{self, ShaderStage},
Device, Queue, ShaderModule,
};
pub struct GraphicResourceManager { pub struct GraphicResourceManager {
texture_atlas: TextureAtlas, texture_atlas: TextureAtlas,
@ -127,7 +124,7 @@ impl GraphicResourceManager {
source: wgpu::ShaderSource::Glsl { source: wgpu::ShaderSource::Glsl {
shader: shader_source.into(), shader: shader_source.into(),
stage, stage,
defines: naga::FastHashMap::default(), defines: Default::default(),
}, },
}) })
} else { } else {

View file

@ -1,6 +1,7 @@
use crate::font::*; use crate::font::*;
use comet_log::*; use comet_log::*;
use image::{DynamicImage, GenericImage, GenericImageView}; use image::{DynamicImage, GenericImage, GenericImageView, RgbaImage};
use rect_packer::{Config, Packer, Rect};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@ -81,335 +82,332 @@ 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_rgba8(1, 1),
textures: HashMap::new(), textures: HashMap::new(),
} }
} }
pub fn texture_paths(&self) -> Vec<String> { pub fn texture_paths(&self) -> Vec<String> {
self.textures.keys().map(|k| k.to_string()).collect() self.textures.keys().cloned().collect()
} }
fn calculate_atlas_width(textures: &Vec<DynamicImage>) -> u32 { #[inline(always)]
let mut last_height: u32 = textures.get(0).unwrap().height(); fn next_power_of_two(mut x: u32) -> u32 {
let mut widths: Vec<u32> = Vec::new(); if x == 0 {
let mut current_width: u32 = 0; return 1;
for texture in textures {
if last_height != texture.height() {
widths.push(current_width);
current_width = 0;
last_height = texture.height();
} }
current_width += texture.width(); x -= 1;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
x + 1
} }
widths.push(current_width); fn pack_textures(
textures: &[(&String, &DynamicImage)],
padding: u32,
) -> (u32, u32, HashMap<String, Rect>) {
let mut atlas_size = 512;
let max_size = 8192;
*widths.iter().max().unwrap() let valid_textures: Vec<(String, DynamicImage)> = textures
} .iter()
.map(|(name, tex)| {
fn calculate_atlas_height(textures: &Vec<DynamicImage>) -> u32 { let (w, h) = (tex.width(), tex.height());
let last_height: u32 = textures.get(0).unwrap().height(); if w == 0 || h == 0 {
let mut height: u32 = 0; warn!(
height += last_height; "Texture '{}' has invalid size {}x{}, replacing with 1x1 transparent dummy.",
name, w, h
for texture in textures {
if last_height == texture.height() {
continue;
}
height += texture.height();
}
height
}
fn insert_texture_at(base: &mut DynamicImage, texture: &DynamicImage, x_pos: u32, y_pos: u32) {
for y in 0..texture.height() {
for x in 0..texture.width() {
let pixel = texture.get_pixel(x, y);
base.put_pixel(x + x_pos, y + y_pos, pixel);
}
}
}
pub fn from_texture_paths(paths: Vec<String>) -> Self {
let mut textures: Vec<DynamicImage> = Vec::new();
let mut regions: HashMap<String, TextureRegion> = HashMap::new();
info!("Loading textures...");
for path in &paths {
textures.push(image::open(&Path::new(path.as_str())).expect("Failed to load texture"));
}
info!("Textures loaded!");
info!("Sorting textures by height...");
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()));
let (sorted_textures, sorted_paths): (Vec<&DynamicImage>, Vec<&String>) =
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 (height, width) = (
Self::calculate_atlas_height(&sorted_textures),
Self::calculate_atlas_width(&sorted_textures),
); );
let mut base = DynamicImage::new_rgba8(width, height); let mut img = RgbaImage::new(1, 1);
img.put_pixel(0, 0, image::Rgba([0, 0, 0, 0]));
(name.to_string(), DynamicImage::ImageRgba8(img))
} else {
((*name).clone(), (*tex).clone())
}
})
.collect();
let mut previous = sorted_textures.get(0).unwrap().height(); if valid_textures.is_empty() {
let mut x_offset: u32 = 0; error!("No valid textures to pack!");
let mut y_offset: u32 = 0; return (0, 0, HashMap::new());
info!("Creating texture atlas...");
for (texture, path) in sorted_textures.iter().zip(sorted_paths.iter()) {
if texture.height() != previous {
y_offset += previous;
x_offset = 0;
previous = texture.height();
} }
Self::insert_texture_at(&mut base, &texture, x_offset, y_offset); loop {
let texel_w = 0.5 / width as f32; let config = Config {
let texel_h = 0.5 / height as f32; width: atlas_size as i32,
height: atlas_size as i32,
border_padding: padding as i32,
rectangle_padding: padding as i32,
};
let u0 = (x_offset as f32 + texel_w) / width as f32; let mut packer = Packer::new(config);
let v0 = (y_offset as f32 + texel_h) / height as f32; let mut placements = HashMap::new();
let u1 = ((x_offset + texture.width()) as f32 - texel_w) / width as f32; let mut max_x = 0i32;
let v1 = ((y_offset + texture.height()) as f32 - texel_h) / height as f32; let mut max_y = 0i32;
let mut failed = false;
for (name, tex) in &valid_textures {
let width = tex.width() as i32;
let height = tex.height() as i32;
if width > atlas_size as i32 || height > atlas_size as i32 {
error!(
"Texture '{}' is too large ({width}x{height}) for current atlas size {atlas_size}x{atlas_size}",
name
);
failed = true;
break;
}
if let Some(rect) = packer.pack(width, height, false) {
max_x = max_x.max(rect.x + rect.width);
max_y = max_y.max(rect.y + rect.height);
placements.insert(name.clone(), rect);
} else {
failed = true;
break;
}
}
if failed {
if atlas_size >= max_size {
error!(
"Failed to pack all textures even at max atlas size ({}x{}).",
max_size, max_size
);
return (max_x as u32, max_y as u32, placements);
}
info!(
"Atlas size {}x{} too small, doubling to {}x{}...",
atlas_size,
atlas_size,
atlas_size * 2,
atlas_size * 2
);
atlas_size *= 2;
} else {
info!(
"Created texture atlas ({}x{}) with {} textures.",
atlas_size,
atlas_size,
placements.len()
);
return (max_x as u32, max_y as u32, placements);
}
}
}
fn build_atlas(
textures: &[(&String, &DynamicImage)],
placements: &HashMap<String, Rect>,
atlas_width: u32,
atlas_height: u32,
) -> (RgbaImage, HashMap<String, TextureRegion>) {
let mut base = RgbaImage::new(atlas_width, atlas_height);
let mut regions = HashMap::new();
for (name, tex) in textures {
if let Some(rect) = placements.get(*name) {
base.copy_from(&tex.to_rgba8(), rect.x as u32, rect.y as u32)
.unwrap_or_else(|_| {
panic!(
"Failed to blit texture '{}' into atlas at ({}, {})",
name, rect.x, rect.y
)
});
let u0 = rect.x as f32 / atlas_width as f32;
let v0 = rect.y as f32 / atlas_height as f32;
let u1 = (rect.x + rect.width) as f32 / atlas_width as f32;
let v1 = (rect.y + rect.height) as f32 / atlas_height as f32;
regions.insert( regions.insert(
path.to_string(), (*name).clone(),
TextureRegion::new(u0, v0, u1, v1, texture.dimensions(), 0.0, 0.0, 0.0),
);
x_offset += texture.width();
}
info!("Texture atlas created!");
TextureAtlas {
atlas: base,
textures: regions,
}
}
pub fn from_textures(names: Vec<String>, textures: Vec<DynamicImage>) -> Self {
let mut regions: HashMap<String, TextureRegion> = HashMap::new();
info!("Sorting textures by height...");
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()));
let (sorted_textures, sorted_paths): (Vec<&DynamicImage>, Vec<&String>) =
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 (height, width) = (
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 x_offset: u32 = 0;
let mut y_offset: u32 = 0;
info!("Creating texture atlas...");
for (texture, name) in sorted_textures.iter().zip(sorted_paths.iter()) {
if texture.height() != previous {
y_offset += previous;
x_offset = 0;
previous = texture.height();
}
Self::insert_texture_at(&mut base, &texture, x_offset, y_offset);
regions.insert(
name.to_string(),
TextureRegion::new( TextureRegion::new(
x_offset as f32 / width as f32, u0,
y_offset as f32 / height as f32, v0,
(x_offset + texture.width()) as f32 / width as f32, u1,
(y_offset + texture.height()) as f32 / height as f32, v1,
texture.dimensions(), (rect.width as u32, rect.height as u32),
0.0, 0.0,
0.0, 0.0,
0.0, 0.0,
), ),
); );
x_offset += texture.width(); }
} }
info!("Texture atlas created!"); (base, regions)
}
pub fn from_texture_paths(paths: Vec<String>) -> Self {
let mut textures = Vec::new();
info!("Loading textures...");
for path in &paths {
let img = image::open(Path::new(path)).expect("Failed to load texture");
textures.push((path, img));
}
info!("Packing textures...");
let tex_refs: Vec<(&String, &DynamicImage)> =
textures.iter().map(|(p, i)| (*p, i)).collect();
let (atlas_w, atlas_h, placements) = Self::pack_textures(&tex_refs, 2);
let atlas_w = Self::next_power_of_two(atlas_w);
let atlas_h = Self::next_power_of_two(atlas_h);
let (base, regions) = Self::build_atlas(&tex_refs, &placements, atlas_w, atlas_h);
info!(
"Created texture atlas ({}x{}) with {} textures.",
atlas_w,
atlas_h,
regions.len()
);
TextureAtlas { TextureAtlas {
atlas: base, atlas: DynamicImage::ImageRgba8(base),
textures: regions, textures: regions,
} }
} }
pub fn from_glyphs(mut glyphs: Vec<GlyphData>) -> Self { pub fn from_textures(names: Vec<String>, textures: Vec<DynamicImage>) -> Self {
glyphs.sort_by(|a, b| b.render.height().cmp(&a.render.height())); assert_eq!(
names.len(),
let height = Self::calculate_atlas_height( textures.len(),
&glyphs.iter().map(|g| g.render.clone()).collect::<Vec<_>>(), "Names and textures must have the same length."
);
let width = Self::calculate_atlas_width(
&glyphs.iter().map(|g| g.render.clone()).collect::<Vec<_>>(),
); );
let padding = (glyphs.len() * 3) as u32; let tex_refs: Vec<(&String, &DynamicImage)> = names.iter().zip(textures.iter()).collect();
let mut base = DynamicImage::new_rgba8(width + padding, height); let (atlas_w, atlas_h, placements) = Self::pack_textures(&tex_refs, 2);
let mut regions = HashMap::new(); let atlas_w = Self::next_power_of_two(atlas_w);
let mut current_row_height = glyphs[0].render.height(); let atlas_h = Self::next_power_of_two(atlas_h);
let mut x_offset: u32 = 0;
let mut y_offset: u32 = 0;
for g in glyphs.iter() { let (base, regions) = Self::build_atlas(&tex_refs, &placements, atlas_w, atlas_h);
let glyph_w = g.render.width();
let glyph_h = g.render.height();
if glyph_h != current_row_height { TextureAtlas {
y_offset += current_row_height + 3; atlas: DynamicImage::ImageRgba8(base),
x_offset = 0; textures: regions,
current_row_height = glyph_h; }
} }
Self::insert_texture_at(&mut base, &g.render, x_offset, y_offset); pub fn from_glyphs(glyphs: Vec<GlyphData>) -> Self {
let textures: Vec<(String, DynamicImage)> = glyphs
.iter()
.map(|g| (g.name.clone(), g.render.clone()))
.collect();
let u0 = x_offset as f32 / (width + padding) as f32; let tex_refs: Vec<(&String, &DynamicImage)> =
let v0 = y_offset as f32 / height as f32; textures.iter().map(|(n, i)| (n, i)).collect();
let u1 = (x_offset + glyph_w) as f32 / (width + padding) as f32;
let v1 = (y_offset + glyph_h) as f32 / height as f32; let (atlas_w, atlas_h, placements) = Self::pack_textures(&tex_refs, 2);
let atlas_w = Self::next_power_of_two(atlas_w);
let atlas_h = Self::next_power_of_two(atlas_h);
let mut base = RgbaImage::new(atlas_w, atlas_h);
let mut regions = HashMap::new();
for g in glyphs.iter() {
if let Some(rect) = placements.get(&g.name) {
base.copy_from(&g.render.to_rgba8(), rect.x as u32, rect.y as u32)
.unwrap();
let u0 = rect.x as f32 / atlas_w as f32;
let v0 = rect.y as f32 / atlas_h as f32;
let u1 = (rect.x + rect.width) as f32 / atlas_w as f32;
let v1 = (rect.y + rect.height) as f32 / atlas_h as f32;
let region = TextureRegion::new( let region = TextureRegion::new(
u0, u0,
v0, v0,
u1, u1,
v1, v1,
(glyph_w, glyph_h), (rect.width as u32, rect.height as u32),
g.advance, g.advance,
g.offset_x, g.offset_x,
g.offset_y, g.offset_y,
); );
regions.insert(g.name.clone(), region); regions.insert(g.name.clone(), region);
}
x_offset += glyph_w + 3;
} }
TextureAtlas { TextureAtlas {
atlas: base, atlas: DynamicImage::ImageRgba8(base),
textures: regions, textures: regions,
} }
} }
pub fn from_fonts(fonts: &Vec<Font>) -> Self { pub fn from_fonts(fonts: &[Font]) -> Self {
if fonts.is_empty() { if fonts.is_empty() {
return Self::empty(); return Self::empty();
} }
let mut all_glyphs: Vec<(String, DynamicImage, TextureRegion)> = Vec::new(); let mut all_glyphs = Vec::new();
let mut font_indices: Vec<usize> = (0..fonts.len()).collect(); for font in fonts {
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 font_name = font.name();
let src_atlas = font.glyphs().atlas();
let atlas_width = src_atlas.width();
let atlas_height = src_atlas.height();
let mut glyph_names: Vec<String> = font.glyphs().textures().keys().cloned().collect(); for (glyph_name, region) in font.glyphs().textures() {
glyph_names.sort(); let src_x = (region.u0() * atlas_width as f32) as u32;
let src_y = (region.v0() * atlas_height as f32) as u32;
let width = region.dimensions().0;
let height = region.dimensions().1;
for glyph_name in glyph_names { let glyph_img = src_atlas.view(src_x, src_y, width, height).to_image();
let region = font.glyphs().textures().get(&glyph_name).unwrap();
let (u0, v0) = (region.u0(), region.v0());
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); let key = format!("{}::{}", font_name, glyph_name);
all_glyphs.push((key, DynamicImage::ImageRgba8(glyph_img), region.clone()));
all_glyphs.push((key, glyph_img, region.clone()));
} }
} }
all_glyphs.sort_by(|a, b| { let tex_refs: Vec<(&String, &DynamicImage)> =
let ha = a.1.height(); all_glyphs.iter().map(|(n, i, _)| (n, i)).collect();
let hb = b.1.height(); let (atlas_w, atlas_h, placements) = Self::pack_textures(&tex_refs, 2);
match hb.cmp(&ha) { let atlas_w = Self::next_power_of_two(atlas_w);
std::cmp::Ordering::Equal => a.0.cmp(&b.0), let atlas_h = Self::next_power_of_two(atlas_h);
other => other,
}
});
let textures: Vec<DynamicImage> = let mut base = RgbaImage::new(atlas_w, atlas_h);
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 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 { for (key, img, original_region) in all_glyphs {
let w = img.width(); if let Some(rect) = placements.get(&key) {
let h = img.height(); base.copy_from(&img.to_rgba8(), rect.x as u32, rect.y as u32)
.unwrap();
if h != current_row_height { let u0 = rect.x as f32 / atlas_w as f32;
y_offset += current_row_height + 3; let v0 = rect.y as f32 / atlas_h as f32;
x_offset = 0; let u1 = (rect.x + rect.width) as f32 / atlas_w as f32;
current_row_height = h; let v1 = (rect.y + rect.height) as f32 / atlas_h as f32;
}
Self::insert_texture_at(&mut base, &img, x_offset, y_offset); regions.insert(
key,
let u0 = x_offset as f32 / (atlas_width + padding) as f32; TextureRegion::new(
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, u0,
v0, v0,
u1, u1,
v1, v1,
(w, h), (rect.width as u32, rect.height as u32),
original_region.advance(), original_region.advance(),
original_region.offset_x(), original_region.offset_x(),
original_region.offset_y(), original_region.offset_y(),
),
); );
}
regions.insert(key, region);
x_offset += w + 3;
} }
TextureAtlas { TextureAtlas {
atlas: base, atlas: DynamicImage::ImageRgba8(base),
textures: regions, textures: regions,
} }
} }