Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sort Pokemon and trainer sprite palettes, with Makefile-specified exceptions #1137

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,50 @@ gfx/pokemon/girafarig/front.animated.tilemap: gfx/pokemon/girafarig/front.2bpp g
tools/pokemon_animation_graphics --girafarig -t $@ $^


### Misc file-specific graphics rules
### Pokemon and trainer sprite rules

gfx/pokemon/%/back.2bpp: rgbgfx += --columns
gfx/pokemon/%/back.2bpp: gfx/pokemon/%/back.png gfx/pokemon/%/normal.gbcpal
$(RGBGFX) $(rgbgfx) --colors gbc:$(word 2,$^) -o $@ $<
gfx/pokemon/%/front.2bpp: gfx/pokemon/%/front.png gfx/pokemon/%/normal.gbcpal
$(RGBGFX) $(rgbgfx) --colors gbc:$(word 2,$^) -o $@ $<
gfx/pokemon/%/normal.gbcpal: gfx/pokemon/%/front.gbcpal gfx/pokemon/%/back.gbcpal
tools/gbcpal $(tools/gbcpal) $@ $^

gfx/trainers/%.2bpp: rgbgfx += --columns
gfx/trainers/%.2bpp: gfx/trainers/%.png gfx/trainers/%.gbcpal
$(RGBGFX) $(rgbgfx) --colors gbc:$(word 2,$^) -o $@ $<

# Egg does not have a back sprite, so it only uses front.gbcpal
gfx/pokemon/egg/front.2bpp: gfx/pokemon/egg/front.png gfx/pokemon/egg/front.gbcpal
gfx/pokemon/egg/front.2bpp: rgbgfx += --colors gbc:$(word 2,$^)
Rangi42 marked this conversation as resolved.
Show resolved Hide resolved

gfx/pokemon/%/back.2bpp: rgbgfx += --columns --colors embedded
gfx/pokemon/%/front.2bpp: rgbgfx += --colors embedded
# Unown letters share one normal.pal, so they don't already build each normal.gbcpal
$(foreach png, $(wildcard gfx/pokemon/unown_*/front.png),\
$(eval $(png:.png=.2bpp): $(png) $(png:front.png=normal.gbcpal)))
gfx/pokemon/unown_%/front.2bpp: rgbgfx += --colors gbc:$(@:front.2bpp=normal.gbcpal)

gfx/trainers/%.2bpp: rgbgfx += --columns --colors embedded

### Misc file-specific graphics rules

gfx/pokemon/egg/unused_front.2bpp: rgbgfx += --columns

gfx/pokemon/caterpie/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/diglett/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/dugtrio/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/farfetch_d/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/fearow/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/jynx/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/magnemite/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/magneton/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/marill/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/porygon2/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/scyther/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/shellder/normal.gbcpal: tools/gbcpal += --reverse
gfx/pokemon/spearow/normal.gbcpal: tools/gbcpal += --reverse

gfx/trainers/swimmer_m.gbcpal: tools/gbcpal += --reverse

gfx/new_game/shrink1.2bpp: rgbgfx += --columns
gfx/new_game/shrink2.2bpp: rgbgfx += --columns

Expand Down Expand Up @@ -315,7 +350,8 @@ gfx/mobile/stadium2_n64.2bpp: tools/gfx += --trim-whitespace
tools/gfx $(tools/gfx) --depth 1 -o $@ $@)

%.gbcpal: %.png
$(RGBGFX) --colors embedded -p $@ $<
$(RGBGFX) -p $@ $<
tools/gbcpal $(tools/gbcpal) $@ $@

%.dimensions: %.png
tools/png_dimensions $< $@
508 changes: 254 additions & 254 deletions data/pokemon/palettes.asm

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions tools/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
bpp2png
gbcpal
gfx
lzcomp
make_patch
Expand Down
2 changes: 2 additions & 0 deletions tools/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CFLAGS := -O3 -flto -std=c11 -Wall -Wextra -pedantic
tools := \
bpp2png \
lzcomp \
gbcpal \
gfx \
make_patch \
png_dimensions \
Expand All @@ -20,6 +21,7 @@ all: $(tools)
clean:
$(RM) $(tools)

gbcpal: common.h
gfx: common.h
png_dimensions: common.h
pokemon_animation: common.h
Expand Down
154 changes: 154 additions & 0 deletions tools/gbcpal.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#define PROGRAM_NAME "gbcpal"
#define USAGE_OPTS "[-h|--help] [-r|--reverse] out.gbcpal in.gbcpal..."

