Light adds up, so several lights are a loop over an array of structures (Uniform Arrays and Structures), summing each light's diffuse term before multiplying by the surface color:
struct Light { vec3 position, color; };
uniform Light uLights[2];
// in main(), after normalizing the normal:
vec3 light = uAmbient;
for (int i = 0; i < 2; i++) {
vec3 toLight = uLights[i].position - vPosition;
float d = length(toLight), fade = clamp(1.0 - pow(d / 4.0, 4.0), 0.0, 1.0) / (d * d);
light += uLights[i].color * max(dot(normal, toLight / d), 0.0) * fade;
}
outColor = vec4(uColor * light, 1.0);GLH.load() names each member separately, so JavaScript sets uniforms['uLights[1].color'] and so on (Lighting the Covers). A constant bound lets the compiler unroll the loop; a uniform int uCount also works. Every light adds work to every fragment of every object, so this forward shading suits a handful of lights; Deferred Rendering's deferred rendering handles dozens.
<!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) {
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) {
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;
uniform mat4 uViewProjection, uModel; uniform mat3 uNormalMatrix;
out vec3 vPosition, vNormal;
void main() {
vec4 world = uModel * vec4(aPosition, 1.0);
vPosition = world.xyz; vNormal = uNormalMatrix * aNormal;
gl_Position = uViewProjection * world;
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision highp float;
struct Light { vec3 position, color; };
const int LIGHTS = 3;
uniform Light uLights[LIGHTS];
uniform vec3 uColor, uAmbient; uniform int uEmissive;
in vec3 vPosition, vNormal; out vec4 outColor;
void main() {
if (uEmissive >= 0) { outColor = vec4(uLights[uEmissive].color * 0.6 + 0.4, 1.0); return; }
vec3 normal = normalize(vNormal);
vec3 light = uAmbient;
for (int i = 0; i < LIGHTS; i++) { // a constant bound lets the compiler unroll
vec3 toLight = uLights[i].position - vPosition;
float d = length(toLight), fade = clamp(1.0 - pow(d / 4.0, 4.0), 0.0, 1.0) / (d * d);
light += uLights[i].color * max(dot(normal, toLight / d), 0.0) * fade;
}
outColor = vec4(uColor * light, 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);
const colors = [[0.9, 0.88, 0.84], [0.8, 0.78, 0.74], [0.9, 0.88, 0.84], [0.8, 0.78, 0.74], [0.9, 0.88, 0.84], [0.8, 0.78, 0.74]];
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.8, 0.72, 0.62], model: m4.model(0, -0.03, 0, 3.4, 0.06, 0.8) });
items.push({ color: [0.85, 0.83, 0.8], model: m4.model(0, 0.8, -0.45, 4.4, 2.2, 0.05) });
// Each member of each array element is its own uniform: uLights[1].color and so on.
const lightColors = [[1.6, 0.5, 0.3], [0.4, 1.4, 0.5], [0.4, 0.6, 1.8]];
lightColors.forEach((c, i) => gl.uniform3fv(u(`uLights[${i}].color`), c));
const positions = lightColors.map((_, i) => u(`uLights[${i}].position`));
gl.uniform3f(u('uAmbient'), 0.12, 0.12, 0.14);
gl.uniformMatrix4fv(u('uViewProjection'), false, m4.multiply(m4.perspective(38, 3, 0.1, 20), m4.lookAt([1.1, 1.3, 3.4], [0.1, 0.35, 0])));
function frame(ms) {
const t = ms * 0.0006;
const lamps = [0, 1, 2].map(i => [Math.sin(t + i * 2.1) * 1.6, 0.4 + 0.4 * Math.cos(t * 1.3 + i), 0.6]);
lamps.forEach((p, i) => gl.uniform3fv(positions[i], p));
gl.clearColor(0.93, 0.91, 0.87, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.uniform1i(u('uEmissive'), -1);
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);
}
lamps.forEach((p, i) => { // small bulbs where the lights are
gl.uniform1i(u('uEmissive'), i);
gl.uniformMatrix4fv(u('uModel'), false, m4.model(...p, 0.06, 0.06, 0.06));
gl.drawArrays(gl.TRIANGLES, 0, 36);
});
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>