NES 模拟器开发教程 09 - PPU 背景

前一节讲过 PPU 分为背景和精灵两个部分,这一节介绍 PPU 背景渲染方法

1. 概念

在了解渲染方式之前,需要接触 PPU 中的几个概念:

  • Pattern table
    位于 PPU 总线的 0x0000 - 0x1FFF,一共 8KB 的数据,
    它保存了图像数据,同时可以分为两个 4KB 的区块,可以由程序控制 backgroud 或者 sprite 使用前 4KB 还是后 4KB
  • Name table
    位于 PPU 总线的 0x2000 - 0x2FFF,之前讲过了,它控制着背景图像,一共 4KB,分 4 块,每块 1KB。其中 2KB 由 NES 主机提供,另外 2KB 可以由卡带提供,或者做为主机提供 2KB RAM 的 mirror。比如,垂直镜像如图所示:
    image.png

    Name table 数据做为 Pattern table 的索引,以此达到压缩数据的目的
    另外需要注意的是,每一块 Name table 只有 960 字节,而非 1KB,剩下的 64 字节为下面要介绍的 Attribute table
  • Attribute table
    Attribute table 跟在每个 Name table 之后,每个 Attribute table 有 64 字节
    Attribute table 控制着当前图像高 2bit 的 palette 偏移量,而 Name table 索引到 Pattern table 数据之后,控制着低 2bit 的 palette 偏移量。两个加起来刚好 4bit,前一章节讲过,背景和精灵分别有 16 个 palette ,刚好对应了 4bit

2. 渲染

我们之前了解过,PPU VRAM 一共 4KB,每 1KB 就控制着一张图像,也就是说,另外 3KB 图像始终是不会输出到屏幕上的。同时这 4 张图像以田字格的形式组合,再结合 PPU 的滚动功能,就能实现背景画面的上下左右移动

下面针对每一帧图像(每一块 VRAM),看看 PPU 是如何渲染的:

NES 以 8 x 8 像素为单位,将图片分成了 32 x 30 个小块,每一个小块称为一个瓦片(tile),同时,每 16 个 tile 组成一个大块,如下图:

image.png

每一个 tile 在 vram 中用一个 byte 表示,该字节表示了当前 tile 在 pattern table 中的偏移量,也就表示像素的数据。pattern table 以 16 bytes 为单位,每 16 bytes 中,有分前 8 bytes 和后 8 bytes。前后每 8 bytes 表示 tile 中每一行的 像素所处 palette 的低 2 bit,前 8 bytes 为 bit0,后 8 byts 为 bit 1

颜色高 2bit 由 attribute table 表示,attribute table 中每一个字节能管理 16 个 tile 的颜色,如上图中红色的大块。16 个 tile 中,每 4 个 tile 整合到一起,再和另外 12 个 tile 组成田字形格局,分别为 左上,右上,左下,右下。每个占 2 bit,刚好 8 bit

这里也能看到 NES 画面表现力不足,即每 4 个 tile 组成的部分田字格中,他们只能有 4 种不同的颜色,因为高 2 bit 固定了,剩下可变了只有低 2 bit

现在总结一下:图像分成了 32 x 30 = 960 个 tile,每个 tile 在 name table 占前 960 字节。同时 tile 在 vram 中表示 pattern table 0 - 255 的偏移量,pattern table 又以 16 bytes 为一个单位,那么总共需要 256 * 16 = 4KB 大小的 pattern table。前面讲过,pattern table 一共 8KB,可分为两个 4KB 分别给 background 或者 sprite 使用,是不是刚刚好?另外 16 个 tile 组成的大的 tile 中,每个由 attribute table 的一个字节表示,一共需要 8 x 8 = 64 bytes。加上 name table 的 960,刚好 64 + 960 = 1024 字节,即 1KB VRAM

通过如此巧妙的设计,硬生生的将一个 320 x 240 的画面压缩到了 1KB,不得不服!

3. 举例

前面说这么多,不如找个例子看看,以 马里奥 1 为例:
点开 Name table viewer:


image.png

以左上角的 'M' 为例,看看内存中的数据

注意 fceux 默认情况下会去除顶部和底部的 8 个像素,具体可以在显示里面设置关掉,或者直接在 Name table viewer 中查看

左上角的 'M' 数一下,大概是第 3 行第 3 列 tile,那么它在 VRAM 中 offset 为 32 * 2 + 3 = 0x43。并且位于左上角的 VRAM,那么 Name table 起始地址为 0x2000,最终 'M' 的地址为 0x2043,查看下改地址的数据:

