Help us understand the problem. What is going on with this article?

1677万色→32768色の変換でマッハバンドを目立たせない(MSX、V9990)

 最近のMSXエミュレーター、openMSXやWebMSX(開発版)では、Yamaha V9990というVDPをエミュレートするようになっています。これはかつて、MSX3用に開発が進められながら、完成しなかった幻のVDP、V9978の後継となんとなく言われています。事実はあんましよくわかりません。1990年代にこのチップを載せたMSX用カートリッジが海外で少数販売され、以降その仕様に基づいた製品が時々制作されている感じです。パターンモードでは1ピクセルあたり16色のスプライトを125個一度に表示できたり、ビットマップモードでは32768色まで表示できたり、当時としてはなかなか強力な仕様になっています。そこでPC上で作成したRGB各8ビットの画像をV9990の32768色、512×424ピクセルモード用に変換して表示してみたところ、当然ですがグラデーションがなめらかにならず、バンディング(マッハバンド)が気になる結果になりました。
15bit01.png
image.png

 なお、元絵はこんな感じです。
image.png

方法

 この場合インデックスカラーにするわけではなく、ダイレクト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はディザ処理です。このオプションを省略すると、最初にあげたようなマッハバンドの残る画像が生成されます。

結果

15bit02.png

image.png

いい感じになりました。WebMSX(開発版)の画面イメージだとこんな風になります。
image.png
512×424ピクセル(インターレース)モードで表示しているため、MSXとは思えない高精細な画面になっています。まあ今の時代ではローレゾなんですけどね。MSX2のV9938や、MSX2+,turboRのV9958では、インターレースモードの画像は偶数、奇数ラインごとに別ページに配置し、合成表示していたので、非常に扱いにくかったのですが、V9990では単純に縦424ラインの画像をインターレース表示してくれるので楽です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした