Transformation

You may need to read the previous articles about attributes and uniforms in order to understand this one.

A transformation is a way to change something. There are three basic transformations: translation, rotation, and scaling.

Translation

A translation is a movement. It is performed by adding a vector value to each vertex. For an initial vector (x0,y0,z0)(x_0,y_0,z_0) and a translation vector (xt,yt,zt)(x_t,y_t,z_t), the resulting vector is (x0+xt,y0+yt,z0+zt)(x_0+x_t,y_0+y_t,z_0+z_t).

In a vertex shader, a translation could be implemented with a vector uniform.

#version 300 es

in vec4 a_position;

uniform vec4 u_translation;

void main() {
	gl_Position = a_position + u_translation;
}

Rotation

A rotation is performed by multiplying each vertex by a point on the unit circle. The unit circle is the circle with a radius of 11. As such, it can be thought of as a rotating 11, in that multiplying a vertex by a point on the unit circle will not change its value (shape) except by rotating it.

A point on the unit circle can be calculated with the sine and cosine functions for the xx and yy values, respectively. For an initial vector (x0,y0)(x_0,y_0) and a rotation value of rr radians, the resulting vector is (xsin(r)+ycos(r),ysin(r)xcos(r))(x\sin(r)+y\cos(r),y\sin(r)-x\cos(r)). Different algorithms are used for different axes in higher dimensions (more details below).

In a vertex shader, a rotation could be implemented with a floating-point uniform.

#version 3000 es

in vec4 a_position;

uniform float u_rotation;

void main() {
	float x = a_position.x * sin(u_rotation)
		+ a_position.y * cos(u_rotation);
	
	float y = a_position.y * sin(u_rotation)
		+ a_position.x * sin(u_rotation);
	
	gl_Position = vec4(x, y, a_position.zw);
}

Scaling

A scaling is performed by multiplying each vertex by a vector. For an initial vector (x0,y0,z0)(x_0,y_0,z_0) and a scaling vector (xs,ys,zs)(x_s,y_s,z_s), the resulting vector is (x0xs,y0ys,z0zs)(x_0x_s,y_0y_s,z_0z_s).

In a vertex shader, a scaling could be implemented with a vector uniform.

#version 300

in vec4 a_position;

uniform vec4 u_scaling;

void main() {
	gl_Position = a_position * u_scaling;
}

Matrices

There are various problems with using individual uniforms for translation, rotation, and scaling as described above. It can be tedious to use three different uniforms to describe the transformation of a vertex. Additionally, since the transformation logic is written into the vertex shader, a completely different shader program is required in order to apply the transformations in a different order.

A better way to pass transformations to the vertex shader is with matrices. Matrices are rectangular arrays of numbers arranged in rows and columns and manipulated according to particular rules.

Matrices are column-major, which means that a matrix defined in JavaScript looks "sideways."

const matrix = [
	1, 2, 3, // Column 1
	4, 5, 6, // Column 2
	7, 8, 9 // Column 3
];

The JavaScript "matrix" above is equivalent to the matrix below.

[147258369]\begin{bmatrix} 1 & 4 & 7 \\ 2 & 5 & 8 \\ 3 & 6 & 9 \end{bmatrix}

A transformation matrix is a matrix with nn rows and nn columns, where n1n-1 is the number of dimensions being transformed. To transform a vertex by a transformation matrix, simply multiply the vertex by the matrix.

[x0y0z01]\begin{bmatrix} x_0 \\ y_0 \\ z_0 \\ 1 \end{bmatrix}
[m1,1m1,2m1,3m1,4m2,1m2,2m2,3m2,4m3,1m3,2m3,3m3,4m4,1m4,2m4,3m4,4]\begin{bmatrix} m_{1,1} & m_{1,2} & m_{1,3} & m_{1,4} \\ m_{2,1} & m_{2,2} & m_{2,3} & m_{2,4} \\ m_{3,1} & m_{3,2} & m_{3,3} & m_{3,4} \\ m_{4,1} & m_{4,2} & m_{4,3} & m_{4,4} \end{bmatrix}

