texParameteri(target, name, value) sets how a texture is read outside 0..1 and between texels:
| 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()).
<!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>