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

シェーダーとGPUパイプライン

模様を貼ってサンプルを取り出す

TT Labで続きを見る

目標

テクスチャを作り、2種類のサンプリング方式を実装し、透視補正補間の違いを目で見ます。最後に、縮小で生じるちらつきと、ミップマップがそれをどうやってなくすかを、数字で確認します。

なぜ重要なのか

頂点ごとに色を付けると、三角形1つに、色が3つだけです。テクスチャは、色を形状から切り離して、別の画像に置き、表面の各地点が、その画像のどこを指すかだけを決めます。その座標がUVで、ラスタライザーがピクセルごとに補間してくれます。

ここで、2つがとくに重要です。1つは、透視補正です。画面座標でUVをそのまま混ぜると、遠近が入った面で、模様がゆがみます。画面で均等な間隔が、3次元空間では均等ではないからです。もう1つは、縮小です。1つのピクセルがテクセル数十個を覆うと、そのうちの1つだけを選んでも、模様を代表できず、カメラが少し動いただけでも、画面がちらつきます。ミップマップが、その答えです。

ステップ

  1. /root/textureに、ツールボックスを置きます。
  2. /root/texture/tex.pyとout/checker.pngに、チェッカー模様を作ります。
  3. /root/texture/sample.pyに、最近傍サンプリングを作ります。
  4. /root/texture/bilinear.pyに、双線形サンプリングを作ります。
  5. /root/texture/out/affine.pngに、補正なしの補間の絵を書きます。
  6. /root/texture/out/correct.pngとdiff.pngに、透視補正の絵を書きます。
  7. /root/texture/out/mip0.png、mip1.png、out/07-mip.txtに、縮小とミップマップの結果を書きます。

参考

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

/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余計に使う代わりに得るのが、この安定性です。