Browse Source

initial commit

main
Sam Blazes 2 years ago
commit
c224c0a244
  1. 1
      .gitignore
  2. 3162
      Cargo.lock
  3. 10
      Cargo.toml
  4. 27
      README.md
  5. 79
      src/main.rs
  6. 114
      src/packing.rs

1
.gitignore vendored

@ -0,0 +1 @@
/target

3162
Cargo.lock generated

File diff suppressed because it is too large Load Diff

10
Cargo.toml

@ -0,0 +1,10 @@
[package]
name = "texture-packing"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
eframe = "0.25.0"
rand = "0.8.5"

27
README.md

@ -0,0 +1,27 @@
# Texture Packing
A simple rectangle packing implementation intended to be used for creating texture atlases.
Notes:
- No claim to be particularly fast or to pack particularly efficiently, the priority was simplicity
- Assumes that swapping rectangle dimensions is allowed (e.g. a 90 degree rotation or transposition)
- Press `r` to generate and pack a new set of rectangles
- Blue rectangles are in their original orientation and red are transposed
## Explanation
For another project I needed to pack a texture atlas and wanted a simple algorithm that did better than a naive implementation.
This is the first algorithm that I could come up with, very roughly inspired by the basic algorithm to build Huffman code tables.
Therefore, I'm not sure if this algorithm already has a name. Here is a very rough outline of the algorithm
1. First, the list of rectangles is transposed so that the width of each rectangle is the larger of the two dimensions.
1. Next, the list of rectangles is sorted by width (the larger of the two dimensions) followed by height in the case of equal widths.
1. While there are more than 2 rectangles left in the list, do the following:
1. The two smallest rectangles are removed from the back of the list and merged into a new rectangle.
1. They are merged into a new rectangle where the width is the largest of the widths of the two merged rectangles and the height is the sum of the height of the two merged rectangles.
1. References to each of the two merged rectangles are attached to the new rectangle.
1. The new rectangle is transposed if the new height is larger than the new width.
1. The new rectangle is inserted back into the list such that it is still sorted.
1. Now, there is one rectangle and a binary tree structure describing how the rectangles are packed
1. The binary tree structure is traversed depth first to compute the final positions and orientations, which are returned in a list.

79
src/main.rs

@ -0,0 +1,79 @@
//#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
mod packing;
use eframe::egui;
use eframe::egui::epaint;
fn main() -> Result<(), eframe::Error> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
..Default::default()
};
eframe::run_native(
"Rectangle Packing",
options,
Box::new(|_cc| {
Box::<MyApp>::default()
}),
)
}
const N_RECTS: usize = 1024;
struct MyApp {
packed_rects: Vec<packing::PackedRect>,
packing_width: f32,
packing_height: f32,
}
impl Default for MyApp {
fn default() -> Self {
let rects = packing::random_rects(N_RECTS);
let start = std::time::Instant::now();
let (packing_width, packing_height, packed_rects) = packing::pack_rects(&rects[..]);
let elapsed = start.elapsed();
println!("Packing Time: {elapsed:.2?}");
println!("Packing Efficiency: {:.2}", packing::packing_efficiency(packing_width, packing_height, &packed_rects));
Self {
packed_rects,
packing_width,
packing_height,
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let painter = ctx.layer_painter(egui::LayerId::background());
let screen = ctx.screen_rect();
let w = screen.width() / self.packing_width;
let h = screen.height() / self.packing_height;
painter.extend(
self.packed_rects.iter()
.map(|pr| {
let r = &pr.rect;
let round = (0.1 * ((w * r.w).min(h * r.h))).min(8.0);
epaint::Shape::Rect(epaint::RectShape::filled(
epaint::Rect::from_min_size(epaint::Pos2::new(w * r.x + 1., h * r.y + 1.), epaint::Vec2::new(w * r.w - 2., h * r.h - 2.)),
epaint::Rounding::same(round),
if pr.rotated {epaint::Color32::RED} else {epaint::Color32::BLUE}
))
})
);
if ctx.input(|i| i.key_pressed(egui::Key::R)) {
// generate new rectangles
*self = Self::default();
}
}
}

114
src/packing.rs

@ -0,0 +1,114 @@
pub struct PackedRect {
pub index: usize,
pub rotated: bool,
pub rect: Rect,
}
pub struct Rect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
pub fn random_rects(n: usize) -> Vec<(i32, i32)> {
use rand::{Rng, thread_rng};
let mut rng = thread_rng();
// (0..n).map(|_| (rng.gen_range(90i32..=110), rng.gen_range(90i32..=110))).collect::<Vec<_>>()
(0..n).map(|_| (rng.gen_range(3i32..=128), rng.gen_range(3i32..=128))).collect::<Vec<_>>()
}
pub fn packing_efficiency(w: f32, h: f32, rects: &[PackedRect]) -> f32 {
let total_area = w * h;
let used_area = rects.iter().map(|pr| pr.rect.w * pr.rect.h).sum::<f32>();
used_area / total_area
}
pub fn pack_rects(rects: &[(i32, i32)]) -> (f32, f32, Vec<PackedRect>) {
enum Node {
Leaf((i32, i32, bool, usize)),
Branch {
w: i32,
h: i32,
m: i32,
rotate: bool,
left: Box<Node>,
right: Box<Node>,
}
}
impl Node {
fn key(&self) -> (i32, i32) {
match self {
&Node::Leaf((w, h, _, _)) => (-w, -h),
&Node::Branch{w, h, rotate: false, ..} => (-w, -h),
&Node::Branch{w, h, rotate: true, ..} => (-h, -w)
}
}
fn wh(&self) -> (i32, i32) {
match self {
&Node::Leaf((w, h, _, _)) => (w, h),
&Node::Branch{w, h, rotate: false, ..} => (w, h),
&Node::Branch{w, h, rotate: true, ..} => (h, w)
}
}
}
let n = rects.len();
let mut rects = rects.iter().enumerate().map(|(i, r)| Node::Leaf((r.0.max(r.1), r.0.min(r.1), r.1 > r.0, i))).collect::<Vec<_>>();
rects.sort_by_key(Node::key);
while rects.len() >= 2 {
let a = rects.pop().unwrap();
let b = rects.pop().unwrap();
let (wa, ha) = a.wh();
let (wb, hb) = b.wh();
let w = wa.max(wb);
let h = ha + hb;
let m = ha;
let rotate = h > w;
let new_rect = Node::Branch{w, h, m, rotate, left: Box::new(a), right: Box::new(b)};
match rects.binary_search_by_key(&new_rect.key(), Node::key) {
Ok(pos) | Err(pos) => rects.insert(pos, new_rect)
}
}
let rect = rects.pop().unwrap();
let (w, h) = rect.wh();
let mut res = Vec::with_capacity(n);
pack(&mut res, rect, 0., 0., false);
fn pack(list: &mut Vec<PackedRect>, node: Node, x: f32, y: f32, rotate: bool) {
match node {
Node::Leaf((w, h, r, i)) => {
let (w, h) = if rotate {(h, w)} else {(w, h)};
let (w, h) = (w as f32, h as f32);
list.push(PackedRect{index: i, rotated: r ^ rotate, rect: Rect { x, y, w, h }});
}
Node::Branch{w: _, h: _, m, rotate: r, left, right} => {
let rotate = r ^ rotate;
let m = m as f32;
pack(list, *left, x, y, rotate);
let (x, y) = if rotate {(x + m, y)} else {(x, y + m)};
pack(list, *right, x, y, rotate);
}
}
}
(w as f32, h as f32, res)
}
Loading…
Cancel
Save