この記事について
Aseprite という描画ツールを使って作成した、256x192 ピクセルの 1枚絵 を MSX1 (TMS9918A) で表示するまでの手順をサンプルコード付きで記します。
(1) 元画像の準備
私は絵の方は全然なので、iPhone のカメラで撮影した写真を使います。
まず、この画像をプレビューで 256x192 に縮小します。
iPhoneのカメラのデフォルトの画角が 256x192 と偶然同じだったので、幅256で縦横比固定で縮小すればOKです。
(2) Aseprite で減色
Aseprite を起動して 256x192 のスプライトを カラーモード Indexed で新規作成します。
そして、パレットを Presets
の MSX1
にします。
その状態で、準備した元画像をプレビューから ⌘A → ⌘C でコピーして、Aseprise で ⌘P でペーストすれば、(思っていたものとちょっと違うかもしれませんが)減色された画像を作ることができます。
とりあえず、この画像をビットマップ形式(image.bmp)で保存しておきます。
(3) TMS9918A 形式への変換
作成した image.bmp を TMS9918A で表示できる形式に変換する良い感じのツールは存在しないので、自前で作る必要があります。
以下に 256x192 8bit カラーの Bitmap (image.bmp) から、TMS9918A の Pattern Generator Table と Color Table を作成するコマンドラインプログラムを示します。
/* 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)
プログラムの実装は次の通りです。
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) 生成までの手順は、次の通りです。
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 で読み込むと、次のように画像が表示されます。
元画像からだいぶ変化しましたが、ひとまず「Asepriteで描いた1枚絵をMSXで表示」という題目通りの目標は達成できました。
減色具合の変化を分かりやすくするため、横に並べてみます。
元画像 | Aseprite減色 | TMS9918A減色 |
---|---|---|
![]() |
![]() |
![]() |
TMS9918A減色でジャギーになってしまっている部分は、bmp2tms で warning が出ないように修正すればキレイにすることができます。(かなり面倒なので省略)
レーザーの部分はスプライトなので、その部分を除けば 横 8px につき 2色 というルールに則って描かれていることがわかります。