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;
}
glsl

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 tiles
glsl

Translation: Add to UVs to scroll the texture:

vec2 scrolledUV = uv + vec2(u_time * 0.1, 0.0);
glsl

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;
glsl

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);
glsl

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);
glsl

Color manipulation: Modify the sampled color:

vec4 color = texture2D(u_texture, uv);
color.rgb = pow(color.rgb, vec3(0.8));  // Brighten
glsl

Blending: 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);
glsl

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