Core Idea
A texture is not decoration — it’s an address system. Every vertex of a mesh carries a (u, v) coordinate: a 2D address into a square image whose corners are (0, 0) and (1, 1), whatever the image’s resolution. Paint the square above and watch your stroke arrive on the mesh: you never touched the 3D surface, you only wrote to addresses it already knew to look up.
Assigning those addresses is UV unwrapping — peeling the surface flat like an orange skin. Where the peel is cut, a seam appears (the terracotta line above: u = 0 and u = 1 are the same line on the mesh). Built-in primitives ship with UVs; custom geometry gets them in a DCC tool or via geometry.attributes.uv — they’re mesh data, like positions and normals.
The Fetch
A fragment’s (u, v) arrives barycentrically interpolated from its triangle’s vertices, and almost never lands exactly on a texel center. The sampler must decide:
Nearest rounds to one texel — cheap and blocky, right for pixel art and data lookups. Bilinear blends the four surrounding texels by proximity — lerp along u, then lerp along v, interpolation wearing its texture hat. That’s magnification; for minification the GPU keeps mipmaps — the texture pre-halved down to 1×1 (1024, 512, 256, …) — and picks the level whose texels roughly match pixel size, killing the Moiré shimmer of distant detail. In Three.js these are the magFilter / minFilter knobs, defaulting to bilinear and trilinear (blending mip levels too).
More Than Color
The looked-up value doesn’t have to be a color — a texture is per-pixel data delivery for any input shading wants:
| Map | Delivers | Effect |
|---|---|---|
albedo (map) |
base color | the picture |
| normal map | a replacement normal per pixel (RGB = XYZ) | fake dents and grooves that react to light |
| roughness / metalness | scatter and conductivity per pixel | varnish vs raw wood, metal vs clay |
| ambient occlusion | pre-baked crevice shadow | contact darkness for free |
| displacement | an actual vertex offset | real bumps — the one that moves geometry |
This is the pipeline’s central bargain: visual complexity decoupled from polygon count. A thousand-triangle wall reads as brick and mortar because the maps fake the interior detail — and the illusion has exactly one failure point, the silhouette, which stays polygon-straight at grazing angles. Spend triangles on outlines, textures on everything else (the economy that level-of-detail scales with distance). A MatCap pushes the trick to its limit: an image of a pre-lit sphere, sampled by normal direction — entire lighting baked into one lookup.
One Honest Gotcha: Color Spaces
Color textures (albedo, emissive) are stored gamma-encoded (sRGB) because screens are; data textures (normal, roughness) are linear numbers. Tag them correctly — texture.colorSpace = THREE.SRGBColorSpace for color, leave data linear — or renders come out washed-out, and an sRGB-tagged normal map is silently corrupted vector data.
Connections
The (u, v) each fragment receives is interpolation’s barycentric mix, computed during rasterization; the fetched values feed every term of the shading recipe, with normal maps delivering the per-pixel normals that make flat surfaces catch light like carved ones. UVs live in the mesh alongside positions, and the sampler that answers texture(map, vUv) in GLSL is configured through Three.js material and texture properties.