11. 影付けとマッピング

第10章では、各ピクセルでどの面が見えるか、そして其の面が光源の下でどれだけ明るいかを決めました。
まだ2つの物が足りず、其れが無いとCGの画像はどれも作り物っぽく見えます。
1つ目は影です。照らされた面が本当に照らされるのは、其の面と光源の間に何も無い時だけです。
2つ目は細部です。本物の表面には木目、傷、レンガ、凹みがあり、其の全てをポリゴンでモデリングするのは不可能です。
此の章では両方を加えます。先ず古典的な影付けのアルゴリズム、次にテクスチャマッピングと其の様々な仲間(バンプ、法線、ディスプレイスメント、環境、プロシージャルのマップ)です。

何故此れが大事なのでしょうか?
影とテクスチャは第3章(3.9節)の奥行き手がかりの2つで、絵が3次元に見えるかどうかを左右します。
そしてリアルタイムレンダラーでは、フレームのメモリの大部分と時間のかなりの部分が、正に此の2つ、シャドウマップとテクスチャに使われます。
検定対策としては、此の章は分野4の2つのシラバス項目、影付けとマッピングを扱います。
第5章の投影行列、第6章のパイプラインとラスタライズ、第2章の標本化とエイリアシングの上に成り立っています。

11.1 影:其の正体と大事な理由

テーブルの上のボールを見て下さい。
2種類の暗い部分があります:

  • ボールのランプと反対側は、光が浅い角度でしか当たらないか全く当たらないので暗くなります。此れが陰(attached shadow、shade)です。
  • テーブルの上の暗い部分は、ボールが光を遮っているので暗くなります。此れが影(cast shadow)です。

日本語では此の2つを別々の漢字で区別しており、検定対策として覚えておきたい点です。
1つ目は既にシェーディングで扱えています。法線と光の方向の成す角が90°を超えると、第10章の拡散反射の項は0になるからです。
2つ目は他の物体の事を知る必要があり、其れを加えるのが此の章です。
物体が自分自身に影を落とす場合(腕が胴体に、鼻が頬に)はセルフシャドウと呼びます。

影は見る人の為に沢山の仕事をしています:

  • 位置と接地。 影が無いと、ボールが床の上にあるのか浮いているのか分かりません。影は物体を地面に「くっ付け」ます。
  • 形。 凸凹の面に落ちた影は、其の凸凹を見せます。
  • 光の向き。 影は光源の位置を教え、シーン全体に一貫性を与えます。

本影と半影

点光源は1つの点なので、シーンの各点は其れが見えるか見えないかのどちらかです。
影の輪郭はくっきりしています。此れがハードシャドウです。
現実のランプ、窓、空は大きさを持つ面光源です。
すると床の上の点からは、光源の全体が見えるか、全く見えないか、一部だけ見えるかになります:

横から見た図:点光源では遮蔽物の後ろに本影だけが出来、面光源では本影の両側に半影が出来る

  • 本影は光源全体が隠れている部分で、完全に暗くなります。
  • 半影は光源の一部だけが隠れている部分で、暗い所から明るい所へのグラデーションになります。

半影を持つ影がソフトシャドウです。
半影の幅はどれくらいでしょうか?
床からの高さhLにある幅Lの光源と、高さhoにある遮蔽物の端を考えます。
光源の両端から遮蔽物の端を通る2本の光線は2組の相似な三角形を作り、床の上の半影の幅は次の様になります:

w=LhohL−ho

計算例。 高さ4 mにある幅1 mの天井照明と、高さ1 mのテーブルの端:

w=1⋅14−1≈0.33m

床に置いた箱の端は高さ0.1 mしかないので、其の影の半影は1⋅0.13.9≈0.026 m、約2.6 cmです。
其の為、影は接地点の近くではくっきりしていて、遮蔽物から離れる程柔らかくなります(コンタクトハードニング)。
全ての影を同じ量だけぼかすレンダラーが不自然に見えるのは此の為です。
図では光源の幅が1.6で高さ5、遮蔽物が高さ1.5なので、半影の幅は其々の側で1.6⋅1.53.5≈0.69です。

影は光源から見た隠面

全ての影付けアルゴリズムの裏にある大事な考え方は此れです:

点が影の中にあるのは、其の点が光源から隠れている時、其の時だけである。

つまり影付けとは、視点を光源に置き換えてもう一度行う隠面消去(第10章)なのです。
以下のアルゴリズムは、此の2回目の可視判定の方法が違うだけです。幾何を投影する(11.2節)、立体を作る(11.3節)、深度画像を使う(11.4節)、レイを飛ばす(11.5節)です。

11.2 平面への投影による影

最も古く最も簡単な方法である投影法(projected shadow、planar shadow)は、影が床の様な1枚の平らな面にだけ落ちる場合に使えます。
物体の各頂点を光源からの光線に沿って平面に投影し、投影した物体を暗い色で描きます。
此の投影は光源を投影中心とする透視投影(第5章)なので、1つの4×4行列で書けます。

平面 y = 0 への影の行列

光源を𝐥=(lx,ly,lz)、其の下にある点を𝐩=(px,py,pz)(py<ly)とします。
光線𝐥+t(𝐩−𝐥)はt=lyly−pyでy=0に届き、影の点は次の様になります:

sx=lypx−lxpyly−py,sy=0,sz=lypz−lzpyly−py

共通の分母は正に同次座標が扱える物です(線型代数の第9章)。
分子をx、y、zに、分母をwに入れれば、残りはwでの除算がやってくれます。

𝐌=[ly−lx0000000−lzly00−10ly],𝐌[pxpypz1]=[lypx−lxpy0lypz−lzpyly−py]

任意の平面𝐠=(a,b,c,d)(ax+by+cz+d=0を満たす点の集まり)と、同次座標で書いた光源𝐥=(lx,ly,lz,1)に対して、同じ行列は次の様になります:

𝐌=(𝐠⋅𝐥)𝐈−𝐥𝐠⊤

此処で𝐥𝐠⊤は4×1の列と1×4の行の積で、4×4の行列です(線型代数の第7章)。
何故此れで上手く行くかは簡単に分かります。
𝐌𝐩=(𝐠⋅𝐥)𝐩−(𝐠⋅𝐩)𝐥は𝐩と𝐥の組み合わせなので、光源と点を通る直線上にあります。
そして𝐠⋅(𝐌𝐩)=(𝐠⋅𝐥)(𝐠⋅𝐩)−(𝐠⋅𝐩)(𝐠⋅𝐥)=0なので、平面上にあります。
𝐠=(0,1,0,0)とすると上の行列になります。
太陽の様な平行光源では、光源への方向を使って𝐥=(lx,ly,lz,0)とします。
すると全ての点でwはlyの儘なので行列はアフィン変換になり、式はsx=px−lxlypyという平行投影になります。

計算例

光源は𝐥=(−2,6,0)、床の上に(−1,0,−1)から(1,2,1)までの箱があります。
此処では𝐠⋅𝐥=ly=6なので:

𝐌=[6200000000600−106]

上の角𝐩=(1,2,1)は:

𝐌(1,2,1,1)=(6+4,0,6,−2+6)=(10,0,6,4)→𝐬=(104,0,64)=(2.5,0,1.5)

光線で確かめます。t=66−2=1.5で、𝐥+1.5(𝐩−𝐥)=(−2+1.5⋅3,6−1.5⋅4,0+1.5⋅1)=(2.5,0,1.5)です。
光源側の上の角(−1,2,1)は箱自身の下の(−0.5,0,1.5)に落ち、下の角は其の場に留まります。py=0なら𝐌𝐩=6𝐩だからです。

投影による影の例を横から見た図:−2, 6にある光源、箱、そして角pの影s = 2.5, 0, 1.5

手計算の確認として、同じ行列をlinalg.hhを使ったC++で書くと次の様になります:

#include <cstdio>
#include "linalg.hh"

// Shadow projection onto the plane a x + b y + c z + d = 0 from the light l
// (l.w = 1: point light at l.xyz, l.w = 0: directional light towards l.xyz).
// M = (plane . l) I - l plane^T
Mat4 shadowMatrix(Vec4 plane, Vec4 l) {
  float pl[4] = {plane.x, plane.y, plane.z, plane.w};
  float lv[4] = {l.x, l.y, l.z, l.w};
  float d = dot(plane, l);
  Mat4 m;
  for (int row = 0; row < 4; ++row) {
    for (int col = 0; col < 4; ++col) {
      m.at(row, col) = (row == col ? d : 0.0f) - lv[row] * pl[col];
    }
  }
  return m;
}

int main() {
  Mat4 s = shadowMatrix({0, 1, 0, 0}, {-2, 6, 0, 1}); // ground y = 0, light at (-2, 6, 0)
  for (int row = 0; row < 4; ++row) {
    printf("%4g %4g %4g %4g\n", s.at(row, 0), s.at(row, 1), s.at(row, 2), s.at(row, 3));
  }
  Vec4 p{1, 2, 1, 1};
  Vec4 q = s * p;
  printf("homogeneous (%g, %g, %g, %g)\n", q.x, q.y, q.z, q.w);
  printf("shadow point (%g, %g, %g)\n", q.x / q.w, q.y / q.w, q.z / q.w);
  return 0;
}

   6    2    0    0
   0    0    0    0
   0    0    6    0
   0   -1    0    6
homogeneous (10, 0, 6, 4)
shadow point (2.5, 0, 1.5)

実際の使い方と限界

