グラフィック
目次
アーキテクチャ
ハードウェアの流れ
ファミコンのグラフィックは、PPU(PictureProcessingUnit)で統括されています。バックグラウンド(BG)とスプライト(Sprite)と言う2つのレイヤを組み合わせることによって、色々なグラフィックを表示させることになります。主にBGは背景マップの表示に使われ、スプライトはマップ上のオブジェクトに使われることが多いです。
ファミコンがグラフィックを描画する時は、PPUは「ネームテーブル」で設定された情報を使って「パターンテーブル」から実際のキャラクタを貼り付けます。そしてその貼り付けたキャラクタに「属性テーブル」で指定された情報から「パレットテーブル」のパレットを使って色を加えて表示される仕組みとなっています。
VRAMアクセス方法
PPUが支配するVRAM領域に対してはCPUから直接アクセスが出来ないため、I/Oポートを使うことになります。手順を説明すると、I/O【$2006】を使ってアクセスしたいVRAMのアドレスを登録します。そしてI/O【$2007】に対して書き込みと読み込みを行うことでアクセスする事が出来ます。VRAMに読み書きを行うとI/O【$2000】の2bit目の値によって、現在指しているVRAMのアドレスが1byteまたは32byteインクリメントされます。
ファミコンの画面サイズは 256 x 240 ピクセルで1秒間に60回画面が更新されますが、実際には240ラインを描画した後に20ライン程分の時間何も描画しない特殊な時間があります。その時間を「VBlank」と言って、通常VRAMにアクセスするのはこの期間内にするべきとされています。VBlank中かどうかはI/O【$2002】のステータスビットで確認できる他、VBlankが始まる時にNMI割り込みを発生させることも出来ます。ちなみに、VBlank期間外にVRAMにデータを書き込もうとすると容赦なく画面がバグりますので注意してください。
画面構成
レイアウト
ファミコンの画面構造は、4枚の横32ブロック縦30ブロックの仮想画面を上下左右に繋ぎ合わせたような構造になっています。ただ、実際には「仮想画面」であってちゃんとメモリバッファを持つ実画面は2枚だけとなります。(高性能なマッパーを使うと実画面を4枚に出来るようですが…)
それぞれの画面には「ネームテーブル」と「属性テーブル」があります。ネームテーブルは表示するキャラクタ番号をレイアウト、属性テーブルはキャラクタに着色するレイアウトを指定するテーブルとなっていますが、それらの詳細はページの下方を参照してください。本ページではそれら2個のテーブルをまとめて「グラフィックテーブル」と呼ぶことにします。
4画面のメモリ空間を説明すると、まず画面1のグラフィックテーブルはVRAM【$2000-$23FF】にマッピングされています。次にスクリーンバッファ#2と#3はINESヘッダによって、VRAM【$2400-$27FF】が画面2と画面3のどちらのグラフィックテーブルにマッピングされるが変わります。垂直画面ミラーに設定の場合は画面3が画面1のミラー、画面4が画面2のミラーになり、水平画面ミラーに設定した場合は画面2が画面1のミラー、画面4が画面3のミラーとなります。
スクロール
グラフィックテーブルにデータを敷き詰めただけではまだ情報が足りません。さらに「表示する範囲」を教えなければならないからです。この表示する範囲を設定するためにはスクロールレジスタI/O【$2005】にスクロールポジションを座標を書き込むことで実現します。
ラスタスクロール
普通は画面の更新はVBlank期間中に行いますが、このスクロールレジスタに関してはVBlank期間外にスクロール値を更新することができます。その時どうなるかと言うと、PPUが次描画しようとしているラインのスクロール情報が更新されますが今まで描画したラインはそのままなので、上のラインと下のラインがずれてしまいます。これを任意のタイミングで意図的に行う技術を「ラスタスクロール」と言います。
ラスタスクロールは多くのゲームに使われています。例えば、画面の上にはマップを表示して下の方に現在の状態を表示したい時などがそうです。しかし、どうすれば「任意のタイミング」でラスタスクロールを起こすことが出来るのか、次に3通りの方法論を挙げてみます。
- 0番スプライトは特殊な扱いを受けており、0番スプライトが描画されている間はステータスレジスタI/O【$2002】にフラグが立つので、これをラスタスクロールを行いたい座標に設置すればタイミングを取ることができます。問題点は、0番スプライトに近づいたライン辺りからステータスレジスタをポーリングしなければならないと言う点です。その為、結局はタイミング処理がややこしくなってしまいます。
- NintendoMMC3(マッパー4)等の特殊なマッパーに設定を行う事で、任意のラインでIRQ割り込みを発生させる事が出来ます。メインの処理にポーリングをする必要がないので非常に便利で処理効率も良いですが、問題点はマッパーを使わなければならない点です。さらに、この辺りの動きはエミュレータによってかなり動きが違うので、どういう処理が「正しい」のかいまいち分かりにくいです。
- 根性でCPUの命令からクロック計算をしてタイミングを計る方法です。例えばウネウネの画面にしたい等ほぼ毎ライン更新しなければならず、0番スプライトを設置したりIRQ割り込みを設定したりをする暇が無い場合に有効です。問題点はかなり厳密にクロックを計算しないと画面が乱れてしまう事です。分岐命令があれば、どちらにジャンプしてもNOP命令等を使ってうまく同期させる必要があります。現在の常識で考えるとかなり無理がありますよね…。
このように、ラスタスクロールは原理的にはそれほど難しくないですが、実装にはそれなりのテクニックが要ると思います。私自身、特に1番の方法でどのようにゲーム進行ルーチンとタイミング処理を共存させて処理しているのか分かりません。ただ、制限の多いファミコンにおいてこのスクロールレジスタをどのように制御するかは、画面クオリティに大きく貢献するので色々と実験してみてください。
データ構造
パターンテーブル
パターンテーブルは、表示に使われるキャラクタパターンを保存するテーブルです。通常コンピュータにはROMに「A」「B」「C」等といったフォントデータが入っています。ファミコンの場合は、「絵」としてキャラクタを使うことでグラフィックを表現します。ファミコンの場合キャラクタの単位は、8 x 8 ピクセルで、1ピクセルの情報量が2bitのフォーマットとなっています。
パターンテーブルはVRAM【$0000-$1FFF】にマッピングされています。BGやスプライトなど全てのグラフィックは、このキャラクタの組み合わせによって表現されます。キャラクタの持つ情報はルックアップテーブルであり、青や赤と言った最終的な色情報は持っていません。キャラクタにパレットから色を塗る事で最終的に人間が見る「画像」となります。イメージとしてはキャラクタは金型に近いかもしれません。その金型に任意の色を流し込むことで同じ金型でいろんな絵を作り出すことが可能です。ファミコンゲームでよくある「姿は同じだが色違いのモンスター」等は同じキャラクタエントリを使いまわし、違う色を流し込むことで情報量を節約しているわけです。
ネームテーブルが1バイトで1キャラクタとなっているので、同時に参照できるキャラクタ数は256種類となります。VRAM【$0000-$1FFF】にマップされた512個のキャラクタデータはBGに256個、スプライトに256個を割り当てます。VRAM【$0000-$0FFF】をBGとして使い、VRAM【$1000-$1FFF】をスプライトとして使う、または逆にするというように設定します。BGとスプライトのベースアドレス設定はI/Oポート【$2000】で設定する事が出来ます。
パレットテーブル
画面に使うパレットの組み合わせを保存するテーブルです。ファミコンのパレットテーブルはVRAM【$3F00-$3F1F】にマッピングされています。
ファミコンは52色のパレットを持っていますが、キャラクタに色情報は含まれていないので、表示の段階でパレットテーブルで定義された色を塗ることになります。
ファミコンの場合1キャラクタ中に4色塗ることが出来ますので、その色パターンを52色の中から選ぶ事になります。
ファミコン固有の52個の原色のどれを使用するかを4個組み合わせたのがパレット1個の単位となっています。4バイトBG用に4パレット、スプライト用に4パレット定義することが出来ます。つまり計算上は画面中にBGに16色、スプライトに16色を最大同時に表示できることとなりますが、実際にはパレットそれぞれの最初の色VRAM【$3F00, $3F04, $3F08...】については、背景色となり少し扱いが変わりますので実際にはBGは背景色を含めて全部で13色、スプライトは背景色が透明色となるので12色となります。
ネームテーブル
ネームテーブル、どのブロック位置にどのキャラクタを配置するかを指定します。例えばVRAM【$2043】へデータ$24が入っていた場合は、画面1の左上から見たブロック座標(3, 2)へ$24番のキャラクタが埋め込まれます。
属性テーブル
属性テーブルと言うのは、表示するキャラクタへ様々な装飾を行うためのテーブルで、ファミコンの場合はキャラクタに着色するためのテーブルとなります。ファミコンの属性テーブルは、隣接した4個のキャラクタ毎に属性を設定しますので、1バイトに対して16ブロック分の色情報を持っている事になります。その1バイト中に対応するビットレイアウトとブロックの関係は次のようになります。ちなみに下図のセルはキャラクタの単位となっています。
I/Oポート
$2000 (PPU制御レジスタ1)
PPUの基本設定を行います。
書き込み
位置 | 内容 | クリア時 | セット時 |
---|---|---|---|
7 | VBlank時にNMI割込の発生 | オフ | オン |
6 | PPUの選択 | マスター | スレーブ |
5 | スプライトサイズ | 8x8 | 8x16 |
4 | BG用CHRテーブル | $0000 | $1000 |
3 | スプライト用CHRテーブル | $0000 | $1000 |
2 | VRAM入出力時のアドレス変化 | +1 | +32 |
1-0 | メインスクリーン | 0=$2000 , 1=$2400 , 2=$2800 , 3=$2C00 |
$2001 (PPU制御レジスタ2)
PPUの表示設定を行います。
書き込み
位置 | 内容 | クリア時 | セット時 |
---|---|---|---|
7 | 赤色を強調 | オフ | オン |
6 | 緑色を強調 | オフ | オン |
5 | 青色を強調 | オフ | オン |
4 | スプライトの表示 | オフ | オン |
3 | BGの表示 | オフ | オン |
2 | 画面左端8ドットのスプライト | クリップ | 表示 |
1 | 画面左端8ドットのBG | クリップ | 表示 |
0 | 色設定 | カラー | モノクロ |
$2002 (PPUステータスレジスタ)
PPUの状態を取得します。
読み込み
位置 | 内容 | クリア時 | セット時 |
---|---|---|---|
7 | スクリーンの描画状況 | 描画中 | VBlank中 |
6 | 描画ラインの0番スプライト | 衝突しない | 衝突した |
5 | 描画ラインスプライト数 | 8個以下 | 9個以上 |
4 | VRAM状態 | 書き込み可能 | 書き込み不可 |
3-0 | 未使用 |
$2003 (スプライトアドレスレジスタ)
スプライトRAMへの書き込みアドレスを設定します。
書き込み
位置 | 内容 | 値 |
---|---|---|
7-0 | 書き込み先アドレス | データ値 |
$2004 (スプライトアクセスレジスタ)
スプライトRAMへ書き込みを行います。
書き込み
位置 | 内容 | 値 |
---|---|---|
7-0 | 書き込むデータ | データ値 |
$2005 (スクロールレジスタ)
スクロールの設定を行います。このレジスタには2回連続で書き込みます。
書き込み1回目
位置 | 内容 | 値 |
---|---|---|
7-0 | 水平スクロール値 | データ値 |
書き込み2回目
位置 | 内容 | 値 |
---|---|---|
7-0 | 垂直スクロール値 | データ値 |
$2006 (VRAMアドレスレジスタ)
VRAMへの書き込むアドレスを設定します。このレジスタには2回連続で書き込みます。
書き込み1回目
位置 | 内容 | 値 |
---|---|---|
7-0 | VRAMアドレス上位8bit | データ値 |
書き込み2回目
位置 | 内容 | 値 |
---|---|---|
7-0 | VRAMアドレス下位8bit | データ値 |
$2007 (VRAMアクセスレジスタ)
VRAMからのデータ読み込み、VRAMへのデータ書き込みを行います。
読み込み
位置 | 内容 | 値 |
---|---|---|
7-0 | VRAMから読み込んだデータ | データ値 |
書き込み
位置 | 内容 | 値 |
---|---|---|
7-0 | VRAMへ書き込むデータ | データ値 |
$4014 (スプライトDMAレジスタ)
WRAMからスプライトRAMにデータを転送します。書き込んだ値を$100倍したアドレスから$100バイト分転送します。
書き込み
位 | 内容 | 値 |
---|---|---|
7-0 | WRAMベースアドレス | N x $100 |