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 liketextureLod()also allow manual LOD selection while retaining normalized coordinates. - Unnormalized (
texelFetch): Requires an explicit integer parameter indicating the exact mipmap level to read from (typically0for 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, andGL_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 totexelFetch()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),texelFetchavoids 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.