影を描くには、物体をもう一度、モデル行列とビュー行列の間に影の行列を挟んだ𝐏𝐕𝐌shadow𝐌modelで、暗い色で、ライティング無しで描きます。
直ぐに3つの実際的な問題が出てきます:

  • 潰した物体は床の平面にぴったり乗るので、深度バッファの中で床と争います(Zファイティング、10.3節)。普通は少しの深度オフセットを付けるか、ステンシルテストを使って影を描きます。
  • 影を半透明で描くと、潰した物体の重なった三角形が床を2回暗くしてしまいます。ステンシルバッファ(第6章)で床のピクセルに印を付ければ、1回だけ暗くする事が出来ます。
  • 光源より上の点(py>ly)はwが負になり、反対側に投影されます。偽の「反影」です。此の様な点はクリップして取り除く必要があります。

そして大きな限界は、影が其の1枚の平面にしか落ちない事です。
曲面にも、他の物体にも、物体自身にも影は出来ません。
平らな床の上のキャラクター用に初期の3Dゲームで人気があり、更に安上がりな親戚である、各キャラクターの下に置くぼかした暗い円(丸影、blob shadow)は今でもよく使われています。

11.3 シャドウボリューム法

シャドウボリューム法は1977年にFrank Crowが提案しました。
影になる空間の領域を幾何として表します。

考え方

光源から見た遮蔽物のシルエットエッジ(輪郭エッジ)を見つけます。光源の方を向いた三角形と、反対を向いた三角形が共有する辺です。
各シルエットエッジを光源から遠ざかる方向へ(無限遠、又は十分遠くまで)押し出し、四角形にします。
此れらの四角形は遮蔽物自身と一緒に、其の後ろの空間の領域を閉じます。此れがシャドウボリュームです。
ボリュームの内側の点は全て影の中にあり、外側の点は(此の光源と此の遮蔽物に関しては)照らされています。

ステンシルバッファで数える

ピクセルに見えている点がボリュームの内側にあるかどうかは、どうやって分かるのでしょうか?
視点から其の点までのレイに沿って進み、横切るボリュームの面を数えます。視点の方を向いた面(表面)を通って入ると1を足し、反対を向いた面(裏面)を通って出ると1を引きます。
数が0でなければ其の点はボリュームの内側にあり、影の中です。

遮蔽物の後ろのシャドウボリュームを横から見た図。地面の3点への視線が面を横切り、カウントは0、+1、+1−1になる

図のボリュームには2つの側面があります。緑の面は視点を向き(+1)、ピンクの面は反対を向いています(−1)。

  • P1:視線はどの面も横切らず、カウントは0で、光が当たります。
  • P2:視線は緑の面から入って内側で止まり、カウントは1で、影の中です。
  • P3:視線は地面に届く前に入って(+1)出ます(−1)。カウントは0で、光が当たります。此の点はシャドウボリュームの中ではなく、其の後ろにあります。

1991年にTim Heidmannが、GPUが此の数え上げをステンシルバッファ(ピクセルごとの小さな整数、第6章)で行える事を示しました:

  1. シーン全体を環境光だけで描きます。此れで深度バッファも埋まります。
  2. 深度とカラーの書き込みを止め、全てのシャドウボリュームの表面を描き、深度テストに合格した所(面が見えている面より手前にある所)でステンシル値に1を足します。
  3. 裏面を描き、深度テストに合格した所で1を引きます。
  4. 光源を点けてシーンをもう一度描きますが、ステンシル値が0の所だけにします。

見えている点より手前の面は深度テストに合格するので、段階2と3は視点と其の点の間の横切りを正確に数えます。
此れをZパス法(depth pass)と呼びます。
重なった複数のボリュームは単純に足し合わされます。判定が「1と等しい」ではなく「0でない」なのは其の為です。

Zパス法とZフェイル法

Zパス法には1つ弱点があります。視点自身がシャドウボリュームの内側にある時(カメラが柱の影の中に立っている時)、レイは入る面を横切らずに内側から始まるので、全てのカウントが1ずれ、光が当たる所と影の所が入れ替わってしまいます。
其の対策は、レイの反対側の半分、つまり見えている点から無限遠までを数える事です。深度テストに不合格の(見えている点より奥にある)裏面で1を足し、不合格の表面で1を引きます。
上のP2では、点より奥のレイが裏面を通ってボリュームを1回出るので+1で、やはり影の中です。P3では奥に何も無いので0で、光が当たります。
此のZフェイル法(depth fail)はBilodeauとSongy(1998年)が見つけ、John Carmack(2000年)も独立に見つけたので「Carmack's reverse」とも呼ばれます。此の場合、ボリュームは両端をキャップで閉じる必要があります。
Doom 3(2004年)は、世界全体をステンシルによるシャドウボリュームで照らした事で有名なゲームです。

長所と短所

  • 自分自身への影も含め、全ての面にピクセル単位で正確なハードシャドウが付き、解像度の問題がありません。
  • 但し、シルエットエッジをCPUかジオメトリ段階で見つける必要があり、メッシュは閉じていなければならず(2次元多様体、第7章)、押し出した巨大な四角形はフィルレートを大量に消費します。
  • ハードシャドウだけです。ソフトシャドウには、多数の光源位置からの多数のボリュームが必要になります。

フィルレートのコストと柔らかい輪郭が無い事から、今日の殆どのエンジンは代わりにシャドウマップを使っています。

11.4 シャドウマッピング

シャドウマッピングは1978年にLance Williamsが発表しました。
光源からの可視判定を、GPUが非常に得意とする深度バッファで行います。

2つのパス

  1. 光源から描く。 光源の位置に、光が照らす方を向いたカメラを置き、深度だけを描きます。結果がシャドウマップ(デプスマップとも言います)で、各テクセルには其の方向で光源が見る一番近い面までの距離が入ります。
  2. カメラから描く。 見えている各点を、光源のカメラと同じ様に光源のビュー変換と投影変換で変換します。すると、シャドウマップ上の位置(u,v)と、光源から見た自分の深度zが得られます。(u,v)に保存された深度zmapを読みます:

z>zmap⇒光源により近い物が遮っている:影

光源のカメラは第5章の行列を其の儘使います。lookAtのビュー行列と投影行列です。
スポットライトには円錐を覆う視野角の透視投影を、平行光源である太陽には光線が平行なので正射影を使います。
全方向を照らす点光源には6つの視点が必要で、キューブマップ(11.9節)の6つの面として保存します。
投影とwでの除算の後、NDCは−1から1まで(OpenGL)なので、最後に拡大と平行移動でテクスチャ座標と0〜1の深度に写します:

u=xndc+12,v=yndc+12,z=zndc+12

行列で書くと、参照の為の変換全体は𝐒=𝐁𝐏light𝐕lightです。𝐁は対角成分と最後の列に0.5を持つ、此の拡大と平行移動の行列(バイアス行列)です。

計算例

数値でやってみましょう。
シーンは床y=0と、(−1,0,−1)から(1,2,1)までの箱です。
太陽は方向𝐟=(0.6,−0.8,0)(右下向き)に照らすので、光源のカメラを𝐞=(−6,8,0)に置き、上方向(0,1,0)で原点を見させます。
第5章のlookAtの作り方から、前方向𝐟=(0.6,−0.8,0)、右方向𝐫=𝐟×𝐮𝐩=(0,0,0.6)を正規化した(0,0,1)、本当の上方向𝐮=𝐫×𝐟=(0.8,0.6,0)が得られます。
点の光源ビュー座標は、𝐩−𝐞との3つの内積です:

xv=𝐫⋅(𝐩−𝐞)=pz,yv=𝐮⋅(𝐩−𝐞)=0.8px+0.6py,dist=𝐟⋅(𝐩−𝐞)=0.6px−0.8py+10

(最後の物は光源カメラの前方の距離で、−zvです。)
正射影は−4≤xv,yv≤4、前方クリップ面n=5、後方クリップ面f=15を覆うので、xndc=xv/4、yndc=yv/4で、深度は線型のz=dist−510です。
シャドウマップは16 × 16テクセルです。

箱の直ぐ右にある床の点F=(1.5,0,0.2)を取ります:

xv=0.2,yv=1.2,dist=0.9+10=10.9
u=0.05+12=0.525,v=0.3+12=0.65,z=10.9−510=0.59

テクセル単位では16u=8.4、16v=10.4なので、列8、行10です。
パス1では其処に何が保存されたでしょうか?
テクセルの中心はu=8.5/16、v=10.5/16、つまりxv=0.25、yv=1.25です。
其のレイを光源から辿ると、最初に出会う面は箱の上面y=2で、0.8px+1.2=1.25からpx=0.0625、dist=0.0375−1.6+10=8.4375、保存された深度はzmap=0.34375です。
0.59>0.344なので、箱がFと太陽の間にあり、Fは影の中です。

計算例を横から見た図:光源の投影面のテクセル行10と12、其のレイが当たる箱の上面と床、床の点FとG

次に、影から十分離れた床の点G=(3,0,0.2)です(箱の上面の影はx=2.5で終わります):

yv=2.4,dist=11.8,z=0.68,16v=12.8→行12

行12の中心はyv=2.25です。
其のレイは箱を外れ、0.8px=2.25、px=2.8125で床に当たり、dist=11.6875、zmap=0.66875です。
すると0.68>0.669なので、Gは日向にあるのに判定は影になってしまいます!

シャドウアクネとバイアス

Gでは何がおかしかったのでしょうか?
各テクセルは1つのサンプル、つまり中心での深度を保存しますが、床は光源に対して傾いているので、テクセルの中で深度が変わります。
Gは(yvで)テクセルの中心から0.15だけ奥側にあるので、サンプルより少し深くなり、床が自分自身に影を落としてしまいます。
面全体では各テクセルの半分が暗くなり、シャドウアクネと呼ばれる典型的な縞模様やモアレになります:

左:保存された深度は傾いた面の上で階段になり、各テクセルの段より奥の半分が誤って影になる。右:バイアスで段を面の下に下げる

