278. 属性表调色板选择

从名称表的属性表中解码背景调色板选择。

278 / 356 · tests/chapter_05_rendering_pipeline/test_278_attribute_table_palette_selection.py

参考

https://www.nesdev.org/wiki/PPU_attribute_tables

待创建的文件

emulator/rendering/attribute_table.py

为什么需要这一步

目前的名称表渲染器对每个图块都使用同一个共享的 4 色调色板。而真实的 NES 背景会通过名称表的属性表在四种背景子调色板之间进行选择。

本步骤还不会真正用属性表来渲染,它只回答一个很小的问题:

For tile coordinate (tile_x, tile_y), which palette ID does the attribute
table select?

什么是属性表?

每个名称表都有

960 bytes tile IDs
64 bytes attribute table

属性表是一个 8x8 字节的网格,每个属性字节覆盖一个 4x4 图块的区域,也就是 32x32 像素。

一个属性字节被划分为四个 2x2 图块的象限

+-----------------------+
| top-left | top-right  |
|  2x2     |   2x2      |
+----------+------------+
| bottom-l | bottom-r   |
|  2x2     |   2x2      |
+-----------------------+

每个象限存储一个 2 位的调色板 ID

0b00 -> palette 0
0b01 -> palette 1
0b10 -> palette 2
0b11 -> palette 3

重要的区别

属性表既不存储 RGB 颜色,也不存储 NES 颜色索引,它只是选择要使用哪一个背景子调色板。之后,调色板 RAM 和 NES RGB 调色板会把这种选择转换为真正的颜色。

一个属性字节内部的位布局

bits 0-1 -> top-left quadrant
bits 2-3 -> top-right quadrant
bits 4-5 -> bottom-left quadrant
bits 6-7 -> bottom-right quadrant

本步骤实现:从字节中解包/读取一个象限。

易读的实现示例

TABLE_SIZE = 64
BYTES_PER_ROW = 8


def get_attribute_palette_id(
    attribute_table: bytes,
    tile_x: int,
    tile_y: int,
) -> int:
    if len(attribute_table) != TABLE_SIZE:
        raise ValueError("Attribute table must be 64 bytes")

    attribute_x = tile_x // 4
    attribute_y = tile_y // 4
    attribute_index = attribute_y * BYTES_PER_ROW + attribute_x
    attribute_byte = attribute_table[attribute_index]

    quadrant_x = (tile_x % 4) // 2
    quadrant_y = (tile_y % 4) // 2

    is_top_left = quadrant_x == 0 and quadrant_y == 0
    is_top_right = quadrant_x == 1 and quadrant_y == 0
    is_bottom_left = quadrant_x == 0 and quadrant_y == 1

    if is_top_left:
        return attribute_byte & 0b11

    if is_top_right:
        return (attribute_byte >> 2) & 0b11

    if is_bottom_left:
        return (attribute_byte >> 4) & 0b11

    return (attribute_byte >> 6) & 0b11

为什么是 tile_x // 4 和 tile_y // 4?因为每个属性字节覆盖 4x4 个图块。

为什么是 (tile_x % 4) // 2?

在那个 4x4 区域内部

tile positions 0,1 belong to quadrant 0
tile positions 2,3 belong to quadrant 1

所以

0 % 4 // 2 -> 0
1 % 4 // 2 -> 0
2 % 4 // 2 -> 1
3 % 4 // 2 -> 1

本步骤不涉及

  • 使用属性渲染名称表
  • PPU 调色板 RAM 查找
  • RGB 调色板转换
  • 滚动
  • 精灵
  • pygame 显示

运行本课

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