Let's use the legacy version of rectpack2D.

This commit is contained in:
Relintai 2019-10-20 21:46:36 +02:00
parent 1eaeafb49a
commit 35267a2c5c
13 changed files with 452 additions and 1087 deletions

1
SCsub
View File

@ -3,3 +3,4 @@ Import('env')
env.add_source_files(env.modules_sources,"register_types.cpp")
env.add_source_files(env.modules_sources,"merge_texture.cpp")
env.add_source_files(env.modules_sources,"rectpack2D/pack.cpp")

View File

@ -1,12 +1,15 @@
#include "merge_texture.h"
MergeTexture::MergeTexture() {
void MergeTexture::test() {
}
MergeTexture::MergeTexture() {
}
MergeTexture::~MergeTexture() {
}
void MergeTexture::_bind_methods() {
ClassDB::bind_method(D_METHOD("test"), &MergeTexture::test);
}

View File

@ -2,11 +2,15 @@
#define MERGE_TEXTURE_H
#include "scene/resources/texture.h"
#include "core/ustring.h"
#include "rectpack2D/pack.h"
class MergeTexture : public ImageTexture {
GDCLASS(MergeTexture, ImageTexture);
public:
void test();
MergeTexture();
~MergeTexture();

View File

@ -1,208 +1,29 @@
If you are looking for the old version of rectpack2D, it can still be found in [a legacy branch](https://github.com/TeamHypersomnia/rectpack2D/tree/legacy).
[Check out this modern re-implementation of the library.](https://github.com/TeamHypersomnia/rectpack2D/tree/master)
# rectpack2D
[![Build Status](https://travis-ci.org/TeamHypersomnia/rectpack2D.svg?branch=master)](https://travis-ci.org/TeamHypersomnia/rectpack2D)
[![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/aojyt3r6ysvadkgl?svg=true)](https://ci.appveyor.com/project/geneotech/rectpack2D)
Tiny rectangle packing library allowing multiple, dynamic-sized bins.
Table of contents:
The code is ugly, I know, I was learning the language at the time.
Nevertheless, it's been years since I used it for the first time in my open-source shooter [Hypersomnia][3].
No crashes so far and works quite fast.
- [Benchmarks](#benchmarks)
- [Usage](#usage)
- [Building the example](#building-the-example)
* [Windows](#windows)
* [Linux](#linux)
- [Algorithm](#algorithm)
* [Insertion algorithm](#insertion-algorithm)
* [Additional heuristics](#additional-heuristics)
Copied from: http://gamedev.stackexchange.com/a/34193/16982
Rectangle packing library (no longer tiny!).
This is a refactored and **highly optimized** branch of the original library which is easier to use and customize.
[This algorithm][1] should meet every gamedeving needs.
It's very, very efficient, lightweight, and I've myself improved it with searching the best possible sorting function
(whether it's by area, perimeter, width, height, max(width, height))
and the best possible bin size so **you don't have to hardcode the width/height yourself anymore**.
![7](https://user-images.githubusercontent.com/3588717/42707552-d8b1c65e-86da-11e8-9412-54c580bd2696.jpg)
It's also easy to design it so it automatically portions out your rectangles into more bins
if one with fixed maximum size is not sufficient, so you probably want to pass all your textures to it
and pass a maximum texture size as the value for maximum bins' dimension, and BAM !
You have your texture atlases ready to be uploaded to GPU. Same goes for font packing.
## Benchmarks
Tests were conducted on a ``Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz``.
The binary was built with ``clang 6.0.0``, using an -03 switch.
### Arbitrary game sprites: 582 subjects.
**Runtime: 0.8 ms**
**Wasted pixels: 10982 (0.24% - equivalent of a 105 x 105 square)**
Output (1896 x 2382):
![1](images/atlas_small.png)
In color:
(black is wasted space)
![2](images/atlas_small_color.png)
### Arbitrary game sprites + Japanese glyphs: 3264 subjects.
**Runtime: 4 ms**
**Wasted pixels: 15538 (0.31% - equivalent of a 125 x 125 square)**
Output (2116 x 2382):
![3](images/atlas_big.png)
In color:
(black is wasted space)
![4](images/atlas_big_color.png)
400 random rectangles, automatically divided into 3 bins of maximum 400x400 size:
![enter image description here][2]
### Japanese glyphs + some GUI sprites: 3122 subjects.
**Runtime: 3.5 - 7 ms**
**Wasted pixels: 9288 (1.23% - equivalent of a 96 x 96 square)**
Output (866 x 871):
![5](images/atlas_tiny.png)
In color:
(black is wasted space)
![6](images/atlas_tiny_color.png)
## Usage
This is a header-only library.
Just include the ``src/finders_interface.h`` and you should be good to go.
For an example use, see ``example/main.cpp``.
## Building the example
### Windows
From the repository's folder, run:
```bash
mkdir build
cd build
cmake -G "Visual Studio 15 2017 Win64" ..
````
Then just build the generated ``.sln`` file using the newest Visual Studio Preview.
### Linux
From the repository's folder, run:
```bash
cmake/build_example.sh Release gcc g++
cd build/current
ninja run
````
Or, if you want to use clang, run:
```bash
cmake/build_example.sh Release clang clang++
cd build/current
ninja run
````
## Algorithm
### Insertion algorithm
The library started as an implementation of this algorithm:
http://blackpawn.com/texts/lightmaps/default.html
The current version somewhat derives from the concept described there -
however, it uses just a **vector of empty spaces, instead of a tree** - this turned out to be a performance breakthrough.
Given
```cpp
struct rect_xywh {
int x;
int y;
int w;
int h;
};
````
Let us create a vector and call it empty_spaces.
```cpp
std::vector<rect_xywh> empty_spaces;
````
Given a user-specified initial bin, which is a square of some size S, we initialize the first empty space.
```cpp
empty_spaces.push_back(rect_xywh(0, 0, S, S));
````
Now, we'd like to insert the first image rectangle.
To do this, we iterate the vector of empty spaces **backwards** and look for an empty space into which the image can fit.
For now, we only have the S x S square: let's save the index of this candidate empty space,
which is ``candidate_space_index = 0;``
If our image is strictly smaller than the candidate space, we have something like this:
![diag01](images/diag01.png)
The blue is our image rectangle.
We now calculate the gray rectangles labeled as "bigger split" and "smaller split",
and save them like this:
```cpp
// Erase the space that we've just inserted to, by swapping and popping.
empty_spaces[candidate_space_index] = empty_spaces.back();
empty_spaces.pop_back();
// Save the resultant splits
empty_spaces.push_back(bigger_split);
empty_spaces.push_back(smaller_split);
````
Notice that we push the smaller split *after* the bigger one.
This is fairly important, because later the candidate images will encounter the smaller splits first,
which will make better use of empty spaces overall.
#### Corner cases:
- If the image dimensions equal the dimensions of the candidate empty space (image fits exactly),
- we just delete the space and create no splits.
- If the image fits into the candidate empty space, but exactly one of the image dimensions equals the respective dimension of the candidate empty space (e.g. image = 20x40, candidate space = 30x40)
- we delete the space and create a single split. In this case a 10x40 space.
To see the complete, modular procedure for calculating the splits (along with the corner cases),
[see this source](src/insert_and_split.h).
If the insertion fails, we also try the same procedure for a flipped image.
### Additional heuristics
Now we know how to insert individual images into a bin of a given initial size S.
1. However, what S should be passed to the algorithm so that the rectangles end up wasting the least amount of space?
- We perform a binary search.
- We start with the size specified by the library user. Typically, it would be the maximum texture size allowed on a particular GPU.
- If the packing was successful on the given bin size, decrease the size and try to pack again.
- If the packing has failed on the given bin size - because some rectangles could not be further inserted - increase the size and try to pack again.
- The search is aborted if we've successfully inserted into a bin and the dimensions of the next candidate would differ from the previous by less than ``discard_step``.
- This variable exists so that we may easily trade accuracy for a speedup. ``discard_step = 1`` yields the highest accuracy. ``discard_step = 128`` will yield worse packings, but will be a lot faster, etc.
- The search is performed first by decreasing the bin size by both width and height, keeping it in square shape.
- Then we do the same, but only decreasing width.
- Then we do the same, but only decreasing height.
- The last two were a breakthrough in packing tightness. It turns out important to consider non-square bins.
2. In what order should the rectangles be inserted so that they pack the tightest?
- By default, the library tries 6 decreasing orders:
- By area.
- By perimeter.
- By the bigger side.
- By width.
- By height.
- By 'a pathological multiplier": ``max(w, h) / min(w, h) * w * h``
- This makes some giant, irregular mutants always go first, which is good, because it doesn't shred our empty spaces to a lot of useless pieces.
[1]: http://www.blackpawn.com/texts/lightmaps/default.html
[2]: http://i.stack.imgur.com/mOgcn.png
[3]: https://github.com/TeamHypersomnia/Hypersomnia

View File

@ -1,270 +0,0 @@
#pragma once
#include <variant>
#include <cassert>
#include "rect_structs.h"
namespace rectpack2D {
enum class callback_result {
ABORT_PACKING,
CONTINUE_PACKING
};
template <class T>
auto& dereference(T& r) {
/*
This will allow us to pass orderings that consist of pointers,
as well as ones that are just plain objects in a vector.
*/
if constexpr(std::is_pointer_v<T>) {
return *r;
}
else {
return r;
}
};
/*
This function will do a binary search on viable bin sizes,
starting from the biggest one: starting_bin.
The search stops when the bin was successfully inserted into,
AND the bin size to be tried next differs in size from the last viable one by *less* then discard_step.
If we could not insert all input rectangles into a bin even as big as the starting_bin - the search fails.
In this case, we return the amount of space (total_area_type) inserted in total.
If we've found a viable bin that is smaller or equal to starting_bin, the search succeeds.
In this case, we return the viable bin (rect_wh).
*/
enum class bin_dimension {
BOTH,
WIDTH,
HEIGHT
};
template <class empty_spaces_type, class O>
std::variant<total_area_type, rect_wh> best_packing_for_ordering_impl(
empty_spaces_type& root,
O ordering,
const rect_wh starting_bin,
const int discard_step,
const bin_dimension tried_dimension
) {
auto candidate_bin = starting_bin;
int starting_step = 0;
if (tried_dimension == bin_dimension::BOTH) {
starting_step = starting_bin.w / 2;
candidate_bin.w /= 2;
candidate_bin.h /= 2;
}
else if (tried_dimension == bin_dimension::WIDTH) {
starting_step = starting_bin.w / 2;
candidate_bin.w /= 2;
}
else {
starting_step = starting_bin.h / 2;
candidate_bin.h /= 2;
}
for (int step = starting_step; ; step = std::max(1, step / 2)) {
root.reset(candidate_bin);
int total_inserted_area = 0;
const bool all_inserted = [&]() {
for (const auto& r : ordering) {
const auto& rect = dereference(r);
if (root.insert(rect.get_wh())) {
total_inserted_area += rect.area();
}
else {
return false;
}
}
return true;
}();
if (all_inserted) {
/* Attempt was successful. Try with a smaller bin. */
if (step <= discard_step) {
return candidate_bin;
}
if (tried_dimension == bin_dimension::BOTH) {
candidate_bin.w -= step;
candidate_bin.h -= step;
}
else if (tried_dimension == bin_dimension::WIDTH) {
candidate_bin.w -= step;
}
else {
candidate_bin.h -= step;
}
root.reset(candidate_bin);
}
else {
/* Attempt ended with failure. Try with a bigger bin. */
if (tried_dimension == bin_dimension::BOTH) {
candidate_bin.w += step;
candidate_bin.h += step;
if (candidate_bin.area() > starting_bin.area()) {
return total_inserted_area;
}
}
else if (tried_dimension == bin_dimension::WIDTH) {
candidate_bin.w += step;
if (candidate_bin.w > starting_bin.w) {
return total_inserted_area;
}
}
else {
candidate_bin.h += step;
if (candidate_bin.h > starting_bin.h) {
return total_inserted_area;
}
}
}
}
}
template <class empty_spaces_type, class O>
std::variant<total_area_type, rect_wh> best_packing_for_ordering(
empty_spaces_type& root,
O&& ordering,
const rect_wh starting_bin,
const int discard_step
) {
const auto try_pack = [&](
const bin_dimension tried_dimension,
const rect_wh starting_bin
) {
return best_packing_for_ordering_impl(
root,
std::forward<O>(ordering),
starting_bin,
discard_step,
tried_dimension
);
};
const auto best_result = try_pack(bin_dimension::BOTH, starting_bin);
if (const auto failed = std::get_if<total_area_type>(&best_result)) {
return *failed;
}
auto best_bin = std::get<rect_wh>(best_result);
auto trial = [&](const bin_dimension tried_dimension) {
const auto trial = try_pack(tried_dimension, best_bin);
if (const auto better = std::get_if<rect_wh>(&trial)) {
best_bin = *better;
}
};
trial(bin_dimension::WIDTH);
trial(bin_dimension::HEIGHT);
return best_bin;
}
/*
This function will try to find the best bin size among the ones generated by all provided rectangle orders.
Only the best order will have results written to.
The function reports which of the rectangles did and did not fit in the end.
*/
template <
class empty_spaces_type,
class OrderType,
class F,
class I
>
rect_wh find_best_packing_impl(F for_each_order, const I input) {
const auto max_bin = rect_wh(input.max_bin_side, input.max_bin_side);
OrderType* best_order = nullptr;
int best_total_inserted = -1;
auto best_bin = max_bin;
/*
The root node is re-used on the TLS.
It is always reset before any packing attempt.
*/
thread_local empty_spaces_type root = rect_wh();
root.flipping_mode = input.flipping_mode;
for_each_order ([&](OrderType& current_order) {
const auto packing = best_packing_for_ordering(
root,
current_order,
max_bin,
input.discard_step
);
if (const auto total_inserted = std::get_if<total_area_type>(&packing)) {
/*
Track which function inserts the most area in total,
just in case that all orders will fail to fit into the largest allowed bin.
*/
if (best_order == nullptr) {
if (*total_inserted > best_total_inserted) {
best_order = std::addressof(current_order);
best_total_inserted = *total_inserted;
}
}
}
else if (const auto result_bin = std::get_if<rect_wh>(&packing)) {
/* Save the function if it performed the best. */
if (result_bin->area() <= best_bin.area()) {
best_order = std::addressof(current_order);
best_bin = *result_bin;
}
}
});
{
assert(best_order != nullptr);
root.reset(best_bin);
for (auto& rr : *best_order) {
auto& rect = dereference(rr);
if (const auto ret = root.insert(rect.get_wh())) {
rect = *ret;
if (callback_result::ABORT_PACKING == input.handle_successful_insertion(rect)) {
break;
}
}
else {
if (callback_result::ABORT_PACKING == input.handle_unsuccessful_insertion(rect)) {
break;
}
}
}
return root.get_rects_aabb();
}
}
}

View File

@ -1,70 +0,0 @@
#pragma once
#include <array>
#include <vector>
#include <type_traits>
#include "rect_structs.h"
namespace rectpack2D {
class default_empty_spaces {
std::vector<space_rect> empty_spaces;
public:
void remove(const int i) {
empty_spaces[i] = empty_spaces.back();
empty_spaces.pop_back();
}
bool add(const space_rect r) {
empty_spaces.emplace_back(r);
return true;
}
auto get_count() const {
return empty_spaces.size();
}
void reset() {
empty_spaces.clear();
}
const auto& get(const int i) {
return empty_spaces[i];
}
};
template <int MAX_SPACES>
class static_empty_spaces {
int count_spaces = 0;
std::array<space_rect, MAX_SPACES> empty_spaces;
public:
void remove(const int i) {
empty_spaces[i] = empty_spaces[count_spaces - 1];
--count_spaces;
}
bool add(const space_rect r) {
if (count_spaces < static_cast<int>(empty_spaces.size())) {
empty_spaces[count_spaces] = r;
++count_spaces;
return true;
}
return false;
}
auto get_count() const {
return count_spaces;
}
void reset() {
count_spaces = 0;
}
const auto& get(const int i) {
return empty_spaces[i];
}
};
}

View File

@ -1,149 +0,0 @@
#pragma once
#include "insert_and_split.h"
namespace rectpack2D {
enum class flipping_option {
DISABLED,
ENABLED
};
class default_empty_spaces;
template <bool allow_flip, class empty_spaces_provider = default_empty_spaces>
class empty_spaces {
rect_wh current_aabb;
empty_spaces_provider spaces;
/* MSVC fix for non-conformant if constexpr implementation */
static auto make_output_rect(const int x, const int y, const int w, const int h) {
return rect_xywh(x, y, w, h);
}
static auto make_output_rect(const int x, const int y, const int w, const int h, const bool flipped) {
return rect_xywhf(x, y, w, h, flipped);
}
public:
using output_rect_type = std::conditional_t<allow_flip, rect_xywhf, rect_xywh>;
flipping_option flipping_mode = flipping_option::ENABLED;
empty_spaces(const rect_wh& r) {
reset(r);
}
void reset(const rect_wh& r) {
current_aabb = {};
spaces.reset();
spaces.add(rect_xywh(0, 0, r.w, r.h));
}
template <class F>
std::optional<output_rect_type> insert(const rect_wh image_rectangle, F report_candidate_empty_space) {
for (int i = static_cast<int>(spaces.get_count()) - 1; i >= 0; --i) {
const auto candidate_space = spaces.get(i);
report_candidate_empty_space(candidate_space);
auto accept_result = [this, i, image_rectangle, candidate_space](
const created_splits& splits,
const bool flipping_necessary
) -> std::optional<output_rect_type> {
spaces.remove(i);
for (int s = 0; s < splits.count; ++s) {
if (!spaces.add(splits.spaces[s])) {
return std::nullopt;
}
}
if constexpr(allow_flip) {
const auto result = make_output_rect(
candidate_space.x,
candidate_space.y,
image_rectangle.w,
image_rectangle.h,
flipping_necessary
);
current_aabb.expand_with(result);
return result;
}
else if constexpr(!allow_flip) {
(void)flipping_necessary;
const auto result = make_output_rect(
candidate_space.x,
candidate_space.y,
image_rectangle.w,
image_rectangle.h
);
current_aabb.expand_with(result);
return result;
}
};
auto try_to_insert = [&](const rect_wh& img) {
return rectpack2D::insert_and_split(img, candidate_space);
};
if constexpr(!allow_flip) {
if (const auto normal = try_to_insert(image_rectangle)) {
return accept_result(normal, false);
}
}
else {
if (flipping_mode == flipping_option::ENABLED) {
const auto normal = try_to_insert(image_rectangle);
const auto flipped = try_to_insert(rect_wh(image_rectangle).flip());
/*
If both were successful,
prefer the one that generated less remainder spaces.
*/
if (normal && flipped) {
if (flipped.better_than(normal)) {
/* Accept the flipped result if it producues less or "better" spaces. */
return accept_result(flipped, true);
}
return accept_result(normal, false);
}
if (normal) {
return accept_result(normal, false);
}
if (flipped) {
return accept_result(flipped, true);
}
}
else {
if (const auto normal = try_to_insert(image_rectangle)) {
return accept_result(normal, false);
}
}
}
}
return std::nullopt;
}
decltype(auto) insert(const rect_wh& image_rectangle) {
return insert(image_rectangle, [](auto&){ });
}
auto get_rects_aabb() const {
return current_aabb;
}
const auto& get_spaces() const {
return spaces;
}
};
}

View File

@ -1,153 +0,0 @@
#pragma once
#include <optional>
#include <vector>
#include <array>
#include <variant>
#include <algorithm>
#include "insert_and_split.h"
#include "empty_spaces.h"
#include "empty_space_allocators.h"
#include "best_bin_finder.h"
namespace rectpack2D {
template <class empty_spaces_type>
using output_rect_t = typename empty_spaces_type::output_rect_type;
template <class F, class G>
struct finder_input {
const int max_bin_side;
const int discard_step;
F handle_successful_insertion;
G handle_unsuccessful_insertion;
const flipping_option flipping_mode;
};
template <class F, class G>
auto make_finder_input(
const int max_bin_side,
const int discard_step,
F&& handle_successful_insertion,
G&& handle_unsuccessful_insertion,
const flipping_option flipping_mode
) {
return finder_input<F, G> {
max_bin_side,
discard_step,
std::forward<F>(handle_successful_insertion),
std::forward<G>(handle_unsuccessful_insertion),
flipping_mode
};
};
/*
Finds the best packing for the rectangles,
just in the order that they were passed.
*/
template <class empty_spaces_type, class F, class G>
rect_wh find_best_packing_dont_sort(
std::vector<output_rect_t<empty_spaces_type>>& subjects,
const finder_input<F, G>& input
) {
using order_type = std::remove_reference_t<decltype(subjects)>;
return find_best_packing_impl<empty_spaces_type, order_type>(
[&subjects](auto callback) { callback(subjects); },
input
);
}
/*
Finds the best packing for the rectangles.
Accepts a list of predicates able to compare two input rectangles.
The function will try to pack the rectangles in all orders generated by the predicates,
and will only write the x, y coordinates of the best packing found among the orders.
*/
template <class empty_spaces_type, class F, class G, class Comparator, class... Comparators>
rect_wh find_best_packing(
std::vector<output_rect_t<empty_spaces_type>>& subjects,
const finder_input<F, G>& input,
Comparator comparator,
Comparators... comparators
) {
using rect_type = output_rect_t<empty_spaces_type>;
using order_type = std::vector<rect_type*>;
constexpr auto count_orders = 1 + sizeof...(Comparators);
thread_local std::array<order_type, count_orders> orders;
{
/* order[0] will always exist since this overload requires at least one comparator */
auto& initial_pointers = orders[0];
initial_pointers.clear();
for (auto& s : subjects) {
if (s.area() > 0) {
initial_pointers.emplace_back(std::addressof(s));
}
}
for (std::size_t i = 1; i < count_orders; ++i) {
orders[i] = initial_pointers;
}
}
std::size_t f = 0;
auto make_order = [&f](auto& predicate) {
std::sort(orders[f].begin(), orders[f].end(), predicate);
++f;
};
make_order(comparator);
(make_order(comparators), ...);
return find_best_packing_impl<empty_spaces_type, order_type>(
[](auto callback){ for (auto& o : orders) { callback(o); } },
input
);
}
/*
Finds the best packing for the rectangles.
Provides a list of several sensible comparison predicates.
*/
template <class empty_spaces_type, class F, class G>
rect_wh find_best_packing(
std::vector<output_rect_t<empty_spaces_type>>& subjects,
const finder_input<F, G>& input
) {
using rect_type = output_rect_t<empty_spaces_type>;
return find_best_packing<empty_spaces_type>(
subjects,
input,
[](const rect_type* const a, const rect_type* const b) {
return a->area() > b->area();
},
[](const rect_type* const a, const rect_type* const b) {
return a->perimeter() > b->perimeter();
},
[](const rect_type* const a, const rect_type* const b) {
return std::max(a->w, a->h) > std::max(b->w, b->h);
},
[](const rect_type* const a, const rect_type* const b) {
return a->w > b->w;
},
[](const rect_type* const a, const rect_type* const b) {
return a->h > b->h;
},
[](const rect_type* const a, const rect_type* const b) {
return a->get_wh().pathological_mult() > b->get_wh().pathological_mult();
}
);
}
}

View File

@ -1,135 +0,0 @@
#pragma once
#include <array>
#include "rect_structs.h"
namespace rectpack2D {
struct created_splits {
int count = 0;
std::array<space_rect, 2> spaces;
static auto failed() {
created_splits result;
result.count = -1;
return result;
}
static auto none() {
return created_splits();
}
template <class... Args>
created_splits(Args&&... args) : spaces({ std::forward<Args>(args)... }) {
count = sizeof...(Args);
}
bool better_than(const created_splits& b) const {
return count < b.count;
}
explicit operator bool() const {
return count != -1;
}
};
inline created_splits insert_and_split(
const rect_wh& im, /* Image rectangle */
const space_rect& sp /* Space rectangle */
) {
const auto free_w = sp.w - im.w;
const auto free_h = sp.h - im.h;
if (free_w < 0 || free_h < 0) {
/*
Image is bigger than the candidate empty space.
We'll need to look further.
*/
return created_splits::failed();
}
if (free_w == 0 && free_h == 0) {
/*
If the image dimensions equal the dimensions of the candidate empty space (image fits exactly),
we will just delete the space and create no splits.
*/
return created_splits::none();
}
/*
If the image fits into the candidate empty space,
but exactly one of the image dimensions equals the respective dimension of the candidate empty space
(e.g. image = 20x40, candidate space = 30x40)
we delete the space and create a single split. In this case a 10x40 space.
*/
if (free_w > 0 && free_h == 0) {
auto r = sp;
r.x += im.w;
r.w -= im.w;
return created_splits(r);
}
if (free_w == 0 && free_h > 0) {
auto r = sp;
r.y += im.h;
r.h -= im.h;
return created_splits(r);
}
/*
Every other option has been exhausted,
so at this point the image must be *strictly* smaller than the empty space,
that is, it is smaller in both width and height.
Thus, free_w and free_h must be positive.
*/
/*
Decide which way to split.
Instead of having two normally-sized spaces,
it is better - though I have no proof of that - to have a one tiny space and a one huge space.
This creates better opportunity for insertion of future rectangles.
This is why, if we had more of width remaining than we had of height,
we split along the vertical axis,
and if we had more of height remaining than we had of width,
we split along the horizontal axis.
*/
if (free_w > free_h) {
const auto bigger_split = space_rect(
sp.x + im.w,
sp.y,
free_w,
sp.h
);
const auto lesser_split = space_rect(
sp.x,
sp.y + im.h,
im.w,
free_h
);
return created_splits(bigger_split, lesser_split);
}
const auto bigger_split = space_rect(
sp.x,
sp.y + im.h,
sp.w,
free_h
);
const auto lesser_split = space_rect(
sp.x + im.w,
sp.y,
free_w,
im.h
);
return created_splits(bigger_split, lesser_split);
}
}

341
rectpack2D/pack.cpp Normal file
View File

@ -0,0 +1,341 @@
#include "pack.h"
#include <cstring>
#include <algorithm>
using namespace std;
bool area(rect_xywhf* a, rect_xywhf* b) {
return a->area() > b->area();
}
bool perimeter(rect_xywhf* a, rect_xywhf* b) {
return a->perimeter() > b->perimeter();
}
bool max_side(rect_xywhf* a, rect_xywhf* b) {
return std::max(a->w, a->h) > std::max(b->w, b->h);
}
bool max_width(rect_xywhf* a, rect_xywhf* b) {
return a->w > b->w;
}
bool max_height(rect_xywhf* a, rect_xywhf* b) {
return a->h > b->h;
}
// just add another comparing function name to cmpf to perform another packing attempt
// more functions == slower but probably more efficient cases covered and hence less area wasted
bool (*cmpf[])(rect_xywhf*, rect_xywhf*) = {
area,
perimeter,
max_side,
max_width,
max_height
};
// if you find the algorithm running too slow you may double this factor to increase speed but also decrease efficiency
// 1 == most efficient, slowest
// efficiency may be still satisfying at 64 or even 256 with nice speedup
int discard_step = 128;
/*
For every sorting function, algorithm will perform packing attempts beginning with a bin with width and height equal to max_side,
and decreasing its dimensions if it finds out that rectangles did actually fit, increasing otherwise.
Although, it's doing that in sort of binary search manner, so for every comparing function it will perform at most log2(max_side) packing attempts looking for the smallest possible bin size.
discard_step = 128 means that the algorithm will break of the searching loop if the rectangles fit but "it may be possible to fit them in a bin smaller by 128"
the bigger the value, the sooner the algorithm will finish but the rectangles will be packed less tightly.
use discard_step = 1 for maximum tightness.
the algorithm was based on http://www.blackpawn.com/texts/lightmaps/default.html
the algorithm reuses the node tree so it doesn't reallocate them between searching attempts
*/
/*************************************************************************** CHAOS BEGINS HERE */
struct node {
struct pnode {
node* pn = nullptr;
bool fill = false;
void set(int l, int t, int r, int b) {
if(!pn) pn = new node(rect_ltrb(l, t, r, b));
else {
(*pn).rc = rect_ltrb(l, t, r, b);
(*pn).id = false;
}
fill = true;
}
};
pnode c[2];
rect_ltrb rc;
bool id = false;
node(rect_ltrb rc = rect_ltrb()) : rc(rc) {}
void reset(const rect_wh& r) {
id = false;
rc = rect_ltrb(0, 0, r.w, r.h);
delcheck();
}
node* insert(rect_xywhf& img, bool allowFlip ) {
if(c[0].pn && c[0].fill) {
if(auto newn = c[0].pn->insert(img,allowFlip)) return newn;
return c[1].pn->insert(img,allowFlip);
}
if(id) return 0;
int f = img.fits(rect_xywh(rc),allowFlip);
switch(f) {
case 0: return 0;
case 1: img.flipped = false; break;
case 2: img.flipped = true; break;
case 3: id = true; img.flipped = false; return this;
case 4: id = true; img.flipped = true; return this;
}
int iw = (img.flipped ? img.h : img.w), ih = (img.flipped ? img.w : img.h);
if(rc.w() - iw > rc.h() - ih) {
c[0].set(rc.l, rc.t, rc.l+iw, rc.b);
c[1].set(rc.l+iw, rc.t, rc.r, rc.b);
}
else {
c[0].set(rc.l, rc.t, rc.r, rc.t + ih);
c[1].set(rc.l, rc.t + ih, rc.r, rc.b);
}
return c[0].pn->insert(img,allowFlip);
}
void delcheck() {
if(c[0].pn) { c[0].fill = false; c[0].pn->delcheck(); }
if(c[1].pn) { c[1].fill = false; c[1].pn->delcheck(); }
}
~node() {
if(c[0].pn) delete c[0].pn;
if(c[1].pn) delete c[1].pn;
}
};
rect_wh _rect2D(rect_xywhf* const * v, int n, int max_s, bool allowFlip, vector<rect_xywhf*>& succ, vector<rect_xywhf*>& unsucc) {
node root;
const int funcs = (sizeof(cmpf)/sizeof(bool (*)(rect_xywhf*, rect_xywhf*)));
rect_xywhf** order[funcs];
for(int f = 0; f < funcs; ++f) {
order[f] = new rect_xywhf*[n];
std::memcpy(order[f], v, sizeof(rect_xywhf*) * n);
sort(order[f], order[f]+n, cmpf[f]);
}
rect_wh min_bin = rect_wh(max_s, max_s);
int min_func = -1, best_func = 0, best_area = 0, _area = 0, step, fit, i;
bool fail = false;
for(int f = 0; f < funcs; ++f) {
v = order[f];
step = min_bin.w / 2;
root.reset(min_bin);
while(true) {
if(root.rc.w() > min_bin.w) {
if(min_func > -1) break;
_area = 0;
root.reset(min_bin);
for(i = 0; i < n; ++i)
if(root.insert(*v[i],allowFlip))
_area += v[i]->area();
fail = true;
break;
}
fit = -1;
for(i = 0; i < n; ++i)
if(!root.insert(*v[i],allowFlip)) {
fit = 1;
break;
}
if(fit == -1 && step <= discard_step)
break;
root.reset(rect_wh(root.rc.w() + fit*step, root.rc.h() + fit*step));
step /= 2;
if(!step)
step = 1;
}
if(!fail && (min_bin.area() >= root.rc.area())) {
min_bin = rect_wh(root.rc);
min_func = f;
}
else if(fail && (_area > best_area)) {
best_area = _area;
best_func = f;
}
fail = false;
}
v = order[min_func == -1 ? best_func : min_func];
int clip_x = 0, clip_y = 0;
root.reset(min_bin);
for(i = 0; i < n; ++i) {
if(auto ret = root.insert(*v[i],allowFlip)) {
v[i]->x = ret->rc.l;
v[i]->y = ret->rc.t;
if(v[i]->flipped) {
v[i]->flipped = false;
v[i]->flip();
}
clip_x = std::max(clip_x, ret->rc.r);
clip_y = std::max(clip_y, ret->rc.b);
succ.push_back(v[i]);
}
else {
unsucc.push_back(v[i]);
v[i]->flipped = false;
}
}
for(int f = 0; f < funcs; ++f)
delete [] order[f];
return rect_wh(clip_x, clip_y);
}
bool pack(rect_xywhf* const * v, int n, int max_s, bool allowFlip, vector<bin>& bins) {
rect_wh _rect(max_s, max_s);
for(int i = 0; i < n; ++i)
if(!v[i]->fits(_rect,allowFlip)) return false;
vector<rect_xywhf*> vec[2], *p[2] = { vec, vec+1 };
vec[0].resize(n);
vec[1].clear();
std::memcpy(&vec[0][0], v, sizeof(rect_xywhf*)*n);
bin* b = 0;
while(true) {
bins.push_back(bin());
b = &bins[bins.size()-1];
b->size = _rect2D(&((*p[0])[0]), static_cast<int>(p[0]->size()), max_s,allowFlip, b->rects, *p[1]);
p[0]->clear();
if(!p[1]->size()) break;
std::swap(p[0], p[1]);
}
return true;
}
rect_wh::rect_wh(const rect_ltrb& rr) : w(rr.w()), h(rr.h()) {}
rect_wh::rect_wh(const rect_xywh& rr) : w(rr.w), h(rr.h) {}
rect_wh::rect_wh(int w, int h) : w(w), h(h) {}
int rect_wh::fits(const rect_wh& r, bool allowFlip) const {
if(w == r.w && h == r.h) return 3;
if(allowFlip && h == r.w && w == r.h) return 4;
if(w <= r.w && h <= r.h) return 1;
if(allowFlip && h <= r.w && w <= r.h) return 2;
return 0;
}
rect_ltrb::rect_ltrb() : l(0), t(0), r(0), b(0) {}
rect_ltrb::rect_ltrb(int l, int t, int r, int b) : l(l), t(t), r(r), b(b) {}
int rect_ltrb::w() const {
return r-l;
}
int rect_ltrb::h() const {
return b-t;
}
int rect_ltrb::area() const {
return w()*h();
}
int rect_ltrb::perimeter() const {
return 2*w() + 2*h();
}
void rect_ltrb::w(int ww) {
r = l+ww;
}
void rect_ltrb::h(int hh) {
b = t+hh;
}
rect_xywh::rect_xywh() : x(0), y(0) {}
rect_xywh::rect_xywh(const rect_ltrb& rc) : x(rc.l), y(rc.t) { b(rc.b); r(rc.r); }
rect_xywh::rect_xywh(int x, int y, int w, int h) : rect_wh(w, h), x(x), y(y) {}
rect_xywh::operator rect_ltrb() {
rect_ltrb rr(x, y, 0, 0);
rr.w(w); rr.h(h);
return rr;
}
int rect_xywh::r() const {
return x+w;
};
int rect_xywh::b() const {
return y+h;
}
void rect_xywh::r(int right) {
w = right-x;
}
void rect_xywh::b(int bottom) {
h = bottom-y;
}
int rect_wh::area() {
return w*h;
}
int rect_wh::perimeter() {
return 2*w + 2*h;
}
rect_xywhf::rect_xywhf(const rect_ltrb& rr) : rect_xywh(rr), flipped(false) {}
rect_xywhf::rect_xywhf(int x, int y, int width, int height) : rect_xywh(x, y, width, height), flipped(false) {}
rect_xywhf::rect_xywhf() : flipped(false) {}
void rect_xywhf::flip() {
flipped = !flipped;
std::swap(w, h);
}

82
rectpack2D/pack.h Normal file
View File

@ -0,0 +1,82 @@
#pragma once
#include <vector>
/* of your interest:
1. rect_xywhf - structure representing your rectangle object
members:
int x, y, w, h;
bool flipped;
2. bin - structure representing resultant bin object
3. bool pack(rect_xywhf* const * v, int n, int max_side, std::vector<bin>& bins) - actual packing function
Arguments:
input/output: v - pointer to array of pointers to your rectangles (const here means that the pointers will point to the same rectangles after the call)
input: n - rectangles count
input: max_side - maximum bins' side - algorithm works with square bins (in the end it may trim them to rectangular form).
for the algorithm to finish faster, pass a reasonable value (unreasonable would be passing 1 000 000 000 for packing 4 50x50 rectangles).
output: bins - vector to which the function will push_back() created bins, each of them containing vector to pointers of rectangles from "v" belonging to that particular bin.
Every bin also keeps information about its width and height of course, none of the dimensions is bigger than max_side.
returns true on success, false if one of the rectangles' dimension was bigger than max_side
You want to your rectangles representing your textures/glyph objects with GL_MAX_TEXTURE_SIZE as max_side,
then for each bin iterate through its rectangles, typecast each one to your own structure (or manually add userdata) and then memcpy its pixel contents (rotated by 90 degrees if "flipped" rect_xywhf's member is true)
to the array representing your texture atlas to the place specified by the rectangle, then finally upload it with glTexImage2D.
Algorithm doesn't create any new rectangles.
You just pass an array of pointers - rectangles' x/y/w/h/flipped are modified in place.
There is a vector of pointers for every resultant bin to let you know which ones belong to the particular bin.
The algorithm may swap the w and h fields for the sake of better fitting, the flag "flipped" will be set to true whenever this occurs.
For description how to tune the algorithm and how it actually works see the .cpp file.
*/
struct rect_ltrb;
struct rect_xywh;
struct rect_wh {
rect_wh(const rect_ltrb&);
rect_wh(const rect_xywh&);
rect_wh(int w = 0, int h = 0);
int w, h, area(), perimeter(),
fits(const rect_wh& bigger, bool allowFlip) const; // 0 - no, 1 - yes, 2 - flipped, 3 - perfectly, 4 perfectly flipped
};
// rectangle implementing left/top/right/bottom behaviour
struct rect_ltrb {
rect_ltrb();
rect_ltrb(int left, int top, int right, int bottom);
int l, t, r, b, w() const, h() const, area() const, perimeter() const;
void w(int), h(int);
};
struct rect_xywh : public rect_wh {
rect_xywh();
rect_xywh(const rect_ltrb&);
rect_xywh(int x, int y, int width, int height);
operator rect_ltrb();
int x, y, r() const, b() const;
void r(int), b(int);
};
struct rect_xywhf : public rect_xywh {
rect_xywhf(const rect_ltrb&);
rect_xywhf(int x, int y, int width, int height);
rect_xywhf();
void flip();
bool flipped;
};
struct bin {
rect_wh size;
std::vector<rect_xywhf*> rects;
};
bool pack(rect_xywhf* const * v, int n, int max_side, bool allowFlip, std::vector<bin>& bins);

View File

@ -1,78 +0,0 @@
#pragma once
#include <utility>
namespace rectpack2D {
using total_area_type = int;
struct rect_wh {
rect_wh() : w(0), h(0) {}
rect_wh(const int w, const int h) : w(w), h(h) {}
int w;
int h;
auto& flip() {
std::swap(w, h);
return *this;
}
int max_side() const {
return h > w ? h : w;
}
int min_side() const {
return h < w ? h : w;
}
int area() const { return w * h; }
int perimeter() const { return 2 * w + 2 * h; }
float pathological_mult() const {
return float(max_side()) / min_side() * area();
}
template <class R>
void expand_with(const R& r) {
w = std::max(w, r.x + r.w);
h = std::max(h, r.y + r.h);
}
};
struct rect_xywh {
int x;
int y;
int w;
int h;
rect_xywh() : x(0), y(0), w(0), h(0) {}
rect_xywh(const int x, const int y, const int w, const int h) : x(x), y(y), w(w), h(h) {}
int area() const { return w * h; }
int perimeter() const { return 2 * w + 2 * h; }
auto get_wh() const {
return rect_wh(w, h);
}
};
struct rect_xywhf {
int x;
int y;
int w;
int h;
bool flipped;
rect_xywhf() : x(0), y(0), w(0), h(0), flipped(false) {}
rect_xywhf(const int x, const int y, const int w, const int h, const bool flipped) : x(x), y(y), w(flipped ? h : w), h(flipped ? w : h), flipped(flipped) {}
rect_xywhf(const rect_xywh& b) : rect_xywhf(b.x, b.y, b.w, b.h, false) {}
int area() const { return w * h; }
int perimeter() const { return 2 * w + 2 * h; }
auto get_wh() const {
return rect_wh(w, h);
}
};
using space_rect = rect_xywh;
}

View File

@ -1,32 +0,0 @@
## Brute force approaches
- Rect feasibility = number of empty spaces into which the rect fits
- Rect difficulty = 1 / rect feasibility
- Space feasibility = number of remaining input rectangles that fit into the space
- How to break ties for huge, initial spaces?
- How much one can insert until current rects AABB is expanded.
- The more one can insert, the more feasible the space is.
- In practice, will there be many ties?
- There shouldn't be many not be if the max_size is carefully chosen
- Determine difficulty recursively?
- e.g. sum all successful insertions and successful insertions after each successful insertion...
- ...quickly becomes exponential
- Minimum of all free dimensions generated by all insertion trials?
- Or some coefficient like pathological mult for rectangles?
- Space difficulty = 1 / space feasibility
- Algorithm: until no more spaces or no more rectangles
- Find the most difficult space
- among rects that fit...
- insert the one that generates least spaces or the most difficult space - this means that, into the most difficult space, the most difficult rect will be inserted
- In case of a perfectly fitting rect, this will be chosen
- in case of a partly fitting or a strictly smaller rect, insert the most difficult rect, e.g. one that leaves the most difficult spaces
- however, when splitting due to the rect being strictly smaller, split in the direction that generates a maximally feasible space
- complexity: we iterate every space, number of which will grow linearly as time progresses, and then we iterate each rect
## Old thoughts
- what about just resizing when there is no more space left?
- then iterate over all empty spaces and resize those that are touching the edge
- there might be none like this, though
- then we can ditch iterating orders
- we could then easily make it an on-line algorithm