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)
vec2input: Divides the single texture coordinate \(s\) by the divisor \(t\):
\[\text{coord} = \frac{P.x}{P.y}\]
vec4input: Divides \(s\) by the homogeneous scale factor \(q\):
\[\text{coord} = \frac{P.x}{P.w}\]
2D Textures (sampler2D)
vec3input: Common when transforming coordinates using a \(3 \times 3\) projective matrix. Divides the \((s, t)\) coordinates by the projection scale component \(r\):
\[\text{coord} = \left(\frac{P.x}{P.z}, \frac{P.y}{P.z}\right)\]
vec4input: Standard for 3D projective transformations using \(4 \times 4\) matrices. Divides \((s, t)\) by homogeneous component \(q\):
\[\text{coord} = \left(\frac{P.x}{P.w}, \frac{P.y}{P.w}\right)\]
3D Textures (sampler3D)
vec4input: Divides the volumetric \((s, t, r)\) coordinates by the homogeneous scale factor \(q\):
\[\text{coord} = \left(\frac{P.x}{P.w}, \frac{P.y}{P.w}, \frac{P.z}{P.w}\right)\]
Shadow Maps
(sampler2DShadow)
vec4input: In shadow mapping, the third component represents the reference depth value to compare against the depth buffer.textureProjscales both the UV coordinates and the reference comparison value by \(w\):
\[\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:
- The raw homogeneous coordinates interpolate across the primitive via standard perspective-correct barycentric interpolation.
- The TMU computes derivatives from the unprojected coordinates prior to division or uses dedicated projection sampling pipelines.
- 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.