uniformMatrix{2|3|4}fv(location, transpose, values) uploads a matrix; WebGL 2 adds non-square forms such as uniformMatrix2x3fv(). The values are column-major: a mat4's first four numbers are its first column, so a translation sits in elements 12 to 14. WebGL 1 requires transpose to be false (true raises INVALID_VALUE); WebGL 2 accepts true and reads rows instead. Transposing a rotation inverts it:
<canvas id="c" width="960" height="240" style="width:480px;height:120px"></canvas>
<script src="webgl-helper.js"></script>
<script>
const gl = GLH.context('c');
const { program, uniforms } = GLH.load(gl, `#version 300 es
uniform mat4 uMatrix; uniform vec2 uOffset; out vec2 vUV;
void main() { // a quad without a buffer
vUV = vec2[6](vec2(0), vec2(1, 0), vec2(1), vec2(0), vec2(1), vec2(0, 1))[gl_VertexID];
vec4 p = uMatrix * vec4((vUV - 0.5) * vec2(0.7, 1.2), 0.0, 1.0);
gl_Position = vec4(p.xy * vec2(0.25, 1.0) + uOffset, 0.0, 1.0); // canvas is 4:1
}`, `#version 300 es
precision mediump float; in vec2 vUV; out vec4 c;
void main() { c = vec4(vec3(0.71, 0.27, 0.18) * (vUV.x < 0.1 ? 0.6 : 1.0), 1.0); }`);
const a = 25 * Math.PI / 180, cos = Math.cos(a), sin = Math.sin(a);
const rotation = [cos, sin, 0, 0, -sin, cos, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; // 4 columns
for (const [transpose, x] of [[false, -0.5], [true, 0.5]]) {
gl.uniformMatrix4fv(gl.getUniformLocation(program, 'uMatrix'), transpose, rotation);
uniforms.uOffset([x, 0.0]);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
window.__done = true;
</script>
The left cover turns 25 degrees counterclockwise, as intended; the transposed upload turns it clockwise. Matrix literals typed row by row are a classic bug.
<!doctype html>
<style>
body { margin: 0; font: 12px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 480px; height: auto; }
.names { display: flex; max-width: 480px; text-align: center; font-family: monospace; font-size: 11px; }
.names div { flex: 1; padding: 4px 0; }
</style>
<canvas id="c" width="960" height="240"></canvas>
<div class="names"><div>uniformMatrix4fv(loc, false, m)</div><div>uniformMatrix4fv(loc, true, m)</div></div>
<script>
const gl = document.getElementById('c').getContext('webgl2');
gl.clearColor(0.93, 0.91, 0.87, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
uniform mat4 uMatrix; uniform vec2 uOffset; out vec2 vUV;
void main() { // a quad without a buffer
vUV = vec2[6](vec2(0), vec2(1, 0), vec2(1), vec2(0), vec2(1), vec2(0, 1))[gl_VertexID];
vec4 p = uMatrix * vec4((vUV - 0.5) * vec2(0.7, 1.2), 0.0, 1.0);
gl_Position = vec4(p.xy * vec2(0.25, 1.0) + uOffset, 0.0, 1.0); // canvas is 4:1
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec2 vUV; out vec4 c;
void main() { c = vec4(vec3(0.71, 0.27, 0.18) * (vUV.x < 0.1 ? 0.6 : 1.0), 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);
gl.bindVertexArray(gl.createVertexArray());
const a = 25 * Math.PI / 180, cos = Math.cos(a), sin = Math.sin(a);
const rotation = [cos, sin, 0, 0, -sin, cos, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; // 4 columns
const uMatrix = gl.getUniformLocation(program, 'uMatrix');
const uOffset = gl.getUniformLocation(program, 'uOffset');
for (const [transpose, x] of [[false, -0.5], [true, 0.5]]) {
gl.uniformMatrix4fv(uMatrix, transpose, rotation); // WebGL 2 accepts transpose = true
gl.uniform2f(uOffset, x, 0.0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
</script>