たまりば

  パソコン・インターネット パソコン・インターネット  三鷹市 三鷹市

ファミコンで全画面に任意の画像(ただしモノクロ)を表示
2017年01月14日 00:00

ファミコンは普通には全画面に自由に画像を表示できないことはよく知られている。
でもモノクロ2値ならできることに気づいたのでやってみた。
ファミコン全画面_猫耳とりあえず猫耳。
ファミコン全画面_羽根っ娘酉年らしく羽根っ娘。
ファミコン全画面_漢字全画面任意なのが分かりやすい図柄も。

まず前提として、ファミコンのBG(背景)面は32×30=960個のキャラクタで構成されるが、BGの(スプライトも)キャラクタパターンの指定は1バイトなので、一度に256種類からしか選べない。
よって普通にはBGだけでは256キャラ、画面の1/4強しか埋められず、スプライトを8×16ドットモードで最大の64個使って128キャラ用意してやっと384/960=40%にしかならない。
何らかのマッパーを使えば描画中にバンク切り替えで全画面を別のキャラクタで埋められるが、マッパーを使ってできるのは面白くないので今回考えないことにする。

そこで今回の手法だが、パレットの色分けとキャラクタテーブルの描画中切り替えで実質キャラ数を4倍に増やしている。
まずパレット。
ファミコンのキャラごとの色数は4色だが、これを2色しか使わなければ例えば「白黒白黒」と「白白黒黒」の2種類のパレットを左右で使い分けることで1つのキャラで2つの絵を表示することができる。
00011011

0111111111111100
1111111111111111
1111000000001111
1111010101011110
1111111111111111
1111101010101111
1111010101011111
1111010101011110
00011011

0111111111111100
1111111111111111
1111000000001111
1111010101011110
1111111111111111
1111101010101111
1111010101011111
1111010101011110
00011011

0111111111111100
1111111111111111
1111000000001111
1111010101011110
1111111111111111
1111101010101111
1111010101011111
1111010101011110
比較のために単一のパレットで表示したものがこちらだ。
単一パレット表示

次にキャラクタテーブル。
「一度に256種類からしか選べない」と書いたが、この「一度に」の部分がポイントだ。
仕組みを詳しく言うと、定義できるキャラは256個の領域が2つあり、BGと8×8のスプライトはそれぞれその2つのどちらの領域からキャラを選ぶかを設定できる。同じ領域をBGとスプライトの両方で使うこともできる。
(なお今回関係ないが8×16のスプライトは一度に2キャラを使うので、どちらかの領域から選ぶのではなく、2つの領域を合わせた512キャラを2キャラづつの256組と見たものから選択する)
ここでこの「どちらの領域から選ぶか」の設定は画面の描画中にも切り替えることができる。これにより上半分と下半分で別のテーブルからキャラを選ぶことが可能なのだ。
比較のためにこの切り換えを行わなかったものがこちら。
切り換えを行わない表示

基本的なアルゴリズムは以上だ。
実際のコードは下に置いたが、少々細かい解説を加えておく。

今回CHR-ROMでなくCHR-RAMを使っている。
そのためまず最初にPRG-ROMに持っている画像データを元にCHR-RAM上にパターンテーブルのデータを生成している。
これは事前に適切な並びにしたデータをCHR-ROMに持っておけば必要ない操作だ。
ただ、この並べ替えのプログラムは、ファミコン自体で行うか外部で事前に行うかの違いでどうせ書かなければならない。
ならば1つにまとめた方が扱いが楽なのでこうした。
下に書いたコードと、任意のモノクロビットマップを用意するだけで、NESASMでビルドできる。

このパターンテーブル書き込みのコードは少々複雑だが、やっていることは単純に、256×240ドットの1bitビットマップを適切な順に並び替えているだけだ。
パターンテーブル前半0x0000~0x0FFFが画像の上128ドット、後半のうち0x1000~0x0DFFが画像の残り。0x1F00~0x1FFFには後述の通り0x0F00~0x0FFFと同じものを重複して配置している。