普通の対策はデプスバイアスです。少し大きめの値と比べる、つまりz−b>zmapとします。
bはどれだけ大きければ良いのでしょうか?
床の上では、yvが1増える毎にdistは0.75増え(px=yv/0.8で、distは0.6pxずつ増えるので)、点は中心から最大で半テクセル、0.25離れられます。
従って最大の誤差は0.25⋅0.75=0.1875、0〜1の深度で0.01875です。
係数0.75はtanθで、θは床の法線と光源への方向の成す角です(cosθ=0.8)。面が光源から傾く程、多くのバイアスが必要になります。
其の為、実際のレンダラーは定数と傾きに比例する部分を足した傾斜スケールバイアス(slope-scaled bias)を使います。
b=0.02とすると、Gは0.68−0.02=0.66<0.669で、正しく光が当たります。

バイアスが大き過ぎると逆の誤りが起きます。
(光の方向に測って)遮蔽物からbより近い受け手は全て光が当たる扱いになるので、薄い物体は影を失い、床に立つ物体の影は根元から少し離れて始まり、物体が浮いている様に見えます。
影が取れてしまうピーターパンにちなんで、此れをピーターパン現象(peter panning)と呼びます。
計算例の光源と48 × 48のシャドウマップを使い、此れらの式で描いた全体像が次の図です:

箱と薄い板の4つのレンダリング:バイアス無しで縞状のアクネ、適切なバイアス、大き過ぎて板の影が消えるバイアス、輪郭が柔らかいPCF

板は床から0.5浮いているので、光に沿った隙間は0.5/0.8=0.625、深度で0.0625です。
バイアス0.08は其れより大きいので、板の影が消えます。
(1枚目の縞模様の四角は、正にシャドウマップが覆う範囲です。其の外は全て光が当たる扱いです。)

解像度とエイリアシング

シャドウマップは標本化された画像なので、第2章の事が全て当てはまります。
計算例では1テクセルが8/16=0.5を覆うので、影の輪郭は約0.5刻みでしか動けず、角張った階段状になります。
更に悪い事に、シャドウマップのテクセルは光源から見て均等に並んでいますが、カメラは自分の近くにずっと多くのテクセルを必要とし、其処では1テクセルが何十ピクセルも覆う事があります(透視エイリアシング)。
典型的な対策:

  • シャドウマップの解像度を上げる(2048²や4096²テクセルがよく使われます)。
  • 光源の視錐台を、カメラが実際に見ている範囲にぴったり合わせる。
  • カスケードシャドウマップ:太陽用に複数のシャドウマップを使い、カメラの近くには細かい物、遠くには粗い物を使う。

PCF(パーセンテージクローサーフィルタリング)

テクスチャの様にシャドウマップをぼかせば、輪郭を柔らかく出来るでしょうか?
其の儘では出来ません。深度を平均してもソフトシャドウにはなりません。
影の輪郭の近くにある深度0.60、バイアス0.02のフラグメントと、其の周りの9つの保存された深度を考えます:

[0.350.350.620.350.350.620.350.620.62]

平均は5⋅0.35+4⋅0.629=0.47で、0.58>0.47なので「完全に影」です。やはり「はい」か「いいえ」の答えで、場所がずれただけです。
PCF(percentage-closer filtering、Reeves・Salesin・Cook、1987年)は先に比較を行い、其の結果を平均します。
此処では0.62の4つのテクセルは合格し(0.58≤0.62)、0.35の5つは不合格なので、点は49≈0.44だけ照らされます。中間の値なので、柔らかい輪郭になります。
GPUは此れをハードウェアで対応しています。「比較サンプラー」(GLSLのsampler2DShadow、SlangやHLSLのSamplerComparisonState)は、1回の参照で4つの隣を比較し、双線型にフィルタリングします。
PCFは輪郭を決まった量だけぼかすだけで、遮蔽物からの距離と共に広がる本物の半影を表す物ではありません。
半影の幅を推定する方法(percentage-closer soft shadowsなど)は第2部で扱います。
上の4枚目の図で、PCFには少し多めのバイアス(0.012ではなく0.025)が必要な事に注意して下さい。中心がより遠い隣のテクセルとも比べるからです。

参照とPCFをシェーダーの関数にすると次の様になります。
座標は計算例と同じOpenGLの規約に従っています。Vulkanの0〜1の深度範囲では、ndc.zを其の儘深度として使って下さい。

// Light-space lookup coordinates: (u, v) in the shadow map and the depth seen from the light.
float3 shadowCoords(float4x4 lightViewProj, float3 worldPos) {
  float4 clip = mul(lightViewProj, float4(worldPos, 1.0));
  float3 ndc = clip.xyz / clip.w;
  return float3(ndc.xy * 0.5 + 0.5, ndc.z * 0.5 + 0.5);
}

// 3 x 3 percentage-closer filtering: 1 = fully lit, 0 = fully in shadow.
float shadowPcf(Texture2D<float> shadowMap, SamplerState pointSampler, float3 uvDepth, float bias) {
  uint w, h;
  shadowMap.GetDimensions(w, h);
  float2 texel = float2(1.0 / float(w), 1.0 / float(h));
  float lit = 0.0;
  for (int y = -1; y <= 1; ++y) {
    for (int x = -1; x <= 1; ++x) {
      float stored = shadowMap.SampleLevel(pointSampler, uvDepth.xy + float2(x, y) * texel, 0);
      lit += (uvDepth.z - bias > stored) ? 0.0 : 1.0;
    }
  }
  return lit / 9.0;
}
// Light-space lookup coordinates: (u, v) in the shadow map and the depth seen from the light.
vec3 shadowCoords(mat4 lightViewProj, vec3 worldPos) {
  vec4 clip = lightViewProj * vec4(worldPos, 1.0);
  vec3 ndc = clip.xyz / clip.w;
  return vec3(ndc.xy * 0.5 + 0.5, ndc.z * 0.5 + 0.5);
}

// 3 x 3 percentage-closer filtering: 1 = fully lit, 0 = fully in shadow.
float shadowPcf(sampler2D shadowMap, vec3 uvDepth, float bias) {
  vec2 texel = 1.0 / vec2(textureSize(shadowMap, 0));
  float lit = 0.0;
  for (int y = -1; y <= 1; ++y) {
    for (int x = -1; x <= 1; ++x) {
      float stored = texture(shadowMap, uvDepth.xy + vec2(x, y) * texel).r;
      lit += (uvDepth.z - bias > stored) ? 0.0 : 1.0;
    }
  }
  return lit / 9.0;
}
// Light-space lookup coordinates: (u, v) in the shadow map and the depth seen from the light.
float3 shadowCoords(float4x4 lightViewProj, float3 worldPos) {
  float4 clip = mul(lightViewProj, float4(worldPos, 1.0));
  float3 ndc = clip.xyz / clip.w;
  return float3(ndc.xy * 0.5 + 0.5, ndc.z * 0.5 + 0.5);
}

// 3 x 3 percentage-closer filtering: 1 = fully lit, 0 = fully in shadow.
float shadowPcf(Texture2D<float> shadowMap, SamplerState pointSampler, float3 uvDepth, float bias) {
  uint w, h;
  shadowMap.GetDimensions(w, h);
  float2 texel = float2(1.0 / float(w), 1.0 / float(h));
  float lit = 0.0;
  for (int y = -1; y <= 1; ++y) {
    for (int x = -1; x <= 1; ++x) {
      float stored = shadowMap.SampleLevel(pointSampler, uvDepth.xy + float2(x, y) * texel, 0);
      lit += (uvDepth.z - bias > stored) ? 0.0 : 1.0;
    }
  }
  return lit / 9.0;
}

結果は光源の拡散反射と鏡面反射の項に掛けます(環境光の項には掛けません)。其の為、影の部分にも環境光は残ります。
2パスのレンダラー全体は、OpenGLシリーズの第23章で自分で作ります。

何故シャドウマッピングがリアルタイムの標準なのでしょうか?
描画出来る幾何なら何にでも使え(閉じたメッシュもシルエットも要りません)、自分自身への影や曲面の受け手も扱え、コストは深度パス1回分の他はシーンの複雑さに殆ど依存せず、フィルタリングも出来るからです。
代償はバイアスの調整とエイリアシングです。

11.5 レイトレーシングによる影とアンビエントオクルージョン

レイトレーサー(第12章)は「此の点は光源から隠れているか?」に最も直接的に答えます。
点から光源に向けてシャドウレイを飛ばし、光源より手前で何かに当たるかを調べるのです。
距離dにある点光源なら、ε<t<dの範囲に交点が無ければ点は照らされています。
小さなε(又はレイの始点を法線方向に少しずらす事)は、丸め誤差で面が自分自身に当たるのを防ぎます。シャドウアクネのレイトレーシング版です。

シャドウレイは何に対しても正確なハードシャドウを与え、ソフトシャドウも自然に作れます。
面光源を多数のサンプル点で置き換え、其々にシャドウレイを1本ずつ飛ばすのです。
計算例。 影の輪郭の近くの点が、面光源上に散らばった16個の点に16本のシャドウレイを飛ばし、其のうち6本が遮られたとします。
其の点は光源の直接光の16−616=0.625を受け取るので、半影の中にあります。
レイが少ないと半影にノイズが出ます。レイを増やすとノイズがどう減るかは第12章(モンテカルロ法)の話題です。

球と箱を、点光源とハードシャドウ、面光源とソフトシャドウ、アンビエントオクルージョンだけで描いた物

