Camera Facing Billboard Matrix Calculation in GLSL
In 3D computer graphics, billboard particles are flat, two-dimensional planes that continuously rotate to face the camera, creating the illusion of three-dimensional volume for effects like smoke, fire, and sparks. This orientation is achieved in OpenGL Shading Language (GLSL) by generating a camera-facing transformation or rotation matrix within the vertex shader. By extracting the camera’s orientation vectors directly from the view matrix or calculating an orthogonal basis toward the camera position, GLSL can construct a matrix that aligns the local vertices of a quad with the viewer's line of sight.
The Mathematics of Camera Alignment
A standard 2D particle quad lies flat on a plane (typically the XY plane). To make this quad face the viewer, its local coordinate system must match the orientation of the camera.
A rotation matrix in three dimensions is composed of three orthonormal basis vectors:
- Right vector (\(\vec{R}\)): Points to the camera’s local horizontal right axis.
- Up vector (\(\vec{U}\)): Points to the camera’s local vertical axis.
- Forward vector (\(\vec{F}\)): Points directly toward or away from the camera.
When these vectors form the columns of a \(3 \times 3\) matrix, multiplying a local 2D vertex position by this matrix reorients the quad into world space while aligning it with the camera:
\[M_{billboard} = \begin{bmatrix} R_x & U_x & F_x \\ R_y & U_y & F_y \\ R_z & U_z & F_z \end{bmatrix}\]
Method 1: View-Plane Aligned Billboards (Extracting Basis Vectors)
View-plane aligned billboards orient the quad parallel to the camera's rendering plane. This approach is computationally efficient because the basis vectors already exist inside the standard view matrix.
In GLSL, the view matrix transforms world space into camera space. The inverse of the view matrix (or the transpose of its upper \(3 \times 3\) rotation portion) contains the camera's world-space Right, Up, and Forward vectors.
// GLSL Vertex Shader
uniform mat4 u_view;
uniform mat4 u_projection;
uniform vec3 u_particleCenterWorld; // World position of particle
in vec2 a_quadVertex; // Local quad offsets (-0.5 to 0.5)
void main() {
// Extract camera right and up from the view matrix
vec3 cameraRight = vec3(u_view[0][0], u_view[1][0], u_view[2][0]);
vec3 cameraUp = vec3(u_view[0][1], u_view[1][1], u_view[2][1]);
// Construct the world-space position
vec3 worldPosition = u_particleCenterWorld
+ cameraRight * a_quadVertex.x
+ cameraUp * a_quadVertex.y;
gl_Position = u_projection * u_view * vec4(worldPosition, 1.0);
}Alternatively, you can bypass the world-space calculation entirely by transforming the particle center to view space, applying the 2D quad offset directly, and then applying projection:
// View-Space Shortcut
void main() {
vec4 viewCenter = u_view * vec4(u_particleCenterWorld, 1.0);
viewCenter.xy += a_quadVertex.xy; // Offset directly in view space
gl_Position = u_projection * viewCenter;
}Method 2: True Camera-Facing Billboards (Spherical Alignment)
Unlike plane-aligned billboards, spherical billboards calculate a direct line of sight from the individual particle's center to the camera position. This avoids distortion when particles appear near the edges of a wide field-of-view perspective camera.
To calculate this matrix in GLSL:
- Compute the normalized forward vector pointing from the particle to the camera.
- Take the cross product of an arbitrary world up vector (usually \((0, 1, 0)\)) and the forward vector to find the right vector.
- Take the cross product of the forward vector and the right vector to derive the orthogonal up vector.
// GLSL Vertex Shader
uniform vec3 u_cameraPositionWorld;
uniform mat4 u_viewProjection;
uniform vec3 u_particleCenterWorld;
in vec2 a_quadVertex;
void main() {
// 1. Calculate Forward vector
vec3 forward = normalize(u_cameraPositionWorld - u_particleCenterWorld);
// 2. Calculate Right vector
vec3 worldUp = vec3(0.0, 1.0, 0.0);
vec3 right = normalize(cross(worldUp, forward));
// 3. Calculate true Up vector
vec3 up = cross(forward, right);
// 4. Construct the 3x3 billboard rotation matrix
mat3 billboardRotation = mat3(right, up, forward);
// 5. Rotate local vertex and translate to world space
vec3 worldPosition = billboardRotation * vec3(a_quadVertex, 0.0) + u_particleCenterWorld;
gl_Position = u_viewProjection * vec4(worldPosition, 1.0);
}Method 3: Axial (Cylindrical) Billboards
For objects that must remain anchored to a vertical axis (such as trees or vertical energy beams), the up vector is constrained to a fixed axis (such as the world Y-axis).
- The Up vector is locked to \((0, 1, 0)\).
- The Forward vector is calculated, and its Y component is set to zero or overridden.
- The Right vector is derived via \(\text{cross}(\vec{U}, \vec{F})\).
- The real Forward vector is recalculated via \(\text{cross}(\vec{R}, \vec{U})\) to preserve orthogonality.
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 forward = normalize(u_cameraPositionWorld - u_particleCenterWorld);
vec3 right = normalize(cross(up, forward));
forward = cross(right, up);
mat3 axialMatrix = mat3(right, up, forward);
vec3 worldPosition = axialMatrix * vec3(a_quadVertex, 0.0) + u_particleCenterWorld;Performance Considerations
- View-Space Offsetting: Applying offsets in view space is the fastest method, as it requires zero trigonometric calculations or cross products per vertex.
- Uniform Matrix Passing: If all particles in a draw
call share the same orientation (as in view-plane alignment), calculate
the inverse view rotation on the CPU and pass it as a single
mat3uniform to eliminate redundant matrix operations in the vertex shader.