画像ファイルはなぜこの形をしているのか
一言でいうと
画像は結局、数値の配列であり、ファイル形式は、その配列をどんな順序で並べて、どう小さくするかという約束にすぎません。PPMは小さくしない側を選び、PNGは小さくする側を選びました。
なぜ必要なのか
3Dグラフィックスを学ぼうとする人が最初にぶつかる壁は、数学ではなく、結果を見る方法がないことです。ウィンドウを出すにはウィンドウマネージャーが必要で、絵を保存するには画像ライブラリが必要です。ラボ環境にその両方がなければ、学ぶことをやめてしまいます。
ところが、画像ファイルを1つ自分で書くのは、思ったよりずっと簡単です。そして、その過程で得られるものがあります。image.save("a.png")の一行で済ませてしまうと、ピクセルがメモリにどんな順序で置かれているのか、なぜ幅が奇数のときにアラインメントの問題が生じるのか、なぜアルファチャンネルがあるとファイルが4/3倍に大きくなるのかを、最後まで知らずに通り過ぎます。グラフィックスでは、こうしたことが、あとで性能の問題として再びやってきます。
どう動くのか
PPM(Portable PixMap)はNetpbm系の形式で、ヘッダーのあとにピクセルをそのまま並べます。バイナリ形式のP6の構造は、これだけです。
P6 <- 매직 넘버 (P3 이면 픽셀을 십진수 글자로 적는다)
256 256 <- 폭과 높이
255 <- 채널 최댓값
<픽셀 바이트> <- R,G,B,R,G,B, ... 왼쪽 위부터 오른쪽으로, 그다음 아래 줄로
ピクセルは行優先(row-major)で置かれます。左上が最初のピクセルで、1行を端まで埋めてから、次の行へ下りていきます。そのため、座標(x, y)のバイトの位置は(y * 폭 + x) * 3です(プレースホルダーは幅です)。yが下に向かって増えるのは、形式がそう決めたからで、3D数学でyが上に向かって増えるのとは食い違います。この食い違いをどこで反転させるかは、あとでずっと出てくる問題です。
PPMの問題は、ブラウザーが描画できないことです。そのため、PNGが必要になります。PNGファイルは8バイトのシグネチャで始まり、そのあとに、長さ・種類・内容・CRCからなるチャンクが続きます。最小限必要なチャンクは3つです。
89 50 4E 47 0D 0A 1A 0A <- 서명
IHDR 폭, 높이, 비트깊이 8, 색 타입 2(트루컬러)
IDAT 각 행 앞에 필터 바이트 1개를 붙인 뒤 zlib 으로 압축한 것
IEND 끝
核心は、行ごとに先頭にフィルターバイトが1つ付くという点です。フィルターは、隣のピクセルとの差だけを保存して圧縮率を上げる仕組みで、番号0は「フィルターなし」です。番号0だけを使うなら、エンコーダーは行の先頭に0を1つ付けるだけで済みます。圧縮とCRCは、Python標準ライブラリのzlibが両方やってくれます。zlib.compressとzlib.crc32です。そのため、PNGエンコーダーは20行以内に収まります。
アルファチャンネルを入れると、カラータイプが6になり、ピクセルあたりのバイト数が3から4に増えます。ファイルサイズが4/3倍に大きくなるのは、ここから来ています。テクスチャのメモリを計算するとき、この1チャンネルがよく問題になり、アルファを実際には使わないテクスチャまで4チャンネルにしておくと、GPUメモリが33%余計に必要になります。
行の間隔も、知っておく価値があります。PNGとPPMは、行が폭 × 채널 수バイトで隙間なく続きますが(プレースホルダーは、幅とチャンネル数です)、グラフィックスAPIのフレームバッファーは、行ごとに4バイトまたはその倍数にそろえる場合が多くあります。そのとき、実際の行の間隔(ストライド)は、幅から計算した値より大きくなります。この差を無視して読むと、行が少しずつずれて、絵が斜めに傾きます。
現場での姿
レンダラーを作るとき、人が最初に出会うバグは2つあり、どちらもこの層で起きます。1つは、絵が上下反転して出てくることです。OpenGLのフレームバッファーは左下が原点なのに、画像形式は左上が原点なので、読み出して保存するときに反転させなければ、そのまま逆さまに出ます。もう1つは、絵が斜めにずれて出てくることで、これはほとんどいつも、行の長さ(ストライド)の計算ミスです。幅×チャンネル数と、実際の行の間隔が異なるときに生じます。
デバッグでも役に立ちます。シェーダーの結果がおかしいとき、途中の値(ノーマル、深度、UV)をそのまま色として塗り、画像として取り出してみるのが、最も速い診断方法です。そのためには、「今持っている数値の配列をファイルに書き出す」関数が、手元にある必要があります。
色の値の意味も、押さえておく必要があります。ファイルに書かれた0–255は、たいていはガンマ補正された値であり、光の強さに比例する値ではありません。2つの色の中間を求めようとして、整数2つをそのまま平均すると、実際の光の強さでは中間になりません。このラボでは無視して進んでも構いませんが、あとで照明の計算がどこか濁って見えるとき、疑うべき場所が、ここです。
次のラボですること
PPMを手で書いて、4×4の絵を作り、256×256のグラデーションに広げたあと、同じピクセルを、zlibとstructだけでPNGとして書きます。そのあと、Pod内に小さなHTTPサーバーを立てて、Webプレビューで自分の絵を見ます。最後に、円と直線を描き込み、絵の中で数えた値と、自分が書いた値が合っているかを確認します。