git.y1.nz

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.