点一つが画面に届くまで
一言でいうと
頂点は、モデル座標から始まって、ワールド、ビュー、クリップ座標を経て、wで割られてはじめて、画面座標になります。遠近感は、この除算1回から生まれます。
なぜ必要なのか
物体を作った人は、原点の近くに置いてモデリングします。シーンを飾る人は、その物体を世界のどこかに置きます。カメラを持つ人は、自分の位置と向きを基準に世界を見ます。画面に描く側は、ピクセル座標しか知りません。この4人の基準がすべて異なるので、座標系を乗り換える段階が必要です。
ここで、各段階を行列1つで表現できることが決定的です。モデル行列、ビュー行列、投影行列を先に掛けてMVP1つにしておけば、頂点ごとに4x4の乗算1回で済みます。頂点シェーダーがすることは、事実上この一行です。
どう動くのか
ビュー行列は、カメラを原点に、カメラが見る方向を-z軸に移す変換です。カメラを動かすのではなく、世界を逆に動かすと考えればよいです。作る方法は、カメラの3つの軸を求めて、それで行ベクトルを立てることです。
f = normalize(target - eye) # 보는 방향
s = normalize(cross(f, up)) # 오른쪽
u = cross(s, f) # 진짜 위쪽 (up 은 대충 준 값이라 다시 구한다)
view = | s.x s.y s.z -dot(s, eye) |
| u.x u.y u.z -dot(u, eye) |
| -f.x -f.y -f.z dot(f, eye) |
| 0 0 0 1 |
3番目の行にマイナスが付く理由は、右手座標系でカメラが-zを見ると約束したからです。この約束を破ると、物体がカメラの後ろに置かれて、何も見えません。
投影行列は、錐台(frustum)の中の空間を、立方体に押し込みます。透視投影行列の最後の2つの行が、この仕事の核心です。
t = 1 / tan(fov/2)
proj = | t/aspect 0 0 0 |
| 0 t 0 0 |
| 0 0 -(f+n)/(f-n) -2*f*n/(f-n) |
| 0 0 -1 0 |
最後の行が(0, 0, -1, 0)であることを見てください。この行のために、結果のw成分が-z、つまりカメラからの距離になります。そのあと、x、y、zをこのwで割ると、遠くにある点ほど、画面の中央に引き寄せられます。これがパースペクティブ除算(perspective divide)で、遠近感は、すべてここから生まれます。行列の積だけでは、遠近は生じません。行列は除算を表現できないので、wに距離を載せておき、あとで割るという迂回路を使うのです。
除算のあとの座標を、正規化デバイス座標(NDC)と呼び、範囲は-1から1です。最後に、ここにビューポート変換をかけて、ピクセル座標に移します。yを反転させるのも、このときです(このあとのコードブロックの韓国語は、画面の幅と画面の高さを表すプレースホルダーです)。
sx = (ndc_x * 0.5 + 0.5) * 화면폭
sy = (1 - (ndc_y * 0.5 + 0.5)) * 화면높이
平行投影は、wに触れません。最後の行が(0, 0, 0, 1)なので、除算が恒等になり、そのため、距離と無関係に大きさが同じです。図面や2D UIが、これを使います。
除算の前に切り落とす必要がある理由も、ここから出てきます。wが0なら除算が破綻し、wが負なら、カメラの後ろにある点です。後ろにある点をそのまま割ると、符号が反転して、画面の反対側に、正常な座標として現れます。カメラの前後にまたがる三角形が、画面を横切って長く伸びるバグが、これです。そのため、パイプラインは、除算の前に、クリップ座標の状態で、錐台の外を切り落とします。クリップ座標という名前は、ここから来ました。
ビューポート変換には、深度もあわせて入ります。NDCのzを、深度バッファーが使う範囲に移す段階ですが、OpenGLは既定が-1..1で、Direct3DとVulkanは0..1です。この違いのために、投影行列の3番目の行がAPIごとに異なり、他人のコードをそのまま持ってくると、深度の判定が反転したり、半分が切り落とされたりします。
現場での姿
near平面を小さくしすぎると(0.001のような値)、深度バッファーの精度が近くに集まって、遠い物体どうしでZファイティングが生じます。深度の値が1/zに比例して分布するからで、この事実は、上の行列の3番目の行から、そのまま読み取れます。実務での解決法は、farを小さくすることではなく、nearを大きくすることです。
もう1つは、アスペクト比です。ウィンドウのサイズが変わったのに、投影行列のaspectを更新しないと、物体が引き伸ばされます。ウィンドウのサイズ変更のコールバックで、投影行列を作り直す必要がある理由です。
次のラボですること
ビュー行列と、透視・平行投影の行列を自分で作り、パースペクティブ除算とビューポート変換を経て、立方体の12本の辺を画面に描きます。同じ立方体を、透視と平行でそれぞれ描いて比べ、視野角を変えながら、物体が画面でどれだけ広く捉えられるかを、数字と絵であわせて確認します。