Improved image packer
parent
1001b8ba4a
commit
10f9776108
|
@ -1,5 +1,5 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use galactica_packer::{SpriteAtlasImage, SpriteAtlasIndex};
|
use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
|
||||||
use image::{imageops, ImageBuffer, Rgba, RgbaImage};
|
use image::{imageops, ImageBuffer, Rgba, RgbaImage};
|
||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
|
@ -11,6 +11,7 @@ use std::{
|
||||||
// TODO: rework texturearray
|
// TODO: rework texturearray
|
||||||
// TODO: reasonable sprite sizes
|
// TODO: reasonable sprite sizes
|
||||||
// TODO: consistent naming
|
// TODO: consistent naming
|
||||||
|
// TODO: parallelize
|
||||||
// spriteatlas: the big images
|
// spriteatlas: the big images
|
||||||
// texture: the same, what we load to wgpu
|
// texture: the same, what we load to wgpu
|
||||||
// image: a single file
|
// image: a single file
|
||||||
|
@ -28,13 +29,16 @@ pub struct AtlasSet {
|
||||||
texture_limit: usize,
|
texture_limit: usize,
|
||||||
|
|
||||||
/// Keeps track of image files
|
/// Keeps track of image files
|
||||||
index: SpriteAtlasIndex,
|
index: SpriteAtlas,
|
||||||
|
|
||||||
/// Array of textures, grows as needed
|
/// Array of textures, grows as needed
|
||||||
texture_list: Vec<ImageBuffer<Rgba<u8>, Vec<u8>>>,
|
texture_list: Vec<ImageBuffer<Rgba<u8>, Vec<u8>>>,
|
||||||
|
|
||||||
/// The size of the smallest image that didn't fit in each texture
|
/// The size of the smallest image that didn't fit in each texture
|
||||||
image_max_sizes: Vec<u32>,
|
image_max_sizes: Vec<[u32; 2]>,
|
||||||
|
|
||||||
|
/// (y-value, image size)
|
||||||
|
image_y_start: Vec<(u32, [u32; 2])>,
|
||||||
|
|
||||||
/// A list of used regions in each texture
|
/// A list of used regions in each texture
|
||||||
/// Format: ([xpos, ypos], [width, height])
|
/// Format: ([xpos, ypos], [width, height])
|
||||||
|
@ -43,105 +47,137 @@ pub struct AtlasSet {
|
||||||
|
|
||||||
/// Used to calculate packing efficiency
|
/// Used to calculate packing efficiency
|
||||||
used_area: f64,
|
used_area: f64,
|
||||||
|
|
||||||
|
/// The root directory that contains all image files.
|
||||||
|
/// Files outside this directory will not be packed.
|
||||||
|
asset_root: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AtlasSet {
|
impl AtlasSet {
|
||||||
pub fn new(texture_width: u32, texture_height: u32, texture_limit: usize) -> Self {
|
pub fn new(
|
||||||
|
texture_width: u32,
|
||||||
|
texture_height: u32,
|
||||||
|
texture_limit: usize,
|
||||||
|
asset_root: &Path,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
asset_root: asset_root.to_path_buf(),
|
||||||
texture_width,
|
texture_width,
|
||||||
texture_height,
|
texture_height,
|
||||||
texture_limit,
|
texture_limit,
|
||||||
texture_list: Vec::new(),
|
texture_list: Vec::new(),
|
||||||
image_max_sizes: Vec::new(),
|
image_max_sizes: Vec::new(),
|
||||||
used_regions: Vec::new(),
|
used_regions: Vec::new(),
|
||||||
index: SpriteAtlasIndex::new(),
|
index: SpriteAtlas::new(),
|
||||||
used_area: 0f64,
|
used_area: 0f64,
|
||||||
|
image_y_start: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if new and fixed overlap,
|
|
||||||
/// or if new exits the atlas.
|
|
||||||
/// Parameters: ([xpos, ypos], [width, height])
|
|
||||||
pub fn boxes_overlap(&self, fixed: ([u32; 2], [u32; 2]), new: ([u32; 2], [u32; 2])) -> bool {
|
|
||||||
if new.0[0] + new.1[0] >= self.texture_width || new.0[1] + new.1[1] >= self.texture_height {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fixed.0[0] <= new.0[0] + new.1[0]
|
|
||||||
&& fixed.0[0] + fixed.1[0] >= new.0[0]
|
|
||||||
&& fixed.0[1] <= new.0[1] + new.1[1]
|
|
||||||
&& fixed.0[1] + fixed.1[1] >= new.0[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a sprite to this atlas set
|
/// Add a sprite to this atlas set
|
||||||
pub fn write_image(&mut self, path: &Path, dim: [u32; 2]) -> Result<usize> {
|
pub fn write_image(&mut self, path: &Path, dim: [u32; 2]) -> Result<usize> {
|
||||||
let mut f = File::open(&path)?;
|
let mut f = File::open(&path)?;
|
||||||
let mut bytes = Vec::new();
|
let mut bytes = Vec::new();
|
||||||
f.read_to_end(&mut bytes)?;
|
f.read_to_end(&mut bytes)?;
|
||||||
let img = image::load_from_memory(&bytes)?;
|
let img = image::load_from_memory(&bytes)?;
|
||||||
let mut pixel_idx = 0;
|
|
||||||
let mut atlas_idx = 0;
|
|
||||||
|
|
||||||
// Find first available region, starting at top-left of atlas 0.
|
let mut x = 0;
|
||||||
// Includes a few speed optimizations
|
let mut y = 0;
|
||||||
loop {
|
let mut final_atlas_idx = None;
|
||||||
|
|
||||||
|
// Loop over atlas textures
|
||||||
|
'outer: for atlas_idx in 0..self.texture_limit {
|
||||||
if atlas_idx >= self.texture_list.len() {
|
if atlas_idx >= self.texture_list.len() {
|
||||||
// We can't start another atlas, we're at the limit
|
|
||||||
if atlas_idx >= self.texture_limit {
|
|
||||||
// TODO: how does a user resolve this?
|
|
||||||
bail!("Sprites didn't fit into atlas");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start a new atlas
|
// Start a new atlas
|
||||||
self.texture_list
|
self.texture_list
|
||||||
.push(RgbaImage::new(self.texture_width, self.texture_height));
|
.push(RgbaImage::new(self.texture_width, self.texture_height));
|
||||||
self.used_regions.push(Vec::new());
|
self.used_regions.push(Vec::new());
|
||||||
self.image_max_sizes.push(u32::MAX)
|
self.image_max_sizes.push([u32::MAX, u32::MAX]);
|
||||||
|
self.image_y_start.push((0, [u32::MAX, u32::MAX]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let x = pixel_idx % self.texture_width;
|
// Optimization: save the smallest sprite that didn't fit in each atlas,
|
||||||
let y = pixel_idx / self.texture_height;
|
// and don't try to add similarly-sized sprites.
|
||||||
let new = ([x, y], dim);
|
if dim[0] >= self.image_max_sizes[atlas_idx][0]
|
||||||
let mut used = false;
|
&& dim[1] >= self.image_max_sizes[atlas_idx][1]
|
||||||
|
{
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
if self.image_y_start.len() != 0 {
|
||||||
|
let (sy, sd) = self.image_y_start[atlas_idx];
|
||||||
|
if dim[0] >= sd[0] || dim[1] >= sd[1] {
|
||||||
|
y = sy;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.image_y_start.push((0, [u32::MAX, u32::MAX]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut free = false;
|
||||||
|
let mut new;
|
||||||
|
'inner: while y < self.texture_height && !free {
|
||||||
|
new = ([x, y], dim);
|
||||||
|
free = true;
|
||||||
for r in &self.used_regions[atlas_idx] {
|
for r in &self.used_regions[atlas_idx] {
|
||||||
if self.boxes_overlap(*r, new) {
|
// If boxes overlap...
|
||||||
// Speed boost: skip the whole box
|
if r.0[0] < new.0[0] + new.1[0]
|
||||||
pixel_idx += new.1[0] - 1;
|
&& r.0[0] + r.1[0] > new.0[0]
|
||||||
used = true;
|
&& r.0[1] < new.0[1] + new.1[1]
|
||||||
break;
|
&& r.0[1] + r.1[1] > new.0[1]
|
||||||
|
{
|
||||||
|
// Skip the whole occupied area
|
||||||
|
x = r.1[0] + r.0[0];
|
||||||
|
if x + dim[0] >= self.texture_width {
|
||||||
|
y += 1;
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
free = false;
|
||||||
|
continue 'inner;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !used {
|
if y + dim[1] >= self.texture_height {
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
pixel_idx += 1;
|
|
||||||
|
|
||||||
// Speed boost: save the smallest sprite that didn't fit in each atlas,
|
|
||||||
// and don't even try to add bigger sprite.
|
|
||||||
if dim[0] * dim[1] >= self.image_max_sizes[atlas_idx] {
|
|
||||||
atlas_idx += 1;
|
|
||||||
pixel_idx = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This sprite didn't fit, move on to the next atlas
|
// This sprite didn't fit, move on to the next atlas
|
||||||
if pixel_idx >= self.texture_width * self.texture_height {
|
self.image_max_sizes[atlas_idx] = [dim[0] / 2, dim[1] / 2];
|
||||||
self.image_max_sizes[atlas_idx] = dim[0] * dim[1];
|
} else if free {
|
||||||
atlas_idx += 1;
|
final_atlas_idx = Some(atlas_idx);
|
||||||
pixel_idx = 0;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let atlas_idx = match final_atlas_idx {
|
||||||
|
None => bail!("textures didn't fit!"),
|
||||||
|
Some(s) => s,
|
||||||
|
};
|
||||||
|
|
||||||
// We found a spot for this image, write it.
|
// We found a spot for this image, write it.
|
||||||
let x = pixel_idx % self.texture_width;
|
//let img = RgbaImage::from_pixel(dim[0], dim[1], Rgba([0, 0, 0, 255]));
|
||||||
let y = pixel_idx / self.texture_height;
|
|
||||||
imageops::overlay(&mut self.texture_list[atlas_idx], &img, x.into(), y.into());
|
imageops::overlay(&mut self.texture_list[atlas_idx], &img, x.into(), y.into());
|
||||||
self.used_regions[atlas_idx].push(([x, y], dim));
|
self.used_regions[atlas_idx].push(([x, y], dim));
|
||||||
self.used_area += dim[0] as f64 * dim[1] as f64;
|
self.used_area += dim[0] as f64 * dim[1] as f64;
|
||||||
|
|
||||||
self.index.insert(
|
let (sy, sd) = self.image_y_start[atlas_idx];
|
||||||
path.to_path_buf(),
|
if dim[0] <= sd[0] && dim[1] <= sd[1] {
|
||||||
|
// Reset start y if both dimensions of this texture are smaller than the previous smallest texture
|
||||||
|
// We check for both, because that ensures that the smaller texture can tile the previous largest one.
|
||||||
|
self.image_y_start[atlas_idx] = (0, [dim[0] / 2, dim[1] / 2]);
|
||||||
|
} else {
|
||||||
|
self.image_y_start[atlas_idx] = (y.max(sy), sd);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = path.strip_prefix(&self.asset_root).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"path `{}` is not relative to asset root `{}`",
|
||||||
|
path.display(),
|
||||||
|
self.asset_root.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.index.index.insert(
|
||||||
|
p.to_path_buf(),
|
||||||
SpriteAtlasImage {
|
SpriteAtlasImage {
|
||||||
atlas: atlas_idx,
|
atlas: atlas_idx,
|
||||||
x: x as f32 / self.texture_width as f32,
|
x: x as f32 / self.texture_width as f32,
|
||||||
|
|
|
@ -3,10 +3,7 @@
|
||||||
//! This crate creates texture atlases from an asset tree.
|
//! This crate creates texture atlases from an asset tree.
|
||||||
//! The main interface for this crate is ... TODO
|
//! The main interface for this crate is ... TODO
|
||||||
|
|
||||||
use std::{
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
collections::HashMap,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -36,26 +33,16 @@ pub struct SpriteAtlasImage {
|
||||||
/// A map between file paths (relative to the root asset dir)
|
/// A map between file paths (relative to the root asset dir)
|
||||||
/// and [`AtlasTexture`]s.
|
/// and [`AtlasTexture`]s.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct SpriteAtlasIndex {
|
pub struct SpriteAtlas {
|
||||||
pub(crate) index: HashMap<PathBuf, SpriteAtlasImage>,
|
/// The images in this atlas
|
||||||
|
pub index: HashMap<PathBuf, SpriteAtlasImage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpriteAtlasIndex {
|
impl SpriteAtlas {
|
||||||
/// Make an empty [`SpriteAtlasIndex`]
|
/// Make an empty [`SpriteAtlasIndex`]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
index: HashMap::new(),
|
index: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make an empty [`SpriteAtlasIndex`]
|
|
||||||
pub fn insert(&mut self, path: PathBuf, atlasimage: SpriteAtlasImage) {
|
|
||||||
self.index.insert(path, atlasimage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get an [`AtlasImage`] for a file `p`.
|
|
||||||
/// Paths must be relative to the root of the asset directory.
|
|
||||||
pub fn get(&self, p: &Path) -> Option<&SpriteAtlasImage> {
|
|
||||||
self.index.get(p)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,14 @@ mod atlasset;
|
||||||
|
|
||||||
use atlasset::AtlasSet;
|
use atlasset::AtlasSet;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{bail, Result};
|
||||||
use image::io::Reader;
|
use image::io::Reader;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
// TODO: procedural sun coloring
|
// TODO: procedural sun coloring
|
||||||
// TODO: transparency buffer
|
// TODO: gap between sprites?
|
||||||
|
// TODO: warning when images have extra transparency
|
||||||
// TODO: don't re-encode. Direct to gpu?
|
// TODO: don't re-encode. Direct to gpu?
|
||||||
// (maybe not, tiling is slow. Make it work with files first.)
|
// (maybe not, tiling is slow. Make it work with files first.)
|
||||||
// TODO: path for atlas files
|
// TODO: path for atlas files
|
||||||
|
@ -24,15 +25,17 @@ use walkdir::WalkDir;
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
|
|
||||||
for e in WalkDir::new("./assets/render")
|
let asset_root = Path::new("./assets/render");
|
||||||
.into_iter()
|
|
||||||
.filter_map(|e| e.ok())
|
// Total number of pixels we want to add
|
||||||
{
|
let mut total_dim = 0f64;
|
||||||
|
|
||||||
|
for e in WalkDir::new(&asset_root).into_iter().filter_map(|e| e.ok()) {
|
||||||
if e.metadata().unwrap().is_file() {
|
if e.metadata().unwrap().is_file() {
|
||||||
// TODO: better warnings
|
// TODO: better warnings
|
||||||
match e.path().extension() {
|
match e.path().extension() {
|
||||||
Some(t) => {
|
Some(t) => {
|
||||||
if t.to_str() != Some("png") {
|
if t.to_str() != Some("png") && t.to_str() != Some("jpg") {
|
||||||
println!("[WARNING] {e:#?} is not a png file, skipping.");
|
println!("[WARNING] {e:#?} is not a png file, skipping.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -46,7 +49,8 @@ fn main() -> Result<()> {
|
||||||
let path = e.path().to_path_buf();
|
let path = e.path().to_path_buf();
|
||||||
let reader = Reader::open(&path)?;
|
let reader = Reader::open(&path)?;
|
||||||
let dim = reader.into_dimensions()?;
|
let dim = reader.into_dimensions()?;
|
||||||
files.push((path, [dim.0, dim.1]))
|
files.push((path, [dim.0, dim.1]));
|
||||||
|
total_dim += dim.0 as f64 * dim.1 as f64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,10 +61,19 @@ fn main() -> Result<()> {
|
||||||
b.cmp(&a)
|
b.cmp(&a)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Make sure we have enough pixels.
|
||||||
|
// This check is conservative and imperfect:
|
||||||
|
// Our tiling algorithm usually has efficiency better than 80% (~90%, as of writing)
|
||||||
|
// We need room for error, though, since this check doesn't guarante success.
|
||||||
|
if total_dim / 0.80 >= (8192.0 * 8192.0 * 16.0) {
|
||||||
|
bail!("Texture atlas is too small")
|
||||||
|
}
|
||||||
|
|
||||||
// Create atlas set
|
// Create atlas set
|
||||||
let mut atlas_set = AtlasSet::new(8192, 8192, 16);
|
let mut atlas_set = AtlasSet::new(8192, 8192, 16, &asset_root);
|
||||||
let total = files.len();
|
let total = files.len();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
let mut peak_efficiency = 0f64;
|
||||||
for (path, dim) in files {
|
for (path, dim) in files {
|
||||||
i += 1;
|
i += 1;
|
||||||
let atlas_idx = atlas_set.write_image(&path, dim)?;
|
let atlas_idx = atlas_set.write_image(&path, dim)?;
|
||||||
|
@ -69,6 +82,7 @@ fn main() -> Result<()> {
|
||||||
100.0 * atlas_set.get_efficiency(),
|
100.0 * atlas_set.get_efficiency(),
|
||||||
path.display()
|
path.display()
|
||||||
);
|
);
|
||||||
|
peak_efficiency = peak_efficiency.max(atlas_set.get_efficiency());
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
|
@ -76,9 +90,11 @@ fn main() -> Result<()> {
|
||||||
100.0 * atlas_set.get_efficiency()
|
100.0 * atlas_set.get_efficiency()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
println!("Peak efficiency: {:.02}%", 100.0 * peak_efficiency);
|
||||||
|
|
||||||
println!("Saving files...");
|
println!("Saving files...");
|
||||||
atlas_set.save_files(
|
atlas_set.save_files(
|
||||||
|x| PathBuf::from(format!("atlas-{x:0.2}.png")),
|
|x| PathBuf::from(format!("atlas-{x:0.2}.bmp")),
|
||||||
&PathBuf::from("spriteatlas.toml"),
|
&PathBuf::from("spriteatlas.toml"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue