模様を貼ってサンプルを取り出す
目標
テクスチャを作り、2種類のサンプリング方式を実装し、透視補正補間の違いを目で見ます。最後に、縮小で生じるちらつきと、ミップマップがそれをどうやってなくすかを、数字で確認します。
なぜ重要なのか
頂点ごとに色を付けると、三角形1つに、色が3つだけです。テクスチャは、色を形状から切り離して、別の画像に置き、表面の各地点が、その画像のどこを指すかだけを決めます。その座標がUVで、ラスタライザーがピクセルごとに補間してくれます。
ここで、2つがとくに重要です。1つは、透視補正です。画面座標でUVをそのまま混ぜると、遠近が入った面で、模様がゆがみます。画面で均等な間隔が、3次元空間では均等ではないからです。もう1つは、縮小です。1つのピクセルがテクセル数十個を覆うと、そのうちの1つだけを選んでも、模様を代表できず、カメラが少し動いただけでも、画面がちらつきます。ミップマップが、その答えです。
ステップ
/root/textureに、ツールボックスを置きます。/root/texture/tex.pyとout/checker.pngに、チェッカー模様を作ります。/root/texture/sample.pyに、最近傍サンプリングを作ります。/root/texture/bilinear.pyに、双線形サンプリングを作ります。/root/texture/out/affine.pngに、補正なしの補間の絵を書きます。/root/texture/out/correct.pngとdiff.pngに、透視補正の絵を書きます。/root/texture/out/mip0.png、mip1.png、out/07-mip.txtに、縮小とミップマップの結果を書きます。
参考
- 絵は、
nohup python3 -m http.server 8080 -d /root/texture/out &で立てて、Webプレビューでhttp://localhost:8080/を開きます。affine.pngとcorrect.pngを並べて見ることが、このラボの核心の場面です。 - よくあるミスの1つ目は、双線形で0.5を引かないことです。テクセルの中心が
(i+0.5)/폭にあるので(プレースホルダーは幅です)、この半マスを抜かすと、絵がずれます。 - よくあるミスの2つ目は、負のUVで
int()で切り捨てることです。int(-0.3)は0で、floor(-0.3)は-1なので、1マスずれます。
描画のツールボックスを置く
/root/texture/gfxlib.pyを例のとおりに保存し、/root/texture/check.pyでテストパターンを描いて、/root/texture/out/00-check.pngを作ってください。パターンは、64x64の黒い背景に、(0,0)から(63,63)まで、白(255,255,255)の対角線を引き、そのあとに、(0,32)から(63,32)まで、赤(255,0,0)の横線を重ねて引いたものです。
このラボからは、PNGエンコーダーを作り直しません。最初のラボで手で作ったものと同じコードを、ツールとして配ります。ここで学ぶのは、ファイル形式ではないからです。
ラボのPodにはボリュームがないので、前のラボで作ったファイルが残っていません。そのため、ラボごとに、ツールボックスを置き直すことから始めます。
Canvas(w, h, bg)を作り、line(x0, y0, x1, y1, rgb)で2本の線を引いたあと、write_png(path)で保存してください。交差点(32,32)が赤になるには、横線をあとで引く必要があります。
このラボでは、テクスチャを絵として書き出して、模様を貼った四角形を取り出すのに使います。
チェッカー模様のテクスチャ
/root/texture/tex.pyに、SIZE = 64、CELL = 8、LIGHT = (240,240,240)、DARK = (40,40,60)を置き、64x64のテクセルを行優先で入れたTEXリストと、texel(i, j)関数を作ってください。マスの色は、((i//CELL) + (j//CELL)) % 2 == 0ならLIGHT、そうでなければDARKで、texelは、インデックスを% SIZEで折り返します。そして、/root/texture/out/checker.png(64x64)として書き出してください。
行優先なので、TEX[j * SIZE + i]が(i, j)のテクセルです。前のコースのピクセルの配置と同じ規則です。
texelがインデックスを折り返すことが、そのまま繰り返しラップモードです。ここで一度決めておけば、サンプリング関数が、範囲を気にしなくて済みます。
チェッカー模様を選んだ理由は、ゆがみとちらつきが最もよく見える模様だからです。そのため、テクスチャ関連のテスト画像として、長く使われてきました。
64x64で8ピクセルのマスなら、縦横8マスずつです。
最近傍サンプリング
/root/texture/sample.pyにsample_nearest(u, v)を作ってください。i = floor(u * SIZE)、j = floor(v * SIZE)を整数で求めて、texel(i, j)を返します。範囲外のUVは、texelの折り返しが処理します。
math.floorを使って、intに変えてください。負のUVでint()だけを使うと、0の側に切り捨てられて、1マスずれます。int(-0.3)は0ですが、floor(-0.3)は-1です。
折り返しは、texelの中の% SIZEが行います。Pythonの%は、負の数に対しても正の余りを返すので、そのまま繰り返しモードになります。
u = 1.5は、u = 0.5と同じ結果が出る必要があります。模様が繰り返されるという意味です。
双線形サンプリング
/root/texture/bilinear.pyにsample_bilinear(u, v)を作ってください。x = u*SIZE - 0.5、y = v*SIZE - 0.5に移したあと、i0 = floor(x)、fx = x - i0を求めて、4つのテクセル(i0,j0)、(i0+1,j0)、(i0,j0+1)、(i0+1,j0+1)を、fx、fyの比率で混ぜた実数3つを返します。
0.5を引くことが核心です。テクセルの中心が(i+0.5)/SIZEにあるからです。この半マスを抜かすと、絵が半テクセルだけずれますが、拡大して見る前には、気づきにくいです。
混ぜる式は、次のとおりです。
c00*(1-fx)*(1-fy) + c10*fx*(1-fy) + c01*(1-fx)*fy + c11*fx*fy
4つの重みの和が1なので、4つのテクセルがすべて同じ色なら、その色がそのまま出ます。これで、実装を確認できます。
テクセルの中心にちょうど置かれたUV(例: 0.5/SIZE)では、そのテクセルの色がそのまま出る必要があります。
補正なしで貼る
/root/texture/quad.pyで、四角形に模様を貼って、/root/texture/out/affine.png(256x256、黒い背景)を作ってください。4つの頂点は、画面座標・w・uvの順に、(30,40)/1.0/(0,0)、(226,90)/4.0/(1,0)、(226,166)/4.0/(1,1)、(30,216)/1.0/(0,1)で、三角形は、(0,1,2)と(0,2,3)です。uvは、重心座標でそのまま補間して、最近傍サンプリングで色を取り出します。
wは、カメラからの距離だと考えれば済みます。右の2つの頂点のwが4だということは、そちらが遠くにあるという意味です。
このステップでは、その事実をわざと無視して、uvを画面で均等に混ぜます。そのため、模様がゆがみます。初期のコンソールゲームの床の模様が波打っていたのが、まさにこれです。
三角形2つに分かれているので、対角線を境に、ゆがみの方向が折れます。その折れが、この方式の特徴的な症状です。
重心座標とバウンディングボックスの走査は、前のコースで作ったものと同じです。
透視補正補間
同じ四角形を、u/w、v/w、1/wを補間して、前の2つを後ろの1つで割る方式で、描き直して/root/texture/out/correct.pngを作り、2つの絵のピクセルの差を3倍に拡大して、/root/texture/out/diff.pngとして書き出してください。
補正の公式は、次のとおりです。
iw = b0/w0 + b1/w1 + b2/w2
u = (b0*u0/w0 + b1*u1/w1 + b2*u2/w2) / iw
何を補間するかが変わるだけで、補間自体はそのままです。画面座標で線形な量は、uvではなく、u/wと1/wだというのが要点です。
差分の画像は、min(255, abs(a - b) * 3)を、チャンネルごとに計算すれば済みます。3倍に拡大する理由は、そのまま引くと暗くて、よく見えないからです。
2つの絵を並べて見てください。補正していないほうは、対角線を境に、模様が折れ、補正したほうは、遠くなるほど細かくなる、自然な姿が出ます。
縮小とミップマップ
/root/texture/mip.pyで、64x64の1テクセルのチェッカー((i+j)%2==0ならLIGHT、そうでなければDARK)を作り、2x2の平均で、32x32のミップを1段階作ってください。2つのテクスチャを、それぞれ128x128の画面に、u = (x+0.5)/128*5、v = (y+0.5)/128*5で、最近傍サンプリングして、/root/texture/out/mip0.pngとmip1.pngを作り、横に隣り合うピクセルの、チャンネルの平均絶対差を、/root/texture/out/07-mip.txtに、energy0=、energy1=、level1_uniform=yes|noとして書いてください。
1テクセルのチェッカーは、最も高い周波数の模様です。2x2の平均を1回だけ取ると、明るいテクセル2つと、暗いテクセル2つが混ざって、すべてのテクセルが同じ色になります。そのため、level1_uniformは、yesが出ます。
エネルギーは、次のように数えます。すべてのyと、x=0..126について、|픽셀(x+1,y) - 픽셀(x,y)|を、3つのチャンネルで足して(プレースホルダーは、ピクセルです)、全体を(個数 × 3)で割ります。
energy0は90を超え、energy1は、ちょうど0が出ます。その差が、ちらつきの大きさです。カメラが少し動くと、energy0のほうの絵は、模様がまるごと変わりますが、energy1のほうは、そのままです。
ミップマップが、メモリを3分の1余計に使う代わりに得るのが、この安定性です。