3枚目はアンビエントオクルージョン(AO、環境遮蔽)です。
第10章の環境光の項は、全ての点で全方向から均等に光が来ると仮定するので、隅、溝、物体の下の地面が平坦で明る過ぎに見えます。
アンビエントオクルージョンは、点の上の半球のうちどれだけが開いているかを測ります。法線の周りの半球に(或る距離まで)レイを飛ばし、何本が抜けるかを数えるのです。
計算例: 32本のレイのうち8本が近くの幾何に当たれば、AOの係数は1−832=0.75で、其の点の環境光に0.75を掛けます。
完全な大域照明(第12章)が計算する暗くなり方の、安上がりな真似です。
映画のレンダラーはレイで計算し、ゲームは深度バッファから画面空間で近似するか(SSAO、2007年のCrysisが最初の製品)、テクスチャに焼き込みます。第2部で更に深く扱います。

影付けの方法の比較

方法 受け手 自分自身への影 ソフトシャドウ 主なコストとアーティファクト
投影法(平面) 1枚の平面だけ 無し 無し(偽のぼかしのみ) 安い。Zファイティング、光源より上の点の反影
シャドウボリューム法 全て 有り 無し シルエット、閉じたメッシュ、フィルレート。輪郭は正確
シャドウマッピング 全て 有り フィルタリング(PCF)で可能 追加の深度パス。アクネ、ピーターパン現象、エイリアシング
レイトレーシングの影 全て 有り 面光源で有り 多数のレイ。半影のノイズ

検定のポイント

  • 陰(attached shadow)はシェーディングから生まれ、影(cast shadow)には光源からの可視判定が必要です。
  • 点光源はハードシャドウ(本影だけ)を、面光源は本影と半影を作ります。
  • 投影法:4×4行列で物体を光源から1枚の平面に投影します。自分自身への影も曲面の受け手もありません。
  • シャドウボリューム法:シルエットエッジを押し出し、ステンシルバッファで横切りを数えます(表面+1、裏面−1)。0でなければ影です。Zパス法は視点がボリュームの内側にあると破綻し、Zフェイル法はキャップを付けたボリュームで其れを直します。
  • シャドウマッピング:光源からの深度を作り、其れから比較します。アクネにはバイアス、大き過ぎるバイアスはピーターパン現象、限られた解像度はエイリアシングを生みます。PCFは先に比較して後で平均します。
  • シャドウレイ:光源(又は光源のサンプル)ごとに1本のレイ。面光源ではソフトシャドウになります。

11.6 テクスチャマッピングとテクスチャ座標

テクスチャマッピングは画像を面に貼り付けます。
1974年にEdwin Catmullが述べた方法で、12個の三角形の箱が木箱に見えるのは此のお陰です。
画面のピクセルと区別する為に、テクスチャ画像の1つ1つの画素をテクセル(texture elementの略)と呼びます。

テクスチャ座標

どのテクセルが面のどの点に対応するかを知る為に、各頂点にテクスチャ座標を持たせます。普通は(u,v)と書きます(UV座標)。
画像の大きさに関係無く、画像全体で0から1まで変化します。(0,0)が1つの角、(1,1)が反対の角です。
ラスタライズは他の頂点の値と同じ様に其れを各三角形の上で補間し(第6章)、フラグメントシェーダーは補間された(u,v)でテクスチャを読みます。
W×Hテクセルのテクスチャでは、テクセル(i,j)はiW≤u<i+1Wを覆い、其の中心はu=i+0.5Wです。第6章のピクセルの中心と全く同じです。
(v=0が画像の上と下のどちらの角かはAPIやファイル形式によって違い、テクスチャが上下逆になるよくある原因です。)

面にテクスチャ座標を割り当てる事をパラメータ化と言います。2次元のテクスチャの正方形から3次元の面への写像です。
第8章のパラメトリック曲面なら無料で手に入ります。其の面自身のパラメータ(u,v)を其の儘テクスチャ座標に使えます。
ポリゴンメッシュでは、単純な投影か展開で作る必要があります。

投影によるマッピング:平面・円筒・球面・ボックス

UVを手早く得る方法は、単純な形からテクスチャを物体に投影する事です。

平面投影は、スライドプロジェクターの様に1つの軸に沿って投影します。(x0,y0)から大きさw×hの長方形へz方向に投影するなら:

u=x−x0w,v=y−y0h

平らな物(床、壁、ラベル)には完璧ですが、投影方向に平行な面ではテクスチャが筋状に伸びてしまいます。

円筒投影は、軸(此処ではy軸)の周りにテクスチャを巻き付け、軸周りの角度をuにします:

φ=atan2(x,z),u=0.5+φ2π,v=y−yminymax−ymin

(atan2(x,z)は点(z,x)の角度で、+zから+xの方へ測った−πからπまでの値です。)
計算例:y=0からy=2までの半径1の円筒上の点(1,0.5,0)はφ=atan2(1,0)=π2なので、u=0.5+0.25=0.75、v=0.52=0.25です。
正面の点(0,1,1)はφ=0でu=0.5です。
背面の(0,1,−1)ではφ=πでu=1ですが、其の少し先の点はφ≈−πでu≈0になります。uが1から0に戻って跳ぶ継ぎ目があるのです。

球面投影は、世界地図の様に経度と緯度を使います:

u=0.5+atan2(x,z)2π,v=0.5+arcsinyπ(単位球上の点)

計算例: 単位ベクトル𝐩=(0.5,0.7071,0.5)。
atan2(0.5,0.5)=π4からu=0.5+0.125=0.625、arcsin0.7071=π4からv=0.5+0.25=0.75です。
球面投影には継ぎ目の他に2つの極があります。y=±1では、テクスチャの一番上の行全体が1点に押し込まれます。

ボックス投影は、箱の各面に1つずつ、6つの平面投影を使い、各点で面の法線に一番近い軸の投影(法線の成分が一番大きい物)を選びます。
法線が(0.3,−0.9,0.2)の点はy方向の投影を使います。
筋は出ませんが、2つの投影が出会う所に継ぎ目が見えます。3つの投影を滑らかに混ぜる方法は、ゲームエンジンではトライプラナーマッピングと呼ばれます。

球に市松模様のテクスチャを平面投影、円筒投影、球面投影、ボックス投影で貼った物

各球の上の方を見て下さい。平面投影は筋になり、円筒投影は極の近くで行が伸び、球面投影は行が極に押し込まれ、ボックス投影は正方形を保つ代わりに継ぎ目があります。
古典的な一般化である2段階マッピング(Bier・Sloan、1986年)は、先ずテクスチャを此の様な単純な中間形状に写し、其れから例えば物体の法線に沿って本当の物体に移します。

UV展開と継ぎ目

キャラクターや車には、1つの投影では間に合いません。
代わりにアーティストはメッシュを展開します(UV展開)。選んだ辺、つまり継ぎ目(シーム)に沿って切り開き、服の型紙の様に部品を平らにしてテクスチャの正方形に並べます。
平らにした部品の集まりはテクスチャアトラスと呼ばれます。良い展開は歪み(伸び)を小さく保ち、継ぎ目を誰も見ない所に隠します。
継ぎ目上の頂点は両側に1つずつ異なるUVを持つので、2回保存する必要があります。第9章のハードエッジで頂点を分割したのと全く同じです。

ラップモード

0から1の外のuではどうすれば良いでしょうか?
其れはラップモード(アドレッシングモードとも言います)で選びます:

  • リピート:小数部分u−⌊u⌋を使うので、テクスチャが繰り返し並びます。uが0から20までの床には20枚のタイルが並びます。
  • エッジにクランプ:[0,1]に収めるので、端のテクセルが外側へ引き伸ばされます。
  • ミラーリピート:繰り返しますが、1枚おきに反転させるので、隣のタイルと段差無く繋がります。

計算例。 u=2.3:リピート0.3、クランプ1.0、ミラー0.3(2は偶数なので反転無し)。
u=−0.2:リピート0.8、クランプ0、ミラー0.2。
u=1.3:リピート0.3、クランプ1.0、ミラー1−0.3=0.7(1は奇数なので反転)。

非対称なテクスチャをuとvが−1から2までの範囲でリピート、エッジにクランプ、ミラーリピートで表示した物。白い四角が0から1

OpenGLではGL_REPEAT、GL_CLAMP_TO_EDGE、GL_MIRRORED_REPEAT、Vulkanでは同じ名前のVK_SAMPLER_ADDRESS_MODE_の値です。4つ目の選択肢であるボーダーへのクランプは、外側で決まった境界色を返します。

11.7 テクスチャのフィルタリング

テクセルがぴったり1つのピクセルに乗る事は殆どありません。
1つのテクセルが複数のピクセルを覆うのが拡大で、カメラが近いかテクスチャが小さい場合です。
1つのピクセルが複数のテクセルを覆うのが縮小で、面が遠いか浅い角度で見ている場合です。
此の2つには異なるフィルタが必要です。

拡大:最近傍と双線型

最近傍補間(ニアレストネイバー)は、点を含むテクセルを取ります:i=⌊uW⌋、j=⌊vH⌋。
速くてピクセルの角がくっきり残りますが(ドット絵のゲームは正に其れを望みます)、拡大したテクスチャはブロック状に見えてしまいます。

双線型補間(バイリニア補間)は、点を囲む中心を持つ4つのテクセルを混ぜます。
テクセルの中心が整数に来る様に座標をずらし、

x=uW−0.5,y=vH−0.5,i=⌊x⌋,j=⌊y⌋,fx=x−i,fy=y−j

両方の行でxに沿って線型補間し、其の2つの結果の間でyに沿ってもう1回補間します:

a=(1−fx)Ti,j+fxTi+1,j,b=(1−fx)Ti,j+1+fxTi+1,j+1,T(u,v)=(1−fy)a+fyb

計算例。 T1,0=40、T2,0=80、T1,1=120、T2,1=200の4 × 4のグレーのテクスチャを、(u,v)=(0.4375,0.3125)でサンプリングします:

x=0.4375⋅4−0.5=1.25,y=0.3125⋅4−0.5=0.75