The product of the above matrices (a vector/vertex is just a matrix with one column) is a vector (x1,y1,z1)(x_1,y_1,z_1).

x1=m1,1x0+m1,2y0+m1,3z0+m1,41x_1=m_{1,1}x_0+m_{1,2}y_0+m_{1,3}z_0+m_{1,4}1
y1=m2,1x0+m2,2y0+m2,3z0+m2,41y_1=m_{2,1}x_0+m_{2,2}y_0+m_{2,3}z_0+m_{2,4}1
z1=m3,1x0+m3,2y0+m3,3z0+m3,41z_1=m_{3,1}x_0+m_{3,2}y_0+m_{3,3}z_0+m_{3,4}1

Matrices can be constructed in specific ways such that the matrix multiplication described above has the same function as various transformations.

Matrix translation

The following matrix translates an initial vector (x0,y0,z0)(x_0,y_0,z_0) by (xt,yt,zt)(x_t,y_t,z_t), resulting in a vector (x1,y1,z1)(x_1,y_1,z_1).

[100xt010yt001zt0001]\begin{bmatrix} 1 & 0 & 0 & x_t \\ 0 & 1 & 0 & y_t \\ 0 & 0 & 1 & z_t \\ 0 & 0 & 0 & 1 \end{bmatrix}
x1=1x0+0y0+0z0+xt1=x0+xtx_1=1x_0+0y_0+0z_0+x_t1=x_0+x_t
y1=0x0+1y0+0z0+yt1=y0+yty_1=0x_0+1y_0+0z_0+y_t1=y_0+y_t
z1=0x0+0y0+1z0+zt1=z0+ztz_1=0x_0+0y_0+1z_0+z_t1=z_0+z_t

Matrix rotation

The following matrix rotates an initial vector (x0,y0,z0)(x_0,y_0,z_0) by rr radians around the zz axis (most common in two dimensions), resulting in a vector (x1,y1,z1)(x_1,y_1,z_1), where c=cos(r)c=\cos(r) and s=sin(r)s=\sin(r).

[cs00sc0000100001]\begin{bmatrix} c & s & 0 & 0 \\ -s & c & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}
x1=cx0+sy0+0z0+01=cx0+sy0x_1=cx_0+sy_0+0z_0+0\cdot1=cx_0+sy_0
y1=sx0+cy0+0z0+01=sx0+cy0y_1=-sx_0+cy_0+0z_0+0\cdot1=-sx_0+cy_0
z1=0x0+0y0+1z0+01=z0z_1=0x_0+0y_0+1z_0+0\cdot1=z_0

The following matrix is the equivalent for the xx axis.

[10000cs00sc00001]\begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & c & s & 0 \\ 0 & -s & c & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

The following matrix is the equivalent for the yy axis.

[c0s00100s0c00001]\begin{bmatrix} c & 0 & -s & 0 \\ 0 & 1 & 0 & 0 \\ s & 0 & c & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

The following matrix is the equivalent for the (xa,ya,za)(x_a,y_a,z_a) axis, where l=xa2+ya2+za2l=\sqrt{{x_a}^2+{y_a}^2+{z_a}^2} and t=1ct=1-c.

[(xal)2t+cyaxal2t+zalszaxal2tyals0xayal2tzals(yal)2t+czayal2t+xals0xazal2t+yalsyazal2txals(zal)2t+c00001]\begin{bmatrix} (x_al)^2t+c & y_ax_al^2t+z_als & z_ax_al^2t-y_als & 0 \\ x_ay_al^2t-z_als & (y_al)^2t+c & z_ay_al^2t+x_als & 0 \\ x_az_al^2t+y_als & y_az_al^2t-x_als & (z_al)^2t+c & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

Matrix scaling

The following matrix scales an initial vector (x0,y0,z0)(x_0,y_0,z_0) by a scaling vector (xs,ys,zs)(x_s,y_s,z_s), resulting in a vector (x1,y1,z1)(x_1,y_1,z_1).

