Integer formats (R8I to RGBA32UI, texture()) are uploaded with a *_INTEGER format and read exactly through isampler2D or usampler2D, whose precision you must declare in a fragment shader. WebGL 2 also separates how a texture is read from the texture: a sampler object bound to a texture unit overrides the texture's own filter and wrap settings, so one texture can be read two ways at once:
const nearest = gl.createSampler();
gl.samplerParameteri(nearest, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.samplerParameteri(nearest, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.bindSampler(0, nearest); // wins over texParameteri() while boundA probe through GLH.evaluate() read a two-texel R8 texture (0, 255) at u = 0.4 and texel 1 of an R16I texture (-7, 300). Through a NEAREST sampler it got 0 and 300; through a LINEAR one, 0.301 (SwiftShader 0.3 2,546 ) and 0, with no error: LINEAR made the integer texture incomplete, and even texelFetch(), which never filters, returned 0. Integer textures need NEAREST, on the texture or on whatever sampler is bound.
<!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: 2px; }
</style>
<canvas id="c" width="600" height="180"></canvas>
<div class="names"><div>RGBA8 via NEAREST sampler</div><div>same texture, LINEAR sampler</div><div>R8UI, usampler2D, NEAREST</div><div>R8UI with a LINEAR sampler: 0</div></div>
<script>
const gl = document.getElementById('c').getContext('webgl2');
function program(fs) {
const p = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
out vec2 vUV; void main() { vUV = vec2(gl_VertexID & 1, gl_VertexID >> 1) * 2.0; gl_Position = vec4(vUV * 2.0 - 1.0, 0.0, 1.0); }`], [gl.FRAGMENT_SHADER, fs]]) {
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(p, s);
}
gl.linkProgram(p);
return p;
}
const floatView = program(`#version 300 es
precision mediump float; in vec2 vUV; uniform sampler2D uTex; out vec4 c; void main() { c = texture(uTex, vec2(vUV.x, 1.0 - vUV.y)); }`);
const intView = program(`#version 300 es
precision mediump float; precision mediump usampler2D; // integer samplers need a precision
in vec2 vUV; uniform usampler2D uTex; out vec4 c;
void main() {
uint rating = texelFetch(uTex, ivec2(vUV * vec2(6.0, 1.0)), 0).r; // exact integers, never filtered
float bar = step(vUV.y * 50.0, float(rating));
c = vec4(mix(vec3(0.93, 0.9, 0.84), vec3(0.88, 0.6, 0.06), bar), 1.0);
}`);
gl.bindVertexArray(gl.createVertexArray());
// An 8x8 pixel-art book, as RGBA8.
const art = ['........', '.bBBBBB.', '.bBGGGB.', '.bBBBBB.', '.bBGGBB.', '.bBBBBB.', '.bBBBBB.', '........'];
const colors = { '.': [247, 244, 238, 255], B: [31, 95, 139, 255], b: [18, 60, 90, 255], G: [224, 154, 16, 255] };
const bookTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, bookTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 8, 8, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(art.flatMap(r => [...r].flatMap(ch => colors[ch]))));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// Six ratings in tenths, as R8UI (uploaded with RED_INTEGER).
const ratings = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, ratings);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8UI, 6, 1, 0, gl.RED_INTEGER, gl.UNSIGNED_BYTE, new Uint8Array([46, 43, 48, 41, 45, 44]));
// Sampler objects: bound to a unit, they override the texture's own filter settings.
function sampler(filter) {
const s = gl.createSampler();
gl.samplerParameteri(s, gl.TEXTURE_MIN_FILTER, filter);
gl.samplerParameteri(s, gl.TEXTURE_MAG_FILTER, filter);
return s;
}
const nearest = sampler(gl.NEAREST), linear = sampler(gl.LINEAR);
gl.clearColor(0.97, 0.96, 0.93, 1); gl.clear(gl.COLOR_BUFFER_BIT);
gl.activeTexture(gl.TEXTURE0);
[[floatView, bookTexture, nearest], [floatView, bookTexture, linear], [intView, ratings, nearest], [intView, ratings, linear]]
.forEach(([p, texture, s], i) => {
gl.useProgram(p);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.bindSampler(0, s); // wins over texParameteri() while bound
gl.viewport(i * 150 + 10, 5, 130, 170);
gl.drawArrays(gl.TRIANGLES, 0, 3);
});
gl.bindSampler(0, null);
</script>