Porytiles
Loading...
Searching...
No Matches
anim_key_frame_mangler.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstdint>
6#include <map>
7#include <optional>
8#include <set>
9#include <string>
10#include <vector>
11
21
22namespace {
23
24using namespace porytiles;
25
26// Pixel priority order for mangling (least visually impactful first)
27// Corners: (0,0), (0,7), (7,0), (7,7) -> indices 0, 7, 56, 63
28// Top edge: 1-6
29// Left edge: 8, 16, 24, 32, 40, 48
30// Right edge: 15, 23, 31, 39, 47, 55
31// Bottom edge: 57-62
32// Interior: remaining pixels
33constexpr std::array<std::size_t, tile::size_pix> pixel_priority_order = {
34 // Corners first (least visible)
35 0,
36 7,
37 56,
38 63,
39 // Top edge
40 1,
41 2,
42 3,
43 4,
44 5,
45 6,
46 // Left edge
47 8,
48 16,
49 24,
50 32,
51 40,
52 48,
53 // Right edge
54 15,
55 23,
56 31,
57 39,
58 47,
59 55,
60 // Bottom edge
61 57,
62 58,
63 59,
64 60,
65 61,
66 62,
67 // Interior (most visible, last resort)
68 9,
69 10,
70 11,
71 12,
72 13,
73 14,
74 17,
75 18,
76 19,
77 20,
78 21,
79 22,
80 25,
81 26,
82 27,
83 28,
84 29,
85 30,
86 33,
87 34,
88 35,
89 36,
90 37,
91 38,
92 41,
93 42,
94 43,
95 44,
96 45,
97 46,
98 49,
99 50,
100 51,
101 52,
102 53,
103 54};
104
110int color_distance_squared(const Rgba32 &a, const Rgba32 &b)
111{
112 const int dr = static_cast<int>(a.red()) - static_cast<int>(b.red());
113 const int dg = static_cast<int>(a.green()) - static_cast<int>(b.green());
114 const int db = static_cast<int>(a.blue()) - static_cast<int>(b.blue());
115 return dr * dr + dg * dg + db * db;
116}
117
131[[nodiscard]] std::vector<std::size_t> find_alternative_colors_sorted(
132 std::size_t current_color_index,
134 const Rgba32 &extrinsic_transparency)
135{
136 struct ColorCandidate {
137 std::size_t index;
138 int distance;
139 };
140
141 // A color index of 0 resolves to the extrinsic transparency color, not to the color stored in palette slot 0
142 const Rgba32 current_color = (current_color_index == 0) ? extrinsic_transparency : palette.at(current_color_index);
143 std::vector<ColorCandidate> candidates;
144
145 // Search ALL palette colors (1-15), not just those in the tile
146 for (std::size_t candidate_index = 1; candidate_index < palette::max_size; ++candidate_index) {
147 if (candidate_index == current_color_index) {
148 continue;
149 }
150
151 const Rgba32 candidate_color = palette.at(candidate_index);
152 if (candidate_color.is_transparent(extrinsic_transparency)) {
153 continue;
154 }
155
156 const int distance = color_distance_squared(current_color, candidate_color);
157 if (distance == 0) {
158 continue;
159 }
160
161 candidates.push_back({candidate_index, distance});
162 }
163
164 std::ranges::sort(candidates, [](const ColorCandidate &a, const ColorCandidate &b) {
165 if (a.distance != b.distance) {
166 return a.distance < b.distance;
167 }
168 return a.index < b.index;
169 });
170
171 std::vector<std::size_t> result;
172 result.reserve(candidates.size());
173 for (const auto &c : candidates) {
174 result.push_back(c.index);
175 }
176 return result;
177}
178
180[[nodiscard]] IndexPixel make_mangled_pixel(std::size_t original_palette_index, std::size_t alt_color)
181{
182 return IndexPixel{(original_palette_index << 4) | alt_color};
183}
184
201std::optional<std::pair<PixelTile<IndexPixel>, TileMangleRecord>> try_mangle_tile(
202 const PixelTile<IndexPixel> &tile,
203 std::size_t tile_index,
205 const Rgba32 &extrinsic_transparency,
206 const std::set<PixelTile<Rgba32>> &existing_canonical_rgba_tiles,
207 const std::set<PixelTile<Rgba32>> &batch_canonical_rgba_tiles)
208{
209 const auto candidate_is_unique = [&](const PixelTile<IndexPixel> &candidate_tile) {
210 const PixelTile<Rgba32> candidate_base =
211 canonical_color_tile_from_index_tile(candidate_tile, palette, extrinsic_transparency);
212 return !existing_canonical_rgba_tiles.contains(candidate_base) &&
213 !batch_canonical_rgba_tiles.contains(candidate_base);
214 };
215
216 // Phase 1: single-pixel swaps (preferred, minimal visual impact)
217 for (std::size_t pixel_index : pixel_priority_order) {
218 const IndexPixel original_pixel = tile.at(pixel_index);
219
220 const std::vector<std::size_t> alternatives =
221 find_alternative_colors_sorted(original_pixel.color_index(), palette, extrinsic_transparency);
222
223 for (const std::size_t alt_color : alternatives) {
224 const IndexPixel mangled_pixel = make_mangled_pixel(original_pixel.palette_index(), alt_color);
225
226 PixelTile<IndexPixel> candidate_tile = tile;
227 candidate_tile.set(pixel_index, mangled_pixel);
228
229 if (candidate_is_unique(candidate_tile)) {
230 TileMangleRecord record{
231 .tile_index = tile_index,
232 .pixel_changes = {PixelMangleChange{
233 .pixel_index = pixel_index, .original_pixel = original_pixel, .mangled_pixel = mangled_pixel}}};
234 return std::make_pair(candidate_tile, record);
235 }
236 }
237 }
238
239 // Phase 2: two-pixel swaps (fallback for heavily saturated canonical tile sets).
240 // Loop ordering: position pair (p1, p2) outermost, then color alternatives innermost. This ensures we prefer the
241 // least visible pixel positions before trying more visible ones, consistent with the Phase 1 priority ordering.
242 for (std::size_t p1_idx = 0; p1_idx < pixel_priority_order.size(); ++p1_idx) {
243 const std::size_t p1 = pixel_priority_order[p1_idx];
244 const IndexPixel p1_original = tile.at(p1);
245
246 const std::vector<std::size_t> p1_alternatives =
247 find_alternative_colors_sorted(p1_original.color_index(), palette, extrinsic_transparency);
248
249 for (std::size_t p2_idx = p1_idx + 1; p2_idx < pixel_priority_order.size(); ++p2_idx) {
250 const std::size_t p2 = pixel_priority_order[p2_idx];
251 const IndexPixel p2_original = tile.at(p2);
252
253 const std::vector<std::size_t> p2_alternatives =
254 find_alternative_colors_sorted(p2_original.color_index(), palette, extrinsic_transparency);
255
256 for (const std::size_t p1_alt : p1_alternatives) {
257 const IndexPixel p1_mangled = make_mangled_pixel(p1_original.palette_index(), p1_alt);
258
259 for (const std::size_t p2_alt : p2_alternatives) {
260 const IndexPixel p2_mangled = make_mangled_pixel(p2_original.palette_index(), p2_alt);
261
262 PixelTile<IndexPixel> candidate_tile = tile;
263 candidate_tile.set(p1, p1_mangled);
264 candidate_tile.set(p2, p2_mangled);
265
266 if (candidate_is_unique(candidate_tile)) {
267 TileMangleRecord record{
268 .tile_index = tile_index,
269 .pixel_changes = {
271 .pixel_index = p1, .original_pixel = p1_original, .mangled_pixel = p1_mangled},
273 .pixel_index = p2, .original_pixel = p2_original, .mangled_pixel = p2_mangled}}};
274 return std::make_pair(candidate_tile, record);
275 }
276 }
277 }
278 }
279 }
280
281 // No valid mangle found
282 return std::nullopt;
283}
284
285} // namespace
286
287namespace porytiles {
288
290 gsl::not_null<const UserDiagnostics *> diag, gsl::not_null<const TilePrinter *> tile_printer)
291 : diag_{diag}, tile_printer_{tile_printer}
292{
293}
294
296 const std::string &anim_name,
297 std::vector<PixelTile<IndexPixel>> tiles,
298 const std::vector<const Palette<Rgba32, palette::max_size> *> &palettes,
299 const Rgba32 &extrinsic_transparency,
300 const std::vector<const std::set<PixelTile<Rgba32>> *> &existing_canonical_rgba_tiles) const
301{
302 if (palettes.size() != tiles.size()) {
303 panic("palettes size " + std::to_string(palettes.size()) + " != tiles size " + std::to_string(tiles.size()));
304 }
305 if (existing_canonical_rgba_tiles.size() != tiles.size()) {
306 panic(
307 "existing_canonical_rgba_tiles size " + std::to_string(existing_canonical_rgba_tiles.size()) +
308 " != tiles size " + std::to_string(tiles.size()));
309 }
310
311 MangleResult result;
312 result.tiles = std::move(tiles);
313
314 // Canonical resolved RGBA forms of the tiles processed so far in this batch. Together with the caller-provided
315 // per-tile existing sets, this is the universe each tile must be unique against. Working on resolved colors means
316 // tiles that reference different palette slots holding the same color are still treated as duplicates.
317 std::set<PixelTile<Rgba32>> batch_canonical_rgba_tiles;
318
319 // Map to track which canonical resolved tiles we've seen at which indices (for duplicate detection)
320 std::map<PixelTile<Rgba32>, std::size_t> canonical_first_occurrence;
321
322 // Each tile index is visited exactly once. A tile is mangled at most once, producing at most one TileMangleRecord.
323 // This guarantees that mangle_records contains non-overlapping entries (no two records share the same tile_index),
324 // so they can be applied independently in any order.
325 for (std::size_t i = 0; i < result.tiles.size(); ++i) {
326 PixelTile<IndexPixel> &current_tile = result.tiles[i];
327
328 // Canonical resolved form of the current tile for duplicate checking
329 const PixelTile<Rgba32> current_base =
330 canonical_color_tile_from_index_tile(current_tile, *palettes[i], extrinsic_transparency);
331
332 // Check if this tile is a duplicate (either of the existing tiles or a previous tile in this batch)
333 const bool is_duplicate_of_previous = canonical_first_occurrence.contains(current_base);
334 const bool is_duplicate_of_existing =
335 existing_canonical_rgba_tiles[i]->contains(current_base) && !is_duplicate_of_previous;
336
337 if (is_duplicate_of_previous || is_duplicate_of_existing) {
338 // Need to mangle this tile
339 const PixelTile<IndexPixel> original_tile = current_tile;
340 const std::optional<std::pair<PixelTile<IndexPixel>, TileMangleRecord>> mangle_result = try_mangle_tile(
341 current_tile,
342 i,
343 *palettes[i],
344 extrinsic_transparency,
345 *existing_canonical_rgba_tiles[i],
346 batch_canonical_rgba_tiles);
347
348 if (!mangle_result.has_value()) {
349 // Could not mangle the tile - this is an error
350 std::vector<std::string> err_msg;
351 err_msg.push_back(diag_->formatter().format(
352 "Failed to mangle duplicate key frame tile {} in animation '{}'.",
353 FormatParam{i, Style::bold},
354 FormatParam{anim_name, Style::bold}));
355 err_msg.emplace_back();
356 err_msg.emplace_back("The tile could not be modified to be unique. This may occur if:");
357 err_msg.emplace_back(" - All possible pixel swaps still produce duplicate tiles");
358 err_msg.emplace_back(" - The palette does not contain enough distinct opaque colors");
359 return FormattableError{err_msg};
360 }
361
362 // Apply the mangle
363 current_tile = mangle_result->first;
364 result.mangle_records.insert(mangle_result->second);
365
366 // Emit a remark about the mangle
367 std::vector<std::string> remark_lines;
368 for (const auto &change : mangle_result->second.pixel_changes) {
369 const auto [pixel_row, pixel_col] = tile::index_to_row_col(change.pixel_index);
370 remark_lines.push_back(diag_->formatter().format(
371 "Mangled tile {} in animation '{}': pixel ({},{}) changed from index {} to {}.",
372 FormatParam{i, Style::bold},
373 FormatParam{anim_name, Style::bold},
374 FormatParam{pixel_row},
375 FormatParam{pixel_col},
376 FormatParam{change.original_pixel.index()},
377 FormatParam{change.mangled_pixel.index()}));
378 }
379
380 const PixelTile<Rgba32> original_rgba =
381 color_tile_from_index_tile(original_tile, *palettes[i], extrinsic_transparency);
382 const PixelTile<Rgba32> mangled_rgba =
383 color_tile_from_index_tile(current_tile, *palettes[i], extrinsic_transparency);
384
385 remark_lines.emplace_back("");
386 remark_lines.emplace_back("Original tile:");
387 remark_lines.append_range(tile_printer_->print_tile(original_rgba, extrinsic_transparency));
388
389 remark_lines.emplace_back("Mangled tile:");
390 remark_lines.append_range(tile_printer_->print_tile(mangled_rgba, extrinsic_transparency));
391
392 diag_->remark("anim-key-frame-mangle", remark_lines);
393 }
394
395 // Re-canonicalize after potential mangle and add to the trackers
396 const PixelTile<Rgba32> final_base =
397 canonical_color_tile_from_index_tile(current_tile, *palettes[i], extrinsic_transparency);
398 canonical_first_occurrence.emplace(final_base, i);
399 batch_canonical_rgba_tiles.insert(final_base);
400 }
401
402 return result;
403}
404
405} // namespace porytiles
AnimKeyFrameMangler(gsl::not_null< const UserDiagnostics * > diag, gsl::not_null< const TilePrinter * > tile_printer)
ChainableResult< MangleResult > mangle_duplicates(const std::string &anim_name, std::vector< PixelTile< IndexPixel > > tiles, const std::vector< const Palette< Rgba32, palette::max_size > * > &palettes, const Rgba32 &extrinsic_transparency, const std::vector< const std::set< PixelTile< Rgba32 > > * > &existing_canonical_rgba_tiles) const
Mangles duplicate tiles to make them unique.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
A text parameter with associated styling for formatted output.
General-purpose error implementation with formatted message support.
Definition error.hpp:57
Represents an indexed color pixel.
std::size_t palette_index() const
Returns the palette index (upper 4 bits).
std::size_t color_index() const
Returns the color index within a palette (lower 4 bits).
A generic palette container for colors that support transparency checking.
Definition palette.hpp:45
ColorType at(std::size_t index) const
Gets the color at a specific index.
Definition palette.hpp:246
An 8x8 tile backed by literal-array-based per-pixel storage of an arbitrary pixel type.
PixelType at(std::size_t i) const
void set(std::size_t i, const PixelType &p)
Represents a 32-bit RGBA color.
Definition rgba32.hpp:21
std::uint8_t red() const
Definition rgba32.hpp:73
bool is_transparent(const Rgba32 &extrinsic) const
Checks if this color should be treated as transparent.
Definition rgba32.cpp:19
std::uint8_t blue() const
Definition rgba32.hpp:83
std::uint8_t green() const
Definition rgba32.hpp:78
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > &params) const
Formats a string with styled parameters using fmtlib syntax.
virtual std::vector< std::string > print_tile(const PixelTile< Rgba32 > &tile, const Rgba32 &extrinsic_transparency) const =0
virtual void remark(const std::string &tag, const std::vector< std::string > &lines) const =0
Display a tagged remark message.
const TextFormatter & formatter() const
constexpr std::size_t max_size
Definition palette.hpp:19
constexpr std::pair< std::size_t, std::size_t > index_to_row_col(std::size_t index)
Converts a linear index to row and column coordinates.
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
PixelTile< ColorType > canonical_color_tile_from_index_tile(const PixelTile< IndexPixel > &index_tile, const Palette< ColorType, N > &palette, const ColorType &extrinsic)
Converts a PixelTile<IndexPixel> to its canonical color form using a palette (extrinsic transparency)...
PixelTile< ColorType > color_tile_from_index_tile(const PixelTile< IndexPixel > &index_tile, const Palette< ColorType, N > &palette)
Converts a PixelTile<IndexPixel> to a PixelTile<ColorType> using a palette (intrinsic transparency).
Result of the mangling operation.
std::set< TileMangleRecord > mangle_records
Record of all modifications made (ordered by tile_index)
std::vector< PixelTile< IndexPixel > > tiles
The tiles after mangling (unique)
A single pixel modification within a tile.
std::size_t pixel_index
Which pixel in the tile (0-63, linear index)
Record of all pixel modifications made to a single tile during mangling.
std::size_t tile_index
Which tile in the key frame (0-based index, unique across records)