himeshi’s blog

Simutrans本体改造まわりのお話をつらつらと

アドオン画像の減色について

アドオン画像の減色メカニズムについて、コードを読みながら整理したいと思います。

本来この話は現在執筆中の『Simutransの本体開発』に載せるつもりでしたが、本の構成上溢れそうなのでこちらで供養するといったものです。Simutransの仕様は数年もすれば様変わりしてしまいますが、いつ何時も原典であるコードの読み方といっしょに分析しておけば時代が下っても参考程度にはなるかなと考え書き残しておきます。

減色処理はpngとdatからアドオンをコンパイルするときに行われるのでdescriptor/writer/image_writer.ccを見れば良いことになります。コードはこちらからオンラインで見れます。

github.com
 

きちんと流れを追うのはしんどいのでわかりやすいところからつまみ食いしていきましょう。62行目からはじまるpixrgb_to_pixval関数を呼んでいきます。名前の通り8bitなrgb値をSimutrans内部で使われているpixval値に変換する関数です。

static uint16 pixrgb_to_pixval(uint32 rgb)
{
	uint16 pix;

	// first: find about alpha
	assert(  rgb < ALPHA_THRESHOLD  );

	int alpha = 30 - (rgb >> 24)/8;	// transparency in 32 steps, but simutrans uses internally the reverse format
	if(  rgb > 0x00FFFFFF  ) {
		// alpha is now between 0 ... 30

		// first see if this is a transparent special color (like player color)
		for (int i = 0; i < SPECIAL; i++) {
			if (image_t::rgbtab[i] == (uint32)(rgb & 0x00FFFFFFu)) {
				// player or light color
				pix = 0x8020 +  i*31 + alpha;
				return endian(pix);
			}
		}
		// else store color as 3 red, 4, green, 3 red
		pix = ((rgb >> 14) & 0x0380) | ((rgb >>  9) & 0x0078) | ((rgb >> 5) & 0x07);
		pix = 0x8020 + 31*31 + pix*31 + alpha;
		return endian(pix);
	}


	// non-transparent pixel
	for (int i = 0; i < SPECIAL; i++) {
		if (image_t::rgbtab[i] == (uint32)rgb) {
			pix = 0x8000 + i;
			return endian(pix);
		}
	}

	const int r = (rgb >> 16);
	const int g = (rgb >>  8) & 0xFF;
	const int b = (rgb >>  0) & 0xFF;

	// RGB 555
	pix = ((r & 0xF8) << 7) | ((g & 0xF8) << 2) | ((b & 0xF8) >> 3);
	return endian(pix);
}

前提として、一般的にピクセルの色はアルファ値を含めて32bitで表現されます。頭8bitが透明度、次の8bitがR、その次の8bitがG、一番最後の8bitがBを表します。
rgb > 0x00FFFFFFで分岐しているので、ざっくりALPHA値が含まれてるか否かで処理が分岐していることがわかると思います。なおココでは数値はすべて16進数で表記されています(頭に0xがついていたら16進数)。まずはALPHA値が入っていない処理から見ていきましょう。以下はpixrgb_to_pixval関数の後半部分を抜粋したものです。

// non-transparent pixel
for (int i = 0; i < SPECIAL; i++) {
	if (image_t::rgbtab[i] == (uint32)rgb) {
		pix = 0x8000 + i;
		return endian(pix);
	}
}

const int r = (rgb >> 16);
const int g = (rgb >>  8) & 0xFF;
const int b = (rgb >>  0) & 0xFF;

// RGB 555
pix = ((r & 0xF8) << 7) | ((g & 0xF8) << 2) | ((b & 0xF8) >> 3);
return endian(pix);

与えられた色に対してプレイヤーカラーや発光色といった特別色に該当していないかまず検査します。特別色の場合は特別色番号に0x8000を加算した値をpixvalとして返します。pixval値は16bitなので最上位bitが1の場合は特別色であることを表すことがわかります(0x8000は2進数で表すと1000000000000000)。なので一般色に使えるbitは15bitしかありません。(ちなみに後で分かりますが、最上位bitが1のときは「透過ナシ一般色ではない」というのが真の意味です。)
特別色でない場合は、色の32bit表現からr,g,bを取り出した上で、各成分8bit(256段階)から5bit(32段階)にダウンサイジングしています。ビット演算で表現されてるので少々わかりにくいですが、やってることは256階調の色の値を8で割ってあまりは切り捨てです。例えばR成分が87であれば87÷8→10となります。最終的に返されるpixval値は16bitになります。元が32bitなのでデータサイズだけで見れば半分に圧縮されたことになります。


つづいてアルファ値が入っている場合を見てみましょう。以下はpixrgb_to_pixval関数の前半部分を抜粋したものです。

int alpha = 30 - (rgb >> 24)/8;	// transparency in 32 steps, but simutrans uses internally the reverse format
if(  rgb > 0x00FFFFFF  ) {
	// alpha is now between 0 ... 30

	// first see if this is a transparent special color (like player color)
	for (int i = 0; i < SPECIAL; i++) {
		if (image_t::rgbtab[i] == (uint32)(rgb & 0x00FFFFFFu)) {
			// player or light color
			pix = 0x8020 +  i*31 + alpha;
			return endian(pix);
		}
	}
	// else store color as 3 red, 4, green, 3 red
	pix = ((rgb >> 14) & 0x0380) | ((rgb >>  9) & 0x0078) | ((rgb >> 5) & 0x07);
	pix = 0x8020 + 31*31 + pix*31 + alpha;
	return endian(pix);
}

やってることは概ね同じで、特別色を検査してから8bitのrgb値を圧縮しています。pixval値で一般色に使えるbit数は15bitなので、ALPHA値がない場合はRGB各成分5bit(32段階)で表現してましたが、今回はALPHA値の表現ですでに5bit(32段階)使っているのでRGBを10bitで表現せねばなりません。そのため、RとBは3bit(8段階)、Gは4bit(4段階)に圧縮されています。計算ルールは同じで、例えば元のRGB値でRが87であれば、87÷(256÷8)→2と計算されます。pixval値は16bitであり、その中で透過ナシ色や特別色と衝突しない表現をする必要があるので、最終的な値の計算式はやや複雑になります。

グラデーションの表現が不自然になることですでに悪名高い減色処理ですが、png画像にALPHA値を含めるとさらに激しく減色されるので、必要ない限りはALPHA値を含めることは避けるべきです。とはいえ大サイズ3Dアドオンが普及するにつれて減色問題による弊害は深刻さを増しているので、次回は減色がなぜ行われているのか、減色問題を解決する手立てはあるのかについて分析する予定です。