Translate

How to 3D

Chapter 2: Triangles and Transformations

Translate

To translate a model, we add an offset to each of its positions. Translation is sometimes expressed as vector addition. Here position \(\mathbf{p}\) is translated by vector \(\textrm{offset}\):

$$\begin{bmatrix}p_x \\ p_y \\ p_z \end{bmatrix} + \begin{bmatrix} \textrm{offset}_x \\ \textrm{offset}_y \\ \textrm{offset}_z \end{bmatrix} = \begin{bmatrix}p_x + \textrm{offset}_x \\ p_y + \textrm{offset}_y \\ p_z + \textrm{offset}_z\end{bmatrix}$$

A translation is not achieved by permanently adding the offset to the positions stored in the vertex buffer. Vertex buffers should generally be treated as read-only data. Instead, we apply the offset to the position in the vertex shader.

To translate in our vertex shader, we declare the offset as a vec3 uniform and sum the position and offset with the usual + operator:

uniform vec3 offset;
in vec3 position;

void main() {
  vec3 translatedPosition = position + offset;
  gl_Position = vec4(translatedPosition, 1.0);
}
uniform vec3 offset;
in vec3 position;

void main() {
  vec3 translatedPosition = position + offset;
  gl_Position = vec4(translatedPosition, 1.0);
}

On the CPU side of our renderer, we set the uniform before drawing. Here the object is shifted a little bit to the right and a bit more up:

shader.setUniform3f('offset', 0.1, 0.3, 0);
shader.setUniform3f('offset', 0.1, 0.3, 0);

Matching Vector Types

Since we are currently confining our geometry to the \(z = 0\) plane, we may be tempted to drop the z-coordinate, make the offset uniform a vec2, and set the uniform with this simpler call:

shader.setUniform2f('offset', 0.1, 0.3);
shader.setUniform2f('offset', 0.1, 0.3);

The GLSL code no longer compiles because we can't add a vec3 and a vec2. We can bridge between these incompatible types by calling the vec3 constructor explicitly and add the components together manually with scalar addition:

// Through 3-component constructor.
vec3 translatedPosition = vec3(
  position.x + offset.x,
  position.y + offset.y,
  position.z
);
// Through 3-component constructor.
vec3 translatedPosition = vec3(
  position.x + offset.x,
  position.y + offset.y,
  position.z
);

This is not an ideal solution because scalar addition is slow. If we use vectors, the GPU will add the corresponding components in parallel. Scalar addition is serial. Another possible fix is to duplicate the position and mutate the x- and y-components with the += operator:

// Through copy-and-mutate.
vec3 translatedPosition = position;
translatedPosition.x += offset.x;
translatedPosition.y += offset.y;
// Through copy-and-mutate.
vec3 translatedPosition = position;
translatedPosition.x += offset.x;
translatedPosition.y += offset.y;

This is still scalar addition, however.

To get the parallelism of vector addition, the vectors must be of the same type. Since position is a vec3, keeping offset as a vec3 is probably the best solution. If we have no control over the uniform type, we can convert the vec2 to a vec3 with the aid of an alternative vec3 constructor:

uniform vec2 offset;
// ...

void main() {
  // Through constructor.
  vec3 translatedPosition = position + vec3(offset, 0);
  // ...
}
uniform vec2 offset;
// ...

void main() {
  // Through constructor.
  vec3 translatedPosition = position + vec3(offset, 0);
  // ...
}

There are many overloads of the vector constructors, with a version for nearly any combination of scalars and vectors of different dimensionality that we can imagine. Use these constructors to produce vectors that can be operated on quickly and with compact code.

← TransformationsScale →