58 lines
1.8 KiB
Python
58 lines
1.8 KiB
Python
"""Nintendo Entertainment System (2C02 PPU) master palette.
|
|
|
|
The NES has no fixed RGB palette -- the PPU generates an NTSC signal. We
|
|
reproduce MAME's exact ``nespal_to_RGB`` (YUV->RGB) formula so the encoder
|
|
matches what the emulator renders. A palette value is a 6-bit index 0-63:
|
|
high nibble = luminance (0-3), low nibble = hue (0-15, with 0/13/14/15 greyscale).
|
|
The byte written to PPU palette RAM IS this index, so ``PALETTE``/``GREYS`` are
|
|
indexed by the hardware value.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
import numpy as np
|
|
|
|
from ..palette import srgb_to_lab
|
|
|
|
_TINT, _HUE = 0.22, 287.0
|
|
_Kr, _Kb, _Ku, _Kv = 0.2989, 0.1145, 2.029, 1.140
|
|
_BRIGHT = [[0.50, 0.75, 1.0, 1.0],
|
|
[0.29, 0.45, 0.73, 0.9],
|
|
[0.0, 0.24, 0.47, 0.77]]
|
|
|
|
|
|
def _rgb(intensity: int, num: int):
|
|
if num == 0:
|
|
sat = rad = 0.0; y = _BRIGHT[0][intensity]
|
|
elif num == 13:
|
|
sat = rad = 0.0; y = _BRIGHT[2][intensity]
|
|
elif num in (14, 15):
|
|
sat = rad = y = 0.0
|
|
else:
|
|
sat = _TINT; rad = math.radians(num * 30 + _HUE); y = _BRIGHT[1][intensity]
|
|
u, v = sat * math.cos(rad), sat * math.sin(rad)
|
|
R = (y + _Kv * v) * 255.0
|
|
G = (y - (_Kb * _Ku * u + _Kr * _Kv * v) / (1 - _Kb - _Kr)) * 255.0
|
|
B = (y + _Ku * u) * 255.0
|
|
cl = lambda x: max(0, min(255, int(math.floor(x + 0.5))))
|
|
return (cl(R), cl(G), cl(B))
|
|
|
|
|
|
PALETTE = np.array([_rgb(i >> 4, i & 0x0F) for i in range(64)], dtype=np.float64)
|
|
|
|
# Distinct grey ramp (R==G==B), sorted dark->light, deduped by luminance.
|
|
_grey = {}
|
|
for _i in range(64):
|
|
r, g, b = PALETTE[_i]
|
|
if r == g == b:
|
|
_grey.setdefault(int(r), _i)
|
|
GREYS = [_grey[k] for k in sorted(_grey)] # e.g. $0F,$1D,$2D,$10,$20
|
|
|
|
|
|
def get_palette() -> np.ndarray:
|
|
return PALETTE
|
|
|
|
|
|
def palette_lab() -> np.ndarray:
|
|
return srgb_to_lab(PALETTE)
|