Normalized vs Unnormalized Coordinates in GLSL

Texture sampling in GLSL can be performed using either standard normalized coordinates (spanning \([0.0, 1.0]\)) or unnormalized pixel coordinates (spanning integer pixel bounds \([0, \text{dimension}-1]\)). This article compares both approaches, examining their GLSL implementation functions, texture filtering behavior, addressing modes, performance implications, and practical use cases in modern graphics programming.

Coordinate Systems Defined

  • Normalized UV Coordinates: These coordinates are resolution-independent floating-point values where \((0.0, 0.0)\) corresponds to the bottom-left (or top-left, depending on convention) of the texture, and \((1.0, 1.0)\) corresponds to the opposite corner. Scaling a texture asset up or down does not require modifying shader code or vertex UV data.
  • Unnormalized Coordinates: These coordinates represent direct texel positions within the texture's native dimensions. For a \(1920 \times 1080\) texture, coordinates range from \((0, 0)\) to \((1919, 1079)\). They are directly tied to the exact pixel resolution of the underlying image.

GLSL Implementation

GLSL handles each coordinate scheme through different functions and sampler types:

Normalized Sampling: texture()

Normalized sampling is the standard technique in GLSL, implemented primarily through the texture() function using standard samplers like sampler2D:

uniform sampler2D u_Texture;
in vec2 v_UV; // Ranges from 0.0 to 1.0

void main() {
    vec4 color = texture(u_Texture, v_UV);
    // ...
}

Unnormalized Fetching: texelFetch() and sampler2DRect

To read texels using unnormalized integer coordinates, GLSL provides texelFetch(). This function operates on integer coordinates (ivec2) and requires an explicit mipmap level:

uniform sampler2D u_Texture;

void main() {
    ivec2 texelCoord = ivec2(gl_FragCoord.xy);
    vec4 color = texelFetch(u_Texture, texelCoord, 0); // 0 is the mipmap level
    // ...
}

Historically, OpenGL provided sampler2DRect (texture rectangles), which accepted unnormalized floating-point coordinates inside the texture() function. However, texelFetch() on standard texture targets has largely superseded this approach in modern pipelines.

Filtering and Interpolation

  • Normalized (texture): Fully supports the hardware texture filtering pipeline. Depending on your OpenGL/Vulkan sampler configuration, the GPU automatically performs nearest-neighbor, bilinear, or trilinear interpolation between texels and mipmap levels.
  • Unnormalized (texelFetch): Completely bypasses the texture filtering hardware. It retrieves a single discrete texel from the specified integer coordinate. There is no automatic linear blending or smooth interpolation between adjacent texels.

Mipmapping and Level of Detail (LOD)

  • Normalized (texture): The GPU calculates screen-space partial derivatives (dFdx, dFdy) to automatically determine the optimal mipmap level. Functions like textureLod() also allow manual LOD selection while retaining normalized coordinates.
  • Unnormalized (texelFetch): Requires an explicit integer parameter indicating the exact mipmap level to read from (typically 0 for base level). It does not compute derivatives or automatically transition between mipmaps.

Wrapping and Addressing Modes

  • Normalized: Works natively with hardware texture wrapping modes: GL_REPEAT, GL_MIRRORED_REPEAT, GL_CLAMP_TO_EDGE, and GL_CLAMP_TO_BORDER. Coordinates outside \([0.0, 1.0]\) are handled entirely by the GPU sampler unit.
  • Unnormalized: Does not support automatic wrap modes like REPEAT. Passing an out-of-bounds integer coordinate to texelFetch() results in undefined behavior or hardware-clamped values (often returning zero or border colors depending on the driver and platform).

Performance Considerations

  • texture() Performance: Highly optimized for arbitrary continuous lookups, filtering, and cache locality over transformed 3D surfaces. However, it incurs the overhead of the filtering unit (lerping multiple texels and mip levels).
  • texelFetch() Performance: Typically exhibits lower instruction overhead because it acts as a direct memory load instruction, bypassing filtering math. When reading a 1:1 pixel-to-fragment mapping (such as post-processing), texelFetch avoids derivative computations and unneeded filter stages.

Comparison Summary

Feature Normalized (texture) Unnormalized (texelFetch)
Coordinate Type Floating-point (vec2, \([0.0, 1.0]\)) Integer (ivec2, \([0, \text{dimension}-1]\))
Resolution Dependency Independent Dependent
Hardware Filtering Yes (Bilinear, Trilinear, Anisotropic) No (Direct texel retrieval)
Mipmapping Automatic derivative-based selection Explicit mip level required
Wrap Modes Full support (REPEAT, CLAMP, etc.) None (Requires manual shader logic)
Primary Use Cases 3D models, UI sprites, scaled assets Post-processing, G-Buffers, compute buffers

When to Use Each

Use normalized coordinates for standard 3D mesh rendering, UV-mapped models, world textures, and any asset that undergoes scaling, perspective transformation, or requires smooth bilinear/trilinear filtering.

Use unnormalized coordinates for deferred rendering G-buffer reads, screen-space post-processing passes (where fragment coordinates map directly to screen pixels), reading data-storage textures (lookup tables, animation bone matrices), or compute shader image processing.