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 and a translation vector , the resulting vector is .
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 . As such, it can be thought of as a rotating , 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 and values, respectively. For an initial vector and a rotation value of radians, the resulting vector is . 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 and a scaling vector , the resulting vector is .
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.
A transformation matrix is a matrix with rows and columns, where is the number of dimensions being transformed. To transform a vertex by a transformation matrix, simply multiply the vertex by the matrix.
The product of the above matrices (a vector/vertex is just a matrix with one column) is a vector .
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 by , resulting in a vector .
Matrix rotation
The following matrix rotates an initial vector by radians around the axis (most common in two dimensions), resulting in a vector , where and .
The following matrix is the equivalent for the axis.
The following matrix is the equivalent for the axis.
The following matrix is the equivalent for the axis, where and .
Matrix scaling
The following matrix scales an initial vector by a scaling vector , resulting in a vector .
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 to a screen space vector , where , , , , , and are the left, right, bottom, top, near, and far bounds of the frustum, respectively.
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.