GLSL ES 3.00 replaces attribute and varying with direction: in is what a stage receives, out what it produces. layout(location = 0) in pins an attribute to a slot, so JavaScript can skip getAttribLocation(); vertex out variables match fragment in variables by name and type. Interpolation qualifiers choose how values cross the triangle: smooth (the default), flat (one vertex's value, mandatory for integers) or centroid.
An interface block groups declarations under one name. GLSL ES 3.00 allows only uniform blocks, filled from a buffer in one call (Light UBOs); in and out blocks need GLSL ES 3.10, which WebGL lacks:
const check = (source) => {
try { GLH.shader(gl, gl.FRAGMENT_SHADER, source); console.log('compiles'); }
catch (e) { console.log(e.message.split('\n')[1]); }
};
const head = '#version 300 es\nprecision mediump float;\nout vec4 outColor;\n';
check(head + 'uniform Light { vec3 color; float power; } light;\n' +
'void main() { outColor = vec4(light.color * light.power, 1.0); }');
check(head + 'in Varyings { vec2 uv; } v;\nvoid main() { outColor = vec4(v.uv, 0.0, 1.0); }');
check(head + 'in int bookId;\nvoid main() { outColor = vec4(float(bookId)); }');
check(head + 'flat in int bookId;\nvoid main() { outColor = vec4(float(bookId)); }');compiles ERROR: 0:4: 'in' : ... interface blocks must be uniform in version lower than GLSL ES 3.10 ERROR: 0:4: 'in' : must use 'flat' interpolation here compiles
A flat in int bookId is how Rotation and Picking passes a per-book ID to the fragment shader for picking.
<!doctype html>
<style>
body { margin: 0; font: 12px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; }
pre { max-width: 600px; margin: 6px 0 0; padding: 6px 8px; background: #fff; border: 1px solid #ddd;
font-size: 11px; white-space: pre-wrap; }
</style>
<canvas id="view" width="600" height="220"></canvas>
<pre id="out"></pre>
<script>
const gl = document.getElementById('view').getContext('webgl2');
function shader(type, src) {
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));
return s;
}
const program = gl.createProgram();
gl.attachShader(program, shader(gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec2 aPos; // pinned slots: no getAttribLocation() needed
layout(location = 1) in float aBook;
flat out int vBookId; // integers must be flat
smooth out float vShade; // smooth is the default
void main() {
vBookId = int(aBook);
vShade = aBook;
gl_Position = vec4(aPos, 0.0, 1.0);
}`));
gl.attachShader(program, shader(gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float;
flat in int vBookId;
smooth in float vShade;
uniform Light { vec3 color; float power; } light; // a uniform interface block
uniform bool uFlat;
out vec4 outColor;
const vec3 palette[6] = vec3[](vec3(0.12,0.37,0.55), vec3(0.36,0.25,0.6), vec3(0.88,0.6,0.06),
vec3(0.25,0.49,0.23), vec3(0.71,0.27,0.18), vec3(0.16,0.62,0.56));
void main() {
vec3 base = uFlat ? palette[vBookId] : vec3(vShade / 5.0, 0.4, 1.0 - vShade / 5.0);
outColor = vec4(base * light.color * light.power, 1.0);
}`));
gl.linkProgram(program);
gl.useProgram(program);
// Six books. Top vertices carry the book number b, bottom vertices b + 1.
// flat takes the value of each triangle's last (provoking) vertex, always a top one here;
// smooth blends from b + 1 at the bottom to b at the top.
const data = [];
for (let b = 0; b < 6; b++) {
const x0 = -0.95 + b * 0.32, x1 = x0 + 0.28;
data.push(x0, -1, b + 1, x1, -1, b + 1, x1, 1, b, // last vertex: top right
x0, -1, b + 1, x1, 1, b, x0, 1, b); // last vertex: top left
}
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 12, 0);
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 1, gl.FLOAT, false, 12, 8);
// Fill the Light block from a buffer: vec3 + float pack into one 16-byte row.
const ubo = gl.createBuffer();
gl.bindBuffer(gl.UNIFORM_BUFFER, ubo);
gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array([1.0, 0.95, 0.85, 1.1]), gl.STATIC_DRAW);
gl.uniformBlockBinding(program, gl.getUniformBlockIndex(program, 'Light'), 0);
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, ubo);
gl.clearColor(0.97, 0.96, 0.93, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
const uFlat = gl.getUniformLocation(program, 'uFlat');
gl.viewport(0, 115, 600, 100); gl.uniform1i(uFlat, 1); gl.drawArrays(gl.TRIANGLES, 0, 36); // flat
gl.viewport(0, 5, 600, 100); gl.uniform1i(uFlat, 0); gl.drawArrays(gl.TRIANGLES, 0, 36); // smooth
// The compiler's verdict on the page's four rules.
const head = '#version 300 es\nprecision mediump float;\nout vec4 outColor;\n';
const checks = [
['uniform block', 'uniform Light { vec3 color; float power; } light;\nvoid main() { outColor = vec4(light.color * light.power, 1.0); }'],
['in block (needs 3.10)', 'in Varyings { vec2 uv; } v;\nvoid main() { outColor = vec4(v.uv, 0.0, 1.0); }'],
['smooth int input', 'in int bookId;\nvoid main() { outColor = vec4(float(bookId)); }'],
['flat int input', 'flat in int bookId;\nvoid main() { outColor = vec4(float(bookId)); }'],
];
document.getElementById('out').textContent = 'top: flat in int bottom: smooth in float\n' +
checks.map(([name, body]) => {
try { shader(gl.FRAGMENT_SHADER, head + body); return name.padEnd(24) + 'compiles'; }
catch (e) { return name.padEnd(24) + e.message.split('\n')[0]; }
}).join('\n');
</script>