TT Lab
はじめる
学ぶ 学習パス コース

3D数学とソフトウェアラスタライザ

回転する物体を八枚に出力する

TT Labで続きを見る

目標

直方体に光を当てて、面ごとに異なる明るさで塗り、角度を変えて8枚を取り出し、ブラウザーで回してみます。このラボが終われば、コース1のすべての断片が、1つにつながります。ベクトル、行列、カメラ、投影、ラスタライズ、深度、照明です。

なぜ重要なのか

1色で塗った立方体は、六角形のシルエットにすぎず、立体には見えません。人が形を読み取る手がかりは、明るさの違いだからです。明るさは、表面の法線と光の方向がなす角度のコサインに比例し、2つのベクトルを正規化してから内積すれば、そのコサインがそのまま出ます。計算はこのように簡単なのに、結果は、すぐに目に見えます。

ここで、よく間違える所が2つあります。1つは、max(0, ...)を抜かして、光に背を向けた面の明るさが負になることで、もう1つは、法線をモデル行列で回すとき、w=1で掛けて、移動量が足されることです。2つ目は、物体が原点の近くにあると表に出ないので、長く残ります。

ステップ

  1. /root/shadingに、ツールボックスのgfxlib.pyとmat3d.pyを置きます。
  2. /root/shading/mesh.pyで直方体と面法線を作り、out/02-normals.txtを書きます。
  3. /root/shading/light.pyに、ランバート拡散反射を作ります。
  4. /root/shading/out/frame00.pngに、光を受ける直方体を描きます。
  5. 45度ずつ8枚(frame00.pngからframe07.pngまで)を取り出します。
  6. /root/shading/out/index.htmlで、ブラウザーで回してみます。
  7. 8枚をつなげて、/root/shading/out/spin.pngとout/07-sheet.txtを作ります。

参考

描画のツールボックスを置く

/root/shading/gfxlib.pyを例のとおりに保存し、/root/shading/check.pyでテストパターンを描いて、/root/shading/out/00-check.pngを作ってください。パターンは、64x64の黒い背景に、(0,0)から(63,63)まで、白(255,255,255)の対角線を引き、そのあとに、(0,32)から(63,32)まで、赤(255,0,0)の横線を重ねて引いたものです。このラボは、行列のツール/root/shading/mat3d.pyもあわせて使います。

このラボからは、PNGエンコーダーを作り直しません。最初のラボで手で作ったものと同じコードを、ツールとして配ります。ここで学ぶのは、ファイル形式ではないからです。

ラボのPodにはボリュームがないので、前のラボで作ったファイルが残っていません。そのため、ラボごとに、ツールボックスを置き直すことから始めます。

Canvas(w, h, bg)を作り、line(x0, y0, x1, y1, rgb)で2本の線を引いたあと、write_png(path)で保存してください。交差点(32,32)が赤になるには、横線をあとで引く必要があります。

mat3d.pyには、rotate_y、mul、apply、normalize、cross、dotが入っています。このラボでは、それを材料として使い、学ぶのは、照明です。

直方体のメッシュと面法線

/root/shading/mesh.pyに、半径が(1.0, 0.6, 0.4)の直方体を作ってください。VERTSは8つの頂点、TRISは12個の三角形のインデックス3つ組、FACE_COLORSは6つの面の基本色、face_normal(t)は、cross(B-A, C-A)を正規化したものです。そして、12個の法線を、/root/shading/out/02-normals.txtに、f1=nx,ny,nzからf12=まで書いてください。

頂点は、[(sx*1.0, sy*0.6, sz*0.4) for sx in (-1,1) for sy in (-1,1) for sz in (-1,1)]で作れば、8つ出ます。

面1つは四角形なので、三角形2つに分けます。4つの頂点をa、b、c、dとすると、(a,b,c)と(a,c,d)です。

頂点を回る順序が決定的です。外から見て、反時計回りに回るときに、法線が外を指します。確認は、dot(법선, 삼각형 무게중심)が正かどうかを見れば済みます(プレースホルダーは、法線と三角形の重心です)。原点が内側にあるので、外向きの法線なら、必ず正になります。

6つの面の法線は、±x、±y、±zの6方向で、それぞれ2回ずつ出る必要があります。

ランバート拡散反射

/root/shading/light.pyにlambert(normal, light_dir, base_rgb, ambient)を作ってください。k = max(0, dot(normalize(normal), normalize(light_dir)))とおいて、색 = base * (ambient + (1-ambient) * k)の3つのチャンネルを、実数で返します(プレースホルダーは、色です)。

