The Battle for Wesnoth  1.19.3+dev
spritesheet_generator.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2018 - 2022
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 
28 #include <SDL2/SDL_image.h>
29 
30 #include <algorithm>
31 #include <future>
32 #include <iostream>
33 #include <numeric>
34 
35 #ifdef __cpp_lib_ranges
36 #include <ranges>
37 #endif
38 
39 #ifdef __APPLE__
40 #include <boost/filesystem.hpp>
41 namespace fs = boost::filesystem;
42 #else
43 #include <filesystem>
44 namespace fs = std::filesystem;
45 #endif
46 
47 namespace image
48 {
49 namespace
50 {
51 /** Intermediate helper struct to manage surfaces while the sheet is being assembled. */
52 struct sheet_element
53 {
54  explicit sheet_element(const fs::path& p)
55  : surf(IMG_Load_RW(filesystem::make_read_RWops(p.string()).release(), true))
56  , filename(p.filename().string())
58  , dst()
59  {
60  }
61 
62  /** Image. */
64 
65  /** Filename. */
66  std::string filename;
67 
68  /** Non-transparent portion of the surface to compose. */
70 
71  /** Location on the final composed sheet. */
73 
74  config to_config() const
75  {
76  return config{
77  "filename", filename,
78 
79  /** Source rect of this image on the final sheet. */
80  "sheet_rect", formatter() << dst.x << ',' << dst.y << ',' << dst.w << ',' << dst.h,
81 
82  /** Offset at which to render this image, equal to the non-transparent offset from origin (0,0 top left). */
83  "draw_offset", formatter() << src.x << ',' << src.y,
84 
85  /** Original image size in case we need it. */
86  "original_size", formatter() << surf->w << ',' << surf->h,
87  };
88  }
89 };
90 
91 /** Tweak as needed. *Must* be floating-point in order to allow rounding. */
92 constexpr double max_items_per_loader = 8.0;
93 
94 void build_sheet_from_images(const std::vector<fs::path>& file_paths)
95 {
96  const unsigned num_loaders = std::ceil(file_paths.size() / max_items_per_loader);
97  const unsigned num_to_load = std::ceil(file_paths.size() / double(num_loaders));
98 
99  std::vector<std::future<std::vector<sheet_element>>> loaders;
100  loaders.reserve(num_loaders);
101 
102 #ifdef __cpp_lib_ranges_chunk // C++23 feature
103  for(auto span : file_paths | std::views::chunk(num_to_load)) {
104  loaders.push_back(std::async(std::launch::async,
105  [span]() { return std::vector<sheet_element>(span.begin(), span.end()); }
106  ));
107  }
108 #else
109  for(unsigned i = 0; i < num_loaders; ++i) {
110  loaders.push_back(std::async(std::launch::async, [&file_paths, &num_to_load, i]() {
111  std::vector<sheet_element> res;
112 #ifdef __cpp_lib_ranges
113  for(const fs::path& p : file_paths | std::views::drop(num_to_load * i) | std::views::take(num_to_load)) {
114  res.emplace_back(p);
115  }
116 #else
117  for(unsigned k = num_to_load * i; k < std::min<unsigned>(num_to_load * (i + 1u), file_paths.size()); ++k) {
118  res.emplace_back(file_paths[k]);
119  }
120 #endif
121  return res;
122  }));
123  }
124 #endif
125 
126  std::vector<sheet_element> elements;
127  elements.reserve(file_paths.size());
128 
129  // Wait for results, then combine them with the master list.
130  for(auto& loader : loaders) {
131  auto res = loader.get();
132  std::move(res.begin(), res.end(), std::back_inserter(elements));
133  }
134 
135  // Sort the surfaces by area, largest last.
136  // TODO: should we use plain sort? Output sheet seems ever so slightly smaller when sort is not stable.
137  std::stable_sort(elements.begin(), elements.end(),
138  [](const auto& lhs, const auto& rhs) { return lhs.surf.area() < rhs.surf.area(); });
139 
140  const unsigned total_area = std::accumulate(elements.begin(), elements.end(), 0,
141  [](const int val, const auto& s) { return val + s.surf.area(); });
142 
143  const unsigned side_length = static_cast<unsigned>(std::sqrt(total_area) * 1.3);
144 
145  unsigned current_row_max_height = 0;
146  unsigned total_height = 0;
147 
148  point origin{0, 0};
149 
150  //
151  // Calculate the destination rects for the images. This uses the Shelf Next Fit algorithm.
152  // Our method forgoes the orientation consideration and works top-down instead of bottom-up.
153  //
154  for(auto& s : elements) {
155  current_row_max_height = std::max<unsigned>(current_row_max_height, s.src.h);
156 
157  // If we can't fit this element without getting cut off, move to the next line.
158  if(static_cast<unsigned>(origin.x + s.src.w) > side_length) {
159  // Reset the origin.
160  origin.x = 0;
161  origin.y += current_row_max_height;
162 
163  // Save this row's max height.
164  total_height += current_row_max_height;
165  current_row_max_height = 0;
166  }
167 
168  // Save this element's rect.
169  s.dst = { origin.x, origin.y, s.src.w, s.src.h };
170 
171  // Shift the rect origin for the next element.
172  origin.x += s.src.w;
173  }
174 
175  // If we never reached max width during rect placement, total_height will be empty.
176  // In that case, fall back to the row's max height.
177  const unsigned res_w = side_length;
178  const unsigned res_h = total_height > 0 ? std::min<unsigned>(side_length, total_height) : current_row_max_height;
179 
180  // Check that we won't exceed max texture size and that neither dimension is 0. TODO: handle?
181  assert(res_w > 0 && res_w <= 8192 && res_h > 0 && res_h <= 8192);
182 
183  surface res(res_w, res_h);
184  assert(res && "Spritesheet surface is null!");
185 
186  // Final record of each image's location on the composed sheet.
187  auto out = filesystem::ostream_file("./_sheet.cfg");
188  config_writer mapping_data{*out, compression::format::gzip};
189 
190  // Assemble everything
191  for(auto& s : elements) {
192  sdl_blit(s.surf, &s.src, res, &s.dst);
193  mapping_data.write_child("image", s.to_config());
194  }
195 
196  image::save_image(res, "./_sheet.png");
197 }
198 
199 void handle_dir_contents(const fs::path& path)
200 {
201  std::vector<fs::path> files_found;
202  for(const auto& entry : fs::directory_iterator{path}) {
203  if(entry.is_directory()) {
204  handle_dir_contents(entry);
205  } else if(entry.is_regular_file()) {
206  // TODO: should we have a better is-image check, and should we include jpgs?
207  // Right now all our sprites are pngs.
208  if(auto path = entry.path(); path.extension() == ".png" && path.stem() != "_sheet") {
209  files_found.push_back(std::move(path));
210  }
211  }
212  }
213 
214  if(!files_found.empty()) {
215  try {
216  // Allows relative paths to resolve correctly. This needs to be set *after* recursive
217  // directory handling or else the path will be wrong when returning to the parent.
218  fs::current_path(path);
219  } catch(const fs::filesystem_error&) {
220  return;
221  }
222 
223  build_sheet_from_images(files_found);
224  }
225 }
226 
227 } // end anon namespace
228 
229 void build_spritesheet_from(const std::string& entry_point)
230 {
231 #ifdef DEBUG_SPRITESHEET_OUTPUT
232  const std::size_t start = SDL_GetTicks();
233 #endif
234 
235  if(auto path = filesystem::get_binary_file_location("images", entry_point)) {
236  try {
237  handle_dir_contents(*path);
238  } catch(const fs::filesystem_error& e) {
239  PLAIN_LOG << "Filesystem Error generating spritesheet: " << e.what();
240  }
241  } else {
242  PLAIN_LOG << "Cannot find entry point to build spritesheet: " << entry_point;
243  }
244 
245 #ifdef DEBUG_SPRITESHEET_OUTPUT
246  PLAIN_LOG << "Spritesheet generation of '" << entry_point << "' took: " << (SDL_GetTicks() - start) << "ms\n";
247 #endif
248 }
249 
250 } // 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:159
std::ostringstream wrapper.
Definition: formatter.hpp:40
Definitions for the interface to Wesnoth Markup Language (WML).
Declarations for File-IO.
std::size_t i
Definition: function.cpp:965
Standard logging facilities (interface).
#define PLAIN_LOG
Definition: log.hpp:299
EXIT_STATUS start(bool clear_id, const std::string &filename, bool take_screenshot, const std::string &screenshot_filename)
Main interface for launching the editor from the title screen.
rwops_ptr make_read_RWops(const std::string &path)
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, bool create_directory)
std::string path
Definition: filesystem.cpp:90
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:884
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:47
mock_party p
static map_location::DIRECTION s
rect get_non_transparent_portion(const surface &nsurf)
Definition: utils.cpp:1517
void sdl_blit(const surface &src, const SDL_Rect *src_rect, surface &dst, SDL_Rect *dst_rect)
Definition: utils.hpp:42
#define e