Perspective Camera
A (row-major) perspective transformation matrix has the following format:
$$\begin{align} \mathrm{T} &= \begin{bmatrix} \mathrm{T}_{00} &0 &0 &0 \\ 0 &\mathrm{T}_{11} &0 &0 \\ 0 &0 &\mathrm{T}_{22} &1 \\ 0 &0 &\mathrm{T}_{32} &0 \end{bmatrix}=\begin{bmatrix} 1/x &0 &0 &0 \\ 0 &1/y &0 &0 \\ 0 &0 &-w &1 \\ 0 &0 &z &0\end{bmatrix}. \end{align}$$
This transformation matrix is used to transform (homogeneous) points from view to projection space, after which the homogeneous divide is applied to transform to NDC (Normalized Device Coordinate) space. (NDC space is technically a 3D space, but for ease of notation, I use 4D points with a $w=1$).
In a deferred renderer, we need to go the other way around while resolving the GBuffer and could use four components ($x$, $y$, $z$, $w$, see above) to transform a point from NDC to view space:
/**
Returns the projection values from the given projection matrix to construct
the view position coordinates from the NDC position coordinates.
@return The projection values from the given projection matrix to
construct the view position coordinates from the NDC position
coordinates.
*/
inline const XMVECTOR XM_CALLCONV GetViewPositionConstructionValues(
FXMMATRIX projection_matrix) noexcept {
// [ 1/X 0 0 0 ]
// p_view [ 0 1/Y 0 0 ] = [p_view.x 1/X, p_view.y 1/Y, p_view.z (-W) + Z, p_view.z] = p_proj
// [ 0 0 -W 1 ]
// [ 0 0 Z 0 ]
//
// p_proj / p_proj.w = [p_view.x/p_view.z 1/X, p_view.y/p_view.z 1/Y, -W + Z/p_view.z, 1] = p_ndc
//
// Construction of p_view from p_ndc and projection values
// 1) p_ndc.z = -W + Z/p_view.z <=> p_view.z = Z / (p_ndc.z + W)
// 2) p_ndc.x = p_view.x/p_view.z 1/X <=> p_view.x = X * p_ndc.x * p_view.z
// 3) p_ndc.y = p_view.y/p_view.z 1/Y <=> p_view.y = Y * p_ndc.y * p_view.z
const F32 x = 1.0f / XMVectorGetX(projection_matrix.r[0]);
const F32 y = 1.0f / XMVectorGetY(projection_matrix.r[1]);
const F32 z = XMVectorGetZ(projection_matrix.r[3]);
const F32 w = -XMVectorGetZ(projection_matrix.r[2]);
return XMVectorSet(x, y, z, w);
}
Orthographic Camera
An (row-major) orthographic transformation matrix has the following format:
$$\begin{align} \mathrm{T} &= \begin{bmatrix} \mathrm{T}_{00} &0 &0 &0 \\ 0 &\mathrm{T}_{11} &0 &0 \\ 0 &0 &\mathrm{T}_{22} &0 \\ 0 &0 &\mathrm{T}_{32} &1 \end{bmatrix}=\begin{bmatrix} 1/x &0 &0 &0 \\ 0 &1/y &0 &0 \\ 0 &0 &1/z &0 \\ 0 &0 &-w &1\end{bmatrix}. \end{align}$$
This transformation matrix is used to transform (homogeneous) points from view to projection space, after which the homogeneous divide (no-op) is applied to transform to NDC (= projection) space. (So basically a non-uniform scaling followed by a translation of the z component).
We could similarly use four components ($x$, $y$, $z$, $w$, see above) to transform a point from NDC (= projection) to view space:
/**
Returns the projection values from the given projection matrix to construct
the view position coordinates from the NDC position coordinates.
@return The projection values from the given projection matrix to
construct the view position coordinates from the NDC position
coordinates.
*/
inline const XMVECTOR XM_CALLCONV GetViewPositionConstructionValues(
FXMMATRIX projection_matrix) noexcept {
// [ 1/X 0 0 0 ]
// p_view [ 0 1/Y 0 0 ] = [p_view.x 1/X, p_view.y 1/Y, p_view.z 1/Z -W, 1] = p_proj = p_ndc
// [ 0 0 1/Z 0 ]
// [ 0 0 -W 1 ]
//
// Construction of p_view from p_ndc and projection values
// 1) p_ndc.z = p_view.z/Z -W <=> p_view.z = Z * (p_ndc.z + W)
// 2) p_ndc.x = p_view.x/X <=> p_view.x = X * p_ndc.x
// 3) p_ndc.y = p_view.y/Y <=> p_view.y = Y * p_ndc.y
const F32 x = 1.0f / XMVectorGetX(projection_matrix.r[0]);
const F32 y = 1.0f / XMVectorGetY(projection_matrix.r[1]);
const F32 z = 1.0f / XMVectorGetZ(projection_matrix.r[2]);
const F32 w = -XMVectorGetZ(projection_matrix.r[3]);
return XMVectorSet(x, y, z, w);
}
Question
How can a deferred renderer support multiple camera types (minimally: perspective and orthographic cameras, ideally: all cameras that a forward renderer could support) without specializing the shaders or specializing in the shaders?
A straightforward solution is to specify some pre-processor directive in the shader, but this unfortunately results in a multiple of all the shader perturbations used so far for lighting in a deferred pipeline. Another solution is to pass a flag in some constant buffer to specify the used camera type, though I am not a fan of the dynamic branching introduced by this flag. Furthermore, both solutions are not as flexible as is the case for forward rendering which can handle arbitrary view-to-projection matrices.
Ideally, a (hypothetical) NDC-to-view transformation matrix should handle the transformation. Such a matrix does, however, not generally exist for perspective cameras since the view to NDC transformation reduces a 4D space to a 3D subspace.