Asepriteで描いた1枚絵をMSXで表示してみる

# この記事について

Aseprite という描画ツールを使って作成した、256x192 ピクセルの 1枚絵 を MSX1 (TMS9918A) で表示するまでの手順をサンプルコード付きで記します。

# (1) 元画像の準備

私は絵の方は全然なので、iPhone のカメラで撮影した写真を使います。
![IMG_1425 2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/aa1a323a-fc22-4ef6-2870-3c184cc3d281.jpeg)

まず、この画像をプレビューで 256x192 に縮小します。
iPhoneのカメラのデフォルトの画角が 256x192 と偶然同じだったので、幅256で縦横比固定で縮小すればOKです。

![スクリーンショット 2021-10-31 14.36.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/9b0828ca-9db1-954d-7322-826b5fc234a4.png)

これで、元画像の準備完了です。
![IMG_1425 2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/5cd118c5-f93d-2c47-ec7c-c18f9fff53ea.jpeg)

# (2) Aseprite で減色

Aseprite を起動して 256x192 のスプライトを カラーモード **Indexed** で新規作成します。
![スクリーンショット 2021-10-31 14.53.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/a71272e0-9e93-350f-7202-988aca6e9b99.png)

そして、パレットを `Presets` の `MSX1` にします。
![スクリーンショット 2021-10-31 14.45.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/c50469e5-6e36-76df-97db-173c7e39cfae.png)

その状態で、準備した元画像をプレビューから ⌘A → ⌘C でコピーして、Aseprise で ⌘P でペーストすれば、（思っていたものとちょっと違うかもしれませんが）減色された画像を作ることができます。
![スクリーンショット 2021-10-31 14.57.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/5d7745d2-f177-aca6-b324-f2a81489b39a.png)

とりあえず、この画像をビットマップ形式（image.bmp）で保存しておきます。

# (3) TMS9918A 形式への変換

作成した image.bmp を TMS9918A で表示できる形式に変換する良い感じのツールは存在しないので、自前で作る必要があります。

以下に 256x192 8bit カラーの Bitmap (image.bmp) から、TMS9918A の Pattern Generator Table と Color Table を作成するコマンドラインプログラムを示します。

