Textures wrap detail onto simple geometry without extra polygons. Learn UV mapping, texture sampling in a shader, and normal maps — the trick that fakes bumpy surface detail with a flat image.
A texture is an image, and UV coordinates map each vertex of a mesh to a point in that image (from 0 to 1 on each axis). The rasterizer interpolates UVs across a triangle so every fragment knows where to look up its color in the texture. UV mapping is how a 2D image is "unwrapped" onto 3D geometry — the bridge between models and their surface art.
A texture is an image addressed by UV coordinates in [0, 1]:
(0,1)-----------(1,1) each mesh VERTEX is assigned a (u, v) in the image.
| | the rasterizer interpolates UVs across the triangle,
| [ image ] | so each FRAGMENT gets a coordinate to sample.
| |
(0,0)-----------(1,0)
"UV unwrapping" (done by artists) flattens a 3D model onto the 2D image so the
texture wraps correctly. The shader then samples the image at each fragment's UV.In the fragment shader you read a texture with the texture() function, passing the sampler (the bound image) and the fragment’s interpolated UV. The GPU handles filtering — smoothly blending between texels so the image does not look blocky when scaled. This one call replaces per-vertex colors with rich painted detail.
#version 330 core
in vec2 vUV; // interpolated texture coordinate
uniform sampler2D uTexture; // the bound texture image
out vec4 fragColor;
void main() {
// sample the texture at this fragment's UV -> its color
vec4 texColor = texture(uTexture, vUV);
fragColor = texColor;
}
// the GPU filters (bilinear/mipmapping) so it stays smooth when scaled.
// combine with lighting: fragColor = vec4(texColor.rgb * diffuse, 1.0);Adding real geometric detail costs polygons, so games fake it: a normal map is a texture that stores a surface normal per texel (encoded as RGB). In the shader you read the normal from the map instead of interpolating the vertex normal, so lighting reacts to fine bumps that are not actually in the mesh. This makes a flat wall look like rough brick — one of the highest-impact tricks in real-time graphics.
void main() {
// read a per-pixel normal from the normal map (stored in [0,1], remap to [-1,1])
vec3 mapNormal = texture(uNormalMap, vUV).rgb * 2.0 - 1.0;
vec3 N = normalize(mapNormal); // (transformed into world/tangent space)
vec3 L = normalize(uLightPos - vFragPos);
float diff = max(dot(N, L), 0.0); // lighting now reacts to fake bumps
vec3 base = texture(uTexture, vUV).rgb;
fragColor = vec4(base * diff, 1.0);
}
// the mesh stays flat, but lighting sees bumps -> a flat wall looks like brick.
// (bump maps store height; parallax mapping adds fake depth — same idea.)A production material is not one image but a set: base color (albedo), normal for surface detail, and — for physically-based rendering — metallic, roughness, and ambient occlusion maps. Each map drives a different input of the lighting model per pixel, so a single surface can vary from shiny metal to rough rust. This texture set is the standard interface between artists and the renderer.
A modern (PBR) material = several texture maps, each driving one input:
ALBEDO / BASE COLOR the surface color (no lighting baked in)
NORMAL per-pixel surface direction -> fine bump detail
ROUGHNESS how blurry vs. sharp reflections are (0 smooth, 1 rough)
METALLIC is this pixel metal (1) or non-metal/dielectric (0)?
AMBIENT OCCLUSION baked crevice shadowing
(+ height, emissive, ...)
The shader samples all of them per fragment and feeds them into the PBR
lighting model (next). This map set is the artist -> renderer interface.