From 35267a2c5c744f19da589acee0008c75a308a462 Mon Sep 17 00:00:00 2001 From: Relintai Date: Sun, 20 Oct 2019 21:46:36 +0200 Subject: [PATCH] Let's use the legacy version of rectpack2D. --- SCsub | 1 + merge_texture.cpp | 7 +- merge_texture.h | 4 + rectpack2D/README.md | 217 ++---------------- rectpack2D/best_bin_finder.h | 270 ---------------------- rectpack2D/empty_space_allocators.h | 70 ------ rectpack2D/empty_spaces.h | 149 ------------ rectpack2D/finders_interface.h | 153 ------------- rectpack2D/insert_and_split.h | 135 ----------- rectpack2D/pack.cpp | 341 ++++++++++++++++++++++++++++ rectpack2D/pack.h | 82 +++++++ rectpack2D/rect_structs.h | 78 ------- rectpack2D/thoughts.md | 32 --- 13 files changed, 452 insertions(+), 1087 deletions(-) delete mode 100644 rectpack2D/best_bin_finder.h delete mode 100644 rectpack2D/empty_space_allocators.h delete mode 100644 rectpack2D/empty_spaces.h delete mode 100644 rectpack2D/finders_interface.h delete mode 100644 rectpack2D/insert_and_split.h create mode 100644 rectpack2D/pack.cpp create mode 100644 rectpack2D/pack.h delete mode 100644 rectpack2D/rect_structs.h delete mode 100644 rectpack2D/thoughts.md diff --git a/SCsub b/SCsub index f899360..c0a4920 100644 --- a/SCsub +++ b/SCsub @@ -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") diff --git a/merge_texture.cpp b/merge_texture.cpp index c1cd573..3d6dd5b 100644 --- a/merge_texture.cpp +++ b/merge_texture.cpp @@ -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); } diff --git a/merge_texture.h b/merge_texture.h index ffef8a9..d754256 100644 --- a/merge_texture.h +++ b/merge_texture.h @@ -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(); diff --git a/rectpack2D/README.md b/rectpack2D/README.md index 404bf71..9dae529 100644 --- a/rectpack2D/README.md +++ b/rectpack2D/README.md @@ -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 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 diff --git a/rectpack2D/best_bin_finder.h b/rectpack2D/best_bin_finder.h deleted file mode 100644 index d14c92b..0000000 --- a/rectpack2D/best_bin_finder.h +++ /dev/null @@ -1,270 +0,0 @@ -#pragma once -#include -#include -#include "rect_structs.h" - -namespace rectpack2D { - enum class callback_result { - ABORT_PACKING, - CONTINUE_PACKING - }; - - template - 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) { - 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 - std::variant 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 - std::variant 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(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(&best_result)) { - return *failed; - } - - auto best_bin = std::get(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(&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(&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(&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(); - } - } -} diff --git a/rectpack2D/empty_space_allocators.h b/rectpack2D/empty_space_allocators.h deleted file mode 100644 index 217ef77..0000000 --- a/rectpack2D/empty_space_allocators.h +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once -#include -#include -#include - -#include "rect_structs.h" - -namespace rectpack2D { - class default_empty_spaces { - std::vector 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 - class static_empty_spaces { - int count_spaces = 0; - std::array 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(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]; - } - }; -} diff --git a/rectpack2D/empty_spaces.h b/rectpack2D/empty_spaces.h deleted file mode 100644 index bb0ad79..0000000 --- a/rectpack2D/empty_spaces.h +++ /dev/null @@ -1,149 +0,0 @@ -#pragma once -#include "insert_and_split.h" - -namespace rectpack2D { - enum class flipping_option { - DISABLED, - ENABLED - }; - - class default_empty_spaces; - - template - 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; - - 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 - std::optional insert(const rect_wh image_rectangle, F report_candidate_empty_space) { - for (int i = static_cast(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 { - 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; - } - }; -} diff --git a/rectpack2D/finders_interface.h b/rectpack2D/finders_interface.h deleted file mode 100644 index 95991e9..0000000 --- a/rectpack2D/finders_interface.h +++ /dev/null @@ -1,153 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include - -#include "insert_and_split.h" -#include "empty_spaces.h" -#include "empty_space_allocators.h" - -#include "best_bin_finder.h" - -namespace rectpack2D { - template - using output_rect_t = typename empty_spaces_type::output_rect_type; - - template - 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 - 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 { - max_bin_side, - discard_step, - std::forward(handle_successful_insertion), - std::forward(handle_unsuccessful_insertion), - flipping_mode - }; - }; - - /* - Finds the best packing for the rectangles, - just in the order that they were passed. - */ - - template - rect_wh find_best_packing_dont_sort( - std::vector>& subjects, - const finder_input& input - ) { - using order_type = std::remove_reference_t; - - return find_best_packing_impl( - [&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 - rect_wh find_best_packing( - std::vector>& subjects, - const finder_input& input, - - Comparator comparator, - Comparators... comparators - ) { - using rect_type = output_rect_t; - using order_type = std::vector; - - constexpr auto count_orders = 1 + sizeof...(Comparators); - thread_local std::array 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( - [](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 - rect_wh find_best_packing( - std::vector>& subjects, - const finder_input& input - ) { - using rect_type = output_rect_t; - - return find_best_packing( - 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(); - } - ); - } -} diff --git a/rectpack2D/insert_and_split.h b/rectpack2D/insert_and_split.h deleted file mode 100644 index 723582f..0000000 --- a/rectpack2D/insert_and_split.h +++ /dev/null @@ -1,135 +0,0 @@ -#pragma once -#include -#include "rect_structs.h" - -namespace rectpack2D { - struct created_splits { - int count = 0; - std::array spaces; - - static auto failed() { - created_splits result; - result.count = -1; - return result; - } - - static auto none() { - return created_splits(); - } - - template - created_splits(Args&&... args) : spaces({ std::forward(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); - } -} diff --git a/rectpack2D/pack.cpp b/rectpack2D/pack.cpp new file mode 100644 index 0000000..f7a8861 --- /dev/null +++ b/rectpack2D/pack.cpp @@ -0,0 +1,341 @@ +#include "pack.h" +#include +#include + +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& succ, vector& 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& bins) { + rect_wh _rect(max_s, max_s); + + for(int i = 0; i < n; ++i) + if(!v[i]->fits(_rect,allowFlip)) return false; + + vector 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(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); +} \ No newline at end of file diff --git a/rectpack2D/pack.h b/rectpack2D/pack.h new file mode 100644 index 0000000..c6e6e87 --- /dev/null +++ b/rectpack2D/pack.h @@ -0,0 +1,82 @@ +#pragma once +#include + +/* 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& 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 rects; +}; + +bool pack(rect_xywhf* const * v, int n, int max_side, bool allowFlip, std::vector& bins); diff --git a/rectpack2D/rect_structs.h b/rectpack2D/rect_structs.h deleted file mode 100644 index 8d50696..0000000 --- a/rectpack2D/rect_structs.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once -#include - -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 - 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; -} \ No newline at end of file diff --git a/rectpack2D/thoughts.md b/rectpack2D/thoughts.md deleted file mode 100644 index 3c1a630..0000000 --- a/rectpack2D/thoughts.md +++ /dev/null @@ -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