Texture sampling
Read an image in a shader with texture(), choose how it wraps at the edges, and scroll, tile or flip through it.
How it works
A texture is an image the GPU can read from a shader: a grid of texelsOne element of a texture, the texture's version of a pixel. A sample blends nearby texels or returns the nearest one. Texel One element of a texture, the texture's version of a pixel. A sample blends nearby texels or returns the nearest one.texel size = 1 / textureSize(sampler, 0)
texture()
In GLSL ES 3.00 a texture arrives as a UV coordinates A 2D position on a surface or the screen, usually from 0 to 1 on both axes. Shaders draw shapes and patterns as functions of it.sampler2D uniform, and texture(sampler, coord) reads it. The coordinate is
normalized: 0 to 1 across the whole image on both axes, whatever its size in pixels. That is the same range as a
UVA 2D position on a surface or the screen, usually from 0 to 1 on both axes. Shaders draw shapes and patterns as functions of it.uv = gl_FragCoord.xy / resolution
// at file scope:
uniform sampler2D uImage; // bound to a texture unit by the host code
// inside main():
vec4 color = texture(uImage, uv); // rgba; an 8-bit image gives 0..1 per channel
The result is always a vec4. A single-channel texture returns its value in r, with g and b at 0 and a at 1,
so a mask stored in the red channel is texture(uMask, uv).r. The cards on this site declare one sampler per texture,
named t_<name>.
Texel centers sit at half-texel positions: in an image 256 texels wide, the first column’s center is at
0.5 / 256.0. textureSize(sampler, 0) returns the size in texels as an ivec2, which gives the step from one texel
to the next:
vec2 texel = 1.0 / vec2(textureSize(uImage, 0)); // one texel, in UV units
vec4 right = texture(uImage, uv + vec2(texel.x, 0.0)); // the neighbor to the right
When you want one exact texel with no blending at all, texelFetch(sampler, ivec2(x, y), 0) takes integer texel
coordinates instead, ignores wrapping and filtering, and returns undefined values outside the image. It is the right
tool for lookup tables and data textures.
Which way is up
Texture coordinate (0, 0) is the first row of texels in memory. Images on the web store their top row first, so a
WebGL texture uploaded as-is appears upside down under a UV whose origin is at the bottom left. Either flip on upload
(gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)) or flip in the shader with vec2(uv.x, 1.0 - uv.y). Engines handle
this for you; hand-written WebGL is where it bites.
Wrap modes
A coordinate outside 0–1 is legal. What it reads is decided by the texture’s wrap mode, which is set on the texture or sampler from the host code, not in GLSL:
- Repeat tiles the image: 1.25 reads the same as 0.25.
- Clamp to edge repeats the outermost texels forever: anything past 1 reads the last column.
- Mirrored repeat tiles, flipping every other copy, which hides the seam of an image that doesn’t tile.
Tiling textures use repeat; sprites, screen copies and lookup strips use clamp to edge, or their borders pick up texels from the opposite side when filtered. WebGL2 allows repeat on any texture size, unlike WebGL 1, which required powers of two.
Tiling, offset and scroll
With repeat wrapping, moving and scaling the coordinate is all it takes:
vec2 tiled = uv * 4.0; // four copies across, four up
vec2 offset = tiled + vec2(0.3, 0.0); // shifted by 0.3 of a copy
vec2 scrolled = tiled + vec2(0.1, 0.0) * time; // moves at 0.1 copies per second
vec4 c = texture(uImage, scrolled);
Multiplying the coordinate makes the image smaller on screen, because each copy covers less of the surface. Prefer
the wrap mode over fract() for tiling: fract jumps from 1 back to 0 between copies, and the GPU sees that jump when
it chooses a mipmap level, which shows up as a thin line at every seam (the filtering-and-mipmaps page explains why).
Flipbooks
A flipbook packs the frames of an animation into one texture, in a grid of columns and rows. Each frame shows one cell: divide the UV by the grid size and add the cell’s corner.
const vec2 grid = vec2(4.0, 4.0); // 4 columns, 4 rows: 16 frames
float frame = mod(floor(time * 12.0), 16.0); // 12 frames per second, looping
vec2 cell = vec2(mod(frame, grid.x), grid.y - 1.0 - floor(frame / grid.x));
vec4 c = texture(uImage, (cell + uv) / grid);
The grid.y - 1.0 - puts frame 0 in the top row, where sheets usually start, when the texture’s origin is at the
bottom left. Leave a few texels of padding around each frame in the sheet: filtering and mipmaps read a little past a
cell’s border, and without padding the neighboring frame bleeds in.
Coordinates you compute
The coordinate can be any expression: a scrolled UV, a UV bent by a sine wave, or a value read from another texture.
A read whose coordinate depends on an earlier read is called a dependent read. It is fine in GLSL ES 3.00, though each
one waits for the previous fetch, so long chains cost time. One caution: inside an if or a loop whose condition varies
from pixel to pixel, texture() can return wrong values at the edges of the branch, because the GPU picks the mipmap
level by comparing neighboring pixels and some of them didn’t run the call. Sample before the branch, or use
textureLod with an explicit level there.