How Does textureProj Divide Coordinates in GLSL?

GLSL provides the textureProj function to simplify projective texture mapping by automatically dividing the texture coordinate vector's directional components by its final component before sampling. This article explains the underlying mathematics of projective texture division, details how GLSL handles different sampler dimensions, highlights hardware execution benefits, and contrasts textureProj with manual coordinate division.

The Mathematics of Projective Texture Division

Projective texturing simulates a slide projector casting an image onto 3D geometry. When geometry is transformed by a projector's view-projection matrix, vertex positions end up in homogeneous clip space \((x, y, z, w)\). To map these coordinates onto a standard normalized texture space \([0, 1]\), the perspective distortion must be normalized via perspective division.

In GLSL, textureProj performs this division intrinsically before evaluating the texture lookup. Given a coordinate vector \(P\), the hardware divides the initial coordinate components by the last component:

\[\text{Projected Coordinates} = \frac{P_{0 \dots n-1}}{P_{\text{last}}}\]

Depending on the dimensionality of the sampler and the type of the coordinate vector passed to textureProj, the specific components used in the division adapt accordingly.

Vector Handling Across Sampler Types

The GLSL specification defines overloads of textureProj for 1D, 2D, 3D, and shadow textures. The division behavior shifts based on the vector dimensionality:

1D Textures (sampler1D)

\[\text{coord} = \frac{P.x}{P.y}\]

\[\text{coord} = \frac{P.x}{P.w}\]

2D Textures (sampler2D)

\[\text{coord} = \left(\frac{P.x}{P.z}, \frac{P.y}{P.z}\right)\]

\[\text{coord} = \left(\frac{P.x}{P.w}, \frac{P.y}{P.w}\right)\]

3D Textures (sampler3D)

\[\text{coord} = \left(\frac{P.x}{P.w}, \frac{P.y}{P.w}, \frac{P.z}{P.w}\right)\]

Shadow Maps (sampler2DShadow)

\[\text{lookup} = \left(\frac{P.x}{P.w}, \frac{P.y}{P.w}\right), \quad \text{compare\_depth} = \frac{P.z}{P.w}\]

Hardware Execution and Derivative Calculation

When manual division is performed in a fragment shader—such as calling texture(tex, coord.xy / coord.w)—the division happens explicitly in shader arithmetic logic units (ALUs). The automatic partial derivatives (dFdx and dFdy) needed for mipmap level-of-detail (LOD) calculations are subsequently evaluated on the post-division values \(\frac{P.xy}{P.w}\). Across triangle silhouettes or near \(P.w \approx 0\), rapid coordinate variations can cause extreme derivative spikes, resulting in incorrect mipmap selection and visual blur or seam artifacts.

Using textureProj delegates perspective division to the dedicated texture mapping unit (TMU) hardware on modern GPUs:

  1. The raw homogeneous coordinates interpolate across the primitive via standard perspective-correct barycentric interpolation.
  2. The TMU computes derivatives from the unprojected coordinates prior to division or uses dedicated projection sampling pipelines.
  3. The hardware divides the coordinates and fetches the sample in an optimized hardware pipeline stage.

Practical Code Example

In applications such as shadow mapping or spotlight projection, homogeneous projective coordinates are passed from the vertex shader to the fragment shader.

// Fragment Shader
#version 450 core

in vec4 vProjTexCoord;

uniform sampler2D spotlightTexture;
uniform sampler2DShadow shadowMap;

out vec4 fragColor;

void main()
{
    // Samples 2D color projection using automatic (x/w, y/w) division
    vec4 projectedColor = textureProj(spotlightTexture, vProjTexCoord);

    // Performs depth comparison with (z/w) at coordinates (x/w, y/w)
    float shadowFactor = textureProj(shadowMap, vProjTexCoord);

    fragColor = projectedColor * shadowFactor;
}

By encapsulating both coordinate normalization and perspective division in a single intrinsic function, textureProj keeps shader code concise, minimizes manual ALU instructions, and maintains consistent hardware-accelerated sampling behavior.