射影変換行列

最近は新しい会社での為に3Dを勉強し直していて、
自作のベクトルクラスや行列クラスを作ったりしている。


色々やっていて一番謎だった行列が、射影変換行列。


DirectGraphicsでは、D3DXMatrixPerspectiveFovLHとして
用意されていて、以下の様な行列となっている。

Mproj = |w, 0, 0,             0|
|0, h, 0, 0|
|0, 0, zf/(zf-zn), 1|
|0, 0, -zn*zf(zf-zn), 0|

h = 1/tan(fovY/2) = cot(fovY/2)
w = h / aspect
zn = 視錘台の近平面までの距離
zf = 視錘台の遠平面までの距離

この行列を、シーン描画中に設定してやる。

pD3DDevice->SetTransform(D3DTS_PROJECTION, &mProj);
こんな感じで射影変換は行われるのだけど、この行列を
自作して、ベクトルにかけてみるとうまく射影されない。
透視射影ではなくて、正射影のような射影に見える。
つまり奥行き(z)が全く反映されない。


行列をよく見てみれば、確かにそれがわかる。

(x, y, z, w) * Mproj = (x', y', z', w')とすると

x' = x*Mproj[0,0] + y*Mproj[1,0] + z*Mproj[2,0] + w*Mproj[3,0]
y' = x*Mproj[0,1] + y*Mproj[1,1] + z*Mproj[2,1] + w*Mproj[3,1]
z' = x*Mproj[0,2] + y*Mproj[1,2] + z*Mproj[2,2] + w*Mproj[3,2]
w' = x*Mproj[0,3] + y*Mproj[1,3] + z*Mproj[2,3] + w*Mproj[3,3]

x' = x*w + y*0 + z*0 + w*0
y' = x*0 + y*h + z*0 + w*0
z' = x*0 + y*0 + z*(zf/(zf-zn)) + w*(-zn*zf(zf-zn))
w' = x*0 + y*0 + z*1 + w*0

xとyにzが影響する要素が微塵もない・・・。
それでもDirectGraphicsではちゃんと透視射影されるのだから、
きっと何かDirectGraphics側でもう一つ変換があるんだろうと
思うのだけど。


じゃあベクトルとかけあわせるだけで射影変換する行列は何ぞや・・・。
こちらのページこの本に載っていた。

Mproj = |2n/(r-l), 0,        (r+l)/(r-l),           0|
|0, 2n/(t-b), (t+b)/(t-b), 0|
|0, 0, -(f+n)/(f-n), -2nf/(f-n)|
|0, 0, -1, 0|

n = 視錘台の近平面までの距離
f = 視錘台の遠平面までの距離
l = 視錘台近平面の左辺x
r = 視錘台近平面の右辺x
t = 視錘台近平面の上辺x
b = 視錘台近平面の下辺x

おいおい全然PerspectiveFovLHと違うぜ・・・。
でもこの行列でもxとyにzが影響しない。
[2,0]と[2,1]が0だから。


と思っていたら違った。この行列は(行)ベクトルにかけるのではなくて、
この行列に(列)ベクトルをかけるというものだった。
すると計算は以下の様になる。

Mproj * (x, y, z, w) = (x', y', z', w')とすると

x' = x*Mproj[0,0] + y*Mproj[0,1] + z*Mproj[0,2] + w*Mproj[0,3]
y' = x*Mproj[1,0] + y*Mproj[1,1] + z*Mproj[1,2] + w*Mproj[1,3]
z' = x*Mproj[2,0] + y*Mproj[2,1] + z*Mproj[2,2] + w*Mproj[2,3]
w' = x*Mproj[3,0] + y*Mproj[3,1] + z*Mproj[3,2] + w*Mproj[3,3]

x' = x*2n/(r-l) + y*0 + z*(r+l)/(r-l) + w*0
y' = x*0 + y*2n/(t-b) + z*(t+b)/(t-b) + w*0
z' = x*0 + y*0 + z*-(f+n)/(f-n) + w*-2nf/(f-n)
w' = x*0 + y*0 + z*-1 + w*0

見事にzも影響する式になった!
しかしこの本によるとまだこれだけでは
ダメらしい。詳細は本を参照されたし。
とりあえず↑で算出されたベクトルの要素を、
全てw'で割ってやると正しい座標が出る。
x' = x' / w'
y' = y' / w'
z' = z' / w'
w' = w' / w'
ただし、コレで算出されるxとyは、-1〜1の範囲に収められている。
以下の変換行列をかけてやるとスクリーン上の座標が出る。
Mscreen = |w/2, 0,   0, 0|
|0, h/2, 0, 0|
|0, 0, 1, 0|
|w/2, h/2, 0, 1|
これでOK!と思いきや、何故かx軸が反対だった(負の方向が右手だった)。
原因を探るのはめんどくさかったので、x'に対して-1をかけてやった。
多分、本やらの解説がOpenGLを基にしたものであったからだと思う。
うん。そういう事にしておこう。


最終的に出来た行列はコレ。

Mproj = |2n/(r-l)*-1, 0,        (r+l)/(r-l),           0|
|0, 2n/(t-b), (t+b)/(t-b), 0|
|0, 0, -(f+n)/(f-n), -2nf/(f-n)|
|0, 0, -1, 0|
ここに辿りつくまでに二日ぐらいかかった。頭ワルス。


ちなみに、l,r,t,bの求め方は以下の通り。
三角関数を使って簡単に出せる。
PerspectiveFovLHに倣って、水平方向の視野角fovVと
aspect(横幅/縦幅)が与えられたとすると、

t = sin(fovV/2) * n/cos(fovV/2) = n * tan(fovV/2)
b = -t

fovH = atan(t*aspect / n)*2
r = sin(fovH/2) * n/cos(fovH/2) = n * tan(fovH/2)
l = -r

もっと最適化すればatanとか消せるかもしれない。
けどめんどくさいのでこれで良し。
図をかけばこの式になる理由はすぐわかる。



数学は好きだけど計算はニガテなので苦労した。
もっと頭良くなりたいなー。