Texture Sampling
Reading and mapping images in shaders
Until now, our shaders have generated colors from pure mathematics. But shaders can also read from images, called textures. This opens up image processing, sprite rendering, material mapping, and effects that combine procedural patterns with photographic detail.
A texture is simply a grid of colors that the shader can look up by coordinate. The GPU is extraordinarily efficient at this operation, performing billions of texture lookups per second with built-in filtering and interpolation.
Reading from a Texture
In GLSL, we sample a texture using the texture2D function (or just texture in newer versions):
uniform sampler2D u_texture;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution;
vec4 color = texture2D(u_texture, uv);
gl_FragColor = color;
}The sampler2D uniform represents our texture. We pass it UV coordinates (ranging from 0 to 1), and it returns the color at that position as a vec4 with red, green, blue, and alpha channels.
Basic Texture Sampling
The texture mapped directly to screen coordinates
The texture appears on screen, mapped directly from UV coordinates. The bottom-left of the screen (UV = 0,0) shows the bottom-left of the texture. The top-right (UV = 1,1) shows the top-right.
UV Coordinate Manipulation
The power comes from manipulating UV coordinates before sampling. Everything we learned about coordinate transformations applies here.
Scaling: Multiply UVs to tile the texture:
vec2 tiledUV = uv * 3.0; // 3x3 tilesTranslation: Add to UVs to scroll the texture:
vec2 scrolledUV = uv + vec2(u_time * 0.1, 0.0);Rotation: Apply a rotation matrix to the UVs:
float angle = u_time;
mat2 rot = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
vec2 rotatedUV = rot * (uv - 0.5) + 0.5;UV Coordinate Effects
UV transformations change how the texture maps to screen space
These transformations happen before the texture lookup. The texture data never changes; we simply read from different locations.
Filtering Modes
When the texture does not align perfectly with screen pixels, the GPU must decide how to interpolate. Two main filtering modes exist:
Nearest neighbor (also called point sampling): Returns the color of the closest texel. This preserves hard edges and is essential for pixel art. But it creates blocky artifacts when scaling.
Bilinear filtering: Blends the four nearest texels based on the sample position. This produces smooth results but can blur sharp edges.
Nearest vs Bilinear Filtering
For pixel art and intentionally blocky aesthetics, use nearest. For photographs and smooth gradients, use bilinear. The choice depends on the visual style you want.
Wrap Modes
What happens when UV coordinates go outside the 0-1 range? The wrap mode (or address mode) determines this:
Repeat: The texture tiles infinitely. UVs wrap around, so 1.5 becomes 0.5.
Clamp to edge: UVs are clamped to [0, 1]. Beyond the edge, you see the edge pixels repeated.
Mirrored repeat: The texture flips at each boundary, creating a seamless mirror effect.
// Manual repeat (fract brings values back to 0-1)
vec2 repeatedUV = fract(uv * 3.0);
// Manual clamp
vec2 clampedUV = clamp(uv * 2.0 - 0.5, 0.0, 1.0);Wrap Mode Comparison
Repeat: texture tiles seamlessly
Repeat mode is common for tiling textures like brick or grass. Clamp is useful when you do not want the texture to repeat. Mirrored repeat creates seamless patterns without visible seams at tile boundaries.
Combining Textures with Procedural Effects
The real magic happens when you combine texture sampling with procedural techniques.
Distortion: Use noise to offset UV coordinates:
vec2 offset = vec2(noise(uv * 10.0), noise(uv * 10.0 + 100.0));
vec4 color = texture2D(u_texture, uv + offset * 0.05);Color manipulation: Modify the sampled color:
vec4 color = texture2D(u_texture, uv);
color.rgb = pow(color.rgb, vec3(0.8)); // BrightenBlending: Mix texture with procedural patterns:
vec4 tex = texture2D(u_texture, uv);
float pattern = sin(uv.x * 50.0);
gl_FragColor = mix(tex, vec4(pattern), 0.3);Texture Manipulation
Combine texture sampling with procedural effects
These combinations create effects impossible with either technique alone: rippling water surfaces, heat distortion, glitch effects, and dynamic materials that respond to game state.
Multiple Textures
Shaders can sample from multiple textures simultaneously. Common uses include:
- Normal maps: Store surface direction to simulate detailed geometry
- Specular maps: Control shininess per-pixel
- Displacement maps: Offset vertices or ray directions
- Blend maps: Control how other textures mix together
Each texture gets its own sampler2D uniform and can be sampled independently.
Performance Considerations
Texture sampling is highly optimized but not free:
- Texture size: Larger textures use more memory and can miss cache more often
- Mipmaps: Pre-computed smaller versions that reduce aliasing and improve performance at distance
- Dependent reads: Sampling one texture to get UVs for another is slower than direct reads
- Format: Compressed textures save memory bandwidth
For real-time applications, texture atlases (combining many images into one) reduce state changes. Mipmaps should almost always be enabled for 3D rendering.
Key Takeaways
- Textures are grids of color data sampled by UV coordinates
texture2D(sampler, uv)returns the color at the given coordinates- UV manipulation (scale, translate, rotate) transforms how textures appear without modifying the data
- Nearest filtering preserves hard edges; bilinear filtering smooths
- Wrap modes control behavior outside [0,1]: repeat, clamp, or mirror
- Combining texture sampling with procedural effects enables powerful visual effects
- Multiple textures allow complex materials with normal maps, masks, and blend layers