#include "common.h"

bool reverse;

void parse_args(int argc, char *argv[]) {
struct option long_options[] = {
{"reverse", no_argument, 0, 'r'},
{"help", no_argument, 0, 'h'},
{0}
};
for (int opt; (opt = getopt_long(argc, argv, "rh", long_options)) != -1;) {
switch (opt) {
case 'r':
reverse = true;
break;
case 'h':
usage_exit(0);
break;
default:
usage_exit(1);
}
}
}

struct Color {
uint8_t r, g, b;
};

uint16_t pack_color(struct Color color) {
return (color.b << 10) | (color.g << 5) | color.r;
Rangi42 marked this conversation as resolved.
Show resolved Hide resolved
}

struct Color unpack_color(uint16_t gbc_color) {
return (struct Color){
.r = gbc_color & 0x1f,
.g = (gbc_color >> 5) & 0x1f,
.b = (gbc_color >> 10) & 0x1f,
};
}

double luminance(struct Color color) {
return 0.299 * color.r * color.r + 0.587 * color.g * color.g + 0.114 * color.b * color.b;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please break up this line, and while I believe these numbers, do you have any source/explanation for them? I've never looked into this, and I would've just naïvely summed the colors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the standard conversion from RGB to Y (luminance), used by jpeg encoding and probably lots of other things.
I use the exact same coefficients here, for example.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What breakup do you have in mind? It's the same function I've used in a few Python helper scripts:

def luminance(c):
	r, g, b = c
	return 0.299 * r**2 + 0.587 * g**2 + 0.114 * b**2

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, actually I didn't notice at first that you are squaring each color component. That is not the standard luminance conversion used by jpeg etc. But it's probably the standard somewhere! I've just never seen it before. I'd be curious to hear the benefits.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used it for many years, quoting from some source that defined luminance as the sqrt of sum of squares (i.e. a Euclidean distance) but omitting the sqrt when it's unnecessary (e.g. for comparisons).

Google leads to https://stackoverflow.com/questions/596216/formula-to-determine-perceived-brightness-of-rgb-color leads to https://alienryderflex.com/hsp.html which looks familiar (and IMO based on the StackOverflow illustrations is the best choice of the given options).

}

int compare_colors(const void *color1, const void *color2) {
double lum1 = luminance(*(const struct Color *)color1);
double lum2 = luminance(*(const struct Color *)color2);
return (lum1 < lum2) - (lum1 > lum2); // sort lightest to darkest
}

void read_gbcpal(const char *filename, struct Color **colors, size_t *num_colors) {
long filesize;
uint8_t *bytes = read_u8(filename, &filesize);
if (filesize == 0) {
error_exit("%s: empty gbcpal file\n", filename);
}
if (filesize % 2) {
error_exit("%s: invalid gbcpal file\n", filename);
}

size_t new_colors = filesize / 2;
*colors = xrealloc(*colors, (sizeof **colors) * (*num_colors + new_colors));
for (size_t i = 0; i < new_colors; i++) {
uint16_t gbc_color = (bytes[i * 2 + 1] << 8) | bytes[i * 2];
(*colors)[*num_colors + i] = unpack_color(gbc_color);
}
*num_colors += new_colors;

free(bytes);
}

void filter_colors(struct Color **colors, size_t *num_colors) {
size_t num_filtered = 0;
struct Color *filtered = xmalloc((sizeof **colors) * *num_colors);

for (size_t i = 0; i < *num_colors; i++) {
struct Color color = (*colors)[i];
if (color.r == 0 && color.g == 0 && color.b == 0) {
continue; // filter out black
}
if (color.r == 31 && color.g == 31 && color.b == 31) {
continue; // filter out white
}
if (num_filtered > 0) {
struct Color last = filtered[num_filtered - 1];
if (color.r == last.r && color.g == last.g && color.b == last.b) {
continue; // filter out duplicates
}
}
filtered[num_filtered++] = color;
}

free(*colors);
*num_colors = num_filtered;
*colors = filtered;
}

