getAttribLocation(program, name) returns an attribute's slot, from 0 to MAX_VERTEX_ATTRIBS - 1 (16 here), or -1. getUniformLocation(program, name) returns an opaque WebGLUniformLocation or null, never -1, so the common check if (location < 0) never fires for a uniform. Both miss variables that are declared but unused, because the linker drops them. getActiveUniform() and getActiveAttrib() list what survived:
const program = GLH.program(gl, `#version 300 es
in vec2 aPosition; in vec3 aUnused; uniform float uNotUsed;
void main() { gl_Position = vec4(aPosition, 0.0, 1.0); }`, `#version 300 es
precision mediump float; out vec4 outColor; struct Light { vec3 color; float power; };
uniform Light uLight; uniform vec3 uColors[6];
void main() { outColor = vec4(uLight.color * uLight.power + uColors[5], 1.0); }`);
for (let i = 0; i < gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); i++) {
const { name, type, size } = gl.getActiveUniform(program, i);
console.log(name, type === gl.FLOAT_VEC3 ? 'vec3' : type, 'x' + size);
}
console.log(gl.getAttribLocation(program, 'aUnused'),
gl.getUniformLocation(program, 'uNotUsed'), gl.getUniformLocation(program, 'uColor'));Output
uColors[0] vec3 x6 uLight.color vec3 x1 uLight.power 5126 x1 -1 null null
Types come back as enums (5126 is FLOAT); a structure lists one uniform per field. To fix an attribute's slot, write layout(location = 1) in vec3 aColor; or call bindAttribLocation() before linking. Look locations up once, never in the render loop.
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
.row { display: flex; gap: 10px; max-width: 600px; align-items: flex-start; }
canvas { width: 300px; height: 200px; flex: none; }
table { border-collapse: collapse; flex: 1; font-family: monospace; }
td, th { padding: 3px 5px; border-bottom: 1px solid #e3ddd0; text-align: left; }
.miss td { color: #b5452f; }
</style>
<div class="row">
<canvas id="view" width="300" height="200"></canvas>
<table id="list"></table>
</div>
<script>
const gl = document.getElementById('view').getContext('webgl2');
function program(vsSource, fsSource) {
const p = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, vsSource], [gl.FRAGMENT_SHADER, fsSource]]) {
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 prog = program(`#version 300 es
in vec2 aPosition; in vec3 aUnused; uniform float uNotUsed; // declared, never used
out vec2 vUV;
void main() { vUV = aPosition * 0.5 + 0.5; gl_Position = vec4(aPosition, 0.0, 1.0); }`, `#version 300 es
precision mediump float; out vec4 outColor; in vec2 vUV;
struct Light { vec3 color; float power; };
uniform Light uLight; uniform vec3 uColors[6];
void main() {
int book = int(vUV.x * 6.0);
float inCover = step(0.12, fract(vUV.x * 6.0)) * step(0.1, vUV.y) * step(vUV.y, 0.9);
vec3 cover = uColors[book] * uLight.color * uLight.power * (0.7 + 0.5 * vUV.y);
outColor = vec4(mix(vec3(0.93, 0.91, 0.87), cover, inCover), 1.0);
}`);
gl.useProgram(prog);
// What survived linking: a struct lists one uniform per field; an array reports its size.
const typeNames = { [gl.FLOAT]: 'float', [gl.FLOAT_VEC3]: 'vec3' };
let rows = '<tr><th>active uniform</th><th>type</th><th>size</th></tr>';
for (let i = 0; i < gl.getProgramParameter(prog, gl.ACTIVE_UNIFORMS); i++) {
const { name, type, size } = gl.getActiveUniform(prog, i);
rows += `<tr><td>${name}</td><td>${typeNames[type] || type}</td><td>${size}</td></tr>`;
}
rows += `<tr class="miss"><td>getAttribLocation('aUnused')</td><td colspan="2">${gl.getAttribLocation(prog, 'aUnused')}</td></tr>`;
rows += `<tr class="miss"><td>getUniformLocation('uNotUsed')</td><td colspan="2">${gl.getUniformLocation(prog, 'uNotUsed')}</td></tr>`;
rows += `<tr class="miss"><td>getUniformLocation('uColor')</td><td colspan="2">${gl.getUniformLocation(prog, 'uColor')} (typo)</td></tr>`;
document.getElementById('list').innerHTML = rows;
// Look locations up once, outside any render loop.
const loc = {
aPosition: gl.getAttribLocation(prog, 'aPosition'),
lightColor: gl.getUniformLocation(prog, 'uLight.color'), // struct fields by full name
lightPower: gl.getUniformLocation(prog, 'uLight.power'),
colors: gl.getUniformLocation(prog, 'uColors'), // same as 'uColors[0]'
};
gl.uniform3f(loc.lightColor, 1.0, 0.95, 0.85);
gl.uniform1f(loc.lightPower, 1.15);
gl.uniform3fv(loc.colors, [0.12,0.37,0.55, 0.36,0.25,0.6, 0.88,0.6,0.06, 0.25,0.49,0.23, 0.71,0.27,0.18, 0.16,0.62,0.56]);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, 1,1, -1,-1, 1,1, -1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(loc.aPosition);
gl.vertexAttribPointer(loc.aPosition, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
</script>