「どちらの領域から選ぶか」の設定、つまりパターンテーブルのベースの切り換えだが、このような描画中のタイミング合わせはファミコンではふつう0番スプライトヒットフラグをポーリングすることで行う。
だが今回別の方法をとった。
0番スプライトヒットはBGとスプライトが共に不透明な点でしか起こらない。つまり画像の当該箇所が透明なとき使えない。
今回任意の画像を表示できるようにしようとしているのでこれは困る。
代わりにあまり知られていないスプライトオーバーフローフラグを使った。
これはライン上にスプライトが9個以上並んで横並び数制限を越えたときに立つはずだったフラグだ。
このフラグはスプライトが透明でも変わらず立つので、BGの内容に関係なく使えて便利だ。
ただ重大な欠点があって、このフラグ、アドレスの桁上がりの誤りで別のデータを参照するせいでまともに動かないという、致命的かつしょうもないバグが存在する。
使い物にならないと思うかもしれないが、バグの条件は厳密に判明している。
詳しくはNesDevの「PPU sprite evaluation」のページを読むとよいが、
スプライト番号順にライン内に存在するかをチェックし、8つめが見つかった次のチェックまでは正しく、その次から誤ったアドレスを参照しだす。
ここから、
・スプライト数が7つ以下なら、必ずフラグは立たない
・スプライト数が9つ以上の時、8つめと9つめのスプライト番号が隣り合っていれば、必ずフラグが立つ
ということが言える。
なので今回はスプライト番号順に連続した9つを目的の場所に置くことで確実にスプライトオーバーフローフラグを立てることにした。
というか残りも面倒なので同じ位置に置いた。

切り換えタイミングだが、画像を単純に上下に分割した場合、きっちりHBlank中に切り換えを起こすようにしないとこのように画面が乱れてしまう。
ファミコン全画面_画面乱れ
そのレベルで合わせるには描画中のスプライトの判定処理の流れをきちんと理解してコードを書かねばならないが、今回面倒なのでそこまではしなかった。
その代わり、1列分(8ライン)のキャラをパターンテーブル前半と後半で重複させておいた。これでこの8ラインの中で切り替えれば画面を乱さないようにできる。

画像データのインクルード部分は「画像データ(生)」と「画像データ(BMP)」の2種類を用意した。(生)は256×240bitの生データを使う時用で、(BMP)はWindowsのMSペイントで出力したBMPファイルをそのまま使えるようにヘッダ分だけずらした位置に配置するものだ。
これを使う時は、まずペイントで適当に描いた絵を上下反転
ペイントで描いて上下反転
モノクロビットマップで保存して
保存
できたファイル名をソースに書き込んでビルドするとできあがり。
ビルドした表示
ぜひ試してみてほしい。

; 全画面に任意のモノクロ2値のビットマップを表示する
; NESASM v3.1でビルド、NNNesterJとNintendulatorで動作確認している。

    ; INESヘッダー
    .inesprg 1 ; PRG-ROM容量(0x4000=16KB単位)
    .ineschr 0 ; CHR-ROM容量、CHR-RAMでは0
    .inesmir 0 ; Don'tCare(水平ミラーリング)
    .inesmap 0 ; マッパーなし

    .bank 0

; レジスタ名定義(してたりしてなかったり、してるのに使ってなかったり)
PPUMASK = $2001
PPUADDR = $2006
PPUDATA = $2007

;(グローバル)変数定義
;RAMの連続したアドレスにラベルを付けたいだけなのだがどうもROMに値をとっている。
;どうせ上書きされる場所で実害ないからいいけど、もっとまともな方法があるのだろうか…。
;まあとりあえず今回はこのままでいいや。
    .org $0000
chrptr .dw 0 ;CHR-RAM書き込み時のポインタ
cnt1 .db 0 ;カウンタ変数
sprpos .db 0 ;スプライト書き込み時のポインタ
posx2 .db 0 ;何だっけ

