前一節(jié)講過(guò) PPU 分為背景和精靈兩個(gè)部分方援,這一節(jié)介紹 PPU 背景渲染方法
1. 概念
在了解渲染方式之前蕴纳,需要接觸 PPU 中的幾個(gè)概念:
-
Pattern table
位于 PPU 總線的 0x0000 - 0x1FFF照筑,一共 8KB 的數(shù)據(jù),
它保存了圖像數(shù)據(jù)晨雳,同時(shí)可以分為兩個(gè) 4KB 的區(qū)塊毫目,可以由程序控制 backgroud 或者 sprite 使用前 4KB 還是后 4KB -
Name table
位于 PPU 總線的 0x2000 - 0x2FFF,之前講過(guò)了师妙,它控制著背景圖像诵肛,一共 4KB,分 4 塊默穴,每塊 1KB怔檩。其中 2KB 由 NES 主機(jī)提供,另外 2KB 可以由卡帶提供蓄诽,或者做為主機(jī)提供 2KB RAM 的 mirror薛训。比如,垂直鏡像如圖所示:
image.png
Name table 數(shù)據(jù)做為 Pattern table 的索引仑氛,以此達(dá)到壓縮數(shù)據(jù)的目的
另外需要注意的是乙埃,每一塊 Name table 只有 960 字節(jié),而非 1KB锯岖,剩下的 64 字節(jié)為下面要介紹的 Attribute table -
Attribute table
Attribute table 跟在每個(gè) Name table 之后介袜,每個(gè) Attribute table 有 64 字節(jié)
Attribute table 控制著當(dāng)前圖像高 2bit 的 palette 偏移量,而 Name table 索引到 Pattern table 數(shù)據(jù)之后出吹,控制著低 2bit 的 palette 偏移量遇伞。兩個(gè)加起來(lái)剛好 4bit,前一章節(jié)講過(guò)趋箩,背景和精靈分別有 16 個(gè) palette 赃额,剛好對(duì)應(yīng)了 4bit
2. 渲染
我們之前了解過(guò)加派,PPU VRAM 一共 4KB叫确,每 1KB 就控制著一張圖像,也就是說(shuō)芍锦,另外 3KB 圖像始終是不會(huì)輸出到屏幕上的竹勉。同時(shí)這 4 張圖像以田字格的形式組合,再結(jié)合 PPU 的滾動(dòng)功能娄琉,就能實(shí)現(xiàn)背景畫(huà)面的上下左右移動(dòng)
下面針對(duì)每一幀圖像(每一塊 VRAM)次乓,看看 PPU 是如何渲染的:
NES 以 8 x 8 像素為單位,將圖片分成了 32 x 30 個(gè)小塊孽水,每一個(gè)小塊稱(chēng)為一個(gè)瓦片(tile)票腰,同時(shí),每 16 個(gè) tile 組成一個(gè)大塊女气,如下圖:
每一個(gè) tile 在 vram 中用一個(gè) byte 表示杏慰,該字節(jié)表示了當(dāng)前 tile 在 pattern table 中的偏移量,也就表示像素的數(shù)據(jù)。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 中每一個(gè)字節(jié)能管理 16 個(gè) tile 的顏色擎颖,如上圖中紅色的大塊榛斯。16 個(gè) tile 中,每 4 個(gè) tile 整合到一起搂捧,再和另外 12 個(gè) tile 組成田字形格局肖抱,分別為 左上,右上异旧,左下意述,右下。每個(gè)占 2 bit吮蛹,剛好 8 bit
這里也能看到 NES 畫(huà)面表現(xiàn)力不足荤崇,即每 4 個(gè) tile 組成的部分田字格中,他們只能有 4 種不同的顏色潮针,因?yàn)楦?2 bit 固定了术荤,剩下可變了只有低 2 bit
現(xiàn)在總結(jié)一下:圖像分成了 32 x 30 = 960 個(gè) tile,每個(gè) tile 在 name table 占前 960 字節(jié)每篷。同時(shí) tile 在 vram 中表示 pattern table 0 - 255 的偏移量瓣戚,pattern table 又以 16 bytes 為一個(gè)單位,那么總共需要 256 * 16 = 4KB 大小的 pattern table焦读。前面講過(guò)子库,pattern table 一共 8KB,可分為兩個(gè) 4KB 分別給 background 或者 sprite 使用矗晃,是不是剛剛好仑嗅?另外 16 個(gè) tile 組成的大的 tile 中,每個(gè)由 attribute table 的一個(gè)字節(jié)表示张症,一共需要 8 x 8 = 64 bytes仓技。加上 name table 的 960,剛好 64 + 960 = 1024 字節(jié)俗他,即 1KB VRAM
通過(guò)如此巧妙的設(shè)計(jì)脖捻,硬生生的將一個(gè) 320 x 240 的畫(huà)面壓縮到了 1KB,不得不服兆衅!
3. 舉例
前面說(shuō)這么多地沮,不如找個(gè)例子看看颜价,以 馬里奧 1 為例:
點(diǎn)開(kāi) Name table viewer:
以左上角的 'M' 為例,看看內(nèi)存中的數(shù)據(jù)
注意 fceux 默認(rèn)情況下會(huì)去除頂部和底部的 8 個(gè)像素诉濒,具體可以在顯示里面設(shè)置關(guān)掉周伦,或者直接在 Name table viewer 中查看
左上角的 'M' 數(shù)一下,大概是第 3 行第 3 列 tile未荒,那么它在 VRAM 中 offset 為 32 * 2 + 3 = 0x43专挪。并且位于左上角的 VRAM,那么 Name table 起始地址為 0x2000片排,最終 'M' 的地址為 0x2043寨腔,查看下改地址的數(shù)據(jù):
數(shù)據(jù)為 0x16,那么對(duì)應(yīng) pattern table offset 為 0x16 * 16 = 0x0160率寡。下面需要計(jì)算 pattern table 的起始地址迫卢,之前講過(guò),background 和 sprite 可以分別設(shè)置為 pattern table 的前 4k 或者后 4k冶共,這個(gè)通過(guò) PPU 寄存器 PPUCTRL(0x2000)的 bit 3-4 控制乾蛤,background 則為 bit4,具體可見(jiàn):http://wiki.nesdev.com/w/index.php/PPU_registers#PPUCTRL
捅僵。那么要知道起始地址家卖,則要先讀取 PPUCTRL 寄存器的值,切換到 NES memory庙楚,跳轉(zhuǎn)到 0x2000:
看到值是 0x90上荡,bit 4 為 1,根據(jù)寄存器定義:
得知 background 使用的是后 4KB馒闷,則 'M' 在 pattern table 中真實(shí)起始地址為:0x1000 + 0x0160 = 0x1160酪捡,切換回 PPU memory,跳轉(zhuǎn)到該地址看看:
之前說(shuō)過(guò) pattern table 以 16 bytes 為一組纳账,所以 1160 這一排都是 M 的數(shù)據(jù)逛薇,我們分為 2 個(gè) 8 byes 寫(xiě)成二進(jìn)制看看:
# 前 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 組合起來(lái)是不是有點(diǎn)像 M?
之前說(shuō)過(guò)塞祈,前 8 bytes 表示 bit 0 的顏色數(shù)據(jù)金刁,后 8 bytes 表示 bit 1 的顏色數(shù)據(jù)帅涂,前 2 bit 則需要從 attribute table 中獲取议薪,第 2 行第 3 列剛好位于 attribute 0 的右下,attribute table 緊跟在 name table 的 960 bytes 之后媳友,則 attribute 0 數(shù)據(jù)為 0x2000 + 960 = 0x23c0斯议,具體數(shù)據(jù)為 0xAA:
右下角為 bit 0-1,則高 2 bit 為 10
那么整合起來(lái)醇锚,M 中的背景色在 palette 中偏移量為 0b1000 = 8哼御,前景色在 palette 中偏移量為 0b1001 = 9
之前講過(guò)坯临,background palette 位于 0x3F00
注意:palette 以 4 個(gè) bytes 為單位,0 號(hào) byte 是共用的恋昼,比如 0x3F00 為 0x12看靠,則 0x3F04, 0x3F08, 0x3F0C 都是 0x12,所以只要遇到 palette 地址 % 4 為 0 的時(shí)候液肌,直接取 0x3F00 或者 0x3F10 的值就行了挟炬。同時(shí) 0 號(hào) byte 也表示透明色,這個(gè)在后面介紹 sprite 優(yōu)先級(jí)的時(shí)候會(huì)遇到
那么對(duì)于 M 的顏色嗦哆,內(nèi)存中數(shù)據(jù)為:
- 背景色:0x22
-
前景色:0x30
對(duì)照這張表看看:
image.png
剛好是藍(lán)色底谤祖,白色字,和 'M' 的顯示一模一樣
4. PPU 滾動(dòng)
之前介紹的都是靜態(tài)的情況老速,實(shí)際上游戲過(guò)程中畫(huà)面都是運(yùn)動(dòng)的粥喜,這就靠 PPU 滾動(dòng)來(lái)完成
之前說(shuō)過(guò),PPU 一共 4 個(gè) 1KB 的 VRAM橘券,他們組成田字布局额湘,把屏幕想像成窗口,PPU 滾動(dòng)的時(shí)候就相當(dāng)于窗口在田字格上滑動(dòng)旁舰,類(lèi)似于這種效果:
以 NTFS 為例缩挑,圖像刷新率為 60fps,PPU 會(huì)在 1s 內(nèi)產(chǎn)生 60 次中斷告知 CPU 刷新圖像(這對(duì)應(yīng)了之前介紹的垂直消隱)鬓梅, CPU 會(huì)設(shè)置 PPUSCROLL 寄存器以更新圖像位置供置,一次達(dá)到圖像運(yùn)動(dòng)的目的。具體可以在 fceux 中打開(kāi) Name table viewer 很形象地看到
具體在 PPU 還有 v绽快,t芥丧,x,w 幾個(gè)內(nèi)部的寄存器用于滾動(dòng)坊罢,這里做為一個(gè)拋磚引玉续担,具體要介紹起來(lái)篇幅就太長(zhǎng)了,建議直接看 NDEDEV 上的這篇文章:http://wiki.nesdev.com/w/index.php/PPU_scrolling
里面詳細(xì)介紹了 PPU 滾動(dòng)機(jī)制活孩,甚至偽代碼都幫你寫(xiě)好了