Skip to content

Commit

Permalink
Added initial implementation of a dynamic spritesheet generator
Browse files Browse the repository at this point in the history
This isn't used anywhere yet. Right now it only takes a directory and generates one
sheet for all the images in that directory and its sub-directories. The sheets are
fully-functional textures, and a path -> rect mapping exists, but I haven't added anything
that uses it yet.

This will definitely need some improvements and changes to properly integrate with @jyrkive 's
OGL work.
  • Loading branch information
Vultraz committed Apr 9, 2018
1 parent 769f102 commit f7d2924
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 0 deletions.
2 changes: 2 additions & 0 deletions projectfiles/VC14/wesnoth.vcxproj.filters
Expand Up @@ -1554,6 +1554,7 @@
<ClCompile Include="..\..\src\help\manager.cpp">
<Filter>Help</Filter>
</ClCompile>
<ClCompile Include="..\..\src\spritesheet_generator.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\src\addon\client.hpp">
Expand Down Expand Up @@ -3014,6 +3015,7 @@
<ClInclude Include="..\..\src\help\manager.hpp">
<Filter>Help</Filter>
</ClInclude>
<ClInclude Include="..\..\src\spritesheet_generator.hpp" />
</ItemGroup>
<ItemGroup>
<CustomBuild Include="..\..\src\tests\test_sdl_utils.hpp">
Expand Down
1 change: 1 addition & 0 deletions source_lists/wesnoth
Expand Up @@ -335,6 +335,7 @@ scripting/plugins/manager.cpp
sdl/point.cpp
settings.cpp
side_filter.cpp
spritesheet_generator.hpp
statistics.cpp
storyscreen/controller.cpp
storyscreen/parser.cpp
Expand Down
218 changes: 218 additions & 0 deletions src/spritesheet_generator.cpp
@@ -0,0 +1,218 @@
/*
Copyright (C) 2018 by Charles Dang <exodia339@gmail.com>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/

#include "spritesheet_generator.hpp"

#include "filesystem.hpp"
#include "image.hpp"
#include "sdl/point.hpp"
#include "sdl/rect.hpp"
#include "sdl/surface.hpp"
#include "sdl/texture.hpp"
#include "sdl/utils.hpp"

#include <algorithm>
#include <iostream>
#include <map>

namespace image
{
namespace
{
struct sheet_elment_data
{
/**
* The spritesheet.
*
* Do note The texture class is really a "texture reference" class; it only
* owns a shared pointer to the actual texture in memory. Therefor, having
* this object does not mean we're keeping multiple copies of each sheet.
*/
texture sheet;

/** The coordinates of this image on the spritesheet. */
SDL_Rect rect;
};

/** Helper sorting struct for build_sheet_from_images to use. */
struct surf_area_sort
{
bool operator()(const surface& lhs, const surface& rhs) const
{
return lhs->w * lhs->h < rhs->w * rhs->h;
}
};

/** Map of path to rect. */
std::map<std::string, sheet_elment_data> path_sheet_mapping;

