From 16f20d528677ed47225af68df05a5759175dbfcc Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Sat, 13 Oct 2018 12:30:57 +0300 Subject: [PATCH] High-level sprite packing code for the 'pack everything' case --- src/ogl/texture_atlas.cpp | 92 ++++++++++++++++++++++++++++++++++----- src/ogl/texture_atlas.hpp | 2 + src/utils/math.hpp | 25 +++++++++++ 3 files changed, 109 insertions(+), 10 deletions(-) diff --git a/src/ogl/texture_atlas.cpp b/src/ogl/texture_atlas.cpp index 50eb398a3232..d169463bba77 100644 --- a/src/ogl/texture_atlas.cpp +++ b/src/ogl/texture_atlas.cpp @@ -20,15 +20,20 @@ #include "log.hpp" #include "sdl/utils.hpp" #include "serialization/string_utils.hpp" +#include "utils/math.hpp" #include #include #include +#include #include +#include +#include #include static lg::log_domain log_opengl("opengl"); +#define DBG_GL LOG_STREAM(debug, log_opengl) #define LOG_GL LOG_STREAM(info, log_opengl) #define ERR_GL LOG_STREAM(err, log_opengl) @@ -193,9 +198,6 @@ namespace gl void texture_atlas::init(const std::vector& images, thread_pool& thread_pool) { - sprites_.clear(); - sprites_by_name_.clear(); - // Determine unique base images. std::unordered_map base_images; for(const std::string& i : images) { @@ -229,7 +231,8 @@ void texture_atlas::init(const std::vector& images, thread_pool& th // Apply IPFs. thread_pool.run(sprites, &apply_IPFs).wait(); - // TODO: pack sprites into the texture atlas + // Pack sprites. + pack_sprites_wrapper(sprites); } bool texture_atlas::sprite_data::operator<(const sprite_data& other) const @@ -239,6 +242,70 @@ bool texture_atlas::sprite_data::operator<(const sprite_data& other) const return my_dims > other_dims; } +void texture_atlas::pack_sprites_wrapper(std::vector& sprites) +{ + using std::chrono::duration_cast; + using std::chrono::milliseconds; + + auto start_time = std::chrono::high_resolution_clock::now(); + + sprites_.clear(); + sprites_by_name_.clear(); + + // Sort the sprites to make them pack better. + std::stable_sort(sprites.begin(), sprites.end()); + + unsigned int total_size = std::accumulate(sprites.begin(), sprites.end(), 0u, + [](const unsigned int& size, const sprite_data& sprite) + { + return size + sprite.surf->w * sprite.surf->h; + }); + + std::pair texture_size = calculate_initial_texture_size(total_size); + if(texture_size.first > texture::MAX_DIMENSION || + texture_size.second > texture::MAX_DIMENSION) { + // No way the sprites would fit. + throw packing_error(); + } + + texture_.set_size(texture_size); + + while(true) { + try { + pack_sprites(sprites); + break; + } catch(packing_error&) { + // Double the shorter dimension (width in case of tie). + if(texture_size.first <= texture_size.second) { + texture_size.first *= 2; + } else { + texture_size.second *= 2; + } + if(texture_size.first > texture::MAX_DIMENSION || + texture_size.second > texture::MAX_DIMENSION) { + // Ran out of space. + throw; + } + texture_.set_size(texture_size); + } + } + + auto end_time = std::chrono::high_resolution_clock::now(); + auto time = end_time - start_time; + + const double BYTES_PER_PIXEL = 4.0; + double efficiency = static_cast(total_size) / + (texture_size.first * texture_size.second); + + DBG_GL << std::setprecision(3u) << "Texture atlas packed: " << sprites.size() << + " sprites (" << (BYTES_PER_PIXEL * total_size / (1 << 20)) << " MB)" << + " packed to a " << texture_size.first << "x" << texture_size.second << + " texture, efficiency << " << 100.0 * efficiency << " %, packing took " << + duration_cast(time).count() << " ms"; + + // TODO: construct the texture atlas +} + void texture_atlas::pack_sprites(std::vector& sprites) { free_rectangles_.clear(); @@ -327,8 +394,7 @@ void texture_atlas::apply_IPFs(sprite_data& sprite) try { surf = (*mod)(surf); - } - catch(const image::modification::imod_exception& e) { + } catch(const image::modification::imod_exception& e) { std::ostringstream ss; ss << "\n"; @@ -359,8 +425,7 @@ bool texture_atlas::better_fit(const sprite_data& sprite, const SDL_Rect& rect_a if(std::min(leftover_a, leftover_b) < 0) { // ...choose the other rectangle, it might fit. return leftover_b < leftover_a; - } - else { + } else { // Otherwise choose the rectangle with less leftover space. return leftover_a < leftover_b; } @@ -381,8 +446,7 @@ std::pair texture_atlas::split_rectangle(const SDL_Rect& rec rect_b.y = rectangle.y; rect_b.w = sprite.surf->w; rect_b.h = rectangle.h - sprite.surf->h; - } - else { + } else { rect_a.x = rectangle.x; rect_a.y = rectangle.y; rect_a.w = rectangle.w; @@ -397,4 +461,12 @@ std::pair texture_atlas::split_rectangle(const SDL_Rect& rec return {rect_a, rect_b}; } +std::pair texture_atlas::calculate_initial_texture_size(unsigned int combined_sprite_size) +{ + double num_bits = bit_width() - count_leading_zeros(combined_sprite_size) + 1; + int width = 1 << static_cast(std::ceil(num_bits / 2.0)); + int height = 1 << static_cast(std::floor(num_bits / 2.0)); + return {width, height}; +} + } diff --git a/src/ogl/texture_atlas.hpp b/src/ogl/texture_atlas.hpp index 72ecf575eb6b..5df832b9d2db 100644 --- a/src/ogl/texture_atlas.hpp +++ b/src/ogl/texture_atlas.hpp @@ -82,6 +82,7 @@ class texture_atlas std::unordered_map sprites_by_name_; std::vector free_rectangles_; + void pack_sprites_wrapper(std::vector& sprites); void pack_sprites(std::vector& sprites); void place_sprite(sprite_data& sprite); static void load_image(sprite_data& sprite); @@ -89,5 +90,6 @@ class texture_atlas /// @return true if it would be better to place the @param sprite to @param rect_a than @param rect_b. static bool better_fit(const sprite_data& sprite, const SDL_Rect& rect_a, const SDL_Rect& rect_b); static std::pair split_rectangle(const SDL_Rect& rectangle, const sprite_data& sprite); + static std::pair calculate_initial_texture_size(unsigned int combined_sprite_size); }; } diff --git a/src/utils/math.hpp b/src/utils/math.hpp index 1a49f8e75382..0c8493bcdd97 100644 --- a/src/utils/math.hpp +++ b/src/utils/math.hpp @@ -207,6 +207,26 @@ inline unsigned int count_leading_zeros_impl(N n, std::size_t w) { } #endif +/** + * Rounds `n` up to the nearest power of two. As examples, returns 64 for 64, + * and 128 for 65. + * + * @tparam N The type of `n`. Requirements are the same as for + * @ref count_leading_zeros(). + * + * @param n An integer upon which to operate. + * + * @returns The nearest power of two that's equal or larger than n. + */ +template +inline N round_up_to_power_of_two(N n) { + if(!is_power_of_two(n)) { + return static_cast(1) << (bit_width() - count_leading_zeros(n)); + } else { + return n; + } +} + /** * Returns the quantity of leading `0` bits in `n` — i.e., the quantity of * bits in `n`, minus the 1-based bit index of the most significant `1` bit in @@ -287,6 +307,11 @@ inline int rounded_division(int a, int b) return 2 * res.rem > b ? (res.quot + 1) : res.quot; } +template +bool is_power_of_two(N n) { + return n != 0 && (n & (n - 1)) == 0; +} + #if 1 typedef int32_t fixed_t;