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:
parent
d762bea734
commit
518e0d2050
6 changed files with 261 additions and 1 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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. */
|
||||
|
|
223
src/utils/spritesheet_generator.cpp
Normal file
223
src/utils/spritesheet_generator.cpp
Normal 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
|
23
src/utils/spritesheet_generator.hpp
Normal file
23
src/utils/spritesheet_generator.hpp
Normal 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
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Reference in a new issue