```bmp2tms.c
/* 8bit Bitmap to TMS9918A mode-2 Pattern Generator Table & Color Table */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* bmpファイルの情報ヘッダ */
struct DatHead {
    int isize;             /* 情報ヘッダサイズ */
    int width;             /* 幅 */
    int height;            /* 高さ */
    unsigned short planes; /* プレーン数 */
    unsigned short bits;   /* 色ビット数 */
    unsigned int ctype;    /* 圧縮形式 */
    unsigned int gsize;    /* 画像データサイズ */
    int xppm;              /* X方向解像度 */
    int yppm;              /* Y方向解像度 */
    unsigned int cnum;     /* 使用色数 */
    unsigned int inum;     /* 重要色数 */
};

int main(int argc, char* argv[])
{
    FILE* fp = NULL;
    int rc = 0;
    char fh[14];
    int pal[256];
    struct DatHead dh;
    int i, j, k, x, ln;
    unsigned char bmp[256 * 192];
    unsigned char ptn[8 * 768]; // 6KB
    unsigned char col[8 * 768]; // 6KB

    /* 引数チェック */
    rc++;
    if (argc < 4) {
        fprintf(stderr, "usage: bmp2tms input.bmp output.ptn output.col\n");
        goto ENDPROC;
    }

    /* bmpファイルをオープン */
    rc++;
    if (NULL == (fp = fopen(argv[1], "rb"))) {
        fprintf(stderr, "ERROR: Could not open: %s\n", argv[1]);
        goto ENDPROC;
    }

    /* ファイルヘッダを読み込む */
    rc++;
    if (sizeof(fh) != fread(fh, 1, sizeof(fh), fp)) {
        fprintf(stderr, "ERROR: Invalid file header.\n");
        goto ENDPROC;
    }

    /* 先頭2バイトだけ読む */
    rc++;
    if (strncmp(fh, "BM", 2)) {
        fprintf(stderr, "ERROR: Inuput file is not bitmap.\n");
        goto ENDPROC;
    }

    /* 情報ヘッダを読み込む */
    rc++;
    if (sizeof(dh) != fread(&dh, 1, sizeof(dh), fp)) {
        fprintf(stderr, "ERROR: Invalid bitmap file header.\n");
        goto ENDPROC;
    }

    printf("INPUT: width=%d, height=%d, bits=%d(%d), cmp=%d\n", dh.width, dh.height, (int)dh.bits, dh.cnum, dh.ctype);

    /* 256x192でなければエラー扱い */
    rc++;
    if (256 != dh.width || 192 != dh.height) {
        fprintf(stderr, "ERROR: Invalid input bitmap size. (256x192 only)");
        goto ENDPROC;
    }

    /* 8ビットカラー以外は弾く */
    rc++;
    if (8 != dh.bits) {
        fprintf(stderr, "ERROR: Invalid input bitmap color. (8bit color only)\n");
        goto ENDPROC;
    }

    /* 無圧縮以外は弾く */
    rc++;
    if (dh.ctype) {
        fprintf(stderr, "ERROR: This program supports only none-compress type.\n");
        goto ENDPROC;
    }

    /* パレットを読み飛ばす */
    rc++;
    if (sizeof(pal) != fread(pal, 1, sizeof(pal), fp)) {
        fprintf(stderr, "ERROR: Could not read palette data.\n");
        goto ENDPROC;
    }

    /* 画像データを上下反転しながら読み込む */
    rc++;
    for (i = 191; 0 <= i; i--) {
        if (256 != fread(&bmp[i * 256], 1, 256, fp)) {
            fprintf(stderr, "ERROR: Could not read graphic data.\n");
            goto ENDPROC;
        }
    }

    /* 色情報を mod 16 (0~15) にしておく*/
    for (i = 0; i < sizeof(bmp); i++) {
        bmp[i] = bmp[i] & 0x0F;
    }

    /* TMS9918A の Pattern Generator Table と Color Table の形式に変換 */
    k = 0;
    for (i = 0; i < 768; i++) {
        j = i % 32 * 8 + i / 32 * 256 * 8;
        for (ln = 0; ln < 8; ln++, k++, j += 256) {
            /* Color Table */
            unsigned char c[2] = {0, 0};
            for (x = 0; x < 8; x++) {
                if (0 == c[0]) {
                    c[0] = bmp[j + x];
                } else if (c[0] != bmp[j + x]) {
                    if (c[1]) {
                        if (c[1] != bmp[j + x]) {
                            // 横 8px に 3色 以上使われているので無視
                            printf("warning: ignore pixel at (%d, %d)\n", (j + x) % 256, (j + x) / 256);
                        }
                    } else {
                        c[1] = bmp[j + x];
                    }
                }
            }
            col[k] = (c[1] << 4) | c[0];
            /* Pattern Generator Table */
            ptn[k] = 0;
            for (x = 0; x < 8; x++) {
                ptn[k] <<= 1;
                ptn[k] |= bmp[j + x] == c[1] ? 1 : 0;
            }
        }
    }

    /* Pattern Generator Table を書き込み */
    fclose(fp);
    if (NULL == (fp = fopen(argv[2], "wb"))) {
        fprintf(stderr, "ERROR: Could not open: %s\n", argv[2]);
        goto ENDPROC;
    }
    if (sizeof(ptn) != fwrite(ptn, 1, sizeof(ptn), fp)) {
        fprintf(stderr, "ERROR: File write error: %s\n", argv[2]);
        goto ENDPROC;
    }

    /* Color Table を書き込み */
    fclose(fp);
    if (NULL == (fp = fopen(argv[3], "wb"))) {
        fprintf(stderr, "ERROR: Could not open: %s\n", argv[3]);
        goto ENDPROC;
    }
    if (sizeof(ptn) != fwrite(col, 1, sizeof(ptn), fp)) {
        fprintf(stderr, "ERROR: File write error: %s\n", argv[3]);
        goto ENDPROC;
    }

    rc = 0;

    printf("succeed.\n");

    /* 終了処理 */
ENDPROC:
    if (fp) fclose(fp);
    return rc;
}
```

上記プログラムを用いて、以下のように実行すれば、image.ptn (Pattern Generator Table)と image.col（Color Table）という 各6KB のファイルが生成されます。

```
clang -o bmp2tms bmp2tms.c
./bmp2tms image.bmp image.ptn image.col
```

なお、TMS9918A の mode-2 のキャラクタパターン（8x8）は、**横 8px につき 2色** しか使えない制約があります。

image.bmp には、この制約に引っかかる箇所が幾つかある状態なので、 `bmp2tms` は次のような warning を出力して減色する仕様です。この warning を全部対処すれば、image.bmp と完全に同じ画像を MSX で表示できますが、面倒なので今回はこのままいきます。

```
warning: ignore pixel at (132, 7)
warning: ignore pixel at (133, 7)
warning: ignore pixel at (135, 7)
warning: ignore pixel at (143, 4)
warning: ignore pixel at (142, 6)
（以下、省略）
```

# (4) ROM を作成

今回の ROM は 16KB で次のメモリ配置で作成します。

- 0x0000 ~ 0x0FFF : プログラム (4KB)
- 0x1000 ~ 0x27FF : image.ptn (6KB)
- 0x2800 ~ 0x3FFF : image.col (6KB)

プログラムの実装は次の通りです。