従ってi=1、j=0、fx=0.25、fy=0.75です:

a=40+0.25(80−40)=50,b=120+0.25(200−120)=140
T=50+0.75(140−50)=117.5

最近傍ではi=⌊1.75⌋=1、j=⌊1.25⌋=1なのでT1,1=120です。

値が40、80、120、200の4つのテクセルの中心、2つの行の結果50と140、双線型の結果117.5

8×8のテクスチャを最近傍補間と双線型補間で32倍に拡大した物

双線型の拡大は滑らかですがぼやけます。テクスチャに無い細部を作り出す事は出来ません。
双三次補間(バイキュービック)の様な高次のフィルタは4 × 4のテクセルを使い、よりシャープです。此れらは画像の再標本化と一緒に第15章で再び出てきます。
2つのサンプラーとラップモードをC++で書くと次の様になります:

#include <cmath>
#include <cstdio>

enum class Wrap { Repeat, Clamp, Mirror };

// Maps any texture coordinate into [0, 1] with the chosen wrap mode.
float wrap(float u, Wrap mode) {
  float k = std::floor(u);
  float f = u - k;
  switch (mode) {
  case Wrap::Repeat: return f;
  case Wrap::Clamp: return std::fmin(std::fmax(u, 0.0f), 1.0f);
  case Wrap::Mirror: return (static_cast<long>(k) % 2 == 0) ? f : 1.0f - f;
  }
  return f;
}

// A gray texture of w x h texels, row 0 first.
struct Texture {
  int w, h;
  const float *texels;
  float at(int i, int j) const { // texel (i, j), clamped to the edge
    i = i < 0 ? 0 : (i >= w ? w - 1 : i);
    j = j < 0 ? 0 : (j >= h ? h - 1 : j);
    return texels[j * w + i];
  }
};

float sampleNearest(const Texture &t, float u, float v) {
  return t.at(static_cast<int>(std::floor(u * t.w)), static_cast<int>(std::floor(v * t.h)));
}

float sampleBilinear(const Texture &t, float u, float v) {
  float x = u * t.w - 0.5f, y = v * t.h - 0.5f; // texel centers sit on whole numbers
  int i = static_cast<int>(std::floor(x)), j = static_cast<int>(std::floor(y));
  float fx = x - i, fy = y - j;
  float a = t.at(i, j) + fx * (t.at(i + 1, j) - t.at(i, j));             // along the row j
  float b = t.at(i, j + 1) + fx * (t.at(i + 1, j + 1) - t.at(i, j + 1)); // along the row j + 1
  return a + fy * (b - a);
}

// Mip level from the texel-space footprint of one pixel step in x and in y.
float mipLevel(float dudx, float dvdx, float dudy, float dvdy) {
  float rho = std::fmax(std::sqrt(dudx * dudx + dvdx * dvdx), std::sqrt(dudy * dudy + dvdy * dvdy));
  return std::log2(rho);
}

int main() {
  const float texels[16] = {0, 40, 80, 0,     // row j = 0
                            0, 120, 200, 0,   // row j = 1
                            0, 0, 0, 0,       // row j = 2
                            0, 0, 0, 0};      // row j = 3
  Texture tex{4, 4, texels};
  printf("nearest  %g\n", sampleNearest(tex, 0.4375f, 0.3125f));
  printf("bilinear %g\n", sampleBilinear(tex, 0.4375f, 0.3125f));
  const float us[3] = {2.3f, -0.2f, 1.3f};
  for (float u : us) {
    printf("u = %4.1f: repeat %.1f, clamp %.1f, mirror %.1f\n", u, wrap(u, Wrap::Repeat),
           wrap(u, Wrap::Clamp), wrap(u, Wrap::Mirror));
  }
  printf("mip level %g\n", mipLevel(4, 0, 0, 8));
  return 0;
}

nearest  120
bilinear 117.5
u =  2.3: repeat 0.3, clamp 1.0, mirror 0.3
u = -0.2: repeat 0.8, clamp 0.0, mirror 0.2
u =  1.3: repeat 0.3, clamp 1.0, mirror 0.7
mip level 3

縮小とミップマップ

危ないのは縮小の方です。
1つのピクセルが8 × 8のテクセルを覆うのに、其のうち1つか4つしか読まなければ、テクスチャをナイキスト周波数よりずっと低い頻度で標本化している事になります。細かい模様はモアレになり、動くとちらつきます。
此れは正に第2章(2.6節)のエイリアシングです。
正直な対策はプリフィルタリング(2.7節)、つまりピクセルの下の全てのテクセルを平均する事です。
其れを実行時にピクセル毎に行うのは遅過ぎるので、前もって1回だけ行います。

ミップマップ(Lance Williams、1983年。「mip」はラテン語のmultum in parvo、「小さな場所に沢山」)は、テクスチャと一緒に、段々小さくなる複製の列を保存します。
レベル0が元の画像で、次のレベルは幅と高さを半分にし、其の各テクセルは上のレベルの2 × 2テクセルの平均です。此れを1 × 1まで続けます。

256×256の市松模様のテクスチャと、1×1までの其のミップマップの各レベルを実寸で並べた物

メモリの計算例。 1チャンネル8ビットの1024 × 1024のRGBAテクスチャは1024⋅1024⋅4=4194304バイト、4 MiBです。
レベルはlog21024+1=11個あります(1024、512、…、1)。
各レベルのテクセル数は前のレベルの4分の1なので、全レベルの合計は元の

1+14+116+⋯≈11−1/4=43

倍、5592404バイト、約5.33 MiBで、3分の1増えるだけです。

レベルの選び方。
GPUは、隣り合うピクセルの間でテクスチャ座標がどれだけ動くかを測ります。1ピクセル右での微分∂u∂x,∂v∂xと、1ピクセル下での∂u∂y,∂v∂yに、テクセル単位になる様にテクスチャの大きさを掛けた物です。
テクスチャ上でのピクセルのフットプリントは、此の2つのベクトルが張るおおよそ平行四辺形で、其の大きさは:

ρ=max((∂u∂x)2+(∂v∂x)2,(∂u∂y)2+(∂v∂y)2),λ=log2ρ

レベルλは、1テクセルがフットプリントとほぼ同じ大きさになるレベルです。
計算例。 斜めから見た床で、1ピクセル右に進むとuが4テクセル、1ピクセル下に進むとvが8テクセル動くとします。
するとρ=max(4,8)=8、λ=log28=3で、レベル3、つまり1テクセルが元の8 × 8テクセルを覆う128 × 128のテクスチャです。
(GPUは此れらの微分を2 × 2のブロックで隣のピクセルと比べて求めます。其の為フラグメントシェーダーではGLSLのdFdx/dFdy、SlangとHLSLのddx/ddyとして使えます。)
拡大ではλ<0となり、GPUはレベル0で拡大用のフィルタに切り替えます。

トライリニアフィルタリング。
λが整数になる事は滅多にありません。
一番近いレベルで双線型フィルタリングをすると、レベルが切り替わる所に帯が見えるので、トライリニアフィルタリングは一番近い2つのレベルを双線型でサンプリングして混ぜます。λ=2.5なら、レベル2とレベル3を半分ずつです。
1サンプルに8テクセルを読む事になります。

異方性フィルタリング。
例のフットプリントは、v方向がu方向の2倍の長さです。
長い方の辺(8テクセル)でレベルを選ぶと短い方向をぼかし過ぎるので、浅い角度で見る床や道がぼやけます。
異方性フィルタリングは長い軸に沿って複数のサンプルを取り、其々を短い方の辺で選んだよりシャープなレベルから読みます。
此処では84=2個のサンプルを、レベルlog24=2から、vに沿って並べて取ります。
GPUは最大16サンプル(「16倍の異方性」)まで使えます。
(ミップマップより古い別の方法である総和テーブル(summed-area table、Crow、1984年)は、軸に平行な任意の長方形の平均を一定の時間で求められます。)

透視で見た市松模様の床を、ミップマップ無しの最近傍、トライリニアのミップマップ、異方性フィルタリングで描いた物と、ピクセルごとに使われたミップレベルの図

ミップマップ無しでは遠くがノイズとモアレに溶けてしまいます。トライリニアのミップマップは其れを正しい平均(灰色)に置き換えますが、床が早くからぼやけます。異方性フィルタリングはずっと遠くまで正方形をくっきり保ちます。
4枚目は各ピクセルを使ったレベルで色分けしており、レベル0から6が赤、橙、黄、緑、青、ピンク、白です。遠い程レベルのテクスチャは小さくなります。
GLSLシリーズの第11章とOpenGLシリーズの第6章では、此れらのフィルタモードを自分で設定します。

パースペクティブコレクトなテクスチャ座標

第6章の6.9節では、透視投影の下で画面空間の重みで補間するのは誤りだと注意しました。
テクスチャでは其の誤りが目に見えます。テクスチャの直線が曲がり、揺れるのです。
計算例。 辺が、w=1、u=0の頂点からw=4、u=1の頂点まで伸びているとします(wはカメラの前方の距離、第6章)。
画面上の中点では、単純な補間はu=0.5を与えます。
パースペクティブコレクトな方法は、uwと1wを画面上で線型に補間して割ります:

uw:0+0.252=0.125,1w:1+0.252=0.625,u=0.1250.625=0.2

画面の中央に見えるのはu=0.5ではなくu=0.2のテクスチャです。辺の手前の部分が画面のより多くを占めるからです。
GPUは此れを自動で行います。
初代PlayStation(1994年)は其れを行わなかった事で有名で、カメラが動くと其のテクスチャが揺れるのは其の為です。