[xs0000ys0000zs00001]\begin{bmatrix} x_s & 0 & 0 & 0 \\ 0 & y_s & 0 & 0 \\ 0 & 0 & z_s & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}
x1=xsx0+0y0+0z0+01=xsx0x_1=x_sx_0+0y_0+0z_0+0\cdot1=x_sx_0
y1=0x0+ysy0+0z0+01=ysy0y_1=0x_0+y_sy_0+0z_0+0\cdot1=y_sy_0
z1=0x0+0y0+zsz0+01=zsz0z_1=0x_0+0y_0+z_sz_0+0\cdot1=z_sz_0

Orthographic projection

It is also possible to make a matrix that converts from screen space to clip space (known as an orthographic projection), since that operation is just a scale and a rotation. The following matrix converts a clip space vector (x0,y0,z0,w0)(x_0,y_0,z_0,w_0) to a screen space vector (x1,y1,z1,w1)(x_1,y_1,z_1,w_1), where ll, rr, bb, tt, nn, and ff are the left, right, bottom, top, near, and far bounds of the frustum, respectively.

[2lr00002bt00002nf0l+rlrb+tbtn+fnf1]\begin{bmatrix} \frac{-2}{l-r} & 0 & 0 & 0 \\ 0 & \frac{-2}{b-t} & 0 & 0 \\ 0 & 0 & \frac{2}{n-f} & 0 \\ \frac{l+r}{l-r} & \frac{b+t}{b-t} & \frac{n+f}{n-f} & 1 \end{bmatrix}
x1=2x0lr+0y0+0z0+01=2x0lrx_1=\frac{-2x_0}{l-r}+0y_0+0z_0+0\cdot1=\frac{-2x_0}{l-r}
y1=0x0+2y0bt+0z0+01=2y0bty_1=0x_0+\frac{-2y_0}{b-t}+0z_0+0\cdot1=\frac{-2y_0}{b-t}
z1=0x0+0y0+2z0nf+01=2z0nfz_1=0x_0+0y_0+\frac{2z_0}{n-f}+0\cdot1=\frac{2z_0}{n-f}
w1=l+rlrx0+b+tbty0+n+fnfz0+11w_1=\frac{l+r}{l-r}x_0+\frac{b+t}{b-t}y_0+\frac{n+f}{n-f}z_0+1\cdot1

Matrix multiplication

Like numbers, matrices can be multiplied together. The product of two transformation matrices has the combined transformations of both. Matrix multiplication is not commutative, which means that the order of the operands matters. The first operand's transformations are applied first, followed by the second operand's transformations.

Matrices can be passed as uniforms.

#version 300 es

in vec4 a_position;

uniform mat4 u_matrix;

void main() {
	gl_Position = u_matrix * a_position;
}

In order to avoid implementing all of the algorithms described above, subsequent examples will use glMatrix to perform matrix math.

Animation

Animation in WebGL is accomplished by changing the uniform (and sometimes attribute) values between frames.

const mat = mat4.create();

function renderStep() {
	requestAnimationFrame(renderStep);

	// Rotate the matrix slightly.
	mat4.rotateZ(mat, mat, 0.01);

	// Set the new uniform value.
	program.uniforms.get("u_matrix").value = mat;
}
requestAnimationFrame(renderStep);

Note that since requestAnimationFrame executes once each frame rather than at regular intervals, the time between render steps can vary. In other words, animations being calculated within the render step can vary in speed based on the framerate of the user. In order to ensure that animations remain smooth, the size of each transformation should be scaled by the amount of time that passed between frames. The first argument passed to any function passed to requestAnimationFrame is the amount of time that that function has been running.

// Store the time of the last frame.
let then = 0;

function renderStep(now) {
	requestAnimationFrame(renderStep);

	// Calculate the amount of time between frames.
	const deltaTime = now - then;
	then = now;

	mat4.rotateZ(mat, mat, 0.001 * deltaTime);
}
requestAnimationFrame(renderStep);

The next article is about the scene graph.