Integer Textures and Samplers

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:

A sampler object overriding a texture's filtering on unit 0JavaScript
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 bound

A 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.

One texture read through NEAREST and LINEAR sampler objects at once, and an integer texture complete only with NEARESTHTMLLive
<!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>