;割り込みベクタ
    .bank 1     ; 0x2000(8KB)単位。ファイルが全16KBなのでbank3でなく1。
    .org $FFFA  ; BFFAだけど。

    .dw intvb   ; VBlank割り込み
    .dw init    ; リセット割り込み。起動時とリセット時
    .dw 0       ; ハードウェア割り込みとソフトウェア割り込み

    .bank 0
    .org $8000

init:
; PPUレジスタに書き込みできるようになるまで時間がかかるらしいので待つ
    ldy #25
.loop:
    dex
    bne .loop ;5*256=1280 ;30000 30000/1280=23.4375
    dey
    bne .loop

.loop2:
    lda $2002  ; VBlank待ち
    bpl .loop2

    ; VBlankのNMIを無効
    lda #%00110000
    ;     ^||||||| VBlank開始時にNMI発生
    ;      ^|||||| PPUマスター/スレーブ
    ;       ^||||| スプライトサイズ 0:8x8 1:8x16
    ;        ^|||| BGパターンテーブル
    ;         ^||| スプライトパターンテーブルアドレス
    ;          ^|| VRAMアドレス増分 0:1 1:32
    ;           ^^ ベースネームテーブルアドレス 0:2000 1:2400 2:2800 3:2C00
    sta $2000
    lda #%00000110 ; 初期化中はスプライトとBGを表示OFFにする
    sta $2001

    ; パレットRAM=$3F00
    lda #$3F
    sta $2006
    lda #$00
    sta $2006

    ldx #$00
loadPal:
    lda pallet, x
    sta $2007
    inx
    cpx #32
    bne loadPal

    lda    #50
    sta    posx2

setBg: ;BGのネームテーブルを適宜セット
; 00,01,...0F,00,01,...0F
; 10,11,...1F,10,11,...1F
; ...
; F0,
; 00,
; ...
; D0,
    ; $2000: BG1 name table
    lda #$20
    sta $2006 ;PPU_ADDR
    lda #$00
    sta $2006
    ldx $00
    lda #30
    sta cnt1
    ldy #0
bgLoop0:
    ldx #16
bgLoop1:
    sty PPUDATA
    iny
    dex
    bne bgLoop1
    tya
    sec
    sbc #$10
    tay
    ldx #16
bgLoop2:
    sty PPUDATA
    iny
    dex
    bne bgLoop2
    dec cnt1
    bne bgLoop0

;CHR-RAM書き込み
setChrRom:
    lda #0
    sta chrptr
    lda #high(chr)
    sta chrptr+1
    ldy #$00
    sty PPUADDR
    sty PPUADDR
    ldx #30

.loop
    lda [chrptr],Y
    sta PPUDATA

; 8: タイル縦px数 000yxxxx ~ 111yxxxx
;Y+=20
    tya
    clc
    adc #$20
    tay
    bcc .loop ; if no carry loop
; 2: 色bit 0000xxxx, 0001xxxx
;Y+=10
    tya
    eor #$10
    tay
    and #$10
    bne .loop ;if !y&10 loop
; 16: 横タイル数 00000000 ~ 00001111
    iny
    tya
    and #$0F
    bne .loop; if neq loop
; 30: 縦タイル数
    ldy #$00
    inc (chrptr+1)
    dex
    bne .loop

setChrRomOverlap: ;BGのベースを変える境目でオーバーラップさせておく
    ldy #$1F
    sty PPUADDR
    ldy #$00
    sty PPUADDR

    lda #high(chr)+$0F
    sta chrptr+1
    ldy #0
.loop
    lda [chrptr],Y
    sta PPUDATA

; 8:タイル縦 000yxxxx ~ 111yxxxx
;Y+=20
    tya
    clc
    adc #$20
    tay
    bcc .loop ; if no carry loop
; 2:パレットbit 0000xxxx, 0001xxxx
;Y+=10
    tya
    eor #$10
    tay
    and #$10
    bne .loop ;if !y&10 loop
