透視投影行列

perspective-matrix.png

概要

透視投影はカメラ座標からクリップ座標に変換するためのもの。

同次座標のwを使って透視投影を行うらしい。
透視投影では投影線1は平行ではない。
投影線は投影中心と呼ばれる1点で交わる。
投影中心は投影面の前にあるため、投影線は面にぶつかる前に交わり、画像は反転する。
ピンホールカメラのような仕組み。ピンホールが投影中心である。

任意の点Pに対して、投影面員投影されたp'の3D座標を計算するには?
ピンホールは原点にあるとする。
まず、ピンホールと投影面の距離を知る必要がある。この距離をdとする。
つまり、投影面はz=-dになる。

しかし、コンピュータの世界では投影面はピンホールよりも手前に来てもいい。

この変換によって視錘台は立方体に変形する

(1)
\begin{align} P'= \begin{bmatrix}x'&y'&z'\\ \end{bmatrix} = \begin{bmatrix}\frac{dx}{z}& \frac{dy}{z}\\ d \end{bmatrix} \end{align}

x,y,zは投影前の点の座標。
同時座標のw成分は$\frac{z}{d}$なのである。

glFrustum(l,r,b,t,n,f)

(2)
\begin{align} \left[ \begin{array}{cc} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & \frac{-(f+n)}{f-n} & \frac{-2fn}{f-n} \\ 0 & 0 & -1 & 0\\ \end{array} \right] \end{align}
left,right,top,bottomはnearクリッピング面を定義する数値

この行列はカメラ座標から、一気に正規化デバイス座標にするような変換行列である。
カメラ座標から、クリップ座標
クリップ座標から正規化デバイス座標と段階を分けるとこうなる

zにかかる3行目の意味

カメラ座標におけるZ_eは、常に-nになる
投影したらzはつぶれるのでなんの意味が??
デプステストやクリッピングテストのためにzの値はそれっぽいのが必要だ。
投影後のzはx,y,に依存しない値なのでまず行列の第3行目成分は(0,0,*,*)であることは決定である。
カメラ座標z=-nearのとき、正規化デバイス座標ではz=-1になる。
カメラ座標z=-farのとき、正規化デバイス座標ではz=1になる。

4行目(0,0,-1,0)の意味

(3)
\begin{pmatrix} . & . & . & . \\ . & . & . & . \\ . & . & . & . \\ 0 & 0 & -1 & 0\\ \end{pmatrix} \cdot \begin{pmatrix} x\\ y\\ z\\ w \end{pmatrix}=\begin{pmatrix} .\\ .\\ .\\ -z \end{pmatrix}

透視投影行列のシンプル化

視錐台が左右多少の場合、上の行列には端折れる箇所がある。

(4)
\begin{align} \Biggl\{ r+l=0\\ r-l=2r(width)\\ \Biggl\{ t+b=0\\ t-b=2t(height)\\ \end{align}

すると行列はこうなるのだ

(5)
\begin{align} \left[ \begin{array}{cc} \frac{n}{r} & 0 & 0 & 0 \\ 0 & \frac{n}{t} & 0 & 0 \\ 0 & 0 & \frac{-(f+n)}{f-n} & \frac{-2fn}{f-n} \\ 0 & 0 & -1 & 0\\ \end{array} \right] \end{align}

さらに、2r=width,2t=heightだとしたら、

(6)
\begin{align} \left[ \begin{array}{cc} \frac{2n}{width} & 0 & 0 & 0 \\ 0 & \frac{2n}{height} & 0 & 0 \\ 0 & 0 & \frac{-(f+n)}{f-n} & \frac{-2fn}{f-n} \\ 0 & 0 & -1 & 0\\ \end{array} \right] \end{align}

gluPerspective

gluPerspective(fovy,aspect,near,far)
near,farは視点とクリッピング面との距離を表す。
クリッピング面は負の軸に沿っている。
このnearとfarに負の値を入れてはいけない。(何も見えなくなる。)
aspect..は画面のアスペクト比。幅÷高さ の値を渡す
θ…視野角

(7)
\begin{align} top= near・tan(\theta); \end{align}
(8)
\begin{align} \left[ \begin{array}{cc} \frac{n}{n \cdot {tan(\theta)} \cdot aspect} & 0 & 0 & 0 \\ 0 & \frac{n}{n \cdot {tan(\theta)}} & 0 & 0 \\ 0 & 0 & \frac{-(f+n)}{f-n} & \frac{-2fn}{f-n} \\ 0 & 0 & -1 & 0\\ \end{array} \right] \end{align}
void CMatrix::gluPerspective(float _fovy,float _aspect,float _near,float _far){ 
float left,right,bottom,top; 
    top= _near * (float)tan(_fovy * M_PI / 360);
    bottom= -top; 
    left= bottom* _aspect; 
    right= top* _aspect; 
    glFrustum(left, right,bottom,top,_near,_far); 
}

glFrustumからの変更点まとめ

  • 基本的に 左右、上下対称である

なので、glFrustumで$\frac{2n}{t-n}$$\frac{2n}{2t}$になり、$\frac{n}{t}$

(9)
\begin{align} top= near・tan(\theta); \end{align}

なので
$\frac{n}{n tan\theta}$になる、てことは$\frac{1}{tan\theta}$

Androidのfrustumは要注意!

Androidライブラリにある、Matrix.frustumM(matrix, 0, left, right, bottom, top, near, far);という関数ですが、
一部の端末で、leftとrightの絶対値が不均一な時、間違った2倍の値が返ってくるので要注意!
こんな風にしておけば安全です

float matrix=new float[16];
Matrix.frustumM(this.matrix, 0, left, right, bottom, top, near, far);//この時点では計算結果が間違ってる場合がある
if((right-left)==0){return;}
matrix[8]=(left+right)/(right-left);

なんなんでしょうね、0割を防ぐために内部で何か特殊な計算方法をしてるのでしょうか?
leftとrightが均等じゃないと正しい結果が出てこないなんてイジワルですねー
topとbottomでも同様の罠があるかもしれないので要注意!
frustum

サポートサイト Wikidot.com frustum