射影変換行列
最近は新しい会社での為に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とyにzが影響する要素が微塵もない・・・。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
それでもDirectGraphicsではちゃんと透視射影されるのだから、
きっと何かDirectGraphics側でもう一つ変換があるんだろうと
思うのだけど。
じゃあベクトルとかけあわせるだけで射影変換する行列は何ぞや・・・。
こちらのページやこの本に載っていた。
Mproj = |2n/(r-l), 0, (r+l)/(r-l), 0|おいおい全然PerspectiveFovLHと違うぜ・・・。
|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
でもこの行列でもxとyにzが影響しない。
[2,0]と[2,1]が0だから。
と思っていたら違った。この行列は(行)ベクトルにかけるのではなくて、
この行列に(列)ベクトルをかけるというものだった。
すると計算は以下の様になる。
Mproj * (x, y, z, w) = (x', y', z', w')とすると見事にzも影響する式になった!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
しかしこの本によるとまだこれだけでは
ダメらしい。詳細は本を参照されたし。
とりあえず↑で算出されたベクトルの要素を、
全てw'で割ってやると正しい座標が出る。
x' = x' / w'ただし、コレで算出されるxとyは、-1〜1の範囲に収められている。
y' = y' / w'
z' = z' / w'
w' = w' / w'
以下の変換行列をかけてやるとスクリーン上の座標が出る。
Mscreen = |w/2, 0, 0, 0|これでOK!と思いきや、何故かx軸が反対だった(負の方向が右手だった)。
|0, h/2, 0, 0|
|0, 0, 1, 0|
|w/2, h/2, 0, 1|
原因を探るのはめんどくさかったので、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)もっと最適化すればatanとか消せるかもしれない。
b = -tfovH = atan(t*aspect / n)*2
r = sin(fovH/2) * n/cos(fovH/2) = n * tan(fovH/2)
l = -r
けどめんどくさいのでこれで良し。
図をかけばこの式になる理由はすぐわかる。
数学は好きだけど計算はニガテなので苦労した。
もっと頭良くなりたいなー。