NES 模拟器开发教程 11 - PPU 精灵

了解 background 如何绘图之后,sprite 也就简单一些了

1. OAM

细心的人可能会注意到,结合到 PPU 内存映射时会发现,PPU 总线上没有和 sprite 直接相关的信息

  • pattern table:存储图像数据
  • name table:存储 background 在 pattern table 的索引
  • palette:调色板

以上数据没有任何一个指明了 sprite 的信息,那么 sprite 信息是放在哪里的?
这里就要提到 OAM 了,OAM 全称 Object Attribute Memory,位于 PPU 芯片中,一共 256 bytes,一个 sprite 用 4 bytes,所以总共能表示 64 个 sprite。但是 OAM 并不存在于 PPU 或 CPU 总线上,需要 CPU 通过 PPU 寄存器或者 DMA 方式才能写入

  • 寄存器写入
    通过 OAMADDR(0x2003)OAMDATA(0x2004) 写入 OAM 数据,写入前首先通过 OAMADDR 写入起始地址,之后通过 OAMDATA 写入数据,数据可以连续写入,每写入一次地址自动 +1
  • DMA 写入
    通过 OAMDMA(0x4014) 写入 CPU PAGE 地址,之后 DMA 会自动将 CPU 整个 PAGE 的数据拷贝到 OAM 中。CPU PAGE 为 256 bytes,比如往 OAMDMA 写入 2,则会将 CPU 总线上的 0x200 ~ 0x2FF 的数据拷贝到 OAM。另外,DMA 会占用 512 个 CPU 时钟(奇数 CPU 周期还会再加一个时钟,前期可以先不考虑)
    DMA 写入速度快于寄存器写入,所以追求效率的时候会采用此方式

2. Sprite 数据

一个 Sprite 在 OAM 中需要 4 bytes:

  • Byte 0:
    Sprite 的 Y 坐标
  • Byte 1:
    76543210
    ||||||||
    |||||||+- Bank ($0000 or $1000) of tiles
    +++++++-- Tile number of top of sprite (0 to 254; bottom half gets the next tile)
    
    该字节类似于 name table,sprite 有 2 种模式:
    1. 8 x 8
      整个 byte 类似于 name table,由 PPUCTRL 的 bit 3 选取 bank 之后,加上自身数据 x 16 得到偏移量
    2. 8 x 16
      该模式下 PPUCTRL 的 bit 3 不再起作用,bank 由 bit 0 决定,并且偏移量不再是 x 16,而是 x 32,具体参考 http://wiki.nesdev.com/w/index.php/PPU_OAM,讲得非常清楚
  • Byte 2:
    76543210
    ||||||||
    ||||||++- Palette (4 to 7) of sprite
    |||+++--- Unimplemented
    ||+------ Priority (0: in front of background; 1: behind background)
    |+------- Flip sprite horizontally
    +-------- Flip sprite vertically
    
    bit 0-1 决定高 2 bit 的 palette,类似于 attribute table 的功能
    bit 5 决定优先级,如果 sprite 像素和 background 像素都不是透明像素的情况下(即 palette index % 4 != 0),则决定了到底显示 sprite 还是 background
    bit 6-7 决定是否翻转像素,比如人物往右走设置为不翻转,往左走则设置为垂直翻转
  • Byte 3:
    Sprite 的 X 坐标

3. Sprite 0 hits

Sprite 有一个非常特殊的地方,叫 精灵 0 命中,如果不实现这个功能的话,很多游戏都没法正常运行,比如马里奥 1,没实现的情况下,会一直卡在主界面

该功能的作用是,如果 PPU 在渲染的时候,如果 background 的不透明像素与 sprite 0 的不透明像素重叠的时候,会产生 sprite 0 hits,并且在当前帧只会产生一次

同时,产生 hit 也是有条件的,具体参考:http://wiki.nesdev.com/w/index.php/PPU_OAM#Sprite_zero_hits

那么它的作用是什么?主要用来分割屏幕。之前在 background 时介绍过,CPU 通过 PPUSCROLL 来控制背景移动,但是如果希望屏幕上半部分静止,下半部分移动呢?比如马里奥 1:


giphy.gif

可以看到顶部的状态栏始终是静止的,如果 CPU 不知道 PPU 绘制到哪里的情况下,这个功能没有办法实现。通过 sprite 0,放置一个 sprite 到需要的地方,产生 hit 之后,CPU 再修改 PPUSCROLL,就能达到屏幕分割的效果了

4. 时序

这个图前面 2 章看过好几遍了


tim

相对于 background,sprite 时序简单一些

sprite 只在 pre-render line 和 visible line 求值,在 visible line 与 background 一起渲染

PPU 内部有一块 Secondary OAM 内存,大小为 4 * 8 = 32 bytes,用来存储当前扫描线上的 8 个 sprite,这也从侧面说明:PPU 最多支持 8 个 sprite 在一条 scanline 上

  • Scanline 的 1 - 64 周期,会清空 Secondary OAM,将 Secondary OAM 的值全写为 0xFF
  • Scanline 的 65 - 256 周期,对下一条 scanline 的 Secondary OAM 求值,遍历 OAM 内存,将符合对应 scanline 的 OAM 写入 Secondary OAM
  • Scanline 的 257 - 320 周期,通过 Secondary OAM,计算对应 scanline 的 OAM 像素值,用于之后的渲染

详细操作参考:http://wiki.nesdev.com/w/index.php/PPU_sprite_evaluation

5. 总结

至此 PPU 的原理已经全过了一遍了,sprite 内存数据和显示这里也就不展示了,和 09 章对 background 数据举例的流程相同。另外代码也就不举例了,PPU 相比 CPU 确实复杂太多,没法切分为小段代码。最好结合 fceux 强大的调试功能,结合源码查看

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容