The Battle for Wesnoth  1.19.5+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 #include "utils/optimer.hpp"
28 
29 #include <SDL2/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>
42 namespace fs = boost::filesystem;
43 #else
44 #include <filesystem>
45 namespace fs = std::filesystem;
46 #endif
47 
48 namespace image
49 {
50 namespace
51 {
52 /** Intermediate helper struct to manage surfaces while the sheet is being assembled. */
53 struct sheet_element
54 {
55  explicit sheet_element(const fs::path& p)
56  : surf(IMG_Load_RW(filesystem::make_read_RWops(p.string()).release(), 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. */
93 constexpr double max_items_per_loader = 8.0;
94 
95 void 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 
200 void 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 
230 void 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:172
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:1028
Standard logging facilities (interface).
#define PLAIN_LOG
Definition: log.hpp:299
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
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 &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