PPMとPNGを手で書く
目標
画像ファイルをライブラリなしで自分で書いて、自分が計算したピクセルを、ブラウザーで見られるようにします。このラボが終われば、このパスの残りのすべてで、結果を目で確認できます。
なぜ重要なのか
グラフィックスの勉強が途中で止まる最もよくある理由は、結果を見る方法がないことです。ところが、画像ファイルを1つ書くことは難しくありません。PPMは、ヘッダー3行のあとにピクセルを並べたものがすべてで、PNGも、シグネチャ8バイトにチャンク3つがあれば済みます。圧縮とCRCは、標準ライブラリのzlibがやってくれます。
自分で書いてみると、ついてくるものがあります。ピクセルが行優先で置かれること、座標(x, y)の位置が(y * 폭 + x) * 3であること(プレースホルダーは幅です)、画像のyは下に向かって増えるのに、3D数学のyは上に向かって増えることです。この3つは、あとのすべてのラボでずっと出てきます。そして、シェーダーがおかしいとき、途中の値を色として塗って取り出してみるのが最も速い診断方法ですが、そのためには、この関数が手元にある必要があります。
ステップ
- 4x4のP6 PPMを、
/root/gfx/out/tiny.ppmに書きます。 - 256x256のグラデーションを、
/root/gfx/out/gradient.ppmに書きます。 zlibとstructだけでPNGを書いて、/root/gfx/out/gradient.pngを作ります。/root/gfx/out/index.htmlを作り、HTTPサーバーを立てて、Webプレビューで見ます。- 円を塗りつぶして、
/root/gfx/out/shapes.pngを作ります。 - 直線を12本引いて、
/root/gfx/out/lines.pngを作ります。 - 2つを合わせて、
/root/gfx/out/final.pngと/root/gfx/out/report.txtを作ります。
参考
- ファイルは、必ず
open(path, "wb")で開きます。テキストモードで開くと、バイトが変形されることがあります。 - サーバーは、
nohup python3 -m http.server 8080 -d /root/gfx/out &で立てます。そのあと、画面のWebプレビューボタンを押して、ポート8080を選んでから、http://localhost:8080/を開きます。 - よくあるミスの1つ目は、PNGの行ごとに付くフィルターバイトを抜かしてしまうことです。絵が斜めにずれて見えます。
- よくあるミスの2つ目は、PPMヘッダーの数字の間は空白、行末は改行だということを守らないことです。ここがずれると、ファイルサイズから変わります。
4x4のPPMを1つ作る
/root/gfx/tiny.pyを作って、4x4のサイズのP6 PPMを/root/gfx/out/tiny.ppmに書いてください。左上の(0,0)は赤(255,0,0)、右上の(3,0)は緑(0,255,0)、左下の(0,3)は青(0,0,255)、右下の(3,3)は白(255,255,255)、残りの12ピクセルは黒です。
P6のヘッダーは3行です。P6、4 4、255を改行でつなげて、そのあとにピクセルのバイトを48個、そのまま書きます。
ピクセルは、左上から右へ、1行が終わったら次の行へ進みます。座標(x, y)の開始位置は、(y * 4 + x) * 3です。
ファイルは必ずバイナリモード(open(path, "wb"))で開いてください。テキストモードで開くと、バイト値10(改行)が、プラットフォームによって変形されることがあります。
256x256のグラデーション
/root/gfx/gradient.pyで、256x256のP6 PPMを/root/gfx/out/gradient.ppmに書いてください。座標(x, y)の色は、ちょうど(x, y, 64)です。
前のステップと同じ構造で、サイズだけが大きくなります。二重のループで、yを外側、xを内側に置けば、バイトが出ていく順序が、そのままファイルの順序になります。
bytearrayに+=でつなげていくほうが、毎回インデックスを計算するよりも速いです。ファイルサイズは、ヘッダー15バイト + 256×256×3 = 196,608バイトになります。
左から右へ行くほど赤くなり、上から下へ行くほど緑が濃くなる絵が出てくるはずです。
zlibとstructだけでPNGを書く
/root/gfx/png.pyにwrite_png(path, w, h, px)関数を作り、前のステップと同じ絵を、/root/gfx/out/gradient.pngとして書いてください。カラータイプは2(トゥルーカラー)、ビット深度は8、行のフィルターはすべて0(None)です。
PNGは、8バイトのシグネチャ\x89PNG\r\n\x1a\nで始まり、そのあとにチャンクが続きます。チャンク1つは、길이(4바이트 빅엔디안) + 종류(4글자) + 내용 + CRC(4바이트)の構成で(プレースホルダーは、長さ(4バイトのビッグエンディアン)、種類(4文字)、内容です)、CRCは種類と内容についてだけ計算します(zlib.crc32)。
IHDRの内容は、struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)です。IDATの内容は、各行の前にフィルターバイト0を1つ付けてつなげたあと、zlib.compressで圧縮したものです。IENDの内容は、空のバイト列です。
行ごとに付くフィルターバイトを抜かすと、絵が斜めにずれて見えます。これが、この形式で最もよくあるミスです。
ブラウザーで自分の絵を見る
/root/gfx/out/index.htmlを作って、gradient.pngを<img>でつなぎ、python3 -m http.server 8080 -d /root/gfx/outを立てたあと、Webプレビューでhttp://localhost:8080/を開いてください。
HTMLは、数行で済みます。画像のパスは同じフォルダーなので、gradient.pngのように相対パスで書きます。
サーバーはターミナルを握るので、python3 -m http.server 8080 -d /root/gfx/out &のように、後ろに&を付けて、バックグラウンドで立ててください。画面の上のほうのWebプレビューボタンを押すと、ポート8080を選んで見られます。
採点は、サーバーではなく、index.htmlファイルとPNGを見ます。立ち上がっていて消えるものを判定に使うと、同じ答えが、ある日は合格し、ある日は失敗するからです。
円を塗りつぶす
/root/gfx/shapes.pyで、256x256のPNGを/root/gfx/out/shapes.pngに書いてください。背景は白(255,255,255)、中心(128,128)、半径60の円の内側は青(40,80,220)で塗りつぶし、画像の外側の枠の1ピクセルは黒(0,0,0)です。
円の内側かどうかは、平方根を使わずに判定します。(x - 128) ** 2 + (y - 128) ** 2 <= 60 ** 2なら内側です。平方根は遅く、ここでは必要もありません。
枠は、xまたはyが0または255のピクセルです。円を先に描いて、枠をあとで描くと、重なる場所で枠が勝ちます。
PNGの書き込みは、前のステップで作ったpng.pyのwrite_pngを、from png import write_pngで持ってきて使ってください。
直線を引く
/root/gfx/line.pyで、256x256のPNGを/root/gfx/out/lines.pngに書いてください。背景は黒で、中心(128,128)から、長さ100の白い直線12本を、30度間隔(0度、30度、...、330度)で伸ばします。角度tの終点は、(128 + 100*cos(t), 128 - 100*sin(t))です。
ブレゼンハムの直線アルゴリズムを使えば、整数演算だけで描けます。誤差の値err = dx + dyをおき(dx = abs(x1-x0)、dy = -abs(y1-y0))、1歩ごとに2*errをdy、dxと比べて、xを進めるかyを進めるかを決めます。
終点のyに引き算が入っていることに注意してください。数学のyは上に向かって増えるのに、画像のyは下に向かって増えるからです。
終点を四捨五入して整数にしてから、描いてください。
重ねて描き、数えた値を書く
/root/gfx/report.pyで、円と直線を1枚に重ねて、/root/gfx/out/final.png(256x256、白い背景、青い円、黒い直線12本、黒い枠)を作り、/root/gfx/out/report.txtに、width=、height=、background=r,g,b、nonbackground=の4行を書いてください。nonbackgroundは、背景色ではないピクセルの個数です。
前の2つのステップをつなげれば済みます。円を先に塗りつぶし、その上に直線を黒で引いてから、枠を描きます。
nonbackgroundは、でっち上げずに、描き終えてからピクセルを数えて書いてください。採点ツールがPNGを直接開いて数え、あなたが書いた値と突き合わせます。値が食い違えば、どちらかが間違っています。
backgroundは、カンマでつなげた3つの整数です。例: background=255,255,255