検定のポイント

  • 拡大 → 最近傍又は双線型。縮小 → ミップマップ(とトライリニア又は異方性フィルタリング)。
  • 双線型は一番近い4テクセルを使い、トライリニアは2つのミップレベルの双線型の結果を混ぜます(8テクセル)。
  • ミップマップのピラミッドは約3分の1の追加メモリを使い、各レベルで大きさが半分になり、レベルはテクセル単位のフットプリントのlog2です。
  • ミップマップはエイリアシングに対するプリフィルタリングで、拡大の画質は良くしません。
  • テクスチャ座標はパースペクティブコレクトに補間しなければなりません。
  • 投影によるマッピング:平面(筋)、円筒(継ぎ目)、球面(継ぎ目と極)、ボックス(面の間の継ぎ目)。

11.8 バンプ・法線・ディスプレイスメント・視差マッピング

テクスチャが色を持つ必要はありません。
シェーダーが点ごとに使う物なら何でも持てますし、最も便利な物の幾つかは、ライティングが見る形を変えます。

バンプマッピング

James Blinnのバンプマッピング(1978年)は、幾何は滑らかな儘で、ライティングに使う法線だけを、面に小さな凸凹があるかの様に傾けます。
凸凹は高さマップh(u,v)から来ます。明るい程高いグレーのテクスチャです。
法線が𝐧=(0,0,1)で、テクスチャのuとvがxとyの方向を向いた平らな面の一部を考えると、h(x,y)だけ持ち上げた面の法線は次の様になります:

𝐧′=normalize(−∂h∂x,−∂h∂y,1)

(丘の斜面と同じ様に、法線は上り坂の方向から離れる向きに傾きます。)
曲面では、同じ事を面自身の座標系で行います:𝐧′=normalize(𝐧−hu𝐭−hv𝐛)。𝐭と𝐛は面の上のuとvの方向の単位ベクトルです(次の節の接空間)。
傾きは、例えば中心差分で隣のテクセルから求めます。

計算例。 或るテクセルの周りで、高さが左0.2、右0.5、下0.3、上0.3で、テクセルの間隔が1だとします:

∂h∂x≈0.5−0.22=0.15,∂h∂y≈0.3−0.32=0
𝐧′=normalize(−0.15,0,1)=(−0.15,0,1)1.0225≈(−0.148,0,0.989)

光の方向が𝐥=(−0.6,0,0.8)(左から)なら、平らな面の拡散反射の係数は𝐧⋅𝐥=0.8、凸凹の付いた面は0.148⋅0.6+0.989⋅0.8≈0.880で、光の方を向いた斜面が明るくなります。
光が右から(0.6,0,0.8)なら≈0.702で、暗くなります。
目が凸凹を見るには、此の明暗の模様だけで十分なのです。

滑らかな球、バンプマッピングした同じ球、輪郭も凸凹になるディスプレイスメントマッピングした球

真ん中と右の輪郭を比べて下さい。
バンプマッピングはライティングだけを変えるので、シルエットは完全な円の儘で、凸凹は影を落とさず、互いを隠す事もありません。
浅い角度でバンプマッピングがばれるのは其の為です。

法線マッピングと接空間

法線マッピングは、高さの代わりに傾けた法線其の物を保存するので、シェーダーは傾きを計算する必要がありません。
法線の成分は−1から1、色のチャンネルは0から1(8ビットでは0から255)なので、各成分はc=n+12として保存し、次の様に復号します:

𝐧=2𝐜−1

計算例。 8ビットのテクセル(204,128,230)は𝐜=(0.8,0.502,0.902)で、復号すると

𝐧=(2⋅0.8−1,2⋅0.502−1,2⋅0.902−1)=(0.6,0.004,0.804)

で、長さは1.003なので、正規化するとおよそ(0.6,0,0.8)、+uの方に傾いた法線です。
傾いていない法線(0,0,1)は(128,128,255)という明るい青紫として保存されるので、法線マップは殆ど青く見えます。

此の様なマップの法線は接空間(タンジェント空間)で与えられています。面の上に乗った座標系で、𝐓(接線、タンジェント)はuが増える方向、𝐁(従接線、バイタンジェント)はvが増える方向、𝐍は面の法線です。
其のお陰で、1枚のレンガのテクスチャがどんな向きの壁にも使えます。
シェーダーは、3つのベクトルを列に持つTBN行列で接空間の法線をワールド空間に変えます:

𝐧world=[𝐓𝐁𝐍]𝐧=nx𝐓+ny𝐁+nz𝐍

接線は三角形の位置とUVから求めます。
辺Δ𝐩1=𝐩1−𝐩0、Δ𝐩2=𝐩2−𝐩0とUVの差(Δu1,Δv1)、(Δu2,Δv2)を使うと:

𝐓=Δv2Δ𝐩1−Δv1Δ𝐩2Δu1Δv2−Δu2Δv1,𝐁=Δu1Δ𝐩2−Δu2Δ𝐩1Δu1Δv2−Δu2Δv1

計算例。 UVが(0,0)、(1,0)、(0,1)の床の三角形𝐩0=(0,0,0)、𝐩1=(2,0,0)、𝐩2=(0,0,−2)。
Δ𝐩1=(2,0,0)、Δu1=1、Δv1=0、Δ𝐩2=(0,0,−2)、Δu2=0、Δv2=1で、分母は1なので、𝐓=(2,0,0)、𝐁=(0,0,−2)、正規化すると(1,0,0)と(0,0,−1)です。
𝐓×𝐁=(0,1,0)=𝐍(線型代数の第5章)は床の法線なので、此の座標系は右手系です。
復号した法線(0.6,0,0.8)はワールド空間で0.6𝐓+0.8𝐍=(0.6,0.8,0)になり、+xの方に傾いています。

丸い突起のプロシージャルな高さマップ、其れから計算した接空間の法線マップ、其の法線マップで照らした平らな正方形

法線マップでは、突起の右側は赤っぽく(nx>0)、上側は緑っぽく(ny>0)、平らな背景は典型的な青です。
法線マップは普通、細かくスカルプトしたモデルからローポリゴンのモデルに焼き込み(ベイク)ます。ゲームのモデルは数千の三角形しか無いのに、数百万あるかの様にライティングされるのです。
第3章で見た様に、法線マップは色ではなくデータを持つので、sRGBとして復号してはいけません。
シェーダーでは、復号とTBNの変換はたった2行です:

// Tangent-space normal from a normal-map texel (components in [0, 1]) to world space.
float3 normalFromMap(float3 texel, float3 T, float3 B, float3 N) {
  float3 n = normalize(texel * 2.0 - 1.0);
  return normalize(n.x * T + n.y * B + n.z * N);
}
// Tangent-space normal from a normal-map texel (components in [0, 1]) to world space.
vec3 normalFromMap(vec3 texel, vec3 T, vec3 B, vec3 N) {
  vec3 n = normalize(texel * 2.0 - 1.0);
  return normalize(n.x * T + n.y * B + n.z * N);
}
// Tangent-space normal from a normal-map texel (components in [0, 1]) to world space.
float3 normalFromMap(float3 texel, float3 T, float3 B, float3 N) {
  float3 n = normalize(texel * 2.0 - 1.0);
  return normalize(n.x * T + n.y * B + n.z * N);
}

法線マッピングはGLSLシリーズの第12章とOpenGLシリーズの第22章で実装します。

ディスプレイスメントマッピング

ディスプレイスメントマッピング(Cook、1984年)は面を本当に動かします。各点を高さの分だけ法線に沿って押し出すのです:

𝐩′=𝐩+sh(u,v)𝐧

sは倍率です。
幾何が変わるので、シルエットも影も遮蔽も全て正しくなります(上の右の球)。
代償は、動かす頂点がある様に、面を細かく分割しなければならない事です。凸凹1つに少なくとも1頂点が要ります。
映画のレンダラーはピクセル程の大きさのマイクロポリゴンまで分割し、GPUはテッセレーションシェーダーで追加の頂点を其の場で作ります。

視差マッピング

視差マッピング(パララックスマッピング、Kanekoら、2001年)は両者の中間です。
凸凹の面を斜めから見ると、盛り上がった部分は手前にずれて見えます。
視差マッピングは、テクスチャを読む前にテクスチャ座標をずらす事で其れを真似します:

(u′,v′)=(u,v)+sh(vx,vy)vz

𝐯は接空間での視点への方向です。
計算例。 𝐯=(0.6,0,0.8)、高さh=0.5、倍率s=0.1なら、ずれはuで0.1⋅0.5⋅0.60.8=0.0375です。
見る角度が浅い程(vzが小さい程)、ずれは大きくなります。
視差オクルージョンマッピングは高さ場の中で視線に沿って進む事で此れを改良し、凸凹同士が隠し合う事も出来ますが、シルエットはやはり平らな儘です。

11.9 環境マッピング

光沢のある面は周りを映します。
本物の反射レイを追うのは第12章の仕事です。環境マッピング(反射マッピングとも言います。Blinn・Newell、1976年)は其れをテクスチャで真似します。

考え方:1つの点から見た全方向の周囲の画像を保存しておきます。
鏡の様な物体の点をシェーディングするには、視線方向の反射ベクトルを計算し、其の方向の環境を参照します。
入射方向𝐈(視点から面へ)と単位法線𝐧を使うと:

𝐫=𝐈−2(𝐧⋅𝐈)𝐧

此れはGLSL、Slang、HLSLのreflect(I, n)で、線型代数の第4章の内積で出来ています。
計算例。 視点が真っ直ぐ−z方向、𝐈=(0,0,−1)を見ていて、面の法線が𝐧=(0,0.6,0.8)だとします:

𝐧⋅𝐈=−0.8,𝐫=(0,0,−1)−2⋅(−0.8)⋅(0,0.6,0.8)=(0,0,−1)+(0,0.96,1.28)=(0,0.96,0.28)

反射は殆ど上を向くので、此の点には空が映ります。
(長さは0.962+0.282=1で、単位ベクトルの反射として当然です。)

