diff --git a/contrib/bash_compl/_rgbgfx.bash b/contrib/bash_compl/_rgbgfx.bash index eb7bff44..9d49dd7e 100755 --- a/contrib/bash_compl/_rgbgfx.bash +++ b/contrib/bash_compl/_rgbgfx.bash @@ -19,6 +19,7 @@ _rgbgfx_completions() { [Z]="columns:normal" [a]="attr-map:glob-*.attrmap" [A]="auto-attr-map:normal" + [B]="background-color:unk" [b]="base-tiles:unk" [c]="colors:unk" [d]="depth:unk" diff --git a/contrib/zsh_compl/_rgbgfx b/contrib/zsh_compl/_rgbgfx index 49d9596a..91f474c6 100644 --- a/contrib/zsh_compl/_rgbgfx +++ b/contrib/zsh_compl/_rgbgfx @@ -28,6 +28,7 @@ local args=( '(-Z --columns)'{-Z,--columns}'[Read the image in column-major order]' '(-a --attr-map -A --auto-attr-map)'{-a,--attr-map}'+[Generate a map of tile attributes (mirroring)]:attrmap file:_files' + '(-B --background-color)'{-B,--background-color}'+[Ignore tiles containing only specified color]:color:' '(-b --base-tiles)'{-b,--base-tiles}'+[Base tile IDs for tile map output]:base tile IDs:' '(-c --colors)'{-c,--colors}'+[Specify color palettes]:palette spec:' '(-d --depth)'{-d,--depth}'+[Set bit depth]:bit depth:_depths' diff --git a/include/gfx/main.hpp b/include/gfx/main.hpp index 28435b49..f951cca6 100644 --- a/include/gfx/main.hpp +++ b/include/gfx/main.hpp @@ -10,6 +10,8 @@ #include #include +#include "helpers.hpp" + #include "gfx/rgba.hpp" struct Options { @@ -21,6 +23,7 @@ struct Options { uint8_t verbosity = 0; // -v std::string attrmap{}; // -a, -A + std::optional bgColor{}; // -B std::array baseTileIDs{0, 0}; // -b enum { NO_SPEC, @@ -116,4 +119,27 @@ static constexpr auto flipTable = ([]() constexpr { return table; })(); +// Parsing helpers. + +static constexpr uint8_t nibble(char c) { + if (c >= 'a') { + assume(c <= 'f'); + return c - 'a' + 10; + } else if (c >= 'A') { + assume(c <= 'F'); + return c - 'A' + 10; + } else { + assume(c >= '0' && c <= '9'); + return c - '0'; + } +} + +static constexpr uint8_t toHex(char c1, char c2) { + return nibble(c1) * 16 + nibble(c2); +} + +static constexpr uint8_t singleToHex(char c) { + return toHex(c, c); +} + #endif // RGBDS_GFX_MAIN_HPP diff --git a/man/rgbgfx.1 b/man/rgbgfx.1 index d9a83a45..369bd937 100644 --- a/man/rgbgfx.1 +++ b/man/rgbgfx.1 @@ -103,6 +103,19 @@ and has the same size. Same as .Fl a Ar base_path Ns .attrmap .Pq see Sx Automatic output paths . +.It Fl B Ar color , Fl \-background-color Ar color +Set a background color to be omitted from output. +Colors are accepted in +.Ql #rgb +or +.Ql #rrggbb +format, or as +.Ql transparent . +Input tiles which are entirely the specified background color are ignored and will not be output in tile data file. +The tilemap, atrribute map, or palette map files +.Em will +use placeholder values where background tiles were. +If a background color is specified, it cannot be used within tiles which are not ignored. .It Fl b Ar base_ids , Fl \-base-tiles Ar base_ids Set the base IDs for tile map output. .Ar base_ids @@ -126,7 +139,7 @@ begins with a hash character .Ql # , it is treated as an inline palette specification. It should contain a comma-separated list of hexadecimal colors, each beginning with a hash. -Colors are accepted either as +Colors are accepted in .Ql #rgb or .Ql #rrggbb diff --git a/src/gfx/main.cpp b/src/gfx/main.cpp index bd0bbc18..06a1187b 100644 --- a/src/gfx/main.cpp +++ b/src/gfx/main.cpp @@ -114,7 +114,7 @@ void Options::verbosePrint(uint8_t level, char const *fmt, ...) const { } // Short options -static char const *optstring = "-Aa:b:Cc:d:hi:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvXx:YZ"; +static char const *optstring = "-Aa:B:b:Cc:d:hi:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvXx:YZ"; // Equivalent long options // Please keep in the same order as short opts. @@ -126,6 +126,7 @@ static char const *optstring = "-Aa:b:Cc:d:hi:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvXx:YZ" static option const longopts[] = { {"auto-attr-map", no_argument, nullptr, 'A'}, {"attr-map", required_argument, nullptr, 'a'}, + {"background-color", required_argument, nullptr, 'B'}, {"base-tiles", required_argument, nullptr, 'b'}, {"color-curve", no_argument, nullptr, 'C'}, {"colors", required_argument, nullptr, 'c'}, @@ -353,6 +354,7 @@ static char *parseArgv(int argc, char *argv[]) { for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) { char *arg = musl_optarg; // Make a copy for scanning uint16_t number; + size_t size; switch (ch) { case 'A': localOptions.autoAttrmap = true; @@ -364,6 +366,43 @@ static char *parseArgv(int argc, char *argv[]) { } options.attrmap = musl_optarg; break; + case 'B': + if (strcasecmp(musl_optarg, "transparent") == 0) { + options.bgColor = Rgba(0x00, 0x00, 0x00, 0x00); + break; + } + if (musl_optarg[0] != '#') { + error("Background color specification must be `#rgb`, `#rrggbb`, or `transparent`"); + break; + } + size = strspn(&musl_optarg[1], "0123456789ABCDEFabcdef"); + switch (size) { + case 3: + options.bgColor = Rgba( + singleToHex(musl_optarg[1]), + singleToHex(musl_optarg[2]), + singleToHex(musl_optarg[3]), + 0xFF + ); + break; + case 6: + options.bgColor = Rgba( + toHex(musl_optarg[1], musl_optarg[2]), + toHex(musl_optarg[3], musl_optarg[4]), + toHex(musl_optarg[5], musl_optarg[6]), + 0xFF + ); + break; + default: + error("Unknown background color specification \"%s\"", musl_optarg); + } + if (musl_optarg[size + 1] != '\0') { + error( + "Unexpected text \"%s\" after background color specification", + &musl_optarg[size + 1] + ); + } + break; case 'b': number = parseNumber(arg, "Bank 0 base tile ID", 0); if (number >= 256) { diff --git a/src/gfx/pal_spec.cpp b/src/gfx/pal_spec.cpp index d87d672c..ac444f58 100644 --- a/src/gfx/pal_spec.cpp +++ b/src/gfx/pal_spec.cpp @@ -23,27 +23,6 @@ using namespace std::string_view_literals; -constexpr uint8_t nibble(char c) { - if (c >= 'a') { - assume(c <= 'f'); - return c - 'a' + 10; - } else if (c >= 'A') { - assume(c <= 'F'); - return c - 'A' + 10; - } else { - assume(c >= '0' && c <= '9'); - return c - '0'; - } -} - -constexpr uint8_t toHex(char c1, char c2) { - return nibble(c1) * 16 + nibble(c2); -} - -constexpr uint8_t singleToHex(char c) { - return toHex(c, c); -} - template // Should be std::string or std::string_view static void skipWhitespace(Str const &str, size_t &pos) { pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length()); diff --git a/src/gfx/process.cpp b/src/gfx/process.cpp index f27fb3e2..8506ab9f 100644 --- a/src/gfx/process.cpp +++ b/src/gfx/process.cpp @@ -25,6 +25,10 @@ #include "gfx/pal_sorting.hpp" #include "gfx/proto_palette.hpp" +static bool isBgColorTransparent() { + return options.bgColor.has_value() && options.bgColor->isTransparent(); +} + class ImagePalette { std::array, NB_COLOR_SLOTS> _colors; @@ -38,7 +42,7 @@ public: Rgba const *registerColor(Rgba const &rgba) { std::optional &slot = _colors[rgba.cgbColor()]; - if (rgba.cgbColor() == Rgba::transparent) { + if (rgba.cgbColor() == Rgba::transparent && !isBgColorTransparent()) { options.hasTransparentPixels = true; } @@ -519,10 +523,12 @@ struct AttrmapEntry { bool yFlip; bool xFlip; - static constexpr decltype(protoPaletteID) transparent = SIZE_MAX; + static constexpr size_t transparent = static_cast(-1); + static constexpr size_t background = static_cast(-2); + bool isBackgroundTile() const { return protoPaletteID == background; } size_t getPalID(DefaultInitVec const &mappings) const { - return protoPaletteID == transparent ? 0 : mappings[protoPaletteID]; + return mappings[isBackgroundTile() || protoPaletteID == transparent ? 0 : protoPaletteID]; } }; @@ -852,13 +858,16 @@ static void outputUnoptimizedTileData( remainingTiles -= options.trim; for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) { - // If the tile is fully transparent, default to palette 0 - Palette const &palette = palettes[attr.getPalID(mappings)]; - for (uint32_t y = 0; y < 8; ++y) { - uint16_t bitplanes = TileData::rowBitplanes(tile, palette, y); - output->sputc(bitplanes & 0xFF); - if (options.bitDepth == 2) { - output->sputc(bitplanes >> 8); + // Do not emit fully-background tiles. + if (!attr.isBackgroundTile()) { + // If the tile is fully transparent, this defaults to palette 0. + Palette const &palette = palettes[attr.getPalID(mappings)]; + for (uint32_t y = 0; y < 8; ++y) { + uint16_t bitplanes = TileData::rowBitplanes(tile, palette, y); + output->sputc(bitplanes & 0xFF); + if (options.bitDepth == 2) { + output->sputc(bitplanes >> 8); + } } } @@ -898,16 +907,21 @@ static void outputUnoptimizedMaps( } if (tilemapOutput.has_value()) { - (*tilemapOutput)->sputc(tileID + options.baseTileIDs[bank]); + (*tilemapOutput) + ->sputc((attr.isBackgroundTile() ? 0 : tileID) + options.baseTileIDs[bank]); } + uint8_t palID = attr.getPalID(mappings); if (attrmapOutput.has_value()) { - uint8_t palID = attr.getPalID(mappings) & 7; - (*attrmapOutput)->sputc(palID | bank << 3); // The other flags are all 0 + (*attrmapOutput)->sputc((palID & 7) | bank << 3); // The other flags are all 0 } if (palmapOutput.has_value()) { - (*palmapOutput)->sputc(attr.getPalID(mappings)); + (*palmapOutput)->sputc(palID); + } + + // Background tiles are skipped in the tile data, so they should be skipped in the maps too. + if (!attr.isBackgroundTile()) { + ++tileID; } - ++tileID; } } @@ -1000,22 +1014,30 @@ static UniqueTiles dedupTiles( bool inputWithoutOutput = !options.inputTileset.empty() && options.output.empty(); for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) { - auto [tileID, matchType] = tiles.addTile({tile, palettes[mappings[attr.protoPaletteID]]}); + if (attr.isBackgroundTile()) { + attr.xFlip = false; + attr.yFlip = false; + attr.bank = 0; + attr.tileID = 0; + } else { + auto [tileID, matchType] = + tiles.addTile({tile, palettes[mappings[attr.protoPaletteID]]}); - if (inputWithoutOutput && matchType == TileData::NOPE) { - error( - "Tile at (%" PRIu32 ", %" PRIu32 - ") is not within the input tileset, and `-o` was not given!", - tile.x, - tile.y - ); + if (inputWithoutOutput && matchType == TileData::NOPE) { + error( + "Tile at (%" PRIu32 ", %" PRIu32 + ") is not within the input tileset, and `-o` was not given!", + tile.x, + tile.y + ); + } + + attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP; + attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP; + attr.bank = tileID >= options.maxNbTiles[0]; + attr.tileID = (attr.bank ? tileID - options.maxNbTiles[0] : tileID) + + options.baseTileIDs[attr.bank]; } - - attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP; - attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP; - attr.bank = tileID >= options.maxNbTiles[0]; - attr.tileID = - (attr.bank ? tileID - options.maxNbTiles[0] : tileID) + options.baseTileIDs[attr.bank]; } // Copy elision should prevent the contained `unordered_set` from being re-constructed @@ -1139,7 +1161,8 @@ void process() { std::unordered_set tileColors; for (uint32_t y = 0; y < 8; ++y) { for (uint32_t x = 0; x < 8; ++x) { - if (Rgba color = tile.pixel(x, y); !color.isTransparent()) { + if (Rgba color = tile.pixel(x, y); + !color.isTransparent() || !options.hasTransparentPixels) { tileColors.insert(color.cgbColor()); } } @@ -1157,6 +1180,7 @@ void process() { if (tileColors.empty()) { // "Empty" proto-palettes screw with the packing process, so discard those + assume(!isBgColorTransparent()); attrs.protoPaletteID = AttrmapEntry::transparent; continue; } @@ -1166,6 +1190,21 @@ void process() { protoPalette.add(cgbColor); } + if (options.bgColor.has_value() + && std::find(RANGE(tileColors), options.bgColor->cgbColor()) != tileColors.end()) { + if (tileColors.size() == 1) { + // The tile contains just the background color, skip it. + attrs.protoPaletteID = AttrmapEntry::background; + continue; + } + fatal( + "Tile (%" PRIu32 ", %" PRIu32 ") contains the background color (#%08x)!", + tile.x, + tile.y, + options.bgColor->toCSS() + ); + } + // Insert the proto-palette, making sure to avoid overlaps for (size_t n = 0; n < protoPalettes.size(); ++n) { switch (protoPalette.compare(protoPalettes[n])) { @@ -1197,7 +1236,7 @@ void process() { } attrs.protoPaletteID = protoPalettes.size(); - if (protoPalettes.size() == AttrmapEntry::transparent) { // Check for overflow + if (protoPalettes.size() == AttrmapEntry::background) { // Check for overflow fatal( "Reached %zu proto-palettes... sorry, this image is too much for me to handle :(", AttrmapEntry::transparent diff --git a/test/gfx/bg_fuse.err b/test/gfx/bg_fuse.err new file mode 100644 index 00000000..ecbbd45c --- /dev/null +++ b/test/gfx/bg_fuse.err @@ -0,0 +1,3 @@ +warning: Fusing colors #a9b9c9ff and #aabbccff into Game Boy color $66f5 [first seen at x: 1, y: 1] +FATAL: Tile (0, 0) contains the background color (#aabbccff)! +Conversion aborted after 1 error diff --git a/test/gfx/bg_fuse.flags b/test/gfx/bg_fuse.flags new file mode 100644 index 00000000..aba6c76b --- /dev/null +++ b/test/gfx/bg_fuse.flags @@ -0,0 +1 @@ +-B #aBc diff --git a/test/gfx/bg_fuse.png b/test/gfx/bg_fuse.png new file mode 100644 index 00000000..2fee135f Binary files /dev/null and b/test/gfx/bg_fuse.png differ diff --git a/test/gfx/bg_in_tile.err b/test/gfx/bg_in_tile.err new file mode 100644 index 00000000..91fa5779 --- /dev/null +++ b/test/gfx/bg_in_tile.err @@ -0,0 +1,2 @@ +FATAL: Tile (16, 0) contains the background color (#ffd800ff)! +Conversion aborted after 1 error diff --git a/test/gfx/bg_in_tile.flags b/test/gfx/bg_in_tile.flags new file mode 100644 index 00000000..569f005c --- /dev/null +++ b/test/gfx/bg_in_tile.flags @@ -0,0 +1 @@ +-B #ffd800 diff --git a/test/gfx/bg_in_tile.png b/test/gfx/bg_in_tile.png new file mode 100644 index 00000000..3d7fc083 Binary files /dev/null and b/test/gfx/bg_in_tile.png differ diff --git a/test/gfx/bg_oval.flags b/test/gfx/bg_oval.flags new file mode 100644 index 00000000..3203f325 --- /dev/null +++ b/test/gfx/bg_oval.flags @@ -0,0 +1 @@ +-B #FF00ff diff --git a/test/gfx/bg_oval.out.2bpp b/test/gfx/bg_oval.out.2bpp new file mode 100644 index 00000000..c7701bb1 Binary files /dev/null and b/test/gfx/bg_oval.out.2bpp differ diff --git a/test/gfx/bg_oval.out.attrmap b/test/gfx/bg_oval.out.attrmap new file mode 100644 index 00000000..bc8840b2 Binary files /dev/null and b/test/gfx/bg_oval.out.attrmap differ diff --git a/test/gfx/bg_oval.out.pal b/test/gfx/bg_oval.out.pal new file mode 100644 index 00000000..0ef9254a Binary files /dev/null and b/test/gfx/bg_oval.out.pal differ diff --git a/test/gfx/bg_oval.out.tilemap b/test/gfx/bg_oval.out.tilemap new file mode 100644 index 00000000..344d9e9a Binary files /dev/null and b/test/gfx/bg_oval.out.tilemap differ diff --git a/test/gfx/bg_oval.png b/test/gfx/bg_oval.png new file mode 100644 index 00000000..465d093e Binary files /dev/null and b/test/gfx/bg_oval.png differ diff --git a/test/gfx/input_tileset_with_bg.flags b/test/gfx/input_tileset_with_bg.flags new file mode 100644 index 00000000..6b93b160 --- /dev/null +++ b/test/gfx/input_tileset_with_bg.flags @@ -0,0 +1,4 @@ +-B #fff +-i input_tileset_with_bg.in.2bpp +-c gbc:input_tileset_with_bg.in.pal +-u diff --git a/test/gfx/input_tileset_with_bg.in.2bpp b/test/gfx/input_tileset_with_bg.in.2bpp new file mode 100644 index 00000000..7116e54f Binary files /dev/null and b/test/gfx/input_tileset_with_bg.in.2bpp differ diff --git a/test/gfx/input_tileset_with_bg.in.pal b/test/gfx/input_tileset_with_bg.in.pal new file mode 100644 index 00000000..6d8bd5bc Binary files /dev/null and b/test/gfx/input_tileset_with_bg.in.pal differ diff --git a/test/gfx/input_tileset_with_bg.out.2bpp b/test/gfx/input_tileset_with_bg.out.2bpp new file mode 100644 index 00000000..7116e54f Binary files /dev/null and b/test/gfx/input_tileset_with_bg.out.2bpp differ diff --git a/test/gfx/input_tileset_with_bg.out.attrmap b/test/gfx/input_tileset_with_bg.out.attrmap new file mode 100644 index 00000000..01d633b2 Binary files /dev/null and b/test/gfx/input_tileset_with_bg.out.attrmap differ diff --git a/test/gfx/input_tileset_with_bg.out.tilemap b/test/gfx/input_tileset_with_bg.out.tilemap new file mode 100644 index 00000000..36aaf3c0 Binary files /dev/null and b/test/gfx/input_tileset_with_bg.out.tilemap differ diff --git a/test/gfx/input_tileset_with_bg.png b/test/gfx/input_tileset_with_bg.png new file mode 100644 index 00000000..c44b7861 Binary files /dev/null and b/test/gfx/input_tileset_with_bg.png differ diff --git a/test/gfx/trans_bg_in_tile.err b/test/gfx/trans_bg_in_tile.err new file mode 100644 index 00000000..ce6bf37e --- /dev/null +++ b/test/gfx/trans_bg_in_tile.err @@ -0,0 +1,2 @@ +FATAL: Tile (16, 0) contains the background color (#00000000)! +Conversion aborted after 1 error diff --git a/test/gfx/trans_bg_in_tile.flags b/test/gfx/trans_bg_in_tile.flags new file mode 100644 index 00000000..45911e3e --- /dev/null +++ b/test/gfx/trans_bg_in_tile.flags @@ -0,0 +1 @@ +-B transparent diff --git a/test/gfx/trans_bg_in_tile.png b/test/gfx/trans_bg_in_tile.png new file mode 100644 index 00000000..d56baad0 Binary files /dev/null and b/test/gfx/trans_bg_in_tile.png differ