ドット絵向けに特化した? 画像フォーマットを考えてみました。
以前は可逆圧縮でしたが、今回は不可逆圧縮の固定長です。
既存の数あるテクスチャ用フォーマットでも良かったのですが、16x16程度のドット絵には不向きな気がするので、わざわざ車輪の再発明っぽいことをした訳です。
ブロックサイズは、4x4、8x8、16x16、32x32 から選択 1ドット当たりのデータ量は 1-4bit 色フォーマットは RGBA8888、RGB888、RGBA4444、RGB444、RGBA2222、RGB222、RGBA1111、RGB111 フォーマットにAが含まれない場合、透過は「する/しない」の2択(半透明不可)。雰囲気的にはRGBAxxx1?
Header +---+-------------+ | 0 | 'T' Sign | |---+-------------| | 1 | 'U' Sign | |---+-------------| | 2 | 'P' Sign | |---+-------------| | 3 | ' ' Sign | |---+-------------| | 4 | '1' Version | |---+-------------| | 5 | Type | |---+-------------| | 6 | | |---| Width | | 7 | | |---+-------------| | 8 | | |---| Height | | 9 | | |---+-------------| | | | |10-| Data | | | | +---+-------------+ Type bit0-1 : Block bit2-3 : bpp bit4-6 : Format bit7 : (Reserve) Block = 0: 4dot 1: 8dot 2:16dot 3:32dot bpp = 0: 1bpp 1: 2bpp 2: 3bpp 3: 4bpp Format 0:RGB111 1:RGB222 2:RGB444 3:RGB888 4:RGBA1111 5:RGBA2222 6:RGBA4444 7:RGBA8888 Data +-----------+ | Index | |-----------| | Palette | |-----------| | Color | |-----------| | Dot | +-----------+ Index +---+-------------+ | 0 | | |---| Number | | 1 | | |---+-------------| | 2-| ColorCode[] | | | | +---+-------------+ Index ColorCode(Format=RGB444) +-------------------+ | |0 1 2 3 4 5 6 7|bit |---+-------+-------|============ | 0 |R R R R G G G G| Color[0] |---| +-------| | 1 |B B B B|R R R R|============ |---+-------+ | Color[1] | 2 |G G G G B B B B| |---+---------------|============ | 3-|R R R R ... | Color[2-] | | | +-------------------+ Palette +---+----------------+ | 0 | | |---| Number | | 1 | | |---+----------------| | 2-| PaletteColor[] | | | | +---+----------------+ PaletteColor(2bpp 9<IndexNumber<=16) +-------------------+ | |0 1 2 3 4 5 6 7|bit |---+-------+-------|============ | 0 |Index0 |Index1 | |---+-------+-------| Palette[0] | 1 |Index2 |Index3 | |---+=======+=======|============ | 2 |Index0 |Index1 | |---+-------+-------| Palette[1] | 3 |Index2 |Index3 | |---+=======+=======|============ | 4-|Index0 ... | | | | Palette[2-] +-------------------+ Color(2bpp IndexNumber=0 Format=RGB444) +-------------------+ | |0 1 2 3 4 5 6 7|bit |---+-------+-------|=================== | 0 |R R R R G G G G| Block[0] Color[0] |---| +-------| | 1 |B B B B|R R R R|=================== |---+-------+ | Block[0] Color[1] | 2 |G G G G B B B B| |---+---------------|=================== | 3 |R R R R G G G G| Block[0] Color[2] |---| +-------| | 4 |B B B B|R R R R|=================== |---+-------+ | Block[0] Color[3] | 5 |G G G G B B B B| |---+---------------|=================== | 6 |R R R R G G G G| Block[1] Color[0] |---| +-------| | 7 |B B B B|R R R R|=================== |---+-------+ | Block[1] Color[1] | 8 |G G G G B B B B| |---+---------------|=================== | 9-|R R R R ... | Block[1] Color[2-] | | | +-------------------+ Color(2bpp 9<IndexNumber<=16 ) +-------------------+ | |0 1 2 3 4 5 6 7|bit |---+-------+-------|========== | 0 |Color0 |Color1 | |---|-------+-------| Block[0] | 1 |Color2 |Color3 | |---|-------+-------|========== | 2 |Color0 |Color1 | |---|-------+-------| Block[1] | 3 |Color2 |Color3 | |---|-------+-------|========== | 4-|Color0 |Color1 | | | ... | Block[2-] +-------------------+ Color(2bpp 129<PaletteNumber<=255 ) +-------------------+ | |0 1 2 3 4 5 6 7|bit |---+---------------|========== | 0 | Palette | Block[0] |---|-------+-------|========== | 1 | Palette | Block[1] |---|-------+-------|========== | 2-| Palette | Block[2-] | | ... | +-------------------+ Dot(4x4dot 2bpp) +---------------------------+ | | 0 1 2 3 4 5 6 7|bit |---+-----+-----+-----+-----| | 0 |d[0] |d[1] |d[2] |d[3] | |---+-----+-----+-----+-----| | 1 |d[4] |d[5] |d[6] |d[7] | |---+-----+-----+-----+-----| Block[0] | 2 |d[8] |d[9] |d[10]|d[11]| |---+-----+-----+-----+-----| | 3 |d[12]|d[13]|d[14]|d[15]| |---+-----+-----+-----+-----|========== | 4 |d[0] |d[1] |d[2] |d[3] | |---+-----+-----+-----+-----| | 5-|d[4] |d[5] |d[6] |d[7] | | | ... | Block[1-] +---------------------------+ (Format=RGB888,RGB444,RGB222,RGB111) if( RGB[0] <= RGB[1] ){ RGB[Max] = Alpha(r,g,b,a)=(0,0,0,0) }
拙作うめつくしの塔で、プレイヤーの皆さんが独自に描いたドット絵を表示出来たら楽しそうだな~、などという気楽な思い付きで実装を始めたら、結果的に画像フォーマットを自作することになってしまったというお話です。
うめつくしの塔は、マルチプレイのゲームです。描いたドット絵を他のプレイヤー全ての方に送信する必要があります。最初は URL 形式で png 画像をどこかへ保存する方式を考えましたが、折角なので WebSocket を使い画像データをバイナリで送り付けるようにしました。
そこでバイナリデータフォーマットの候補に挙がった画像形式は3種類。結果的にどれも採用しませんでしたが、今回の用途での評価は下記の通りです。
一番簡単無難な画像フォーマット。圧縮も出来ますが、基本無圧縮です。ヘッダのフォーマットにはいくつか種類がありますが、基本シンプルで Web 上の情報も多く実装は簡単です。
今回の用途に於いては、欠点は画像サイズのみです。のみなんですが……やはりここは妥協したくなかったw スマホの通信制限中でもストレスなく動くのが理想ですので、通信サイズは極力抑えたかったのです。無圧縮と言っても、インデックスカラーを使えばそれなりに小さくはなるのですが、やはりそれなりです。残念。
大本命。ギリギリまで採用を迷いました。ゲーム用の画像フォーマットとして普通に考えたらまずこの形式でしょう。が、この万能と思われる形式にも、限定された用途では意外な欠点が存在します。
今回の使い方は、少ないドット数、色数、つまりファミコンレベルの画像を小まめに複数送信することです。皆様 PNG は小さいドット絵を高圧縮してくれるイメージだとお思いでしょうし、実際そうなのですが、その「少ない」ドット、色、の少なさレベルがどの程度なのか、という話なのです。
48x48ドット、256色、これくらいが2018年今現在の「少ない」の基準なんじゃないかなと思います。それに対して、今回私が望んた「少ない」は、10x10ドット、4色なのです。これでは PNG も本領発揮できません。
とはいえ、それでも天下の PNG 様ですので、素人が作った独自フォーマットなんかと比べたら、高圧縮を実現してくれそうな気がします。しかし実際は、PNG のヘッダサイズの大きさが邪魔をして、ある程度以上は小さくならないのです。
PNG はヘッダ部分の構成に、ある程度の柔軟性があります。が、それに対応した機能は、残念ながら.NET の基本フレームワークには用意されていないようです。また、PNG データは、ブラウザ JavaScript 側での展開も意外と大変です。URL 形式の PNG ファイルは簡単に読み込めるわけですから楽そうなのですが、実はそうでもないのです。
つまりまとめると、PNG 自体は優秀だが、こだわると実装が大変で意外と頑張らないといけない、ということになります。で、どうせ頑張るなら独自形式の方が……という感じです。
いまどきの画像圧縮技術と言ったらテクスチャ圧縮が熱いですよね! その中でもETCは広く普及していて実装も簡単そう、ヘッダサイズも小さい(16byte)ので、今回の用途にももってこい! なのですが、結局採用は見送りました。その理由は2つ。
1つ目は、データサイズが意外と小さくならない事。32bit無圧縮に比べて1/6にしかならないですからね。いくらヘッダが小さくても、これではイマイチです。
もうひとつは、色の問題。ETCは不可逆圧縮です。が、ドット絵で不可逆圧縮は困るので、ETCの仕様に沿ったドット絵を描いて可逆圧縮的な使い方をすることになります。元々ファミコンレベルを目指していたわけですから、この制約はそれほど苦にならないというか、寧ろ楽しそう! というマゾ感覚w 制約のもとに描くドット絵は面白いと思います個人的には。
が、それを面白いと思えるかどうかは、人それぞれなんですよね。そして今回の要件は、私がドット絵を描くのではなく、プレイヤーの皆さんに描いてもらうということです。その際に ETC の色ルールを理解して楽しんでもらえるのか? と考えた場合、この選択肢も無いな、という結論に至りました。
可逆圧縮です。一応 32bit カラーまで対応していますが、規格上対応しているだけで実用的ではありません。実質的にドット絵専用で自然画には不向きです。インデックスカラー専用のため、巨大な自然画には対応していません。バイナリ形式はリトルエンディアンです。
●シグネチャ
4byte。「TUI 」です。16進数で、54 55 49 20
●バージョン情報
1byte。現状は「1」固定です。16進数で、31
●種別
1byte。
0bit :画像の「幅」「高さ」が256ドットを超えるかどうか。又はパレット数が255個を超えるかどうか。0:超えない。1:超える。
1-2bit:色の各要素ビット深度。0:1bit。1:2bit。2:4bit。3:8bit。
3bit :アルファチャネルを使用するかどうか。0:使用しない。1:使用する。
4-7bit:パレットで選べる最大色数-1。0~15で1~16色。
1-3bit目をまとめると、色フォーマットは下記のようになります。
000 RGB111
001 RGB222
010 RGB444
011 RGB888
100 RGBA1111
101 RGBA2222
110 RGBA4444
111 RGBA8888
●サイズ
「種別」0bit目が「0」の場合、3byte
・画像の幅-1。1byte。
・画像の高さ-1。1byte。
・パレット数。1byte。
「種別」0bit目が「1」の場合、6byte
・画像の幅-1。2byte。
・画像の高さ-1。2byte。
・パレット数。2byte。
★★★★★★★★
ここから先の情報は、バイト境界を無視します。全てビット単位での書き込みになります。ただし、情報種別は跨ぎません(「パレット」「インデックス」「ピクセル」)
★★★★★★★★
●パレット情報
画像全体で使用するパレット情報です。直前に格納されている「パレット数」ぶんの情報が連番で格納されています。個々のパレットデータは基本固定長ですが、例外も存在します。
個々のパレットは、「種別」で指定された「最大色数」ぶんの情報を格納しています。ですので基本固定長ですが、同じ色が連続で現れた場合、そこで打ち切られます。
色情報のビット数は、色フォーマットで指定された長さになります(例:RGB111:3bit。RGB2222:8bit)。
例1)パレット数=3、最大色数=4、RGBA2222の場合。12byte。
00 11 22 33 (パレットNo.1)
44 55 66 77 (パレットNo.2)
88 99 AA BB (パレットNo.3)
例1)パレット数=3、最大色数=4、RGBA2222、色数不定の場合。11byte。
00 11 22 33 (パレットNo.1)
44 55 55 (パレットNo.2。2色のみ)
66 77 88 88 (パレットNo.3。3色のみ)
また、例外的なパターンとして「パレットの数」が0の場合、この項目は存在しません。色情報が不必要なデータ(透過情報のみ)等での利用を想定しています。
●インデックス情報
画像は、4x4ドットのブロック単位に分割されます。そのブロック単位で使用するパレットを選択でき、パレット番号が格納されたものがこの領域となります。
画像の左上を(x,y)=(0,0)とし、(0,0)、(1,0)、(2,0)、…、(0,1)、(1,1)の順に格納されます。パレット番号のサイズは「パレット数」に依存し、2個:1bit、3-4個:2bit、5-8個:3bit、9-16個:4bit、……以下略です。
「パレット数」が0又は1の場合、この項目は存在しません。
例1)縦横4ブロック、パレット数=6の場合、4x4x3=48bit(=6byte)。
例2)縦横3ブロック、パレット数=4の場合、3x3x2=18bit(=3byte)。余りの6bitは未定義領域。
●ピクセル情報
ブロック単位での個々のピクセル情報が格納されます。1ブロックは4x4ですので、16ピクセルの情報が1単位となります。画像のドット数が4で割り切れない場合、ブロックは多めに確保され、はみ出した領域は無視されます(中身は未定義)。
1ピクセルを表すのに必要なビット数は、選ばれたパレットが所持する色数に応じて、1色:0bit、2色:1bit、3-4色:2bit、5-8色:3bit、9-16色:4bitとなります。1色の場合、そのブロックに関するピクセル情報は存在しません。
ブロック内のドットの並び順は、左上(x,y)=(0,0)から、(1,0)、(2,0)、(3,0)、(0,1)、(1,1)……です。
●アニメーション情報
ファイルに格納されている画像が1枚の場合、ここから先のデータは存在しません。この情報が存在するかどうかは、ファイルサイズで判断します(ピクセル情報でピッタリ終わるかどうか)。
最初に、ヘッダ情報として 1byteの領域が存在します。現状リザーブ領域です(0固定)。
その後の2枚目以降のデータには、「パレット情報」が存在せず「インデックス情報」と「ピクセル情報」のみになります。それぞれの情報の前には 1byte のヘッダ情報が付与され、それによって構造が変化します。
全体のフレーム数情報は格納されず、ファイルデータが続くかどうかで判断します。
「インデックス情報」の前に存在する 1byte のヘッダー内容は以下の通りです。
0:標準のインデックス情報と同じ構造。無圧縮
1-7:前フレームとの差分インデックス情報
8-255:未使用
差分情報の構造は、基本的には「変更箇所」と「変更値」の組み合わせが複数並んだものになります。変更個数の情報は存在せず、「変更箇所」の情報が末尾まで到達したかどうかで判断します。
「変更箇所」のbit数は、ヘッダ情報+1 です。「変更値」のbit数はパレット数に依存します(基本と同じ)。この組み合わせが複数続き、「変更箇所」は直前箇所からの移動量が格納されます(1つ目は1から)。
「変更箇所」の値は、移動量-1ですが、最上位ビットが1の場合、残りのビットが表す値をヘッダーの値で左シフトした分だけ移動し、その後も「変更箇所」のデータが続きます。
EOFを意味する最後の位置情報は、本来の長さを超えているかで判断し、長さ=位置とする必要はありません。
例1)ヘッダー情報=3、パレット数=16、データ長=16の場合
前: 0 1 2 3 4 5 6 7 8 9 A B C D E F
今: 0 1 2 0 4 5 6 7 8 9 A 1 C D E F
変更箇所:位置4と12。
結果:3 0 7 1 4
意味:
3:現在の位置(1)から3進む。位置4。
0:値を0に設定。そして次の位置へ(5)
7:現在の位置(5)から7進む。位置12。
1:値を1に設定。そして次の位置へ(13)
4:現在の位置(13)から4進む。位置17になり、終了
例2)ヘッダー情報=3、パレット数=16、データ長=24の場合
前: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0 1 2 3 4 5 6 7
今: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0 1 2 0 4 5 6 7
変更箇所:位置20。
結果:A 3 0 4
意味:
A:現在の位置(1)から16進む。Aは2進数で1010。最上位ビットが立っているため、位置移動のみ。移動量2x8=16。位置17。
3:現在の位置(17)から3進む。位置20。
0:値を0に設定。そして次の位置へ(21)
4:現在の位置(21)から4進む。位置25になり、終了
「ピクセル情報」の前に存在する 1byte のヘッダー内容は以下の通りです。
0:標準のピクセル情報と同じ構造。無圧縮
1-7:前フレームとの差分ピクセル情報
8-255:未使用
差分情報の構造は、インデックス情報と同一です。