gbdk-2020 | GameBoy Development Kit |
| download: https://git.y1.nz/archives/gbdk.tar.gz | |
| README | Files | Log | Refs | LICENSE |
gbdk-support/nespal/nespal.py
1 #!/usr/bin/env python3
2 import sys
3 import argparse
4 from operator import itemgetter
5 from pathlib import Path
6
7 from typing import Optional, Tuple, List, Sequence, Dict, Set, NewType
8
9
10 def get_ppu_color_name(ppu_color: int) -> str:
11 """
12 Returns a human-readable name for a PPU color
13
14 Naming convention from https://www.nesdev.org/wiki/PPU_palettes#Color_names
15
16 With following simplifications:
17 'Dark gray' -> 'Gray'
18 'Light gray or silver' -> 'Silver'
19
20 :param ppu_color: 6-bit NES PPU color
21 :return: Human-readable name for PPU color
22 """
23 ppu_hue_names = [
24 'Gray',
25 'Azure',
26 'Blue',
27 'Violet',
28 'Magenta',
29 'Rose',
30 'Red',
31 'Orange',
32 'Yellow',
33 'Chartreuse',
34 'Green',
35 'Spring',
36 'Cyan'
37 ]
38 ppu_luma_names = [
39 'Dark',
40 'Medium',
41 'Light',
42 'Pale'
43 ]
44 c = ppu_color & 0x3F
45 # Special-cases for black/white/grays
46 if c in [0x20, 0x30]:
47 return 'White'
48 elif c in [0x3D]:
49 return 'Silver'
50 elif c in [0x00, 0x2D]:
51 return 'Gray'
52 elif c in [0x1D, 0x0E, 0x1E, 0x2E, 0x3E, 0x0F, 0x1F, 0x2F, 0x3F]:
53 return 'Black'
54 elif c == 0x0D:
55 return 'BlackerThanBlack'
56 # All normal hues
57 ppu_hue_name = ppu_hue_names[c & 0xF]
58 ppu_luma_name = ppu_luma_names[(c >> 4) & 0x3]
59 return f'{ppu_luma_name}-{ppu_hue_name}'
60
61
62 def get_script_directory() -> Path:
63 """
64 Return path to current scripts directory.
65 (or the executable if built with pyinstaller)
66
67 :return: Path to directory of this script
68 """
69 if getattr(sys, 'frozen', False):
70 return Path(sys.executable).parent
71 # or a script file (e.g. `.py` / `.pyw`)
72 elif __file__:
73 return Path(__file__).parent
74
75
76 def nes_closest_palette_entry(rgb: Tuple[int, int, int], NESPaletteRGB: List[Tuple[int, int, int]]) -> int:
77 def dist2(rgbA, rgbB):
78 return sum((rgbA[i] - rgbB[i])**2 for i in range(3))
79 minIndex = min(enumerate([dist2(rgb, nprgb) for i, nprgb in enumerate(NESPaletteRGB)]), key=itemgetter(1))[0]
80 return minIndex
81
82
83 def to_triplets(data: List[int]) -> List[Tuple[int, int, int]]:
84 """
85 Convert a linear list into a triplet list
86
87 :param data: List formatted as [0, 1, 2 ... N, N+1, N+2]
88 :return: Nested list formatted as [[0, 1, 2] ... [N, N+1, N+2]]
89 """
90 assert(len(data) % 3 == 0)
91 length = len(data) // 3
92 data_triplets = []
93 for i in range(length):
94 data_triplets.append((data[3 * i + 0], data[3 * i + 1], data[3 * i + 2]))
95 return data_triplets
96
97
98 def map_to_PPU_colors(target_colors: List[int], rgb_palette_nes: List[int], invalid_colors: List[int]) -> Tuple[List[int], List[int]]:
99 """
100 Creates a mapping from target colors in RGB format to NES PPU colors
101
102 :param target_colors: List of length N, with target colors in RGB format
103 :param rgb_palette_nes: List of length 64, giving RGB values for NES colors
104 :return: List of length N with each entry indicating the best NES color
105 """
106 target_colors = to_triplets(target_colors)
107 rgb_palette_nes = to_triplets(rgb_palette_nes)
108 # Set invalid_colors to large value to prevent being picked
109 rgb_palette_nes = rgb_palette_nes[:]
110 for c in invalid_colors:
111 rgb_palette_nes[c] = (1000000, 1000000, 1000000)
112 nes_palette_colors = []
113 for rgb_color in target_colors:
114 # print(rgb_color)
115 closest_palette_entry = nes_closest_palette_entry(rgb_color, rgb_palette_nes)
116 nes_palette_colors.append(closest_palette_entry)
117 return nes_palette_colors
118
119
120 def get_pal_file_path(pal_file_path: str) -> Path:
121 """
122 Convert a string path to Pathlib path.
123 If None, use default path and print warning message.
124 If file is missing, print error.
125
126 :param pal_file_path: path to .pal file
127 :return: path as pathlib Path
128 """
129 default_pal_file_path = get_script_directory() / 'palettes' / 'palgen.pal'
130 if pal_file_path is None:
131 print(f'WARNING: Palette file not specified - falling back to default palette file {str(default_pal_file_path)}')
132 return default_pal_file_path
133 elif not Path(pal_file_path).exists():
134 print(f'WARNING: Palette file {str(Path(pal_file_path))} does not exist - falling back to default palette file {str(default_pal_file_path)}')
135 return default_pal_file_path
136 else:
137 return Path(pal_file_path)
138
139
140 def write_c_lut(lut: List[int], filename: Path):
141 """
142 Writes NES color mapping as a C-array rgb_to_nes
143
144 :param lut: Lookup table of 64 values, indexed as BBGGRR
145 :param filename: Source text file to write
146 """
147 with open(filename, 'wt') as f:
148 print('// File auto-generated file by nespal.py', file=f)
149 print(f'unsigned char rgb_to_nes[{len(lut)}] = {{', file=f)
150 for i, c in enumerate(lut):
151 r = ((i >> 0) & 0x3)
152 g = ((i >> 2) & 0x3)
153 b = ((i >> 4) & 0x3)
154 print(f' 0x{c:0{2}X}, // RGB({r},{g},{b}) -> {c:0{2}X} ({get_ppu_color_name(c)})', file=f)
155 print(f'}};', file=f)
156
157
158 def write_c_macro(lut: List[int], filename: Path):
159 """
160 Writes NES color mapping as a C-macro RGB_TO_NES(c)
161
162 :param lut: Lookup table of 64 values, indexed as BBGGRR
163 :param filename: Source text file to write
164 """
165 with open(filename, 'wt') as f:
166 print('// File auto-generated file by nespal.py', file=f)
167 print('#ifndef __RGB_TO_NES_MACRO_H__', file=f)
168 print('#define __RGB_TO_NES_MACRO_H__', file=f)
169 print(f'#define RGB_TO_NES(c) \\', file=f)
170 for i, c in enumerate(lut):
171 print(f' (c == 0x{i:0{2}X}) ? 0x{c:0{2}X} : \\', file=f)
172 print(f' 0xFF // out-of-range value - set to 0xFF', file=f)
173 print('#endif', file=f)
174
175
176 def main(palette_file: Path,
177 invalid_colors: List[int],
178 output_c_lut_path: Path,
179 output_c_macro_path: Path) -> int:
180 # Read NES palette mapping file
181 with open(palette_file, 'rb') as f:
182 nes_palette = f.read(192)
183 # Use a 6-bit RGB palette in BBGGRR format
184 gbdk_palette = []
185 for i in range(64):
186 r = (((i >> 0) & 0x3) / 3.0) * 255.0
187 g = (((i >> 2) & 0x3) / 3.0) * 255.0
188 b = (((i >> 4) & 0x3) / 3.0) * 255.0
189 gbdk_palette.append(r)
190 gbdk_palette.append(g)
191 gbdk_palette.append(b)
192 nes_palette_colors = map_to_PPU_colors(gbdk_palette, nes_palette, invalid_colors)
193 if output_c_lut_path is not None:
194 write_c_lut(nes_palette_colors, output_c_lut_path)
195 if output_c_macro_path is not None:
196 write_c_macro(nes_palette_colors, output_c_macro_path)
197
198
199 if __name__ == '__main__':
200 parser = argparse.ArgumentParser(description=f'Generates lookup tables to convert RGB to NES PPU compatible colors.')
201 parser.add_argument('--pal_file', type=str,
202 default=None,
203 help='Binary 192-byte file specifying a particular NES palette flavor.')
204 parser.add_argument('--output_c_lut', type=str,
205 default='rgb_to_nes_lut.c',
206 help='Output .c file with generated rgb_to_nes lookup table.')
207 parser.add_argument('--output_c_macro', type=str,
208 default='rgb_to_nes_macro.h',
209 help='Output .h file with generated RGB_TO_NES C macro.')
210 parser.add_argument('--invalid_colors', type=str,
211 nargs='+',
212 default=['0D', '0E', '1E', '2E', '3E', '0F', '1F', '2F', '3F'],
213 help='Specify which NES PPU colors should be considered invalid.')
214 args = parser.parse_args()
215 # Call main program
216 rc = main(get_pal_file_path(args.pal_file),
217 [int(s, 16) for s in args.invalid_colors],
218 Path(args.output_c_lut),
219 Path(args.output_c_macro))
220 sys.exit(rc)
This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.