max(0, ...)を抜かすと、光に背を向けた面で内積が負になり、明るさが負に下がります。整数に切り捨てるときにあふれて、色が逆に跳ねます。

ambientは、ほかの表面で跳ね返ってきた光をひとまとめにした定数です。これがないと、背を向けた面が完全に黒くなって、形が消えます。物理的に正確な値ではなく、大域照明を計算しない代わりに入れる補正です。

2つのベクトルを、必ず正規化してください。長さが1でないと、内積がコサインではなくなって、明るさが倍に跳ね上がったり、消えたりします。

光を受ける直方体を1枚

/root/shading/render.pyで、/root/shading/out/frame00.png(256x256、黒い背景)を作ってください。カメラはlook_at((2.2,1.8,3.0),(0,0,0),(0,1,0))、投影はperspective(45,1,0.1,100)、光の方向はnormalize((0.4,0.8,1.0))、ambientは0.2で、モデル行列はrotate_y(0)です。深度バッファーで前後を分け、面ごとに、FACE_COLORSの色にランバートを適用してください。

法線は、モデル行列で一緒に回す必要があります。ただし、位置ではなく方向なので、mat3d.apply(model, n, 0.0)のように、w=0で掛けてください。w=1で掛けると、移動量まで足されて、照明が狂います。

深度は、クリップ座標のzをwで割った値を使い、三角形の中では、重心座標で補間します。前のラボで作ったものと同じ、Zバッファーです。

色は、整数に変換する前に、255で切り詰めてください。ランバート係数は1を超えませんが、丸めであふれることがあります。

3つの面が、互いに異なる明るさで見えれば、正しくできています。1種類の明るさしか見えなければ、法線がすべて同じ値か、正規化が抜けています。

45度ずつ8枚

角度を0度から45度ずつ増やして、/root/shading/out/frame00.pngからframe07.pngまで、8枚を作ってください。ファイル名の数字は2桁(00、01、...、07)です。

前のステップのrender(angle, path)を、そのまま呼び出せば済みます。ループ1つで終わります。

角度を蓄積して、回転行列を掛けていかないでください。浮動小数点の誤差が積み重なって、物体がだんだん歪みます。角度を状態として持ち、フレームごとに行列を作り直すほうが安全です。

直方体の半径が(1.0, 0.6, 0.4)と互いに異なるので、90度回るごとに、見た目がはっきり変わります。立方体なら、90度ごとに同じ絵が出て、回転が見えなかったはずです。

ブラウザーで回してみる

/root/shading/out/index.htmlを作って、8枚を順番に切り替えて見せるようにし、python3 -m http.server 8080 -d /root/shading/outを立てて、Webプレビューでhttp://localhost:8080/を開いてください。HTMLの中には、frame00.pngからframe07.pngまで、8つの名前がすべて入っている必要があります。

最も単純な方法は、<img>を1つ置いて、JavaScriptのsetIntervalでsrcを変えることです。ファイル名の配列を作っておき、インデックスを回せば済みます。

サーバーは、nohup python3 -m http.server 8080 -d /root/shading/out &で、バックグラウンドに立ててください。そうしないと、ターミナルが塞がれます。

採点は、サーバーではなく、index.htmlと8枚のPNGを見ます。立ち上がっていて消えるものを判定に使うと、同じ答えが、ある日は合格し、ある日は失敗するからです。

8枚を1枚にする

8つのフレームを半分のサイズ(128x128)に縮めて、4列2行につなげた/root/shading/out/spin.png(512x256)を作り、/root/shading/out/07-sheet.txtに、cells=8、cell=128x128、sheet=512x256の3行を書いてください。縮小は、縦横2ピクセルのうち左上の1つだけを取る最近傍方式で、フレームkは、행 = k // 4、열 = k % 4の位置に置かれます(プレースホルダーは、行と列です)。

gfxlib.read_png(path)で、作っておいたPNGを読み直せます。読み直したCanvasのget(x, y)で、ピクセルを取り出してください。

最近傍の縮小は、すなわちcell.get(i, j) = frame.get(2*i, 2*j)です。平均を取らないでください。採点ツールが、値をそのまま突き合わせます。

置く位置は、x = 열*128 + i、y = 행*128 + jです(プレースホルダーは、行と列です)。

こうした比較シートは、実務でも使います。フレームを1枚ずつ開いて比べるより、アニメーションの流れが、ずっとよく見えます。