```image.asm
org $4000

.Header
    ; MSX の ROM ヘッダ (16 bytes)
    defb 'A', 'B', $10, $40, $00, $00, $00, $00
    defb $00, $00, $00, $00, $00, $00, $00, $00

.Start
    ld sp, $F380
    call VDP_Initialize

    ; Pattern Generator Table を ROM から VRAM へ転送
    ld hl, $0000
    call SetVramAddressFromHL
    ld hl, $4000 + 4096
    ld a, ($0007)
    ld c, a
    ld b, 0
    ld d, 24 ; 256 x 24 = 6144
PatternSetLoop:
    otir
    dec d
    jnz PatternSetLoop

    ; Color Table を ROM から VRAM へ転送
    ld hl, $2000
    call SetVramAddressFromHL
    ld hl, $4000 + 4096 + 6144
    ld a, ($0007)
    ld c, a
    ld b, 0
    ld d, 24
ColorSetLoop:
    otir
    dec d
    jnz ColorSetLoop

    ; Name Table を 上8行 = 0 ~ 255, 中8行 = 0 ~ 255, 下8行 = 0 ~ 255 で埋める
    ld hl, $1800
    call SetVramAddressFromHL
    ld a, ($0007)
    ld c, a
    ld b, 0
    ld d, 3
    ld a, 0
NameSetLoop:
    out (c), a
    inc a
    djnz NameSetLoop
    dec d
    jnz NameSetLoop

.End
    jmp End

; VRAM のアドレスを HL 設定値に書き込みモードで設定
.SetVramAddressFromHL
    di
    ld a, ($0007)
    inc a
    ld c, a
    ld a, l
    out (c), a
    ld a, h
    or $40
    out (c), a
    ei
    ret

; VDP を mode-2 で 768 パターン使う構成に初期化
.VDP_Initialize
    ld a, ($0007)        ; TMS9918A の書き込みアクセスポート番号を取得
    inc a                ; ポート番号を+1
    ld c, a
    ; ld c, $BF          ; 参考: SG-1000で動かす場合
    di

    ; レジスタ #0 (CTRL1) を更新
    ld a, %00000010      ; M2=1, EXTVID=0
    out (c), a
    ld a, %10000000 + 0
    out (c), a

    ; レジスタ #1 (CTRL2) を更新
    ld a, %11100010      ; 4K/16K=1, BL=1, GINT=1, M1=0, M3=0, SI=1, MAG=0
    out (c), a
    ld a, %10000000 + 1
    out (c), a

    ; レジスタ #2 (Pattern Name Tableアドレス) を更新
    ld a, %00000110      ; PN = $1800 (PN13=0, PN12=1, PN11=1, PN10=0)
    out (c), a
    ld a, %10000000 + 2
    out (c), a

    ; レジスタ #3 (Color Tableアドレス+マスク) を更新
    ld a, %11111111      ; CT = $2000, MASK = %11 (768 パターン)
    out (c), a
    ld a, %10000000 + 3
    out (c), a

    ; レジスタ #4 (Pattern Generator Tableアドレス+マスク) を更新
    ld a, %00000011      ; PG = $0000, MASK = %11 (768 パターン)
    out (c), a
    ld a, %10000000 + 4
    out (c), a

    ; レジスタ #5 (Sprite Attributeアドレス) を更新
    ld a, %01101100      ; SA = $1B00
    out (c), a
    ld a, %10000000 + 5
    out (c), a

    ; レジスタ #6 (Sprite Generatorアドレス) を更新
    ld a, %00000111      ; SG = $3800
    out (c), a
    ld a, %10000000 + 6
    out (c), a

    ; レジスタ #7 (TextColor, BackdropColor) を更新
    ld a, %11110001      ; TC = 15 (White), BD = 1 (Black)
    out (c), a
    ld a, %10000000 + 7
    out (c), a

    ei
    ret
```

アセンブルから ROMファイル (image.rom) 生成までの手順は、次の通りです。

```bash
z80asm -b image.asm
dd bs=4k conv=sync if=image.bin of=image_4k.rom
cat image_4k.rom image.ptn image.col > image.rom
```

# (5) WebMSX で表示

作成した image.rom を https://webmsx.org の MSX1 で読み込むと、次のように画像が表示されます。

![スクリーンショット 2021-10-31 15.19.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/6f2c3b2b-1892-e720-df41-91290e301e3d.png)

元画像からだいぶ変化しましたが、ひとまず「Asepriteで描いた1枚絵をMSXで表示」という題目通りの目標は達成できました。

減色具合の変化を分かりやすくするため、横に並べてみます。

|元画像|Aseprite減色|TMS9918A減色|
|:-:|:-:|:-:|
|![IMG_1425 2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/5cd118c5-f93d-2c47-ec7c-c18f9fff53ea.jpeg)|![スクリーンショット 2021-10-31 14.57.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/5d7745d2-f177-aca6-b324-f2a81489b39a.png)|![スクリーンショット 2021-10-31 15.19.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/6f2c3b2b-1892-e720-df41-91290e301e3d.png)|

TMS9918A減色でジャギーになってしまっている部分は、bmp2tms で warning が出ないように修正すればキレイにすることができます。（かなり面倒なので省略）

頑張ればこういう感じの画像を描くこともできます。
![スクリーンショット 2021-10-31 15.39.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2161417/97f1c9ca-2093-8f3f-f43b-3e83d0d40078.png)

> レーザーの部分はスプライトなので、その部分を除けば **横 8px につき 2色** というルールに則って描かれていることがわかります。