キューブマップ

キューブマップ(キューブ環境マップ、Greene、1986年)は、点の周りの立方体の6つの面、+X、−X、+Y、−Y、+Z、−Zに周囲を保存します。各面は90°のカメラで描いた普通の正方形のテクスチャです。
方向を参照するには、其の絶対値が一番大きい成分を取ります。其れで面が決まり、其の成分で割ると面上の位置が分かります。
𝐫=(0,0.96,0.28)では一番大きい成分はy=0.96なので+Y面で、𝐫0.96=(0,1,0.29)がレイが立方体を貫く点です。

y–z平面:入射レイI、法線n、反射r、そしてrがキューブマップの+Y面に当たる点

キューブマップは全方向でほぼ均一な解像度を持ち、GPU自身で描け(6回の描画で、動的な反射なら毎フレームでも)、2000年頃からGPUのハードウェアで対応されています。
今日の標準で、OpenGLシリーズの第18章ではスカイボックスと反射に使います。

スフィアマップと緯度経度マップ

スフィアマップ(球面環境マップ)は、完全な鏡のボールを遠くから写真に撮った(又は描いた)1枚の画像です。
古典的な固定機能のOpenGLの式(GL_SPHERE_MAP)は、視点座標系の反射ベクトルを次の様に写します:

m=2rx2+ry2+(rz+1)2,s=rxm+0.5,t=rym+0.5

上の𝐫=(0,0.96,0.28)での計算例:m=20+0.9216+1.6384=22.56=3.2なので、s=0.5、t=0.963.2+0.5=0.8です。
視点に真っ直ぐ戻る方向(0,0,1)は中心に来ますが(m=4、s=t=0.5)、視点から真っ直ぐ遠ざかる方向(0,0,−1)はm=0となり、縁全体に引き伸ばされます。
其の為、スフィアマップは1つの視線方向に縛られ、縁の解像度が低く、キューブマップに置き換えられました。
3つ目のよく使われる形式は緯度経度マップ(正距円筒図法)で、11.6節の球面投影を方向に当てはめただけの物です。HDRの環境写真(ライトプローブ)は普通此の形式で保存され、第12章ではイメージベーストライティングに使います。

プロシージャルな環境を緯度経度マップで表した物と、反射ベクトルで其れを映す鏡面の球

限界と屈折

環境マッピングは周囲が無限に遠いと仮定しています。参照は𝐫の方向だけで決まり、反射する点の位置には依存しません。
其の為、近くの物体の映り込みは間違った位置に出て、物体は自分自身を映せず、2つの光沢のある物体は互いを正しく映しません(物体ごとに別のマップを描いても、近似にしかなりません)。
リアルタイムレンダラーは複数のローカルなマップ(リフレクションプローブ)や画面空間反射で此れを改善します。正確な反射にはレイトレーシング(第12章)が必要です。
同じ参照は屈折にも使えます。視線ベクトルを反射させる代わりにスネルの法則で曲げ(GLSL、Slang、HLSLのrefract(I, n, eta))、ガラスの物体の向こうの環境を参照します。スネルの法則自体は第12章で扱います。

11.10 其の他のマップ

テクスチャの参照はとても安いので、殆ど全ての材質のパラメータが自分のマップを持っています。

鏡面反射マップ等の材質マップ。
鏡面反射(グロス)マップは、テクセルごとに光沢度やハイライトの強さを決めます。濡れた所は光り、乾いた所は光りません。
物理ベースのレンダラーはラフネスやメタリックのマップを同じ様に使います(第2部)。ライティングマップはOpenGLシリーズの第11章で使います。

ライトマップ。
静的なシーンでは、ライティング(ソフトシャドウや間接光も含めて)を前もってオフラインで1回だけ計算し、レベル全体に掛かる2枚目の低解像度のテクスチャに保存出来ます。
実行時の色はアルベド×ライトマップだけです。アルベド0.8の壁のテクセルは、ライトマップが0.5の場所では0.4で描かれます。
Quake(1996年)がライトマップを有名にし、「焼き込んだライティング」は今でもゲームのレベルの静的な部分の標準です。
代償は、光源が動けない事と、全ての面にライトマップ用の重ならないUVが必要な事です。

アルファマップと切り抜き(透明度マップ)。
テクスチャのアルファチャンネルには不透明度を入れられます。
葉、草、フェンス、髪では、単純な四角形とアルファマップで複雑な輪郭を見せられます。アルファテストでは、フラグメントシェーダーがアルファが閾値(例えば0.5)より小さいフラグメントを捨てるので、並べ替えは要りません。半透明のガラスにはアルファブレンディングを使い、其の順序の問題は第10章で扱います。

投影テクスチャマッピング(Segalら、1992年)。
此処ではテクスチャ座標を頂点に保存せず、スライドプロジェクターの様に計算します。点をプロジェクターのビュー行列と投影行列で変換し、得られた(u,v)を使うのです。正に11.4節の𝐒=𝐁𝐏𝐕です。
懐中電灯が模様を投影したり、車のヘッドライトの形が壁に映ったりするのは此の方法です。
シャドウマッピングは、同じ参照に深度の比較を加えた物です。

ソリッドテクスチャとプロシージャルテクスチャ

2次元のテクスチャは面に貼り付けられているので、物体を切ると其れが分かります。6つの面にテクスチャを貼った木のブロックは、木から削り出した様には見えません。
ソリッドテクスチャ(3次元テクスチャ。Perlin、1985年、Peachey、1985年)は代わりに3次元の位置の関数f(x,y,z)なので、物体は材料から「削り出され」、UVは全く要りません。
3次元の画像として保存するとメモリを沢山使うので(4バイトの2563テクセルで64 MiB)、普通は計算します。プロシージャルテクスチャは、位置から色を計算する短いプログラムです。

  • 木目:軸の周りの年輪、ring=frac(kx2+z2+a⋅noise)を、明るい木と暗い木の色に写します。
  • 大理石:筋、sin(x+a⋅turbulence(x,y,z))を、白と灰色に写します。

どちらにもノイズ関数が必要です。ランダムに見えるけれど滑らかな関数です。
一番簡単なのはバリューノイズで、整数の格子点ごとにランダムな値を与え、其の間を滑らかに補間します。
1次元で格子の値がviなら、iとi+1の間のxで:

t=x−i,s=3t2−2t3,v(x)=vi+s(vi+1−vi)

「スムーズステップ」曲線sは両端で傾きが0なので、曲線は格子点で折れ曲がりません。
計算例。 v2=0.2、v3=0.9でx=2.3なら、t=0.3、s=3⋅0.09−2⋅0.027=0.216、v(2.3)=0.2+0.216⋅0.7=0.3512です。
普通の線型補間なら0.2+0.3⋅0.7=0.41になります。

格子の値0.5、0.1、0.2、0.9、0.6、0.3、0.7を通る1次元のバリューノイズと、x = 2.3の点

パーリンノイズは、格子点に値ではなくランダムな勾配(傾きの向き)を保存するので、バリューノイズのブロック状の見た目を避けられます。格子点での値は0です。
周波数を2倍、振幅を半分にしたノイズを何オクターブも足すとフラクタルノイズ(fBm)になり、絶対値を足すと乱流(turbulence)になります。Perlinは此れで大理石、雲、炎を作りました。

プロシージャルテクスチャ:バリューノイズ、5オクターブのフラクタルノイズ、ノイズで乱した木目の年輪、大理石の筋

プロシージャルテクスチャはメモリを殆ど使わず、解像度に限りが無く、繰り返しもしませんが、サンプルごとに計算のコストが掛かり、フィルタリングが難しいです(ミップマップが無いので、縮小するとエイリアシングが出ます)。
ノイズ関数はGLSLシリーズの第8章で自分で書きます。

検定のポイント

  • バンプマッピング:高さマップから法線を傾けます。幾何とシルエットは変わりません。
  • 法線マッピング:法線を保存します(普通は接空間で、2𝐜−1とTBN行列で復号します)。
  • ディスプレイスメントマッピング:幾何を動かし、シルエットが変わります。細かいテッセレーションが必要です。
  • 環境マッピング:反射ベクトル𝐫=𝐈−2(𝐧⋅𝐈)𝐧で周囲を参照します。スフィアマップ(1枚の画像、視線に依存)とキューブマップ(6つの面)。周囲が遠いと仮定し、自分自身は映りません。
  • ソリッドテクスチャは3次元の位置の関数(木目、大理石)で、プロシージャルなノイズ(パーリンノイズ)で自然に見せます。
  • ライトマップは前もって計算したライティングを保存し、投影テクスチャはプロジェクターの行列からUVを計算します。

11.11 まとめ

  • 点は光源から隠れている時に影の中にあります。陰はシェーディングから、影は光源からの可視判定から生まれます。
  • 点光源はハードシャドウを、面光源は本影と、遮蔽物からの距離と共に広がる半影を作ります。
  • 投影法は行列(𝐠⋅𝐥)𝐈−𝐥𝐠⊤で物体を平面に潰します。安いですが、平らな受け手1枚にしか使えません。
  • シャドウボリューム法はシルエットエッジを押し出し、ステンシルバッファで横切りを数えます(Zパス法、又は視点が内側に入り得るならZフェイル法)。
  • シャドウマッピングは光源からの深度を描いて比較します。バイアスでアクネを防ぎ、大き過ぎるバイアスはピーターパン現象を起こし、PCFは比較結果を平均して輪郭を柔らかくします。
  • シャドウレイは可視性を直接調べ、面光源ではソフトシャドウを作ります。アンビエントオクルージョンは囲まれた場所を暗くします。
  • テクスチャ座標はテクスチャの正方形を面に写します(平面・円筒・球面・ボックス投影又は展開)。ラップモードは0から1の外の座標を扱います。
  • 拡大には最近傍か双線型、縮小にはミップマップ(3分の1の追加メモリ、レベルlog2ρ)とトライリニア・異方性フィルタリングを使います。補間はパースペクティブコレクトでなければなりません。
  • バンプマッピングと法線マッピングはライティングの法線を、ディスプレイスメントマッピングは幾何を、視差マッピングはテクスチャ座標を変えます。
  • 環境マップ(キューブマップ、スフィアマップ)は遠い周囲の反射と屈折を真似します。ライトマップ、アルファマップ、投影テクスチャ、プロシージャル(ソリッド)テクスチャが道具箱を完成させます。

