The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
spritesheet_generator.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2018 - 2025
3 by Charles Dang <exodia339@gmail.com>
4 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY.
12
13 See the COPYING file for more details.
14*/
15
17
18#include "config.hpp"
19#include "filesystem.hpp"
20#include "log.hpp"
21#include "picture.hpp"
22#include "sdl/point.hpp"
23#include "sdl/rect.hpp"
24#include "sdl/surface.hpp"
25#include "sdl/utils.hpp"
27#include "utils/optimer.hpp"
28
29#include <SDL3_image/SDL_image.h>
30
31#include <algorithm>
32#include <future>
33#include <iostream>
34#include <numeric>
35
36#ifdef __cpp_lib_ranges
37#include <ranges>
38#endif
39
40#ifdef __APPLE__
41#include <boost/filesystem.hpp>
42namespace fs = boost::filesystem;
43#else
44#include <filesystem>
45namespace fs = std::filesystem;
46#endif
47
48namespace image
49{
50namespace
51{
52/** Intermediate helper struct to manage surfaces while the sheet is being assembled. */
53struct sheet_element
54{
55 explicit sheet_element(const fs::path& p)
56 : surf(IMG_Load_IO(SDL_IOFromFile(p.string().c_str(), "rb"), true))
57 , filename(p.filename().string())
59 , dst()
60 {
61 }
62
63 /** Image. */
65
66 /** Filename. */
67 std::string filename;
68
69 /** Non-transparent portion of the surface to compose. */
71
72 /** Location on the final composed sheet. */
74
75 config to_config() const
76 {
77 return config{
78 "filename", filename,
79
80 /** Source rect of this image on the final sheet. */
81 "sheet_rect", formatter() << dst.x << ',' << dst.y << ',' << dst.w << ',' << dst.h,
82
83 /** Offset at which to render this image, equal to the non-transparent offset from origin (0,0 top left). */
84 "draw_offset", formatter() << src.x << ',' << src.y,
85
86 /** Original image size in case we need it. */
87 "original_size", formatter() << surf->w << ',' << surf->h,
88 };
89 }
90};
91
92/** Tweak as needed. *Must* be floating-point in order to allow rounding. */
93constexpr double max_items_per_loader = 8.0;
94
95void build_sheet_from_images(const std::vector<fs::path>& file_paths)
96{
97 const unsigned num_loaders = std::ceil(file_paths.size() / max_items_per_loader);
98 const unsigned num_to_load = std::ceil(file_paths.size() / double(num_loaders));
99
100 std::vector<std::future<std::vector<sheet_element>>> loaders;
101 loaders.reserve(num_loaders);
102
103#ifdef __cpp_lib_ranges_chunk // C++23 feature
104 for(auto span : file_paths | std::views::chunk(num_to_load)) {
105 loaders.push_back(std::async(std::launch::async,
106 [span]() { return std::vector<sheet_element>(span.begin(), span.end()); }
107 ));
108 }
109#else
110 for(unsigned i = 0; i < num_loaders; ++i) {
111 loaders.push_back(std::async(std::launch::async, [&file_paths, &num_to_load, i]() {
112 std::vector<sheet_element> res;
113#ifdef __cpp_lib_ranges
114 for(const fs::path& p : file_paths | std::views::drop(num_to_load * i) | std::views::take(num_to_load)) {
115 res.emplace_back(p);
116 }
117#else
118 for(unsigned k = num_to_load * i; k < std::min<unsigned>(num_to_load * (i + 1u), file_paths.size()); ++k) {
119 res.emplace_back(file_paths[k]);
120 }
121#endif
122 return res;
123 }));
124 }
125#endif
126
127 std::vector<sheet_element> elements;
128 elements.reserve(file_paths.size());
129
130 // Wait for results, then combine them with the master list.
131 for(auto& loader : loaders) {
132 auto res = loader.get();
133 std::move(res.begin(), res.end(), std::back_inserter(elements));
134 }
135
136 // Sort the surfaces by area, largest last.
137 // TODO: should we use plain sort? Output sheet seems ever so slightly smaller when sort is not stable.
138 std::stable_sort(elements.begin(), elements.end(),
139 [](const auto& lhs, const auto& rhs) { return lhs.surf.area() < rhs.surf.area(); });
140
141 const unsigned total_area = std::accumulate(elements.begin(), elements.end(), 0,
142 [](const int val, const auto& s) { return val + s.surf.area(); });
143
144 const unsigned side_length = static_cast<unsigned>(std::sqrt(total_area) * 1.3);
145
146 unsigned current_row_max_height = 0;
147 unsigned total_height = 0;
148
149 point origin{0, 0};
150
151 //
152 // Calculate the destination rects for the images. This uses the Shelf Next Fit algorithm.
153 // Our method forgoes the orientation consideration and works top-down instead of bottom-up.
154 //
155 for(auto& s : elements) {
156 current_row_max_height = std::max<unsigned>(current_row_max_height, s.src.h);
157
158 // If we can't fit this element without getting cut off, move to the next line.
159 if(static_cast<unsigned>(origin.x + s.src.w) > side_length) {
160 // Reset the origin.
161 origin.x = 0;
162 origin.y += current_row_max_height;
163
164 // Save this row's max height.
165 total_height += current_row_max_height;
166 current_row_max_height = 0;
167 }
168
169 // Save this element's rect.
170 s.dst = { origin.x, origin.y, s.src.w, s.src.h };
171
172 // Shift the rect origin for the next element.
173 origin.x += s.src.w;
174 }
175
176 // If we never reached max width during rect placement, total_height will be empty.
177 // In that case, fall back to the row's max height.
178 const unsigned res_w = side_length;
179 const unsigned res_h = total_height > 0 ? std::min<unsigned>(side_length, total_height) : current_row_max_height;
180
181 // Check that we won't exceed max texture size and that neither dimension is 0. TODO: handle?
182 assert(res_w > 0 && res_w <= 8192 && res_h > 0 && res_h <= 8192);
183
184 surface res(res_w, res_h);
185 assert(res && "Spritesheet surface is null!");
186
187 // Final record of each image's location on the composed sheet.
188 auto out = filesystem::ostream_file("./_sheet.cfg");
189 config_writer mapping_data{*out, compression::format::gzip};
190
191 // Assemble everything
192 for(auto& s : elements) {
193 sdl_blit(s.surf, &s.src, res, &s.dst);
194 mapping_data.write_child("image", s.to_config());
195 }
196
197 image::save_image(res, "./_sheet.png");
198}
199
200void handle_dir_contents(const fs::path& path)
201{
202 std::vector<fs::path> files_found;
203 for(const auto& entry : fs::directory_iterator{path}) {
204 if(entry.is_directory()) {
205 handle_dir_contents(entry);
206 } else if(entry.is_regular_file()) {
207 // TODO: should we have a better is-image check, and should we include jpgs?
208 // Right now all our sprites are pngs.
209 if(auto path = entry.path(); path.extension() == ".png" && path.stem() != "_sheet") {
210 files_found.push_back(std::move(path));
211 }
212 }
213 }
214
215 if(!files_found.empty()) {
216 try {
217 // Allows relative paths to resolve correctly. This needs to be set *after* recursive
218 // directory handling or else the path will be wrong when returning to the parent.
219 fs::current_path(path);
220 } catch(const fs::filesystem_error&) {
221 return;
222 }
223
224 build_sheet_from_images(files_found);
225 }
226}
227
228} // end anon namespace
229
230void build_spritesheet_from(const std::string& entry_point)
231{
232 const utils::ms_optimer timer([&](const auto& timer) {
233 PLAIN_LOG << "Spritesheet generation of '" << entry_point << "' took: " << timer;
234 });
235
236 if(auto path = filesystem::get_binary_file_location("images", entry_point)) {
237 try {
238 handle_dir_contents(*path);
239 } catch(const fs::filesystem_error& e) {
240 PLAIN_LOG << "Filesystem Error generating spritesheet: " << e.what();
241 }
242 } else {
243 PLAIN_LOG << "Cannot find entry point to build spritesheet: " << entry_point;
244 }
245}
246
247} // namespace image
Class for writing a config out to a file in pieces.
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
std::ostringstream wrapper.
Definition formatter.hpp:40
Definitions for the interface to Wesnoth Markup Language (WML).
Declarations for File-IO.
Standard logging facilities (interface).
#define PLAIN_LOG
Definition log.hpp:296
utils::optional< std::string > get_binary_file_location(const std::string &type, const std::string &filename)
Returns a complete path to the actual file of a given type, if it exists.
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode=std::ios_base::binary, bool create_directory=true)
std::string path
Functions to load and save images from/to disk.
void build_spritesheet_from(const std::string &entry_point)
save_result save_image(const locator &i_locator, const std::string &filename)
Definition picture.cpp:921
STL namespace.
Contains the SDL_Rect helper code.
rect dst
Location on the final composed sheet.
surface surf
Image.
rect src
Non-transparent portion of the surface to compose.
std::string filename
Filename.
Holds a 2D point.
Definition point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition rect.hpp:49
Reports time elapsed at the end of an object scope.
Definition optimer.hpp:37
mock_party p
static map_location::direction s
rect get_non_transparent_portion(const surface &surf)
Definition utils.cpp:1440
void sdl_blit(const surface &src, const SDL_Rect *src_rect, surface &dst, SDL_Rect *dst_rect)
Definition utils.hpp:43
#define i
#define e