Wrapping and Filtering

Wrapping and Filtering Texture Parameters

texParameteri(target, name, value) sets how a texture is read outside 0..1 and between texels:

Texture wrapping and filtering parameters
Parameter Values Default
TEXTURE_WRAP_S, TEXTURE_WRAP_T REPEAT, MIRRORED_REPEAT, CLAMP_TO_EDGE REPEAT
TEXTURE_MAG_FILTER (enlarging) NEAREST, LINEAR LINEAR
TEXTURE_MIN_FILTER (shrinking) the two above, or {NEAREST,LINEAR}_MIPMAP_{NEAREST,LINEAR} NEAREST_MIPMAP_LINEAR

NEAREST takes the closest texel (pixel art); LINEAR averages the nearest four. The minification default needs mipmaps: without them the texture is incomplete and samples black, the first suspect for a black texture. WebGL 1 also forbids mipmaps and REPEAT on non-power-of-two sizes (NPOT Textures): a 3-wide REPEAT texture sampled black here, a 4-wide one gray. WebGL 2 lifts the rule and adds sampler objects (createSampler()).

An 8x8 pixel-art book texture under three wrap modes, two magnification filters, and an incomplete textureHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  .names { display: flex; max-width: 600px; text-align: center; font-family: monospace; font-size: 10px; }
  .names div { flex: 1; padding: 4px 1px; }
</style>
<canvas id="c" width="600" height="110"></canvas>
<div class="names"><div>REPEAT</div><div>MIRRORED_REPEAT</div><div>CLAMP_TO_EDGE</div><div>MAG NEAREST</div><div>MAG LINEAR</div><div>no mipmaps, default MIN</div></div>
<script>
// An 8x8 RGBA texture drawn by hand: a little blue book with a gold title band.
const art = [
  '........', '.BBBBBB.', '.bBBBBB.', '.bGGGGG.', '.bBBBBB.', '.bBBBBB.', '.bBBBBB.', '........'];
const colors = { '.': [247, 244, 238], B: [31, 95, 139], b: [18, 60, 90], G: [224, 154, 16] };
const pixels = new Uint8Array(art.flatMap(row => [...row].flatMap(ch => [...colors[ch], 255])));

const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec2 aPos; uniform float uScale; out vec2 vUV;
void main() { vUV = (aPos - 0.5) * uScale + 0.5; gl_Position = vec4(aPos * 2.0 - 1.0, 0.0, 1.0); }`],
  [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec2 vUV; uniform sampler2D uArt; out vec4 c;
void main() { c = texture(uArt, vUV); }`]]) {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
  gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0);

function makeTexture(params) {
  const t = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, t);
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 8, 8, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
  for (const [name, value] of Object.entries(params)) gl.texParameteri(gl.TEXTURE_2D, gl[name], gl[value]);
  return t;
}
const nearest = { TEXTURE_MIN_FILTER: 'NEAREST', TEXTURE_MAG_FILTER: 'NEAREST' };
const panels = [
  [3, { ...nearest, TEXTURE_WRAP_S: 'REPEAT', TEXTURE_WRAP_T: 'REPEAT' }],          // uv from -1 to 2
  [3, { ...nearest, TEXTURE_WRAP_S: 'MIRRORED_REPEAT', TEXTURE_WRAP_T: 'MIRRORED_REPEAT' }],
  [3, { ...nearest, TEXTURE_WRAP_S: 'CLAMP_TO_EDGE', TEXTURE_WRAP_T: 'CLAMP_TO_EDGE' }],
  [0.6, { ...nearest }],                                                             // enlarged: blocky
  [0.6, { TEXTURE_MIN_FILTER: 'LINEAR', TEXTURE_MAG_FILTER: 'LINEAR' }],              // enlarged: smooth
  [3, {}],     // default MIN filter NEAREST_MIPMAP_LINEAR with no mipmaps: incomplete, samples black
];
gl.clearColor(0.97, 0.96, 0.93, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
panels.forEach(([scale, params], i) => {
  gl.bindTexture(gl.TEXTURE_2D, makeTexture(params));
  gl.uniform1f(gl.getUniformLocation(program, 'uScale'), scale);
  gl.viewport(i * 100 + 5, 5, 90, 100);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
});
</script>