Note: This is a work in progress - working on LaTeX and useful illustrations.
My derivation notes were based partially on the perspective projection matrix derivation in Computer Graphics: Principles and Practice (2nd Ed)
. My understanding is that the 3rd edition takes a more contemporary approach with references to OpenGL and Direct3D. I haven't read this edition, and can't vouch for it.
This derivation also assumes an understanding of homogeneous coordinates and the notion of a projective space. The wikipedia entry is concise, but lacks the intuition provided by a few good 2D example illustrations. (If you're really keen, search for early articles on the subject by Jim Blinn).
First, we need to outline a few assertions and conventions:
- We always assume a 'right-handed' coordinate space, unless otherwise stated. The 'eye' is at:
(0, 0, 0)
. (+Z)
may be considered the direction pointing 'out' of the screen (or window / viewport), while (X, Y)
are Cartesian coordinates, where: (0, 0)
corresponds to the centre of that viewport.
- The view frustum has a near plane at:
Z = - N
and a far plane at: Z = - F
, with normals in the +Z
direction. The near plane: Z = - N
, has X: L <= X <= R
, and Y: B <= Y <= T
, which will eventually map to a 3D viewport. This frustum encapsulates all visible geometry. (Technically speaking, R < L
, T < B
, and N > F > 0
are permitted by glFrustum
. Arbitrary matrices are of course permitted in core profile OpenGL contexts.)
The mathematics will use what are somewhat misleadingly called 'row-major' matrices, but I insist on the use of column vectors when describing a system of transforms, i.e., the column vector is 'pre-multiplied' by the matrix: v' = [M]v
. Disagree all you want - just know that you are sick and need help.
In all seriousness, either convention can be made to work; it's just a matter of being aware of the conventions and sticking with a choice. The 'column-vector' convention is typically the one taught in linear algebra. We apply successive transforms: [A]
, [B]
, [C]
to a vector: v
, like we would apply functions: C(B(A(v)))
, hence: v' = [C][B][A]v
.
Any view frustum (camera) in space can be transformed by pre-multiplying the (homogeneous) coordinates by an 'orientation matrix'. That's beyond your question, but typically requires a general PRP (camera eye point), VRP (the centre of the 'near' projection plane), and the VUP point, which references a point defining 'up'. In effect, it's sufficient to form a orthonormal coordinate system. This is the view coordinate space (VCS).
The orthonormal basis, along with the PRP translation, can be used to construct an inverse transform that 'premultiplies' geometry to world coordinate space (WCS). Again, you can save the concept of orthonormal basis / coordinate system transforms for a rainy day...
We do not assume the view frustum is centred on the Z axis - although this is almost certainly the case for most projections. There are more exotic transforms (a mirror in a scene comes to mind); we'll concentrate on the general case.
The direction of projection: $\small\\\mathbf{DOP}=\left[(R+L)/2,\: (T+B)/2,\: -N\right]\,^T$. The view frustum is typically symmetric about the Z axis s.t. R = - L
, T = - B
, yielding: $\small\\\mathbf{DOP}=\left[0,\: 0,\: -N\right]\,^T$.
Of course, this is not required, so our first step in the perspective projection derivation is to apply a shear matrix that maps the $\small\\\mathbf{DOP}$ to the (negative) Z-axis:
(R + L) / 2 + shear.x (- N) = 0
=> shear.x = (R + L) / (2.N)
(T + B) / 2 + shear.y (- N) = 0
=> shear.y = (T + B) / (2.N)
Consider the shear transformation applied to the frustum's near-clipping plane vertex coordinates:
$\tiny\begin{pmatrix}
1 & 0 & (R+L)/(2.N) & 0 \\ 0 & 1 & (T+B)/(2.N) & 0 \\
0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1
\end{pmatrix}
\cdot
\begin{pmatrix}
R \\ T \\ -N \\ 1
\end{pmatrix}
= \begin{pmatrix}
(R-L)/2 \\ (T-B)/2 \\ -N \\ 1
\end{pmatrix}
\:,$
$\tiny\begin{pmatrix}
1 & 0 & (R+L)/(2.N) & 0 \\ 0 & 1 & (T+B)/(2.N) & 0 \\
0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1
\end{pmatrix}
\cdot
\begin{pmatrix}
L \\ B \\ -N \\ 1
\end{pmatrix}
= \begin{pmatrix}
(L-R)/2 \\ (B-T)/2 \\ -N \\1
\end{pmatrix}
$
Following this transform, the $\small\\\mathbf{DOP}$ is in direction of the - Z
axis; specifically, the centre of the near view plane: $\small\left((R+L)/2,\: (T+B)/2,\: -N,\: 1\right)\,^T$ has been mapped to: $\small\left(0,\: 0,\: -N,\: 1\right)\,^T$.
Now we want to scale the perspective projection transform such that the R, L, T, B
planes of the frustum have have a unit gradient. The current gradients are (R - L) / (2.N)
(right and left), and (T - B) / (2.N)
(top and bottom). The symmetry is due to the previous shearing transform.
(R - L) / (2.N) * scale.x = 1
=> scale.x = 2.N / (R - L)
(T - B) / (2.N) * scale.y = 1
=> scale.y = 2.N / (T - B)
Furthermore, we want to scale by (1 / F)
s.t. the far clipping plane is mapped to: Z = - 1
, while the unit slopes of the right, left, top, and bottom clipping planes are preserved. These scaling factors premultiply the perspective projection matrix:
$
\tiny\begin{pmatrix}
2.N/(F(R-L)) & 0 & 0 & 1 \\ 0 & 2.N/(F(T-B)) & 0 & 1 \\
0 & 0 & 1/F & 0 \\ 0 & 0 & 0 & 1
\end{pmatrix}
\cdot
\begin{pmatrix}
1 & 0 & (R+L)/(2.N) & 0 \\ 0 & 1 & (T+B)/(2.N) & 0 \\
0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1
\end{pmatrix}
=\\
\tiny\begin{pmatrix}
2.N/(F(R-L)) & 0 & (R+L)/(F(R-L)) & 0 \\
0 & 2.N/(F(T-B)) & (T+B)/(F(T-B)) & 0 \\
0 & 0 & 1/F & 0 \\ 0 & 0 & 0 & 1
\end{pmatrix}
$
This transform yields what may be considered the normalized frustum.
(TODO: need an image to show this transform)
The vertices of near clipping plane are mapped to: $\small\left(\pm N/F,\: \pm N/F,\: -N/F,\: 1\right)\,^T$, while the vertices of the far-clipping plane are mapped to: $\small\left(\pm 1,\: \pm 1,\: -1,\: 1\right)\,^T$.
(TODO: perspective (projection) transform unification -> CCS)
As a homogeneous transform, the matrix can be simplified by scaling all elements by F
, since: $\small\left(X,\: Y,\: Z,\: W\right)\,^T \equiv \left(F.X,\: F.Y,\: F.Z,\: F.W\right)\,^T$ :
$
\tiny\begin{pmatrix}
2.N/(R-L) & 0 & (R+L)/(R-L) & 0 \\
0 & 2.N/(T-B) & (T+B)/(T-B) & 0 \\
0 & 0 & 1 & 0 \\ 0 & 0 & 0 & F
\end{pmatrix}
$
The normalized perspective projection view volume can be transformed into a parallel projection view volume:
(TODO: partial justification for this step. Perspective projection is just another homogeneous transform. Final CCS / NDCS have exquisite clip test properties)
$
\tiny\begin{pmatrix}
1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\
0 & 0 & F/(F-N) & N/(F-N) \\ 0 & 0 & -1 & 0
\end{pmatrix}
\cdot
\begin{pmatrix}
2.N/(R-L) & 0 & (R+L)/(R-L) & 0 \\
0 & 2.N/(T-B) & (T+B)/(T-B) & 0 \\
0 & 0 & 1 & 0 \\ 0 & 0 & 0 & F
\end{pmatrix}
=\\
\tiny\begin{pmatrix}
2.N/(R-L) & 0 & (R+L)/(R-L) & 0 \\
0 & 2.N/(T-B) & (T+B)/(T-B) & 0 \\
0 & 0 & F/(F-N) & F.N/(F-N) \\ 0 & 0 & -1 & 0
\end{pmatrix}
$
(TODO: need an image to show this transform)
Finally, we apply the following transforms to the current projection matrix:
- Scale in
Z
by (2)
to yield a cubic view volume.
- Translate by: $\small\left(0,\: 0,\: 1,\: 1\right)\,^T$ to centre the cube at the origin.
- Convert to a left-handed coordinate system to satisfy NDCS conventions.
$
\tiny\begin{pmatrix}
1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1
\end{pmatrix}
\cdot
\begin{pmatrix}
1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 1 \\ 0 & 0 & 0 & 1
\end{pmatrix}
\cdot
\begin{pmatrix}
1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 2 & 0 \\ 0 & 0 & 0 & 1
\end{pmatrix}
=\
\begin{pmatrix}
1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & -2 & -1 \\ 0 & 0 & 0 & 1
\end{pmatrix}
$
This yields:
$
\tiny\begin{pmatrix}
1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & -2 & -1 \\ 0 & 0 & 0 & 1
\end{pmatrix}
\cdot
\begin{pmatrix}
2.N/(R-L) & 0 & (R+L)/(R-L) & 0 \\
0 & 2.N/(T-B) & (T+B)/(T-B) & 0 \\
0 & 0 & F/(F-N) & F.N/(F-N) \\
0 & 0 & -1 & 0
\end{pmatrix}
=\\
\tiny\begin{pmatrix}
2.N/(R-L) & 0 & (R+L)/(R-L) & 0 \\
0 & 2.N/(T-B) & (T+B)/(T-B) & 0 \\
0 & 0 & -(F+N)/(F-N) & -2.F.N/(F-N) \\
0 & 0 & -1 & 0
\end{pmatrix}
$
(TODO: need an image to show this transform)
This is the canonical OpenGL perspective projection matrix:
$
\small\begin{pmatrix}
2.N/(R-L) & 0 & (R+L)/(R-L) & 0 \\
0 & 2.N/(T-B) & (T+B)/(T-B) & 0 \\
0 & 0 & -(F+N)/(F-N) & -2.F.N/(F-N) \\
0 & 0 & -1 & 0
\end{pmatrix}
$