Once a scene has depth, drawing order matters: nearer surfaces must hide farther ones, faces turned away can be skipped, and translucent surfaces must mix with what lies behind them. The depth test, face culling and blending handle this after the fragment shader. To show them, the covers become books with a thickness; the twelfth part of webgl-helper.js builds boxes with normals, the shelf as seven boxes, side-by-side panes and an unlit program:
// Section 3.8: a w x h x d box around the origin; vertices are x, y, z, nx, ny, nz, u, v, and
// every face winds counterclockwise seen from outside
GLH.box = function (w = 1, h = 1, d = 1) {
const vertices = [], indices = [], half = [w / 2, h / 2, d / 2];
for (let face = 0; face < 6; face++) { // +x, -x, +y, -y, +z, -z
const a = face >> 1, sign = face & 1 ? -1 : 1; // the axis the face looks along
const [b, c] = sign > 0 ? [(a + 1) % 3, (a + 2) % 3] : [(a + 2) % 3, (a + 1) % 3];
for (const [s, t] of [[-1, -1], [1, -1], [1, 1], [-1, 1]]) {
const p = [0, 0, 0], n = [0, 0, 0];
p[a] = sign; p[b] = s; p[c] = t; n[a] = sign;
vertices.push(...p.map((k, i) => k * half[i]), ...n, (s + 1) / 2, (t + 1) / 2);
}
indices.push(face * 4, face * 4 + 1, face * 4 + 2, face * 4, face * 4 + 2, face * 4 + 3);
}
return { vertices: new Float32Array(vertices), indices: new Uint16Array(indices) };
};
// Section 3.8: BookNest's shelf as seven boxes (six books, then the plank), using gl-matrix;
// draw() sets uModel, uNormalMatrix, uColor and uAlpha for each box, if the program has them
GLH.shelf = function (gl, attributes) {
const { mat4, mat3, glMatrix: { toRadian } } = glMatrix, box = GLH.box();
const layout = [['aPosition', 3], ['aNormal', 3], ['aUV', 2]];
const vao = GLH.vao(gl, attributes, box.vertices, layout, box.indices);
const items = [...GLH.covers.map((hex, i) =>
[hex, 1.6 * (-0.81 + 0.32 * i), 0, 30 - 12 * i, [0.42, 0.98, 0.12]]),
['#8a6240', 0, -0.52, 0, [3.5, 0.06, 0.8]]].map(([hex, x, y, turn, size]) => {
const model = mat4.fromTranslation(mat4.create(), [x, y, 0]);
mat4.scale(model, mat4.rotateY(model, model, toRadian(turn)), size);
return { color: GLH.rgb(hex), model, normal: mat3.normalFromMat4(mat3.create(), model) };
});
const camera = (aspect, eye = [0.9, 0.9, 2.9]) => mat4.multiply(mat4.create(),
mat4.perspective(mat4.create(), toRadian(40), aspect, 0.1, 20),
mat4.lookAt(mat4.create(), eye, [0.1, -0.2, 0], [0, 1, 0]));
const draw = (uniforms, list = items) => {
gl.bindVertexArray(vao);
for (const { color, model, normal, alpha = 1 } of list) {
uniforms.uModel?.(model); uniforms.uNormalMatrix?.(normal); uniforms.uColor?.(color);
uniforms.uAlpha?.(alpha);
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
}
};
return { items, camera, draw };
};
// Section 3.8: draw(i, aspect) for n side-by-side panes, each with its own viewport and clear
GLH.panes = function (gl, n, draw) {
const w = gl.canvas.width / n, h = gl.canvas.height;
gl.enable(gl.SCISSOR_TEST);
for (let i = 0; i < n; i++) {
gl.viewport(i * w, 0, w, h); gl.scissor(i * w, 0, w, h);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); draw(i, w / h);
}
gl.disable(gl.SCISSOR_TEST);
};
// Section 3.8: the figures' unlit program: a fixed tint per face until Section 3.9 adds light
GLH.tint = (gl) => GLH.load(gl, `#version 300 es
in vec3 aPosition; in vec3 aNormal; uniform mat4 uViewProjection, uModel; out float vShade;
void main() { vShade = 0.75 + 0.25 * aNormal.z + 0.1 * aNormal.y;
gl_Position = uViewProjection * uModel * vec4(aPosition, 1.0); }`,
`#version 300 es
precision mediump float; uniform vec3 uColor; uniform float uAlpha; in float vShade;
out vec4 c; void main() { c = vec4(uColor * vShade, uAlpha); }`);