Shader Helper

A Reusable Shader-Loading Helper

The fourth part of webgl-helper.js adds a loader that accepts source strings or <script> ids (a string containing main( is source), performs step 10, and returns the program's attribute locations and one setter per uniform, chosen from the type the linker reports. Three shorthands for the demos follow:

webgl-helper.js, fourth part: loading a program and its variablesJavaScript
// Section 3.4: load a program from source strings or <script> ids and collect its variables
GLH.load = function (gl, vs, fs) {
  const text = (s) => (/\bmain\s*\(/.test(s) ? s : document.getElementById(s).textContent);
  const program = GLH.program(gl, text(vs), text(fs));
  const attributes = {}, uniforms = {}, count = (p) => gl.getProgramParameter(program, p);
  for (let i = 0; i < count(gl.ACTIVE_ATTRIBUTES); i++) {
    const { name } = gl.getActiveAttrib(program, i);
    attributes[name] = gl.getAttribLocation(program, name);
  }
  for (let i = 0; i < count(gl.ACTIVE_UNIFORMS); i++) {
    const { name, type } = gl.getActiveUniform(program, i);
    const location = gl.getUniformLocation(program, name);   // null inside uniform blocks
    if (location) uniforms[name.replace(/\[0\]$/, '')] = GLH.setter(gl, type, location);
  }
  gl.useProgram(program);
  return { program, attributes, uniforms };
};
// the uniform*() method for a type from getActiveUniform(): FLOAT_VEC3 -> uniform3fv, ...
GLH.setter = function (gl, type, location) {
  const W = WebGL2RenderingContext, name = Object.keys(W).find((k) => W[k] === type);
  const n = name.match(/\d(x\d)?$/)?.[0] ?? '1';        // FLOAT_MAT2x3 -> 2x3; samplers -> 1
  if (name.includes('_MAT')) return (m) => gl[`uniformMatrix${n}fv`](location, false, m);
  const t = name.startsWith('FLOAT') ? 'f' : /^UNSIGNED_INT(_VEC|$)/.test(name) ? 'ui' : 'i';
  return (v) => gl[`uniform${n}${t}v`](location, typeof v === 'number' ? [v] : v);
};
// shorthands for the chapter's demos: BookNest's six cover colors and a cleared WebGL 2 canvas
GLH.covers = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];   // books 1-6
GLH.rgb = (hex) => [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255);
GLH.context = function (id, color = [0.93, 0.91, 0.87, 1.0]) {   // cleared to the shelf color
  const gl = document.getElementById(id).getContext('webgl2');
  gl.clearColor(...color); gl.clear(gl.COLOR_BUFFER_BIT); return gl;
};

A page then writes uniforms.uTint([1, 0.9, 0.8]) or uniforms.uModel(matrix) whatever the type. Like every uniform*() call, a setter acts on the current program, so call gl.useProgram() first when you juggle several.

A loader that returns attribute locations and a type-aware setter for every uniformHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  pre { max-width: 600px; margin: 4px 0 0; padding: 6px 8px; background: #fff; border: 1px solid #ddd; font-size: 11px; }
</style>
<canvas id="wall" width="600" height="170"></canvas>
<pre id="out"></pre>
<script>
const GLH = {
  program(gl, vsSource, fsSource) {
    const program = gl.createProgram();
    for (const [type, src] of [[gl.VERTEX_SHADER, vsSource], [gl.FRAGMENT_SHADER, fsSource]]) {
      const s = gl.createShader(type);
      gl.shaderSource(s, src.trim()); gl.compileShader(s);
      if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
      gl.attachShader(program, s);
    }
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
    return program;
  },
};
// Load a program from source strings or <script> ids and collect its variables.
GLH.load = function (gl, vs, fs) {
  const text = (s) => (/\bmain\s*\(/.test(s) ? s : document.getElementById(s).textContent);
  const program = GLH.program(gl, text(vs), text(fs));
  const attributes = {}, uniforms = {}, count = (p) => gl.getProgramParameter(program, p);
  for (let i = 0; i < count(gl.ACTIVE_ATTRIBUTES); i++) {
    const { name } = gl.getActiveAttrib(program, i);
    attributes[name] = gl.getAttribLocation(program, name);
  }
  for (let i = 0; i < count(gl.ACTIVE_UNIFORMS); i++) {
    const { name, type } = gl.getActiveUniform(program, i);
    const location = gl.getUniformLocation(program, name);   // null inside uniform blocks
    if (location) uniforms[name.replace(/\[0\]$/, '')] = GLH.setter(gl, type, location);
  }
  gl.useProgram(program);
  return { program, attributes, uniforms };
};
// The uniform*() method for a type from getActiveUniform(): FLOAT_VEC3 -> uniform3fv, ...
GLH.setter = function (gl, type, location) {
  const W = WebGL2RenderingContext, name = Object.keys(W).find((k) => W[k] === type);
  const n = name.match(/\d(x\d)?$/)?.[0] ?? '1';        // FLOAT_MAT2x3 -> 2x3; samplers -> 1
  if (name.includes('_MAT')) return (m) => gl[`uniformMatrix${n}fv`](location, false, m);
  const t = name.startsWith('FLOAT') ? 'f' : /^UNSIGNED_INT(_VEC|$)/.test(name) ? 'ui' : 'i';
  return (v) => gl[`uniform${n}${t}v`](location, typeof v === 'number' ? [v] : v);
};
GLH.covers = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
GLH.rgb = (hex) => [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255);

// --- a page using the helper: no getUniformLocation() calls anywhere below ---
const gl = document.getElementById('wall').getContext('webgl2');
const { attributes, uniforms } = GLH.load(gl, `#version 300 es
  in vec2 aCorner; uniform float uLift[6]; uniform mat2 uTilt; out vec2 vUV; flat out int vBook;
  void main() {
    vBook = gl_InstanceID; vUV = aCorner;
    vec2 p = uTilt * ((aCorner - 0.5) * vec2(0.26, 1.5)) + vec2(-0.8 + float(vBook) * 0.32, uLift[vBook]);
    gl_Position = vec4(p, 0.0, 1.0);
  }`, `#version 300 es
  precision mediump float;
  in vec2 vUV; flat in int vBook; uniform vec3 uColors[6]; uniform int uSelected; out vec4 outColor;
  void main() {
    vec3 c = uColors[vBook] * mix(0.55, 1.0, smoothstep(0.07, 0.1, vUV.x)) * mix(0.75, 1.2, vUV.y);
    if (vBook == uSelected && any(greaterThan(abs(vUV - 0.5), vec2(0.45)))) c = vec3(1.0, 0.84, 0.3);
    outColor = vec4(c, 1.0);
  }`);
document.getElementById('out').textContent =
  'attributes: ' + JSON.stringify(attributes) + '\nuniform setters: ' + Object.keys(uniforms).join(', ');

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.enableVertexAttribArray(attributes.aCorner);
gl.vertexAttribPointer(attributes.aCorner, 2, gl.FLOAT, false, 0, 0);
uniforms.uColors(GLH.covers.flatMap(GLH.rgb));   // vec3[6]  -> uniform3fv
uniforms.uTilt([1, 0, 0, 1]);                     // mat2     -> uniformMatrix2fv
gl.clearColor(0.93, 0.91, 0.87, 1);
function frame(ms) {
  const t = ms * 0.002;
  uniforms.uLift([0, 1, 2, 3, 4, 5].map(i => 0.05 * Math.sin(t + i)));   // float[6] -> uniform1fv
  uniforms.uSelected(Math.floor(ms / 1000) % 6);                           // int      -> uniform1iv
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, 6);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>