A light far away, such as the sun, sends parallel rays: a directional light is just a unit vector toward the light and a color. The cheapest place to evaluate it is the vertex shader, letting the rasterizer interpolate the color. Normals go to world space through the normal matrix (Normal Matrices for Lighting) and are renormalized:
#version 300 es
in vec3 aPosition; in vec3 aNormal; out vec3 vColor;
uniform mat4 uViewProjection, uModel; uniform mat3 uNormalMatrix;
uniform vec3 uColor, uToLight, uLightColor, uAmbient;
void main() {
vec3 normal = normalize(uNormalMatrix * aNormal); // world space, unit length
float diffuse = max(dot(normal, uToLight), 0.0); // Lambert's cosine law
vColor = uColor * (uAmbient + uLightColor * diffuse);
gl_Position = uViewProjection * uModel * vec4(aPosition, 1.0);
}Pass it as GLH.lit()'s third argument with a fragment shader that outputs vColor, then set uToLight to vec3.normalize(vec3.create(), [-0.5, 0.8, 0.6]) (up, left and in front), a warm uLightColor such as (1, 0.95, 0.85) and a cool uAmbient such as (0.25, 0.27, 0.32). Rendered on the shelf (tried for this section), the books on the right, turned left toward the light, came out brightest and faces turned away kept only the ambient fill. For flat box faces per-vertex lighting is exact; on curved surfaces it is not (Vertex vs Fragment Shading). uToLight points toward the light, the opposite of the direction the light travels.
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; }
canvas { display: block; width: 100%; max-width: 600px; }
</style>
<canvas id="c" width="1200" height="400"></canvas>
<script>
const m4 = {
perspective(fovy, aspect, near, far) {
const f = 1 / Math.tan(fovy * Math.PI / 360), d = near - far;
return [f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) / d, -1, 0, 0, 2 * far * near / d, 0];
},
lookAt(eye, target, up = [0, 1, 0]) {
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
const unit = (v) => v.map((c) => c / Math.hypot(...v));
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const z = unit(eye.map((c, i) => c - target[i])), x = unit(cross(up, z)), y = cross(z, x);
return [x[0], y[0], z[0], 0, x[1], y[1], z[1], 0, x[2], y[2], z[2], 0, -dot(x, eye), -dot(y, eye), -dot(z, eye), 1];
},
multiply(a, b) {
const out = new Array(16).fill(0);
for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) for (let k = 0; k < 4; k++) out[c * 4 + r] += a[k * 4 + r] * b[c * 4 + k];
return out;
},
model(x, y, z, sx, sy, sz, turn = 0) { // T * Ry(turn) * S
const c = Math.cos(turn), s = Math.sin(turn);
return [c * sx, 0, -s * sx, 0, 0, sy, 0, 0, s * sz, 0, c * sz, 0, x, y, z, 1];
},
normal(m) { // inverse transpose of the upper-left 3x3
const [a, b, c, , d, e, f, , g, h, i] = m;
const n = [e * i - f * h, f * g - d * i, d * h - e * g, c * h - b * i, a * i - c * g,
b * g - a * h, b * f - c * e, c * d - a * f, a * e - b * d];
const det = a * n[0] + b * n[1] + c * n[2];
return n.map((v) => v / det);
},
};
function cube() {
const faces = [[[0,0,1],[1,0,0],[0,1,0]], [[0,0,-1],[-1,0,0],[0,1,0]], [[1,0,0],[0,0,-1],[0,1,0]],
[[-1,0,0],[0,0,1],[0,1,0]], [[0,1,0],[1,0,0],[0,0,-1]], [[0,-1,0],[1,0,0],[0,0,1]]];
const out = [];
for (const [n, u, v] of faces) for (const [a, b] of [[-1,-1],[1,-1],[1,1],[-1,-1],[1,1],[-1,1]])
out.push(...[0, 1, 2].map(i => (n[i] + a * u[i] + b * v[i]) / 2), ...n);
return new Float32Array(out);
}
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 vec3 aPosition; layout(location = 1) in vec3 aNormal; out vec3 vColor;
uniform mat4 uViewProjection, uModel; uniform mat3 uNormalMatrix;
uniform vec3 uColor, uToLight, uLightColor, uAmbient;
void main() {
vec3 normal = normalize(uNormalMatrix * aNormal); // world space, unit length
float diffuse = max(dot(normal, uToLight), 0.0); // Lambert's cosine law
vColor = uColor * (uAmbient + uLightColor * diffuse); // lit once per vertex
gl_Position = uViewProjection * uModel * vec4(aPosition, 1.0);
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec3 vColor; out vec4 c; void main() { c = vec4(vColor, 1.0); }`]]) {
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);
const u = (n) => gl.getUniformLocation(program, n);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, cube(), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0); gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12); gl.enableVertexAttribArray(1);
gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE);
// The shelf: books fanned from facing right to facing left, on a plank.
const 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]];
const items = colors.map((color, i) => ({ color,
model: m4.model(-1.3 + i * 0.52, 0.5, 0, 0.42, 0.98, 0.12, (30 - i * 12) * Math.PI / 180) }));
items.push({ color: [0.54, 0.38, 0.25], model: m4.model(0, -0.03, 0, 3.4, 0.06, 0.7) });
gl.uniform3f(u('uLightColor'), 1, 0.95, 0.85); // warm light
gl.uniform3f(u('uAmbient'), 0.25, 0.27, 0.32); // cool fill
gl.uniformMatrix4fv(u('uViewProjection'), false, m4.multiply(m4.perspective(38, 1200 / 400, 0.1, 20), m4.lookAt([1.1, 1.3, 3.4], [0.1, 0.35, 0])));
function frame(ms) {
const a = -0.5 + 0.6 * Math.sin(ms * 0.0006); // swing between left and right
const L = [a, 0.8, 0.6], len = Math.hypot(...L);
gl.uniform3fv(u('uToLight'), L.map(c => c / len)); // toward the light, unit length
gl.clearColor(0.93, 0.91, 0.87, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
for (const { color, model } of items) {
gl.uniformMatrix4fv(u('uModel'), false, model);
gl.uniformMatrix3fv(u('uNormalMatrix'), false, m4.normal(model));
gl.uniform3fv(u('uColor'), color);
gl.drawArrays(gl.TRIANGLES, 0, 36);
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>