mirror of
https://github.com/gbdk-2020/gbdk-2020.git
synced 2026-02-20 00:32:21 +01:00
RGB -> NES color conversion support:
* Add Python script gbdk-support/nespal/nespal.py, to allow generating C LUT / macro from a specific 192-byte .pal file * Add example .pal file from Drag's palette generator as default * Add generated C macro file gbdk-lib/include/nes/rgb_to_nes_macro.h, providing RGB_TO_NES macro * Update RGB / RGB8 / RGBHTML in nes.h to invoke RGB_TO_NES macro * Manually enter EGA-like color constants from nesdev wiki
This commit is contained in:
@@ -8,34 +8,38 @@
|
||||
#include <stdint.h>
|
||||
#include <gbdk/version.h>
|
||||
#include <nes/hardware.h>
|
||||
#include <nes/rgb_to_nes_macro.h>
|
||||
|
||||
#define NINTENDO_ENTERTAINMENT_SYSTEM
|
||||
#ifdef SEGA
|
||||
#undef SEGA
|
||||
#endif
|
||||
|
||||
#define RGB(r,g,b) ((r) | ((g) << 2) | ((b) << 4))
|
||||
#define RGB8(r,g,b) (((r) >> 6) | (((g) >> 6) << 2) | (((b) >> 6) << 4))
|
||||
#define RGBHTML(RGB24bit) (((RGB24bit) >> 22) | ((((RGB24bit) & 0xFFFF) >> 14) << 2) | ((((RGB24bit) & 0xFF) >> 6) << 4))
|
||||
#define RGB(r,g,b) RGB_TO_NES(((r) | ((g) << 2) | ((b) << 4)))
|
||||
#define RGB8(r,g,b) RGB_TO_NES((((r) >> 6) | (((g) >> 6) << 2) | (((b) >> 6) << 4)))
|
||||
#define RGBHTML(RGB24bit) RGB_TO_NES((((RGB24bit) >> 22) | ((((RGB24bit) & 0xFFFF) >> 14) << 2) | ((((RGB24bit) & 0xFF) >> 6) << 4)))
|
||||
|
||||
/** Common colors based on the EGA default palette.
|
||||
*
|
||||
* Manually entered from https://www.nesdev.org/wiki/PPU_palettes#RGBI
|
||||
*
|
||||
*/
|
||||
#define RGB_RED RGB( 3, 0, 0)
|
||||
#define RGB_DARKRED RGB( 2, 0, 0)
|
||||
#define RGB_GREEN RGB( 0, 3, 0)
|
||||
#define RGB_DARKGREEN RGB( 0, 2, 0)
|
||||
#define RGB_BLUE RGB( 0, 0, 3)
|
||||
#define RGB_DARKBLUE RGB( 0, 0, 2)
|
||||
#define RGB_YELLOW RGB( 3, 3, 0)
|
||||
#define RGB_DARKYELLOW RGB( 2, 2, 0)
|
||||
#define RGB_CYAN RGB( 0, 3, 3)
|
||||
#define RGB_AQUA RGB( 3, 1, 2)
|
||||
#define RGB_PINK RGB( 3, 0, 3)
|
||||
#define RGB_PURPLE RGB( 2, 0, 2)
|
||||
#define RGB_BLACK RGB( 0, 0, 0)
|
||||
#define RGB_DARKGRAY RGB( 1, 1, 1)
|
||||
#define RGB_LIGHTGRAY RGB( 2, 2, 2)
|
||||
#define RGB_WHITE RGB( 3, 3, 3)
|
||||
#define RGB_RED 0x16 // EGA12
|
||||
#define RGB_DARKRED 0x06 // EGA4
|
||||
#define RGB_GREEN 0x2A // EGA10
|
||||
#define RGB_DARKGREEN 0x1A // EGA2
|
||||
#define RGB_BLUE 0x12 // EGA9
|
||||
#define RGB_DARKBLUE 0x02 // EGA1
|
||||
#define RGB_YELLOW 0x28 // EGA14
|
||||
#define RGB_DARKYELLOW 0x18 // EGA6
|
||||
#define RGB_CYAN 0x2C // EGA11
|
||||
#define RGB_AQUA 0x1C // EGA3
|
||||
#define RGB_PINK 0x24 // EGA13
|
||||
#define RGB_PURPLE 0x14 // EGA5
|
||||
#define RGB_BLACK 0x0F // EGA0
|
||||
#define RGB_DARKGRAY 0x00 // EGA8
|
||||
#define RGB_LIGHTGRAY 0x10 // EGA7
|
||||
#define RGB_WHITE 0x30 // EGA15
|
||||
|
||||
typedef uint8_t palette_color_t;
|
||||
|
||||
|
||||
70
gbdk-lib/include/nes/rgb_to_nes_macro.h
Normal file
70
gbdk-lib/include/nes/rgb_to_nes_macro.h
Normal file
@@ -0,0 +1,70 @@
|
||||
// File auto-generated file by nespal.py
|
||||
#ifndef __RGB_TO_NES_MACRO_H__
|
||||
#define __RGB_TO_NES_MACRO_H__
|
||||
#define RGB_TO_NES(c) \
|
||||
(c == 0x00) ? 0x1D : \
|
||||
(c == 0x01) ? 0x06 : \
|
||||
(c == 0x02) ? 0x17 : \
|
||||
(c == 0x03) ? 0x16 : \
|
||||
(c == 0x04) ? 0x19 : \
|
||||
(c == 0x05) ? 0x18 : \
|
||||
(c == 0x06) ? 0x17 : \
|
||||
(c == 0x07) ? 0x27 : \
|
||||
(c == 0x08) ? 0x2A : \
|
||||
(c == 0x09) ? 0x29 : \
|
||||
(c == 0x0A) ? 0x28 : \
|
||||
(c == 0x0B) ? 0x27 : \
|
||||
(c == 0x0C) ? 0x2A : \
|
||||
(c == 0x0D) ? 0x29 : \
|
||||
(c == 0x0E) ? 0x29 : \
|
||||
(c == 0x0F) ? 0x28 : \
|
||||
(c == 0x10) ? 0x01 : \
|
||||
(c == 0x11) ? 0x04 : \
|
||||
(c == 0x12) ? 0x15 : \
|
||||
(c == 0x13) ? 0x15 : \
|
||||
(c == 0x14) ? 0x1C : \
|
||||
(c == 0x15) ? 0x00 : \
|
||||
(c == 0x16) ? 0x15 : \
|
||||
(c == 0x17) ? 0x26 : \
|
||||
(c == 0x18) ? 0x2B : \
|
||||
(c == 0x19) ? 0x2A : \
|
||||
(c == 0x1A) ? 0x10 : \
|
||||
(c == 0x1B) ? 0x26 : \
|
||||
(c == 0x1C) ? 0x2B : \
|
||||
(c == 0x1D) ? 0x2A : \
|
||||
(c == 0x1E) ? 0x39 : \
|
||||
(c == 0x1F) ? 0x38 : \
|
||||
(c == 0x20) ? 0x02 : \
|
||||
(c == 0x21) ? 0x13 : \
|
||||
(c == 0x22) ? 0x14 : \
|
||||
(c == 0x23) ? 0x14 : \
|
||||
(c == 0x24) ? 0x11 : \
|
||||
(c == 0x25) ? 0x13 : \
|
||||
(c == 0x26) ? 0x10 : \
|
||||
(c == 0x27) ? 0x25 : \
|
||||
(c == 0x28) ? 0x2C : \
|
||||
(c == 0x29) ? 0x10 : \
|
||||
(c == 0x2A) ? 0x3D : \
|
||||
(c == 0x2B) ? 0x36 : \
|
||||
(c == 0x2C) ? 0x2C : \
|
||||
(c == 0x2D) ? 0x3B : \
|
||||
(c == 0x2E) ? 0x3A : \
|
||||
(c == 0x2F) ? 0x37 : \
|
||||
(c == 0x30) ? 0x12 : \
|
||||
(c == 0x31) ? 0x13 : \
|
||||
(c == 0x32) ? 0x14 : \
|
||||
(c == 0x33) ? 0x24 : \
|
||||
(c == 0x34) ? 0x12 : \
|
||||
(c == 0x35) ? 0x22 : \
|
||||
(c == 0x36) ? 0x23 : \
|
||||
(c == 0x37) ? 0x24 : \
|
||||
(c == 0x38) ? 0x21 : \
|
||||
(c == 0x39) ? 0x22 : \
|
||||
(c == 0x3A) ? 0x32 : \
|
||||
(c == 0x3B) ? 0x34 : \
|
||||
(c == 0x3C) ? 0x2C : \
|
||||
(c == 0x3D) ? 0x3C : \
|
||||
(c == 0x3E) ? 0x3C : \
|
||||
(c == 0x3F) ? 0x20 : \
|
||||
0xFF // out-of-range value - set to 0xFF
|
||||
#endif
|
||||
220
gbdk-support/nespal/nespal.py
Normal file
220
gbdk-support/nespal/nespal.py
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import argparse
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Optional, Tuple, List, Sequence, Dict, Set, NewType
|
||||
|
||||
|
||||
def get_ppu_color_name(ppu_color: int) -> str:
|
||||
"""
|
||||
Returns a human-readable name for a PPU color
|
||||
|
||||
Naming convention from https://www.nesdev.org/wiki/PPU_palettes#Color_names
|
||||
|
||||
With following simplifications:
|
||||
'Dark gray' -> 'Gray'
|
||||
'Light gray or silver' -> 'Silver'
|
||||
|
||||
:param ppu_color: 6-bit NES PPU color
|
||||
:return: Human-readable name for PPU color
|
||||
"""
|
||||
ppu_hue_names = [
|
||||
'Gray',
|
||||
'Azure',
|
||||
'Blue',
|
||||
'Violet',
|
||||
'Magenta',
|
||||
'Rose',
|
||||
'Red',
|
||||
'Orange',
|
||||
'Yellow',
|
||||
'Chartreuse',
|
||||
'Green',
|
||||
'Spring',
|
||||
'Cyan'
|
||||
]
|
||||
ppu_luma_names = [
|
||||
'Dark',
|
||||
'Medium',
|
||||
'Light',
|
||||
'Pale'
|
||||
]
|
||||
c = ppu_color & 0x3F
|
||||
# Special-cases for black/white/grays
|
||||
if c in [0x20, 0x30]:
|
||||
return 'White'
|
||||
elif c in [0x3D]:
|
||||
return 'Silver'
|
||||
elif c in [0x00, 0x2D]:
|
||||
return 'Gray'
|
||||
elif c in [0x1D, 0x0E, 0x1E, 0x2E, 0x3E, 0x0F, 0x1F, 0x2F, 0x3F]:
|
||||
return 'Black'
|
||||
elif c == 0x0D:
|
||||
return 'BlackerThanBlack'
|
||||
# All normal hues
|
||||
ppu_hue_name = ppu_hue_names[c & 0xF]
|
||||
ppu_luma_name = ppu_luma_names[(c >> 4) & 0x3]
|
||||
return f'{ppu_luma_name}-{ppu_hue_name}'
|
||||
|
||||
|
||||
def get_script_directory() -> Path:
|
||||
"""
|
||||
Return path to current scripts directory.
|
||||
(or the executable if built with pyinstaller)
|
||||
|
||||
:return: Path to directory of this script
|
||||
"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
return Path(sys.executable).parent
|
||||
# or a script file (e.g. `.py` / `.pyw`)
|
||||
elif __file__:
|
||||
return Path(__file__).parent
|
||||
|
||||
|
||||
def nes_closest_palette_entry(rgb: Tuple[int, int, int], NESPaletteRGB: List[Tuple[int, int, int]]) -> int:
|
||||
def dist2(rgbA, rgbB):
|
||||
return sum((rgbA[i] - rgbB[i])**2 for i in range(3))
|
||||
minIndex = min(enumerate([dist2(rgb, nprgb) for i, nprgb in enumerate(NESPaletteRGB)]), key=itemgetter(1))[0]
|
||||
return minIndex
|
||||
|
||||
|
||||
def to_triplets(data: List[int]) -> List[Tuple[int, int, int]]:
|
||||
"""
|
||||
Convert a linear list into a triplet list
|
||||
|
||||
:param data: List formatted as [0, 1, 2 ... N, N+1, N+2]
|
||||
:return: Nested list formatted as [[0, 1, 2] ... [N, N+1, N+2]]
|
||||
"""
|
||||
assert(len(data) % 3 == 0)
|
||||
length = len(data) // 3
|
||||
data_triplets = []
|
||||
for i in range(length):
|
||||
data_triplets.append((data[3 * i + 0], data[3 * i + 1], data[3 * i + 2]))
|
||||
return data_triplets
|
||||
|
||||
|
||||
def map_to_PPU_colors(target_colors: List[int], rgb_palette_nes: List[int], invalid_colors: List[int]) -> Tuple[List[int], List[int]]:
|
||||
"""
|
||||
Creates a mapping from target colors in RGB format to NES PPU colors
|
||||
|
||||
:param target_colors: List of length N, with target colors in RGB format
|
||||
:param rgb_palette_nes: List of length 64, giving RGB values for NES colors
|
||||
:return: List of length N with each entry indicating the best NES color
|
||||
"""
|
||||
target_colors = to_triplets(target_colors)
|
||||
rgb_palette_nes = to_triplets(rgb_palette_nes)
|
||||
# Set invalid_colors to large value to prevent being picked
|
||||
rgb_palette_nes = rgb_palette_nes[:]
|
||||
for c in invalid_colors:
|
||||
rgb_palette_nes[c] = (1000000, 1000000, 1000000)
|
||||
nes_palette_colors = []
|
||||
for rgb_color in target_colors:
|
||||
# print(rgb_color)
|
||||
closest_palette_entry = nes_closest_palette_entry(rgb_color, rgb_palette_nes)
|
||||
nes_palette_colors.append(closest_palette_entry)
|
||||
return nes_palette_colors
|
||||
|
||||
|
||||
def get_pal_file_path(pal_file_path: str) -> Path:
|
||||
"""
|
||||
Convert a string path to Pathlib path.
|
||||
If None, use default path and print warning message.
|
||||
If file is missing, print error.
|
||||
|
||||
:param pal_file_path: path to .pal file
|
||||
:return: path as pathlib Path
|
||||
"""
|
||||
default_pal_file_path = get_script_directory() / 'palettes' / 'palgen.pal'
|
||||
if pal_file_path is None:
|
||||
print(f'WARNING: Palette file not specified - falling back to default palette file {str(default_pal_file_path)}')
|
||||
return default_pal_file_path
|
||||
elif not Path(pal_file_path).exists():
|
||||
print(f'WARNING: Palette file {str(Path(pal_file_path))} does not exist - falling back to default palette file {str(default_pal_file_path)}')
|
||||
return default_pal_file_path
|
||||
else:
|
||||
return Path(pal_file_path)
|
||||
|
||||
|
||||
def write_c_lut(lut: List[int], filename: Path):
|
||||
"""
|
||||
Writes NES color mapping as a C-array rgb_to_nes
|
||||
|
||||
:param lut: Lookup table of 64 values, indexed as BBGGRR
|
||||
:param filename: Source text file to write
|
||||
"""
|
||||
with open(filename, 'wt') as f:
|
||||
print('// File auto-generated file by nespal.py', file=f)
|
||||
print(f'unsigned char rgb_to_nes[{len(lut)}] = {{', file=f)
|
||||
for i, c in enumerate(lut):
|
||||
r = ((i >> 0) & 0x3)
|
||||
g = ((i >> 2) & 0x3)
|
||||
b = ((i >> 4) & 0x3)
|
||||
print(f' 0x{c:0{2}X}, // RGB({r},{g},{b}) -> {c:0{2}X} ({get_ppu_color_name(c)})', file=f)
|
||||
print(f'}};', file=f)
|
||||
|
||||
|
||||
def write_c_macro(lut: List[int], filename: Path):
|
||||
"""
|
||||
Writes NES color mapping as a C-macro RGB_TO_NES(c)
|
||||
|
||||
:param lut: Lookup table of 64 values, indexed as BBGGRR
|
||||
:param filename: Source text file to write
|
||||
"""
|
||||
with open(filename, 'wt') as f:
|
||||
print('// File auto-generated file by nespal.py', file=f)
|
||||
print('#ifndef __RGB_TO_NES_MACRO_H__', file=f)
|
||||
print('#define __RGB_TO_NES_MACRO_H__', file=f)
|
||||
print(f'#define RGB_TO_NES(c) \\', file=f)
|
||||
for i, c in enumerate(lut):
|
||||
print(f' (c == 0x{i:0{2}X}) ? 0x{c:0{2}X} : \\', file=f)
|
||||
print(f' 0xFF // out-of-range value - set to 0xFF', file=f)
|
||||
print('#endif', file=f)
|
||||
|
||||
|
||||
def main(palette_file: Path,
|
||||
invalid_colors: List[int],
|
||||
output_c_lut_path: Path,
|
||||
output_c_macro_path: Path) -> int:
|
||||
# Read NES palette mapping file
|
||||
with open(palette_file, 'rb') as f:
|
||||
nes_palette = f.read(192)
|
||||
# Use a 6-bit RGB palette in BBGGRR format
|
||||
gbdk_palette = []
|
||||
for i in range(64):
|
||||
r = (((i >> 0) & 0x3) / 3.0) * 255.0
|
||||
g = (((i >> 2) & 0x3) / 3.0) * 255.0
|
||||
b = (((i >> 4) & 0x3) / 3.0) * 255.0
|
||||
gbdk_palette.append(r)
|
||||
gbdk_palette.append(g)
|
||||
gbdk_palette.append(b)
|
||||
nes_palette_colors = map_to_PPU_colors(gbdk_palette, nes_palette, invalid_colors)
|
||||
if output_c_lut_path is not None:
|
||||
write_c_lut(nes_palette_colors, output_c_lut_path)
|
||||
if output_c_macro_path is not None:
|
||||
write_c_macro(nes_palette_colors, output_c_macro_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=f'Generates lookup tables to convert RGB to NES PPU compatible colors.')
|
||||
parser.add_argument('--pal_file', type=str,
|
||||
default=None,
|
||||
help='Binary 192-byte file specifying a particular NES palette flavor.')
|
||||
parser.add_argument('--output_c_lut', type=str,
|
||||
default='rgb_to_nes_lut.c',
|
||||
help='Output .c file with generated rgb_to_nes lookup table.')
|
||||
parser.add_argument('--output_c_macro', type=str,
|
||||
default='rgb_to_nes_macro.h',
|
||||
help='Output .h file with generated RGB_TO_NES C macro.')
|
||||
parser.add_argument('--invalid_colors', type=str,
|
||||
nargs='+',
|
||||
default=['0D', '0E', '1E', '2E', '3E', '0F', '1F', '2F', '3F'],
|
||||
help='Specify which NES PPU colors should be considered invalid.')
|
||||
args = parser.parse_args()
|
||||
# Call main program
|
||||
rc = main(get_pal_file_path(args.pal_file),
|
||||
[int(s, 16) for s in args.invalid_colors],
|
||||
Path(args.output_c_lut),
|
||||
Path(args.output_c_macro))
|
||||
sys.exit(rc)
|
||||
BIN
gbdk-support/nespal/palettes/palgen.pal
Normal file
BIN
gbdk-support/nespal/palettes/palgen.pal
Normal file
Binary file not shown.
Reference in New Issue
Block a user