void build_sheet_from_images(const std::vector<std::string>& file_paths)
{
// Surface -> [path, sheet element data] map, sorted by surface area (largest last).
// TODO: simplify
std::multimap<surface, std::pair<std::string, sheet_elment_data>, surf_area_sort> surf_path_map;

This comment has been minimized.

Copy link
@jyrkive

jyrkive Apr 9, 2018

Member

This multimap with so many types is ugly. I plan to instead use a struct of {sprite, surface, string}, and store such structs in a vector.


// Load all the images.
for(const auto& f : file_paths) {

This comment has been minimized.

Copy link
@jyrkive

jyrkive Apr 9, 2018

Member

Needs multithreaded image loading. Also, I plan to split image loading into two stages, where the first stage loads the images from disk and the second stage runs the IPFs.

This comment has been minimized.

Copy link
@CelticMinstrel

CelticMinstrel Apr 9, 2018

Member

Won't the second stage ultimately become unnecessary? At least for unit sprites…

This comment has been minimized.

Copy link
@jyrkive

jyrkive Apr 9, 2018

Member

If we can replace IPFs with shaders, then we can indeed stop running IPFs when we load images.

surface temp = image::get_image(f);

if(!temp.null()) {
std::pair<std::string, sheet_elment_data> another_temp(f, {});
surf_path_map.emplace(std::move(temp), another_temp);
}
}

if(surf_path_map.empty()) {
return;
}

const unsigned total_area = std::accumulate(surf_path_map.begin(), surf_path_map.end(), 0,
[](const int val, const auto& s) { return val + (s.first->w * s.first->h); });

const unsigned side_length = static_cast<unsigned>(std::sqrt(total_area) * 1.3);

This comment has been minimized.

Copy link
@jyrkive

jyrkive Apr 9, 2018

Member

In the future, we need to store width and height separately. If nothing else, they will be different when new images are added to an existing sprite sheet.

This comment has been minimized.

Copy link
@Vultraz

Vultraz Apr 9, 2018

Author Member

Height is dynamic. See below when res_h is set.


std::vector<sheet_elment_data> packaged_data;
packaged_data.reserve(surf_path_map.size());

std::vector<unsigned> max_row_heights;

This comment has been minimized.

Copy link
@jyrkive

jyrkive Apr 9, 2018

Member

If this whole vector exists only for the purpose of determining the total height of the sprite sheet, it could be replaced with a single height counter that's increased every time a line is finished or the sprites run out.

unsigned current_row_max_height = 0;

point origin(0, 0);

//
// Calculate the destination rects for the images.
// This uses the Shelf Next Fit algorithm as described here: http://clb.demon.fi/files/RectangleBinPack.pdf
// Our method forgoes the orientation consideration and works top-down instead of bottom-up, however.

This comment has been minimized.

Copy link
@jyrkive

jyrkive Apr 9, 2018

Member

Where exactly is the origin, anyway? In OpenGL coordinates, Y = 0 is the bottom edge.

This comment has been minimized.

Copy link
@Vultraz

Vultraz Apr 9, 2018

Author Member

Top-left. y = 0 is the top edge.

//
for(auto& iter : surf_path_map) {
SDL_Rect r = get_non_transparent_portion(iter.first);

current_row_max_height = std::max<unsigned>(current_row_max_height, r.h);

This comment has been minimized.

Copy link
@jyrkive

jyrkive Apr 9, 2018

Member

This should be done after checking if the element fits in this line.


// If we can't fit this element without getting cut off, move to the next line.
if(static_cast<unsigned>(origin.x + r.w) > side_length) {
// Reset the origin.
origin.x = 0;
origin.y += current_row_max_height;

// Save this row's max height.
max_row_heights.push_back(current_row_max_height);
current_row_max_height = 0;
}

r.x = origin.x;
r.y = origin.y;

// Save this element's rect.
iter.second.second.rect = r;

// Shift the rect origin for the next element.
origin.x += r.w;
}

// If we never reached max width during rect placement, max_row_heights will be empty.
// In that case, that forces res_h below to be 0. Add the current max height to compensate.
// TODO: maybe we should just handle a 0 value in res_h's calculation?
if(max_row_heights.empty()) {
max_row_heights.push_back(current_row_max_height);
}

const unsigned res_w = side_length;
const unsigned res_h = std::min<unsigned>(side_length, std::accumulate(max_row_heights.begin(), max_row_heights.end(), 0,
[](const int val, const unsigned h) { return val + h; }));

// Check that we won't exceed max texture size and that neither dimension is 0. TODO: handle?
assert(res_w > 0 && res_w <= 8192 && res_h > 0 && res_h <= 8192);

// Assemble everything
surface res = create_neutral_surface(res_w, res_h);
assert(!res.null() && "Spritesheet surface is null!");

for(auto& iter : surf_path_map) {
const surface& s = iter.first;
sheet_elment_data& data = iter.second.second;

SDL_Rect src_rect = get_non_transparent_portion(s);
sdl_blit(s, &src_rect, res, &data.rect);
}

#ifdef DEBUG_SPRITESHEET_OUTPUT
static unsigned test_i = 0;
image::save_image(res, "spritesheets/sheet_test_" + std::to_string(test_i++) + ".png");
#endif

// Convert the sheet to a texture.
texture sheet_tex(res);

// Add path mappings.
for(auto& iter : surf_path_map) {
// Copy a texture reference;
sheet_elment_data& data = iter.second.second;
data.sheet = sheet_tex;

#ifdef HAVE_CXX17
path_sheet_mapping.insert(std::exchange(iter->second, {}));
#else
path_sheet_mapping.emplace(iter.second.first, data);
#endif
}
}

void build_spritesheet_from_impl(const std::string& dir, const std::string& subdir)
{
const std::string checked_dir = dir + subdir;

if(!filesystem::is_directory(checked_dir)) {
return;
}

std::vector<std::string> files_found;
std::vector<std::string> dirs_found;

filesystem::get_files_in_dir(checked_dir, &files_found, &dirs_found,
filesystem::FILE_NAME_ONLY,
filesystem::NO_FILTER,
filesystem::DONT_REORDER
);

for(std::string& file : files_found) {
file = subdir + file;
}

if(!files_found.empty()) {
build_sheet_from_images(files_found);
}

for(const auto& d : dirs_found) {
build_spritesheet_from_impl(dir, subdir + d + "/");
}
}

} // end anon namespace
void build_spritesheet_from(const std::string& subdir)
{
#ifdef DEBUG_SPRITESHEET_OUTPUT
const std::size_t start = SDL_GetTicks();
#endif

for(const auto& p : filesystem::get_binary_paths("images")) {
build_spritesheet_from_impl(p, subdir);
}

#ifdef DEBUG_SPRITESHEET_OUTPUT
std::cerr << "Spritesheet generation of '" << subdir << "' took: " << (SDL_GetTicks() - start) << "ms\n";
#endif
}

} // namespace image
22 changes: 22 additions & 0 deletions src/spritesheet_generator.hpp
@@ -0,0 +1,22 @@
/*
Copyright (C) 2018 by Charles Dang <exodia339@gmail.com>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/

#pragma once

#include <string>

namespace image
{
void build_spritesheet_from(const std::string& subdir);
} // namespace image

0 comments on commit f7d2924

Please sign in to comment.