Perspective Division in GLSL Vertex Pipelines
This article examines the consequences of altering perspective division within OpenGL Shading Language (GLSL) vertex pipelines. In standard GPU rasterization, perspective division is a fixed-function hardware step that divides clip-space coordinates by their homogeneous component (\(w\)) to produce normalized device coordinates (NDC). Attempting to bypass, preempt, or modify this process inside the vertex shader alters depth calculation, disables perspective-correct attribute interpolation, and breaks frustum clipping, leading to distinct rendering artifacts.
The Standard Perspective Pipeline
In a standard GLSL pipeline, the vertex shader outputs coordinates to the built-in output variable:
\[\text{gl\_Position} = (x_c, y_c, z_c, w_c)\]
Under normal operation, the vertex shader terminates in clip space. The GPU’s fixed-function hardware then performs two critical steps before rasterization:
- Frustum Clipping: The hardware clips geometric primitives against the view volume defined by \(-w_c \le x_c, y_c, z_c \le w_c\).
- Perspective Division: The hardware computes Normalized Device Coordinates (NDC) by dividing the clip-space components by \(w_c\):
\[(x_{ndc}, y_{ndc}, z_{ndc}) = \left(\frac{x_c}{w_c}, \frac{y_c}{w_c}, \frac{z_c}{w_c}\right)\]
When developers alter this handling—such as manually dividing by \(w\) inside the vertex shader and setting \(w=1.0\)—they fundamentally break how the rasterizer processes geometry.
Loss of Perspective-Correct Attribute Interpolation
The most visible consequence of manual perspective division in GLSL is the failure of perspective-correct interpolation across surface attributes (texture coordinates, normals, and colors).
Modern rasterizers interpolate varying variables across a triangle by interpolating \(\frac{\text{attribute}}{w}\) in screen space, along with \(\frac{1}{w}\), and performing a per-fragment division to recover the true value.
If you divide coordinates by \(w\) in the vertex shader and set the final \(w\) component to \(1.0\):
- The hardware treats \(w\) as a constant \(1.0\) during rasterization.
- Attributes are interpolated linearly in screen space (affine mapping) rather than hyperbolically in 3D space.
- Visual Effect: Textures warp, bend, and shear depending on camera distance and angle, producing a distorted appearance similar to rendering on legacy consoles like the Sony PlayStation 1.
Frustum Clipping Failures
Frustum clipping occurs strictly in homogeneous clip space before the hardware perspective divide occurs. This step prevents division by zero and rejects geometry located behind the camera plane (\(w \le 0\)).
Preemptively dividing by \(w\) in the vertex shader introduces several clipping bugs:
- Behind-Camera Inversion: Geometry located behind the near plane can have a negative \(w\) value. Dividing by a negative \(w\) inverts the sign of \((x, y)\), causing vertices behind the viewer to project upside-down and backwards onto the screen.
- Division by Zero: Triangles spanning the \(w = 0\) plane cause divide-by-zero
operations in the shader, resulting in
NaNorInfinitycoordinates that cause primitives to violently stretch or disappear. - Incorrect Edge Clipping: Because the hardware assumes the output is still in clip space, setting \(w = 1.0\) causes the clipping unit to test \((x_{ndc}, y_{ndc}, z_{ndc})\) against \([-1, 1]\) directly without the proper geometric boundary calculation, leading to clipped geometry popping in and out of view at viewport borders.
Depth Precision and Non-Linear Z Distribution
The perspective divide accounts for the non-linear distribution of the depth buffer, where precision is concentrated near the near plane.
When modifying perspective division manually:
- The fixed-function pipeline will still divide the components by whatever value is left in \(\text{gl\_Position.w}\). If you manually divide \(z\) by \(w\) and leave the original \(w\) intact, \(z\) will be divided twice.
- If \(w\) is overwritten with an incorrect value, depth buffer mapping between \([0.0, 1.0]\) (or \([-1.0, 1.0]\) in older OpenGL contexts) fails, leading to depth fighting (Z-fighting), reversed occlusion, or models prematurely clipping out of the depth buffer.
Valid Applications of Perspective Component Manipulation
While performing the complete perspective divide in the vertex shader causes breaking issues, intentionally modifying individual components of the projection calculation is common for specific rendering techniques:
- Infinite Far Planes: Manipulating the projection matrix to produce \(w_c = -z_{view}\) and a fixed \(z_c\) allows rendering skyboxes or shadows extending to infinity without floating-point overflow during hardware division.
- Orthographic Overrides: Setting \(w_c = 1.0\) natively via the projection matrix (rather than post-divide) creates orthographic projections where parallel lines remain parallel and screen-space interpolation is intended to be linear.
- Logarithmic Depth Buffers: Modifying the relation between \(z_c\) and \(w_c\) directly in vertex code allows massive scale shifts in space simulations while leaving the final division step to the rasterizer.
Perspective division relies on coordinated interaction between GLSL vertex code, dedicated clipping units, and the rasterizer. To ensure correct attribute interpolation, view-frustum clipping, and depth testing, the final division by \(w\) must remain in the domain of the GPU's fixed-function hardware.