image.png

数据为 0x16,那么对应 pattern table offset 为 0x16 * 16 = 0x0160。下面需要计算 pattern table 的起始地址,之前讲过,background 和 sprite 可以分别设置为 pattern table 的前 4k 或者后 4k,这个通过 PPU 寄存器 PPUCTRL(0x2000)的 bit 3-4 控制,background 则为 bit4,具体可见:http://wiki.nesdev.com/w/index.php/PPU_registers#PPUCTRL
。那么要知道起始地址,则要先读取 PPUCTRL 寄存器的值,切换到 NES memory,跳转到 0x2000:
image.png

看到值是 0x90,bit 4 为 1,根据寄存器定义:
image.png

得知 background 使用的是后 4KB,则 'M' 在 pattern table 中真实起始地址为:0x1000 + 0x0160 = 0x1160,切换回 PPU memory,跳转到该地址看看:
image.png

之前说过 pattern table 以 16 bytes 为一组,所以 1160 这一排都是 M 的数据,我们分为 2 个 8 byes 写成二进制看看:

# 前 8 bytes
c6: 1 1 0 0 0 1 1 0
ee: 1 1 1 0 1 1 1 0
fe: 1 1 1 1 1 1 1 0
fe: 1 1 1 1 1 1 1 0
d6: 1 1 0 1 0 1 1 0
c6: 1 1 0 0 0 1 1 0
c6: 1 1 0 0 0 1 1 0

# 后 8 bytes
00: 0 0 0 0 0 0 0 0
00: 0 0 0 0 0 0 0 0
00: 0 0 0 0 0 0 0 0
00: 0 0 0 0 0 0 0 0
00: 0 0 0 0 0 0 0 0
00: 0 0 0 0 0 0 0 0
00: 0 0 0 0 0 0 0 0
00: 0 0 0 0 0 0 0 0

前面 8 bytes 的 1 组合起来是不是有点像 M?
之前说过,前 8 bytes 表示 bit 0 的颜色数据,后 8 bytes 表示 bit 1 的颜色数据,前 2 bit 则需要从 attribute table 中获取,第 2 行第 3 列刚好位于 attribute 0 的右下,attribute table 紧跟在 name table 的 960 bytes 之后,则 attribute 0 数据为 0x2000 + 960 = 0x23c0,具体数据为 0xAA:


image.png

右下角为 bit 0-1,则高 2 bit 为 10
那么整合起来,M 中的背景色在 palette 中偏移量为 0b1000 = 8,前景色在 palette 中偏移量为 0b1001 = 9
之前讲过,background palette 位于 0x3F00

注意:palette 以 4 个 bytes 为单位,0 号 byte 是共用的,比如 0x3F00 为 0x12,则 0x3F04, 0x3F08, 0x3F0C 都是 0x12,所以只要遇到 palette 地址 % 4 为 0 的时候,直接取 0x3F00 或者 0x3F10 的值就行了。同时 0 号 byte 也表示透明色,这个在后面介绍 sprite 优先级的时候会遇到

那么对于 M 的颜色,内存中数据为:


image.png
  • 背景色:0x22
  • 前景色:0x30
    对照这张表看看:


    image.png

    刚好是蓝色底,白色字,和 'M' 的显示一模一样

4. PPU 滚动

之前介绍的都是静态的情况,实际上游戏过程中画面都是运动的,这就靠 PPU 滚动来完成
之前说过,PPU 一共 4 个 1KB 的 VRAM,他们组成田字布局,把屏幕想像成窗口,PPU 滚动的时候就相当于窗口在田字格上滑动,类似于这种效果:


NTS_scrolling_seam.gif

以 NTFS 为例,图像刷新率为 60fps,PPU 会在 1s 内产生 60 次中断告知 CPU 刷新图像(这对应了之前介绍的垂直消隐), CPU 会设置 PPUSCROLL 寄存器以更新图像位置,一次达到图像运动的目的。具体可以在 fceux 中打开 Name table viewer 很形象地看到

具体在 PPU 还有 v,t,x,w 几个内部的寄存器用于滚动,这里做为一个抛砖引玉,具体要介绍起来篇幅就太长了,建议直接看 NDEDEV 上的这篇文章:http://wiki.nesdev.com/w/index.php/PPU_scrolling
里面详细介绍了 PPU 滚动机制,甚至伪代码都帮你写好了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。