commit
c224c0a244
6 changed files with 3393 additions and 0 deletions
@ -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" |
||||||
@ -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. |
||||||
@ -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(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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…
Reference in new issue