; 16: 横タイル数 00000000 ~ 00001111
    iny
    tya
    and #$0F
    bne .loop; if neq loop


;属性テーブル (BGパレット)
    ldy #$23
    sty PPUADDR
    ldy #$C0
    sty PPUADDR
    ldx #8
attrloop:
    lda #$00 ; 左半分 00 00 00 00
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA
    lda #$55 ; 右半分 01 01 01 01
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA
    sta PPUDATA
    dex
    bne attrloop

;scroll
    lda #0
    sta $2005
    sta $2005

    ; スプライト配置
    lda #$00
    sta $2003 ; OAMADDR

; Byte 0-3: Y,TileNo,Attr,X
; Attr:
;76543210
;||||||||
;||||||++- Palette (4 to 7) of sprite
;|||+++--- Unimplemented
;||+------ Priority (0: in front of background; 1: behind background)
;|+------- Flip sprite horizontally
;+-------- Flip sprite vertically

    ldx #64
sprLoop:
    lda #$7B ; BGのベースの切り替わりあたり
    sta $2004
    lda #$E1 ; 1:E0 空スプライト
    sta $2004
    lda #$00
    sta $2004
    lda #$B0 ; 特に意味はない
    sta $2004

    dex
    bne sprLoop

    ; 表示開始
    lda #%00011110    ; スプライトとBGの表示をONにする
    sta $2001

intvb:
    lda #%11100000 ;BGパターンテーブルベース=0
    sta $2000

waitVBlankEnd: ;VBlank終了時点でスプライトオーバーフローフラグが消えるのを待つ
    lda $2002
    and #$20
    bne waitVBlankEnd
   
waitSprOvr: ;スプライトオーバーフローフラグが立つのを待ってBGパターンを切り替える
    lda $2002
    and #$20
    beq waitSprOvr

    lda #%11110000 ;BGパターンテーブルベース=1
    sta $2000

waitInt:
    jmp waitInt

;色0・色1
C0 = $0F
C1 = $30

pallet:
    .db C0, C1, C0, C1, C0, C0, C1, C1, C0, C0, C0, C0, C0, C0, C0, C0
    .db C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0, C0

; 画像データ(生)
    .bank 1
    .org $A000
chr:
    .incbin "rawfile.bin" ; 画像。256×240/8=0x1E00バイト

; 画像データ(BMP)
;    .bank 1
;    .org $A0C2
;    .incbin "bitmap.bmp" ; 画像。MSペイントなどで作成したモノクロビットマップ
;    .org $A100
;chr:

  • 同じカテゴリー(プログラム)の記事画像
    JPEG圧縮を繰り返しても際限なく劣化するわけではない
    ゲームボーイの吸い出し機を作った (後編)
    最近のWindowsのビットマップフォントの太字
    PCのキーボードのアーウが反応しなくなったあどうすえばよいか
    ARMマイコンはじめました。
    SDカードから1セクタ読み取るまでの手順解説
    同じカテゴリー(プログラム)の記事
     JPEG圧縮を繰り返しても際限なく劣化するわけではない (2017-02-10 01:47)
     ゲームボーイの吸い出し機を作った (後編) (2017-01-16 22:44)
     最近のWindowsのビットマップフォントの太字 (2017-01-09 18:49)
     浮動小数点数の10進指数表示のアルゴリズム (2016-12-28 01:28)
     PCのキーボードのアーウが反応しなくなったあどうすえばよいか (2016-07-17 04:34)
     ARMマイコンはじめました。 (2016-05-28 14:43)
    URL欄を実験的に消してる間に廃止されてしまいました。まあいいか。
     
    <ご注意>
    書き込まれた内容は公開され、ブログの持ち主だけが削除できます。
    <?=$this->translate->_('削除')?>
    ファミコンで全画面に任意の画像(ただしモノクロ)を表示
      コメント(0)