Skip to content

Commit

Permalink
High-level sprite packing code for the 'pack everything' case
Browse files Browse the repository at this point in the history
  • Loading branch information
jyrkive committed Oct 13, 2018
1 parent 62bbdab commit 16f20d5
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 10 deletions.
92 changes: 82 additions & 10 deletions src/ogl/texture_atlas.cpp
Expand Up @@ -20,15 +20,20 @@
#include "log.hpp"
#include "sdl/utils.hpp"
#include "serialization/string_utils.hpp"
#include "utils/math.hpp"

#include <boost/algorithm/string/trim.hpp>
#include <SDL_image.h>

#include <algorithm>
#include <chrono>
#include <future>
#include <iomanip>
#include <numeric>
#include <set>

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)

Expand Down Expand Up @@ -193,9 +198,6 @@ namespace gl

void texture_atlas::init(const std::vector<std::string>& images, thread_pool& thread_pool)
{
sprites_.clear();
sprites_by_name_.clear();

// Determine unique base images.
std::unordered_map<std::string, surface> base_images;
for(const std::string& i : images) {
Expand Down Expand Up @@ -229,7 +231,8 @@ void texture_atlas::init(const std::vector<std::string>& 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
Expand All @@ -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<sprite_data>& 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<int, int> 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<double>(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<milliseconds>(time).count() << " ms";

// TODO: construct the texture atlas
}

void texture_atlas::pack_sprites(std::vector<sprite_data>& sprites)
{
free_rectangles_.clear();
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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;
}
Expand All @@ -381,8 +446,7 @@ std::pair<SDL_Rect, SDL_Rect> 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;
Expand All @@ -397,4 +461,12 @@ std::pair<SDL_Rect, SDL_Rect> texture_atlas::split_rectangle(const SDL_Rect& rec
return {rect_a, rect_b};
}

std::pair<int, int> texture_atlas::calculate_initial_texture_size(unsigned int combined_sprite_size)
{
double num_bits = bit_width<unsigned int>() - count_leading_zeros(combined_sprite_size) + 1;
int width = 1 << static_cast<unsigned int>(std::ceil(num_bits / 2.0));
int height = 1 << static_cast<unsigned int>(std::floor(num_bits / 2.0));
return {width, height};
}

}
2 changes: 2 additions & 0 deletions src/ogl/texture_atlas.hpp
Expand Up @@ -82,12 +82,14 @@ class texture_atlas
std::unordered_map<std::string, const sprite*> sprites_by_name_;
std::vector<SDL_Rect> free_rectangles_;

void pack_sprites_wrapper(std::vector<sprite_data>& sprites);
void pack_sprites(std::vector<sprite_data>& sprites);
void place_sprite(sprite_data& sprite);
static void load_image(sprite_data& sprite);
static void apply_IPFs(sprite_data& sprite);
/// @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<SDL_Rect, SDL_Rect> split_rectangle(const SDL_Rect& rectangle, const sprite_data& sprite);
static std::pair<int, int> calculate_initial_texture_size(unsigned int combined_sprite_size);
};
}
25 changes: 25 additions & 0 deletions src/utils/math.hpp
Expand Up @@ -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<typename N>
inline N round_up_to_power_of_two(N n) {
if(!is_power_of_two(n)) {
return static_cast<N>(1) << (bit_width<N>() - 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
Expand Down Expand Up @@ -287,6 +307,11 @@ inline int rounded_division(int a, int b)
return 2 * res.rem > b ? (res.quot + 1) : res.quot;
}

template<typename N>
bool is_power_of_two(N n) {
return n != 0 && (n & (n - 1)) == 0;
}


#if 1
typedef int32_t fixed_t;
Expand Down

0 comments on commit 16f20d5

Please sign in to comment.