最近のMSXエミュレーター、openMSXやWebMSX(開発版)では、Yamaha V9990というVDPをエミュレートするようになっています。これはかつて、MSX3用に開発が進められながら、完成しなかった幻のVDP、V9978の後継となんとなく言われています。事実はあんましよくわかりません。1990年代にこのチップを載せたMSX用カートリッジが海外で少数販売され、以降その仕様に基づいた製品が時々制作されている感じです。パターンモードでは1ピクセルあたり16色のスプライトを125個一度に表示できたり、ビットマップモードでは32768色まで表示できたり、当時としてはなかなか強力な仕様になっています。そこでPC上で作成したRGB各8ビットの画像をV9990の32768色、512×424ピクセルモード用に変換して表示してみたところ、当然ですがグラデーションがなめらかにならず、バンディング(マッハバンド)が気になる結果になりました。
方法
この場合インデックスカラーにするわけではなく、ダイレクトRGBのままビット数を減らし、かつ色の境目を目立たなくしたいわけですが、そもそも24bitから15bitへの減色というのは、通常は単純にRGB各ユニットの下位3ビットを切り捨てる処理です。色解像度が減ってしまうので、なめらかな変化だった部分が急に変化することが避けられません。そこで切り捨てられる下位3ビットを有効活用することにします。
下位3ビットを切り捨てるということは、それを0とみなしているのと同じですが、実際は0から7の範囲の値を持ちます。下位3ビットが0であれば、切り捨てるのが正しく、7であれば上位5ビットが1繰り上がるのとほぼ同じです。要するに四捨五入みたいな感じで処理すればいいわけですが、これだけだとマッハバンドの発生位置が少しずれるだけでなめらかにはなりません。なので、確率的に繰り上がるかどうかを求めることにします。下位3ビットをウェイトとみなし、ウェイトから0~7の乱数を引いて、0以上なら上位5ビットに1を足すことにします。こうすればウェイトの値が小さければなかなか繰り上がらず、大きければ繰り上がりやすい。かつ乱数なので結果がばらけ、ディザをかけたことになるでしょう。
unsigned char rndunit(unsigned char u, int dither){
unsigned char unit = (u & 0b11111000)>>3;
unsigned char weight = ( u & 0b00000111);
if (dither && (weight - (rand() % 7) >=0) && (unit < 31)){
unit ++;
}
return unit;
}
これはR,GまたはBのユニット(8bit)を受け取って、下位3ビットの重みと乱数から上位5bitのユニットに1を足すか足さないか決め、結果の5bitユニットを返す関数です。画像ファイルからRGBを取得し、それぞれこれで変換して、15bitRGB形式にして書き出します。なお、V9990の32768色モードは、VRAM上で
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | |
---|---|---|---|---|---|---|---|---|
byte1 | R2 | R1 | R0 | B4 | B3 | B2 | B1 | B0 |
byte2 | 0 | G4 | G3 | G2 | G1 | G0 | R4 | R3 |
の順番で並んでいます。1ピクセルに15bit必要なので、当然ピクセルあたり2バイトということになります。今回元絵はPhotoshopで汎用フォーマット(RGBの順番でひたすらデータが並び、ヘッダなどはない)にした上で、Linux(WSL)上で変換してしまいます。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
unsigned char rndunit(unsigned char u, int dither){
unsigned char unit = (u & 0b11111000)>>3;
unsigned char weight = ( u & 0b00000111);
if (dither && (weight - (rand() % 7) >=0) && (unit < 31)){
unit ++;
}
return unit;
}
int main(int argc, char *argv[])
{
FILE *infp;
FILE *outfp;
char *inFileName=NULL;
char *outFileName=NULL;
int outfile=-1;
int i;
int units=0;
int c;
int dither=0;
unsigned char in[3];
srand(1);
if (argc <=1){
return 0;
}
// args check
for (i=1;i<argc;i++){
if (strcmp(argv[i],"-o") ==0){
outfile = i+1;
}else if (strcmp(argv[i],"-d") ==0){
dither = 1;
}else if (i==outfile){
outFileName = argv[i];
}else {
inFileName = argv[i];
}
}
// file open
if ((infp = fopen(inFileName, "rb"))==NULL){
printf("Input file open error.\n");
return 1;
}
if ((outfp = fopen(outFileName, "wb"))==NULL){
printf("Output file open error.\n");
return 1;
}
// read file and convert it.
while ((c = fgetc(infp)) != EOF){
in[units]=c;
units++;
if (units >2){
unsigned char r,g,b;
unsigned char byte1,byte2;
units=0;
r=rndunit(in[0],dither);
g=rndunit(in[1],dither);
b=rndunit(in[2],dither);
// Convert 3byte RGB to V9990 2byte RGB
byte1=b | ((r & 0b00000111) << 5);
byte2=(g<<2) | ((r & 0b00011000) >>3);
fputc(byte1,outfp);
fputc(byte2,outfp);
}
}
fclose(infp);
fclose(outfp);
return 0;
}
conv24to15.c
これをコンパイルして、
conv24to15 -o 出力ファイル名 -d 入力ファイル名
などとすればOK。-dはディザ処理です。このオプションを省略すると、最初にあげたようなマッハバンドの残る画像が生成されます。
結果
いい感じになりました。WebMSX(開発版)の画面イメージだとこんな風になります。
512×424ピクセル(インターレース)モードで表示しているため、MSXとは思えない高精細な画面になっています。まあ今の時代ではローレゾなんですけどね。MSX2のV9938や、MSX2+,turboRのV9958では、インターレースモードの画像は偶数、奇数ラインごとに別ページに配置し、合成表示していたので、非常に扱いにくかったのですが、V9990では単純に縦424ラインの画像をインターレース表示してくれるので楽です。