Image Textures

Loading an Image into a WebGL Texture

Images load asynchronously, so upload after decoding: await GLH.image(url) wraps image.decode(), which replaces the old onload callbacks. GLH.texture() then calls createTexture(), bindTexture() and texImage2D(target, level, internalFormat, format, type, source). To draw with it, bind it to a unit and point the sampler there: gl.activeTexture(gl.TEXTURE0), gl.bindTexture(gl.TEXTURE_2D, cover), uniforms.uCover(0).

Images store their top row first, while v = 0 is the bottom, hence UNPACK_FLIP_Y_WEBGL. A cross-origin image taints the texture and WebGL throws a SecurityError, unless the server sends CORS headers and you set image.crossOrigin = 'anonymous' first. WebGL 1 required the internal format to equal the format (RGBA, LUMINANCE); WebGL 2 adds sized formats such as RGBA8, plus texStorage2D() to allocate all levels.

A cover image decoded with image.decode() and uploaded with texImage2D, without and with UNPACK_FLIP_Y_WEBGLHTMLLive
<!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; }
  .names div { flex: 1; padding: 4px 2px; }
</style>
<canvas id="c" width="600" height="300"></canvas>
<div class="names"><div>no flip: the top row lands at v = 0</div><div>pixelStorei(UNPACK_FLIP_Y_WEBGL, true)</div></div>
<script type="module">
// The image: an SVG cover in a data: URL (the page may not fetch files, but data: images are fine).
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="384">
  <rect width="256" height="384" fill="#1f5f8b"/>
  <circle cx="190" cy="110" r="42" fill="#e09a10"/>
  <path d="M0 270 Q64 230 128 262 T256 250 V384 H0Z" fill="#2a9d8f"/>
  <path d="M70 250 h90 l-18 26 h-56z" fill="#b5452f"/><rect x="112" y="190" width="5" height="60" fill="#f2ead8"/>
  <text x="128" y="330" font-family="Georgia" font-size="30" fill="#fff" text-anchor="middle">The Quiet</text>
  <text x="128" y="364" font-family="Georgia" font-size="30" fill="#fff" text-anchor="middle">Harbor</text>
</svg>`;
const image = new Image();
image.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
await image.decode();                     // resolves once the image can be uploaded

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; out vec2 vUV;
void main() { vUV = aPos; gl_Position = vec4(aPos * 2.0 - 1.0, 0.0, 1.0); }`],   // v = 0 at the bottom
  [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec2 vUV; uniform sampler2D uCover; out vec4 c;
void main() { c = texture(uCover, 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 texture(source, flip) {
  const t = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, t);
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flip);        // images store their top row first
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE, source);   // WebGL 2 sized format
  gl.generateMipmap(gl.TEXTURE_2D);
  return t;
}
const textures = [texture(image, false), texture(image, true)];

gl.clearColor(0.97, 0.96, 0.93, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.activeTexture(gl.TEXTURE0);                          // unit 0...
gl.uniform1i(gl.getUniformLocation(program, 'uCover'), 0);   // ...is where the sampler reads
textures.forEach((t, i) => {
  gl.bindTexture(gl.TEXTURE_2D, t);
  gl.viewport(i * 300 + 85, 10, 130, 280);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
});
</script>