Added spritsheet generator (#6665)

This takes a folder with images and writes an assembled spritesheet to disk along with a config file.
This commit is contained in:
Charles Dang 2024-08-08 11:29:08 -04:00 committed by GitHub
parent d762bea734
commit 518e0d2050
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 261 additions and 1 deletions

View file

@ -397,6 +397,7 @@ utils/irdya_datetime.cpp
utils/markov_generator.cpp
utils/name_generator_factory.cpp
utils/parse_network_address.cpp
utils/spritesheet_generator.cpp
variable.cpp
variable_info.cpp
wesnothd_connection.cpp

View file

@ -124,6 +124,7 @@ commandline_options::commandline_options(const std::vector<std::string>& args)
, password()
, render_image()
, render_image_dst()
, generate_spritesheet()
, screenshot(false)
, screenshot_map_file()
, screenshot_output_file()
@ -197,7 +198,8 @@ commandline_options::commandline_options(const std::vector<std::string>& args)
("password", po::value<std::string>(), "uses <password> when connecting to a server, ignoring other preferences.")
("plugin", po::value<std::string>(), "(experimental) load a script which defines a wesnoth plugin. similar to --script below, but Lua file should return a function which will be run as a coroutine and periodically woken up with updates.")
("render-image", po::value<two_strings>()->multitoken(), "takes two arguments: <image> <output>. Like screenshot, but instead of a map, takes a valid Wesnoth 'image path string' with image path functions, and writes it to a .png file." IMPLY_TERMINAL)
("report,R", "initializes game directories, prints build information suitable for use in bug reports, and exits." IMPLY_TERMINAL)
("generate-spritesheet", po::value<std::string>(), "generates a spritesheet from all png images in the given path, recursively (one sheet per directory)")
("report,R", "initializes game directories, prints build information suitable for use in bug reports, and exits." IMPLY_TERMINAL)
("rng-seed", po::value<unsigned int>(), "seeds the random number generator with number <arg>. Example: --rng-seed 0")
("screenshot", po::value<two_strings>()->multitoken(), "takes two arguments: <map> <output>. Saves a screenshot of <map> to <output> without initializing a screen. Editor must be compiled in for this to work." IMPLY_TERMINAL)
("script", po::value<std::string>(), "(experimental) file containing a Lua script to control the client")
@ -448,6 +450,8 @@ commandline_options::commandline_options(const std::vector<std::string>& args)
render_image = vm["render-image"].as<two_strings>().first;
render_image_dst = vm["render-image"].as<two_strings>().second;
}
if(vm.count("generate-spritesheet"))
generate_spritesheet = vm["generate-spritesheet"].as<std::string>();
if(vm.count("screenshot"))
{
screenshot = true;

View file

@ -180,6 +180,8 @@ public:
utils::optional<std::string> render_image;
/** Output file to put rendered image path in. Optional second parameter after --render-image */
utils::optional<std::string> render_image_dst;
/** Path of which to generate a spritesheet */
utils::optional<std::string> generate_spritesheet;
/** True if --screenshot was given on the command line. Starts Wesnoth in screenshot mode. */
bool screenshot;
/** Map file to make a screenshot of. First parameter given after --screenshot. */

View file

@ -0,0 +1,223 @@
/*
Copyright (C) 2018 - 2022
by Charles Dang <exodia339@gmail.com>
Part of the Battle for Wesnoth Project https://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 "utils/spritesheet_generator.hpp"
#include "config.hpp"
#include "filesystem.hpp"
#include "log.hpp"
#include "picture.hpp"
#include "sdl/point.hpp"
#include "sdl/rect.hpp"
#include "sdl/surface.hpp"
#include "sdl/utils.hpp"
#include "serialization/binary_or_text.hpp"
#include <SDL2/SDL_image.h>
#include <algorithm>
#include <filesystem>
#include <future>
#include <iostream>
#include <numeric>
namespace image
{
namespace
{
/** Intermediate helper struct to manage surfaces while the sheet is being assembled. */
struct sheet_element
{
explicit sheet_element(const std::filesystem::path& p)
: surf{IMG_Load_RW(filesystem::make_read_RWops(p.string()).release(), true)}
, filename{p.filename().string()}
, src{get_non_transparent_portion(surf)}
, dst{}
{
}
/** Image. */
surface surf;
/** Filename. */
std::string filename;
/** Non-transparent portion of the surface to compose. */
rect src;
/** Location on the final composed sheet. */
rect dst;
config to_config() const
{
return config{
"filename", filename,
/** Source rect of this image on the final sheet. */
"sheet_rect", formatter() << dst.x << ',' << dst.y << ',' << dst.w << ',' << dst.h,
/** Offset at which to render this image, equal to the non-transparent offset from origin (0,0 top left). */
"draw_offset", formatter() << src.x << ',' << src.y,
/** Original image size in case we need it. */
"original_size", formatter() << surf->w << ',' << surf->h,
};
}
};
/** Tweak as needed. *Must* be floating-point in order to allow rounding. */
constexpr double max_items_per_loader = 8.0;
void build_sheet_from_images(const std::vector<std::filesystem::path>& file_paths)
{
const unsigned num_loaders = std::ceil(file_paths.size() / max_items_per_loader);
const unsigned num_to_load = std::ceil(file_paths.size() / double(num_loaders));
std::vector<std::future<std::vector<sheet_element>>> loaders{};
loaders.reserve(num_loaders);
for(unsigned i = 0; i < num_loaders; ++i) {
loaders.push_back(std::async(std::launch::async, [&file_paths, &num_to_load, i]() {
std::vector<sheet_element> res;
for(unsigned k = num_to_load * i; k < std::min<unsigned>(num_to_load * (i + 1u), file_paths.size()); ++k) {
res.emplace_back(file_paths[k]);
}
return res;
}));
}
std::vector<sheet_element> elements;
elements.reserve(file_paths.size());
// Wait for results, then combine them with the master list.
for(auto& loader : loaders) {
auto res = loader.get();
std::move(res.begin(), res.end(), std::back_inserter(elements));
}
// Sort the surfaces by area, largest last.
// TODO: should we use plain sort? Output sheet seems ever so slightly smaller when sort is not stable.
std::stable_sort(elements.begin(), elements.end(),
[](const auto& lhs, const auto& rhs) { return lhs.surf->w * lhs.surf->h < rhs.surf->w * rhs.surf->h; });
const unsigned total_area = std::accumulate(elements.begin(), elements.end(), 0,
[](const int val, const auto& s) { return val + (s.surf->w * s.surf->h); });
const unsigned side_length = static_cast<unsigned>(std::sqrt(total_area) * 1.3);
unsigned current_row_max_height = 0;
unsigned total_height = 0;
point origin{0, 0};
//
// Calculate the destination rects for the images. This uses the Shelf Next Fit algorithm.
// Our method forgoes the orientation consideration and works top-down instead of bottom-up.
//
for(auto& s : elements) {
current_row_max_height = std::max<unsigned>(current_row_max_height, s.src.h);
// If we can't fit this element without getting cut off, move to the next line.
if(static_cast<unsigned>(origin.x + s.src.w) > side_length) {
// Reset the origin.
origin.x = 0;
origin.y += current_row_max_height;
// Save this row's max height.
total_height += current_row_max_height;
current_row_max_height = 0;
}
// Save this element's rect.
s.dst = { origin.x, origin.y, s.src.w, s.src.h };
// Shift the rect origin for the next element.
origin.x += s.src.w;
}
// If we never reached max width during rect placement, total_height will be empty.
// In that case, fall back to the row's max height.
const unsigned res_w = side_length;
const unsigned res_h = total_height > 0 ? std::min<unsigned>(side_length, total_height) : current_row_max_height;
// 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);
surface res(res_w, res_h);
assert(res && "Spritesheet surface is null!");
// Final record of each image's location on the composed sheet.
auto out = filesystem::ostream_file("./_sheet.cfg");
config_writer mapping_data{*out, compression::format::gzip};
// Assemble everything
for(auto& s : elements) {
sdl_blit(s.surf, &s.src, res, &s.dst);
mapping_data.write_child("image", s.to_config());
}
image::save_image(res, "./_sheet.png");
}
void handle_dir_contents(const std::filesystem::path& path)
{
std::vector<std::filesystem::path> files_found;
for(const auto& entry : std::filesystem::directory_iterator{path}) {
if(entry.is_directory()) {
handle_dir_contents(entry);
} else if(entry.is_regular_file()) {
// TODO: should we have a better is-image check, and should we include jpgs?
// Right now all our sprites are pngs.
if(auto path = entry.path(); path.extension() == ".png" && path.stem() != "_sheet") {
files_found.push_back(std::move(path));
}
}
}
if(!files_found.empty()) {
try {
// Allows relative paths to resolve correctly. This needs to be set *after* recursive
// directory handling or else the path will be wrong when returning to the parent.
std::filesystem::current_path(path);
} catch(const std::filesystem::filesystem_error&) {
return;
}
build_sheet_from_images(files_found);
}
}
} // end anon namespace
#define DEBUG_SPRITESHEET_OUTPUT
void build_spritesheet_from(const std::string& entry_point)
{
#ifdef DEBUG_SPRITESHEET_OUTPUT
const std::size_t start = SDL_GetTicks();
#endif
try {
handle_dir_contents(filesystem::get_binary_file_location("images", entry_point));
} catch(const std::filesystem::filesystem_error& e) {
PLAIN_LOG << "Error generating spritesheet: " << e.what();
}
#ifdef DEBUG_SPRITESHEET_OUTPUT
PLAIN_LOG << "Spritesheet generation of '" << entry_point << "' took: " << (SDL_GetTicks() - start) << "ms\n";
#endif
}
} // namespace image

View file

@ -0,0 +1,23 @@
/*
Copyright (C) 2018 - 2022
by Charles Dang <exodia339@gmail.com>
Part of the Battle for Wesnoth Project https://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& entry_point);
} // namespace image

View file

@ -54,6 +54,7 @@
#include "widgets/button.hpp" // for button
#include "wml_exception.hpp" // for wml_exception
#include "utils/spritesheet_generator.hpp"
#ifdef _WIN32
#include "log_windows.hpp"
@ -503,6 +504,12 @@ static int process_command_args(commandline_options& cmdline_opts)
return 0;
}
if(cmdline_opts.generate_spritesheet) {
PLAIN_LOG << "sheet path " << *cmdline_opts.generate_spritesheet;
image::build_spritesheet_from(*cmdline_opts.generate_spritesheet);
return 0;
}
// Options changing their behavior dependent on some others should be checked below.
if(cmdline_opts.preprocess) {