-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSplitter.cpp
285 lines (247 loc) · 12.8 KB
/
Splitter.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
#include <vector>
#include <syncstream>
#include <omp.h>
#include <functional>
#include "Splitter.h"
#include "util/SimpleTimer.h"
#include "logging/LoggerTags.hpp"
namespace logger = LoggerTags;
void Splitter::work(std::vector <SplitterOpts> &jobs) {
SpriteSplittingStatus jobStats;
int jobCounter = 0;
for (auto& job : jobs) {
// NOTE: to really push the concurrency, any job could be its own process.
// It looks like this won't be necessary due to performance at this time.
// Scatter the relevant options to Splitter and SpriteSheetIO
ground_matcher = job.groundFilePattern.get();
ssio.setIOOptions(job);
// If the IO cannot work with this (most likely the file paths were bad), skip the job.
if (! ssio.validOptions()) {
std::cout << logger::error << "This job has invalid IO settings\n";
std::cout << logger::error << "This job will be skipped.\n";
continue;
}
std::queue<std::string> pngQueue;
ssio.fillPNGQueue(pngQueue);
if (pngQueue.empty()) {
std::cout << logger::error << "Zero '.png' files were found in input path:";
std::cout << "\n\t\t" << job.inDirectory << "\n";
std::cout << logger::error << "This job will be skipped.\n";
continue;
}
if (job.isPNGInDirectory) {
std::cout << logger::info << "Begin working on file \"" << job.inDirectory << "\"with " << job;
std::string& onlyFile = pngQueue.front();
split(onlyFile, jobStats, std::cout);
pngQueue.pop();
} else {
std::cout << logger::info << "Begin working on folder \"" << job.inDirectory << "\" with " << job;
workFolder(job.workAmount, pngQueue, jobStats);
}
std::cout << logger::info << "DONE with job " << ++jobCounter << " out of " << jobs.size() << "\n";
// assert PNG Queue is empty.
if (! pngQueue.empty()) {
throw std::logic_error("End of job reached but PNG queue not empty.");
}
}
std::cout << logger::info << "COMPLETED all pending jobs. " << jobStats;
}
/**
* Split all PNGs of a folder by following the string filepaths in the pngs queue.
*
* This is done by assigning one thread per file for adequate performance
*
* @param workCap the maximum amount of files to process before stopping
* @param pngs the queue of FilePaths to SpriteSheets
* @param jobStats stat tracking object
*/
void Splitter::workFolder(int workCap, std::queue<std::string> &pngs, SpriteSplittingStatus &jobStats) {
const int work = std::min(workCap, static_cast<int>(pngs.size()));
SimpleTimer folder("Splitting this folder");
#pragma omp parallel for schedule(dynamic) shared(work, pngs, std::cout, jobStats, logger::info) default(none)
for (int tid = 0; tid < work; ++tid) {
if (tid == 0) std::cout << logger::info << " Begin working on a folder using " << omp_get_num_threads() << " threads\n";
std::string file;
#pragma omp critical(queueAccess)
{
file = std::move(pngs.front());
pngs.pop();
}
// for printing without data races. Downside, only prints when the object is destroyed (end of loop iteration).
std::osyncstream synced_out(std::cout);
SpriteSplittingStatus individualJobStats;
split(file, individualJobStats, synced_out);
#pragma omp critical(updateStats)
{
jobStats += individualJobStats;
}
}
}
/**
* Load a SpriteSheet from the given fileDirectory, split the data in single sprites with the correct name, then save.
*
* Automatically detects the SpriteSheet type (if any).
* Does not split fully invisible (alpha 0 on every pixel) objects or chars. Chars with only some invisible frames are OK.
*
* Automatically determines the name of a folder based on the SpriteSheet name.
*
* @param fileDirectory A path to a .png SpriteSheet file.
* @param outStream stream for printing characters. Normally std::cout, but could be std::osyncstream from threading.
* @param jobStats struct for counting stats of splitting.
*/
void Splitter::split(const std::string &fileDirectory, SpriteSplittingStatus &jobStats, std::basic_ostream<char> &outStream) {
const std::string& fileName = fs::path(fileDirectory).filename().string();
SimpleTimer timer {std::string("Splitting ") + fileName, outStream};
std::vector<unsigned char> img;
SpriteSheetPNGData pngData;
outStream << logger::threaded_info << "Loading " << fileDirectory << "\n";
SpriteSheetIO::loadPNG(fileDirectory, img, pngData);
if (pngData.error) {
outStream << logger::threaded_error << "LodePNG decode error: " << pngData.error << ". (Most likely a corrupt png)\n"; // if it's an incorrect path at this point then that is a bug!
jobStats.n_load_error += 1;
return;
}
SpriteSheetType type;
if (validSpriteSheet(pngData.width, pngData.height, OBJ_SHEET_ROW)) {
// ground and object sheets are indistinguishable from dimensions alone.
// One must be assumed, and the other has to be deduced by some rules. e.g. configured pattern matching.
bool isGround = std::regex_search(fileName, ground_matcher);
type = isGround ? SpriteSheetType::GROUND : SpriteSheetType::OBJECT;
} else if (validSpriteSheet(pngData.width, pngData.height, CHAR_SHEET_ROW)) {
type = SpriteSheetType::CHARACTER;
} else {
outStream << logger::threaded_error << "An image of size " << pngData.width << ", " << pngData.height << " is not a valid SpriteSheet.\n";
jobStats.n_load_error += 1;
return;
}
outStream << logger::threaded_info << "Processing file as " << type << " sheet\n";
unsigned int spriteSize; // size of a sprite (8, 16, 32..)
unsigned int spriteCount; // amount of unsigned char* to expect back from splitting.
// pointer representing the specific function to call splitting on. Strategy pattern go brr.
// This is to prevent allocating spriteData in each switch branch, by doing it after because it always has the same size.
// But also preventing the need for calling a generic entry point which then _again_ has a switch on SpriteSheetType.
std::function<void(SpriteSplittingData&)> splitFunction;
switch(type) {
case SpriteSheetType::OBJECT:
case SpriteSheetType::GROUND:
// these two are exactly the same in splitting, only the way the split is saved is different.
// Namely, ground gets an apron of alpha around the split data.
// This insertion is part of the saving routine, handled by SpriteSheetIO.
spriteSize = pngData.width / OBJ_SHEET_ROW;
spriteCount = (img.size() / 4) / (spriteSize * spriteSize);
// assign the correct function
splitFunction = Splitter::splitObjectSheet;
break;
case SpriteSheetType::CHARACTER:
spriteSize = pngData.width / CHAR_SHEET_ROW;
// correct for column 3 being empty, and 5+6 being joined (see splitCharSheet function comment)
spriteCount = ((img.size() / 4) / (spriteSize * spriteSize));
spriteCount = (spriteCount / CHAR_SHEET_ROW) * (CHAR_SHEET_ROW - 2);
// assign the correct function
splitFunction = Splitter::splitCharSheet;
break;
default: // did you add a new type to the enum?
std::stringstream ss; // easiest way to stringify SpriteSheetType. We're crashing anwyway, performance loss is whatever.
ss << "unknown SpriteSheetType: " << type << "\n";
throw std::logic_error(ss.str());
}
// rows per sprite * amount of sprites that fit on the sheet
auto spriteData = new unsigned char* [spriteSize * spriteCount];
// bundle all these parameters into one struct
SpriteSplittingData splitData(img.data(), spriteData, spriteSize, spriteCount, type, pngData.lodeState, fileDirectory, jobStats);
// split the sprites
splitFunction(splitData);
// and save them
ssio.saveSplits(splitData, outStream);
outStream << logger::threaded_info << "Finished splitting SpriteSheet.\n";
delete[] spriteData;
}
/**
* Given a pointer to a SpriteSheets raw pixel data, and the amount of object sprites there are in it,
* fills a collection of byte pointers such that every [spriteSize] pointers forms a singe sprite.
* Each individual pointer is a row of sprite data.
*
* Assumes that the collection has enough space allocated to do this (spriteSize * spriteCount).
*
* @param ssd Struct containing all necessary data. See SpriteSplittingData.h.\n
* In particular, these members are used:\n
* ssd.spriteSheet, ssd.spriteSize, ssd.SpriteCount, ssd.splitSprites
*/
void Splitter::splitObjectSheet(SpriteSplittingData& ssd) {
unsigned char* imgData = ssd.spriteSheet;
const unsigned int spriteSize = ssd.spriteSize;
const unsigned int spriteCount = ssd.spriteCount;
unsigned char** out = ssd.splitSprites;
// constant * SZ rows, 4 uchar per pixel.
unsigned int sheetPixelWidth = spriteSize * OBJ_SHEET_ROW * 4;
#pragma omp simd collapse(2)
for (int i = 0; i < spriteCount; ++i) {
for (int j = 0; j < spriteSize; ++j) {
// (linear index) = (sprite row offset) + (sprite column offset) + (pixel row offset)
out[i * spriteSize + j] = imgData + (i / OBJ_SHEET_ROW) * spriteSize * sheetPixelWidth + (i % OBJ_SHEET_ROW) * spriteSize * 4 + j * sheetPixelWidth;
}
}
}
/**
* Given a pointer to a SpriteSheets raw pixel data, and the amount of character sprites there are in it,
* fills a collection of byte pointers such that every [spriteSize] pointers forms a singe sprite.
* Each individual pointer is a row of sprite data.
*
* Assumes that the collection has enough space allocated to do this (spriteSize * spriteCount).
*
* @param ssd Struct containing all necessary data. See SpriteSplittingData.h.\n
* In particular, these members are used:\n
* ssd.spriteSheet, ssd.spriteSize, ssd.SpriteCount, ssd.splitSprites
*/
void Splitter::splitCharSheet(SpriteSplittingData& ssd) {
/* Char sheets are special. They consist of seven columns, where each row belongs to a single char.
* Column 0 is their idle frame.
* Column 1 and 2 are their walking frames.
* Column 3 is always empty. (Might've been intended for a fancy walk frame. Never happened.)
* Column 4 and (5+6) are attack frames.
* Column 5 and 6 are joined as one sprite for i.e. an extended arm holding a sword.
* Meaning all sprites here are square except for the 5th, it is a size x 2size rectangle!
*/
unsigned char* imgData = ssd.spriteSheet;
const unsigned int spriteSize = ssd.spriteSize;
const unsigned int spriteCount = ssd.spriteCount;
unsigned char** out = ssd.splitSprites;
unsigned int sheetPixelWidth = spriteSize * CHAR_SHEET_ROW * 4;
const int CHARS_PER_ROW = CHAR_SHEET_ROW - 2;
auto columnOffset = [](int i)->bool { return i % CHARS_PER_ROW >= 3; };
#pragma omp simd collapse(2)
for (int i = 0; i < spriteCount; ++i) {
for (int j = 0; j < spriteSize; ++j) {
// linear index = (sprite row offset) + (sprite col offset + (skips column 3)) + (pixel row offset)
out[i * spriteSize + j] = imgData + (i / CHARS_PER_ROW) * spriteSize * sheetPixelWidth + ((i % CHARS_PER_ROW) + columnOffset(i)) * spriteSize * 4 + j * sheetPixelWidth;
}
}
}
/**
* tests if the given image dimensions are that of a correctly formed SpriteSheetData.
* A SpriteSheetData has equally sized columns (objects vs chars), where each column is at least 8px wide, and the column width is a power of 2.
* Furthermore, the height of the SpriteSheetData are divisible by the sprite size.
*
* @param width width of the png
* @param height height of the png
* @param columnCount amount of columns in the png
* @return whether or not it is a valid SpriteSheetData.
*/ // static
bool Splitter::validSpriteSheet(unsigned int width, unsigned int height, unsigned int columnCount) {
// columns are equally sized?
float columnSize = static_cast<float>(width) / static_cast<float>(columnCount);
if (columnSize != static_cast<float>(static_cast<int>(columnSize))) {
return false;
}
// columns are a power of 2 and at least 8?
unsigned int spriteSize = width / columnCount;
if (spriteSize < 8 || 0 != (spriteSize & (spriteSize - 1))) {
return false;
}
// height correct in terms of SpriteSheet rows?
float rowSize = static_cast<float>(height) / static_cast<float>(spriteSize);
if (rowSize != static_cast<float>(static_cast<int>(rowSize))) {
return false;
}
return true;
}