11.12 用語

English 日本語
shadowing 影付け
attached shadow, shade 陰
cast shadow 影
self-shadowing セルフシャドウ
umbra 本影
penumbra 半影
hard shadow, soft shadow ハードシャドウ、ソフトシャドウ
projected (planar) shadow 投影法(平面への投影による影)
shadow volume シャドウボリューム法
silhouette edge シルエットエッジ(輪郭エッジ)
stencil buffer ステンシルバッファ
depth pass, depth fail Zパス法、Zフェイル法
shadow mapping, shadow map シャドウマッピング、シャドウマップ
shadow acne シャドウアクネ
depth bias デプスバイアス
peter panning ピーターパン現象
percentage-closer filtering (PCF) PCF(パーセンテージクローサーフィルタリング)
shadow ray シャドウレイ
ambient occlusion アンビエントオクルージョン(環境遮蔽)
texture mapping テクスチャマッピング
texel テクセル
texture coordinates (UV) テクスチャ座標(UV座標)
parameterization パラメータ化
planar, cylindrical, spherical, box mapping 平面投影、円筒投影、球面投影、ボックス投影
UV unwrapping, seam UV展開、継ぎ目(シーム)
wrap mode (repeat, clamp, mirror) ラップモード(リピート、クランプ、ミラー)
magnification, minification 拡大、縮小
nearest-neighbor interpolation 最近傍補間(ニアレストネイバー)
bilinear interpolation 双線型補間(バイリニア補間)
mipmap ミップマップ
trilinear filtering トライリニアフィルタリング
anisotropic filtering 異方性フィルタリング
bump mapping バンプマッピング
height map 高さマップ
normal mapping 法線マッピング
tangent space 接空間(タンジェント空間)
displacement mapping ディスプレイスメントマッピング
parallax mapping 視差マッピング(パララックスマッピング)
environment (reflection) mapping 環境マッピング(反射マッピング)
cube map, sphere map キューブマップ、スフィアマップ(球面環境マップ)
light map ライトマップ
alpha map アルファマップ(透明度マップ)
projective texture mapping 投影テクスチャマッピング
solid texture ソリッドテクスチャ(3次元テクスチャ)
procedural texture プロシージャルテクスチャ
noise function, Perlin noise ノイズ関数、パーリンノイズ

11.13 課題

  1. ★ 平面y=0と(1,5,−1)にある点光源の影の行列を作り、点(2,1,0)と(0,3,1)を投影して下さい。次に(0,6,0)を投影し、何がおかしくなるかを説明して下さい。
  2. ★ 2 × 2のグレーのテクスチャがT0,0=0、T1,0=100、T0,1=200、T1,1=50です。エッジにクランプで、(u,v)=(0.625,0.375)と(0.25,0.75)での最近傍と双線型のサンプルを求めて下さい。u=−0.4とu=3.25は、リピート、クランプ、ミラーリピートで其々どの座標になりますか?
  3. ★★ 単位ベクトル(−0.6,0,0.8)と(0,0.8,−0.6)の円筒投影(yは−1から1)と球面投影のテクスチャ座標を(11.6節の式で)求めて下さい。どちらが継ぎ目の上にありますか?継ぎ目を跨ぐ三角形の頂点が計算した通りのuを持っていたら、どう見えるでしょうか?
  4. ★★ 壁が+zを向いていて、𝐓=(1,0,0)、𝐁=(0,1,0)、𝐍=(0,0,1)です。法線マップのテクセル(128,230,204)を復号してワールド空間に変換し、𝐥=(0,0,1)と𝐥=(0,1,0)について、平らな壁と法線マッピングした壁の拡散反射の係数max(𝐧⋅𝐥,0)を比べて下さい。
  5. ★★★ シャドウマップを、比較の前に保存された深度を双線型フィルタリングしてサンプリングするとします。深度が0.3、0.3、0.9、0.9の4つのテクセルの1次元の列と、最初のテクセルから最後のテクセルまで動く深度0.7の受け手を使って影がどう見えるかを示し、先に比較して後で平均する方法(PCF)と比べて下さい。深度の平均では何故決して半影が出来ないのでしょうか?

選択問題

問1 ★ 次のうち、影付けについて正しい物はどれか。

あ シャドウマッピングは、先ず光源からシーンの深度を描き、次にカメラから描く時に、各点の光源から見た深度を保存された深度と比べる。
い 投影法(平面への投影による影)は、曲面や物体自身にも影を落とせる。
う シャドウボリューム法は、レイがボリュームの面を何枚横切るかを数えずに影を判定する。
え 点光源は、全ての影の周りに幅の広い半影を作る。
お シャドウマッピングは深度の比較が正確なので、エイリアシングが起きない。

問2 ★ 次のうち、マッピングの手法について正しい物はどれか。

あ バンプマッピングは、面の頂点を法線に沿って動かす。
い 法線マッピングは、物体のシルエットを変える。
う 環境マッピングは、物体自身を含めて近くの物体を正しく映す。
え ディスプレイスメントマッピングは面を実際に動かすので、シルエットや影も変わる。
お 接空間の法線マップは、高さをグレーの値として保存する。

問3 ★★ 1024 × 1024のテクスチャを完全なミップマップと共に使う。次のうち、正しい物はどれか。

あ ミップマップは、主に拡大の画質を良くする為に使う。
い 全てのレベルを合わせると、元のテクスチャの約2倍のメモリが必要になる。
う 元の画像を含めて、ミップマップのレベルは10個である。
え トライリニアフィルタリングは、隣り合う4つのレベルの最近傍のサンプルを混ぜる。
お レベルはテクセル単位のピクセルのフットプリントの大きさから選ばれ、約8テクセルのフットプリントではレベル3(128 × 128のテクスチャ)が選ばれる。

11.14 実践課題

小さなシャドウマップと小さなミップマップを手で作り、どちらも正確な答えと照らし合わせます。
方眼紙、定規、鉛筆、2色のペン、電卓を用意して下さい。

パートA:横から見たシャドウマップ。
シーンは2次元(xは右、yは上)で、x=0からx=8までの床y=0と、x=2からx=4、y=0からy=2までの箱があります。
太陽の光は方向𝐝=(0.8,−0.6)に進みます。
光源空間の座標にはs=0.6x+0.8y(光を横切る方向)と深度z=0.8x−0.6y(光に沿った方向。zが小さい程太陽に近い)を使います。

  1. 描く。 1単位 = 2 cmでシーンを描き、平行な光線を何本か描き込みます。
  2. 正確な影。 箱の上の角を𝐝に沿って床に投影して下さい(点(x,y)はx+43yに落ちます)。床のどの区間が影になりますか?1色目のペンで印を付けます。
  3. シャドウマップを埋める。 マップはsで幅1の5つのテクセルを持ち、0≤s<5を覆います。各テクセルの中心s=0.5,1.5,…,4.5について、直線0.6x+0.8y=s上で光が最初に届く面(床、箱の左側面x=2、又は上面y=2。zが一番小さい交点が勝ちます)を見つけ、其の深度を書いて下さい。
  4. 床を調べる。 床の点x=0.5,1.5,4.5,5.5,6.5,7.5について、s、テクセル、zを計算し、保存された深度と比べて下さい(z>zmapなら影)。影と判定された点に2色目のペンで印を付けます。誤って影になる点(アクネ)はどれで、其れは何故ですか?
  5. バイアス。 床の点はテクセルの中心から最大で半テクセル離れられ、其れにより深度が最大で0.5⋅0.80.6≈0.67変わる事を示して下さい。判定をz−0.7>zmapにして段階4を繰り返します。

パートB:手で作るミップマップ。
4 × 4のグレーのテクスチャ(行j=0が先):20、60、100、140 / 60、20、140、100 / 100、140、20、60 / 140、100、60、20。

  1. ピラミッドを作る。 レベル1(2 × 2、各テクセルは2 × 2のブロックの平均)とレベル2(1 × 1)を計算して下さい。
  2. 2つのレベルで双線型。 (u,v)=(0.4,0.55)を、レベル0(4 × 4)とレベル1(2 × 2)で双線型にサンプリングして下さい。其のレベルの大きさでx=uW−0.5、y=vH−0.5を使います。
  3. 選んで混ぜる。 1ピクセル進むと、座標はxで(1,1)テクセル、yで(−1,1)テクセル(レベル0のテクセル)動きます。ρとλを求め、2つの双線型の結果を混ぜてトライリニアの結果を求めて下さい。

自分で確認する方法:

  • パートA:正確な影は4≤x≤6.67です(段階2)。バイアス付きでは、シャドウマップの判定は全ての点で其れと一致する筈です。バイアス無しでは丁度1つの点が間違い、其の点はテクセルの奥側の半分にあります。
  • パートA:床だけを見るテクセルの保存された深度は、其の中心線上の床の点の0.8xと等しくなります。
  • パートB:レベル2は16個全てのテクセルの平均と等しくなります。各双線型の結果は、使った4つのテクセルの最小値と最大値の間にあります。トライリニアの結果は2つの双線型の結果の間にあり、λ=0.5なら丁度真ん中です。


コメント(0)

まだコメントはありません。