int main(int argc, char *argv[]) {
parse_args(argc, argv);

argc -= optind;
argv += optind;
if (argc < 2) {
usage_exit(1);
}

const char *out_filename = argv[0];

struct Color *colors = NULL;
size_t num_colors = 0;
for (int i = 1; i < argc; i++) {
read_gbcpal(argv[i], &colors, &num_colors);
}

qsort(colors, num_colors, sizeof(*colors), compare_colors);
filter_colors(&colors, &num_colors);

uint16_t first, second;
if (num_colors == 0) {
// colors are white and black
first = 0x7fff;
second = 0x0000;
} else if (num_colors == 1) {
// colors are both the same one
first = pack_color(colors[0]);
second = first;
} else if (num_colors == 2) {
// colors are light and dark
first = pack_color(colors[0]);
second = pack_color(colors[1]);
if (reverse) {
// darker color is first
uint16_t tmp = first;
first = second;
second = tmp;
}
} else {
error_exit("%s: more than 2 colors besides black and white (%zu)\n", out_filename, num_colors);
}
Comment on lines +121 to +142
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the algorithm of white + reversed?(colors) + black * (1 + (max_colors - num_colors)) would make this cleaner, something like

if (num_colors > (MAX_COLORS - 2)) error;

uint16_t colors[MAX_COLORS];
int c = 0;
colors[c++] = 0x7fff;  // First color is white
for (int i = 0; i < num_colors; i++) colors[c++] = pack_color(colors[reverse ? num_colors - 1 - i : i]);  // Add the maximum of 2 custom colors
while (c < MAX_COLORS / 2) colors[c++] = 0x7fff;  // If not enough colors are present, fill up to half with white
while (c < MAX_COLORS) colors[c++] = 0;  // Fill the remainder with black

// Alternatively: fill the remaining colors from a predefined grayscale array, to make battle anims nicer?
for (c < MAX_COLORS; c++) colors[c] = pack_color((struct Color){32/4*(3-c), 32/4*(3-c), 32/4*(3-c)});

This is less verbose and I like the option of expanding this for more colors/maybe gen3 in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this a lot harder to understand and grasp "when my input was such-and-such, what order will my colors be in?" I think the current verbose code is useful for being able to point users to it and show just how this new approach works.

Extending it to support 4bpp is a cool idea, but that would involve more rewriting than just this section, and it's a short enough file that I don't think we need to future-proof it like that already. (YAGNI; a gbapal.c tool will be its own thing, not much harder to write than this was.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fill the remaining colors from a predefined grayscale array

I did consider this, but it would be error-prone. Imagine a front sprite that uses one or two colors, and a back sprite using zero, just black+white. Each one gets gbapal.c run on it, so the back sprite now has two gray colors... and then generating normal.gbcpal fails because there are too many combined colors.


uint8_t bytes[8] = {
0xff, 0x7f, // white
first & 0xff, first >> 8,
second & 0xff, second >> 8,
0x00, 0x00, // black
};
write_u8(out_filename, bytes, COUNTOF(bytes));

free(colors);
return 0;
}
2 changes: 1 addition & 1 deletion tools/make_patch.c
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ struct Buffer *process_template(const char *template_filename, const char *patch
int compare_patch(const void *patch1, const void *patch2) {
unsigned int offset1 = ((const struct Patch *)patch1)->offset;
unsigned int offset2 = ((const struct Patch *)patch2)->offset;
return offset1 > offset2 ? 1 : offset1 < offset2 ? -1 : 0;
return (offset1 > offset2) - (offset1 < offset2);
}

bool verify_completeness(FILE *restrict orig_rom, FILE *restrict new_rom, struct Buffer *patches) {
Expand Down
40 changes: 40 additions & 0 deletions tools/revpals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Usage: python revpals.py

List Pokemon and trainer sprites with reversed palettes (darker color first).
"""

import sys
import glob

import png

def luminance(c):
r, g, b = c
return 0.299 * r**2 + 0.587 * g**2 + 0.114 * b**2

def is_reversed(filename):
with open(filename, 'rb') as file:
reader = png.Reader(file)
reader.preamble()
try:
palette = [c[:3] for c in reader.palette()]
except:
return False
if len(palette) != 4 or palette[0] != (255, 255, 255) or palette[3] != (0, 0, 0):
return False
return luminance(palette[1]) < luminance(palette[2])

def main():
for filename in sorted(glob.glob('gfx/pokemon/*/front.png')):
if is_reversed(filename):
print(filename)
for filename in sorted(glob.glob('gfx/trainers/*.png')):
if is_reversed(filename):
print(filename)

if __name__ == '__main__':
main()