Between two vectors, or a scalar and anything, * multiplies component by component. When a matrix meets a vector or matrix it becomes linear algebra: m * v treats v as a column vector, v * m as a row vector, and m1 * m2 is the (non-commutative) matrix product. matrixCompMult(), dot() and cross() cover the rest:
const m = 'mat2 m = mat2(1.0, 2.0, 3.0, 4.0); // columns (1, 2) and (3, 4)\n';
show(m + 'vec2 v = vec2(1.0, 1.0); result = vec4(m * v, v * m);');
show(m + 'result = vec4((m * m)[0], matrixCompMult(m, m)[0]);');
show('vec3 a = vec3(1.0, 2.0, 3.0); result = vec4(a * a, dot(a, a));');
show('result = vec4(cross(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)), 0.0);');Output
4, 6, 3, 7 7, 10, 1, 4 1, 4, 9, 14 0, 0, 1, 0
m * v sums the columns weighted by v; v * m dots v with each column. Vertex shaders later in this chapter compute uProjection * uView * uModel * position, read right to left: the model matrix applies first.
<!doctype html>
<style>
body { margin: 0; font: 12px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; }
.names { display: flex; max-width: 600px; text-align: center; font-family: monospace; }
.names div { flex: 1; padding: 4px 0; }
</style>
<canvas id="view" width="600" height="170"></canvas>
<div class="names"><div>v (original)</div><div>v * vec2(1.4, 0.6)</div><div>m * v</div><div>v * m</div></div>
<script>
const gl = document.getElementById('view').getContext('webgl2');
const vs = `#version 300 es
in vec2 aPos;
uniform int uMode; uniform float uAngle;
void main() {
float c = cos(uAngle), s = sin(uAngle);
mat2 m = mat2(c, s, -s, c); // columns (c, s) and (-s, c): a rotation
vec2 v = aPos;
if (uMode == 1) v = v * vec2(1.4, 0.6); // vector * vector: component by component
if (uMode == 2) v = m * v; // column vector: rotates counter-clockwise
if (uMode == 3) v = v * m; // row vector: same as transpose(m) * v, the other way
gl_Position = vec4(v * 0.8, 0.0, 1.0);
}`;
const fs = `#version 300 es
precision mediump float;
uniform vec3 uColor; out vec4 outColor;
void main() { outColor = vec4(uColor, 1.0); }`;
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) {
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);
// A book cover outline as line segments, with a title bar and an arrow so orientation shows.
const lines = [
-0.5, -0.8, 0.5, -0.8, 0.5, -0.8, 0.5, 0.8, 0.5, 0.8, -0.5, 0.8, -0.5, 0.8, -0.5, -0.8,
-0.38, -0.8, -0.38, 0.8, // spine
-0.25, 0.5, 0.35, 0.5, -0.25, 0.4, 0.2, 0.4, // title lines
0.0, -0.5, 0.0, 0.15, 0.0, 0.15, -0.12, 0.0, 0.0, 0.15, 0.12, 0.0, // up arrow
];
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(lines), gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(program, 'aPos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
const uMode = gl.getUniformLocation(program, 'uMode');
const uAngle = gl.getUniformLocation(program, 'uAngle');
const uColor = gl.getUniformLocation(program, 'uColor');
const colors = [[0.3, 0.3, 0.3], [0.25, 0.49, 0.23], [0.12, 0.37, 0.55], [0.71, 0.27, 0.18]];
function frame(ms) {
gl.viewport(0, 0, 600, 170);
gl.clearColor(0.97, 0.96, 0.93, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform1f(uAngle, 0.6 + 0.4 * Math.sin(ms * 0.001));
for (let mode = 0; mode < 4; mode++) {
gl.viewport(mode * 150 + 5, 0, 140, 170);
gl.uniform1i(uMode, 0); gl.uniform3f(uColor, 0.85, 0.82, 0.76); // faint original
gl.drawArrays(gl.LINES, 0, lines.length / 2);
gl.uniform1i(uMode, mode); gl.uniform3fv(uColor, colors[mode]);
gl.drawArrays(gl.LINES, 0, lines.length / 2);
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>