274. NES 调色板 RGB

定义一个最小化的 NES RGB 调色板近似值。

274 / 356 · tests/chapter_05_rendering_pipeline/test_274_nes_palette_rgb.py

参考调色板文件

https://www.nesdev.org/wiki/File:2C02G_U_wiki.pal

待创建的文件

emulator/rendering/nes_palette.py

为什么需要这一步

渲染管线现在可以把颜色索引网格转换成 Framebuffer 数据,但调用者仍然需要手动提供 RGB 调色板。

这一步定义了一个实用的 64 色 NES RGB 调色板近似值

NES color index $00-$3F -> RGB tuple

重要的硬件模型

NES 本身并不直接存储 RGB 颜色。PPU 拥有存放 NES 颜色索引的调色板 RAM,PPU 的模拟视频电路再把这些索引转换成视频信号。在模拟器中,我们用 RGB 元组来近似这种输出。

近似真实的流程

pixel palette entry
    -> PPU palette RAM value $00-$3F
    -> PPU hardware color generator / analog video signal
    -> TV color

模拟器的流程

pixel palette entry
    -> PPU palette RAM value $00-$3F
    -> NES_PALETTE_RGB[index]
    -> Framebuffer RGB pixel

什么是 2C02G_U_wiki.pal?2C02G 是一种 NTSC 制式的 NES PPU 版本。引用的 .pal 文件是针对该 PPU/输出行为的 RGB 调色板近似值,并不是 NES 硬件内部真实存储的 RGB 表。

为什么源文件的颜色数超过 64 种?有些 .pal 文件包含了 PPUMASK 强调(emphasis)变体。NES 基础颜色索引范围是 64 种颜色,但强调效果可以产生 8 种显示变体:

64 base colors * 8 emphasis states = 512 RGB entries

在本教程步骤中,只使用前 64 个 RGB 条目,也就是没有强调效果的普通调色板。强调支持可以在以后渲染使用 PPUMASK 第 5-7 位时再添加。

如何从下载的 .pal 文件中重现这张表:

file = "2C02G_U_wiki.pal"
data = open(file, "rb").read()

for i in range(0, 64 * 3, 3):
    rgb = data[i:i + 3]
    if len(rgb) == 3:
        r, g, b = rgb[0], rgb[1], rgb[2]
        print(f"({r},{g},{b}),")

建议的实现示例

from emulator.rendering.framebuffer import RGBColor

NES_PALETTE_SIZE = 64

NES_PALETTE_RGB: list[RGBColor] = [
    (87,87,87),     (0,12,142),     (8,0,166),      (52,0,150),      (85,0,97),      (99,0,21),      (90,0,0),       (60,14,0),
    (17,40,0),      (0,59,0),       (0,66,0),       (0,58,5),        (0,38,82),      (0,0,0),        (0,0,0),        (0,0,0),

    (165,165,165),  (0,65,217),     (47,30,255),    (103,4,242),     (148,0,180),    (170,0,87),     (163,24,0),     (128,57,0),
    (75,91,0),      (19,118,0),     (0,129,0),      (0,121,35),      (0,98,136),     (0,0,0),        (0,0,0),        (0,0,0),

    (255,255,255),  (74,159,255),   (121,126,255),  (175,99,255),    (221,85,255),   (247,87,194),   (247,106,99),   (220,136,16),
    (174,169,0),    (120,196,0),    (74,210,17),    (47,207,100),    (47,189,196),   (65,65,65),     (0,0,0),        (0,0,0),

    (255,255,255),  (185,221,255),  (202,209,255),  (222,198,255),   (240,192,255),  (252,192,238),  (253,198,202),  (245,208,170),
    (228,221,149),  (208,232,146),  (189,238,162),  (178,238,192),   (176,232,227),  (179,179,179),  (0,0,0),        (0,0,0),
]

def get_nes_rgb_color(index: int) -> RGBColor:
    return NES_PALETTE_RGB[index & 0x3F]

为什么要用 $3F 做掩码?

NES 颜色索引是 6 位的值

$00-$3F

掩码可以让查找保持在合法范围内

index & 0x3F

示例

get_nes_rgb_color(0x40) == get_nes_rgb_color(0x00)
get_nes_rgb_color(0x41) == get_nes_rgb_color(0x01)

本步骤不涉及

  • PPUMASK 强调颜色
  • PAL/Dendy 调色板
  • CRT/NTSC 信号模拟
  • 在 $3F00-$3F1F 处查找 PPU 调色板 RAM
  • pygame 显示

运行本课

uv run pytest tests/chapter_05_rendering_pipeline/test_274_nes_palette_rgb.py -v