Skip to main content

Geometric transformations

In the previous article, we learned the fundamentals of WebGPU by drawing a colored triangle. However, to create more complex scenes, we need to be able to manipulate these shapes to move, resize, and rotate them. This is where geometric transformations come into play.

Let’s take a 5-pointed star as an example. This figure can be decomposed into 10 triangles, 5 inner ones forming a pentagon plus 5 outer ones, each with its base on a side of the pentagon:

x y 1 1
La stella a 5 punte nello spazio locale dell'oggetto.

The vertex coordinates are defined in a local space, independent of the final scene to be rendered. This reference system is called object space. A typical choice, which makes applying transformations more intuitive, is to organize the vertices so that the entire figure is centered at the origin and contained within a unit square. This way, without transformations in the vertex shader, if we wanted to draw multiple stars (or other shapes), they would all appear overlapping at the center of the scene.

A minimal shader that draws the star is the following:

const star = array<vec2f, 30>(
// Outer star points
5 collapsed lines
vec2f(0.000000, -0.197140), vec2f(0.309011, -0.425297), vec2f(0.187467, -0.060929),
vec2f(0.187467, -0.060929), vec2f(0.500000, 0.162443), vec2f(0.115866, 0.159500),
vec2f(0.115866, 0.159500), vec2f(0.000000, 0.525707), vec2f(-0.115866, 0.159500),
vec2f(-0.115866, 0.159500), vec2f(-0.500000, 0.162443), vec2f(-0.187467, -0.060929),
vec2f(-0.187467, -0.060929), vec2f(-0.309011, -0.425297), vec2f(0.000000, -0.197140),
// Inner star points
5 collapsed lines
vec2f(0.000000, -0.197140), vec2f(0.000000, 0.000000), vec2f(0.187467, -0.060929),
vec2f(0.187467, -0.060929), vec2f(0.000000, 0.000000), vec2f(0.115866, 0.159500),
vec2f(0.115866, 0.159500), vec2f(0.000000, 0.000000), vec2f(-0.115866, 0.159500),
vec2f(-0.115866, 0.159500), vec2f(0.000000, 0.000000), vec2f(-0.187467, -0.060929),
vec2f(-0.187467, -0.060929), vec2f(0.000000, 0.000000), vec2f(0.000000, -0.197140)
);
@vertex
fn vs(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
return vec4f(star[index], 0.0, 1.0);
}
@fragment
fn fs() -> @location(0) vec4f {
return vec4(1, 1, 1, 1);
}

The star array contains the vertex coordinates of the star in object space. The geometry is defined statically in the shader code, but we will see later how to pass data dynamically. Since the star is composed of 10 triangles, the render pass invokes the vertex shader 30 times (3 vertices per triangle):

renderPass.draw(30);

#Transformation functions

In mathematics, a planar figure is represented by the infinite set of its points. We can define the star in a generic and abstract way with the following set:

S={(x,y)R2the pair (x,y) is a point of the star}S = \{ (x, y) \in \mathbb{R}^2 \mid \text{the pair } (x, y) \text{ is a point of the star} \}

In the plane, a geometric transformation is a function:

f:R2R2f: \mathbb{R}^2 \rightarrow \mathbb{R}^2

that maps each point (x,y)(x, y) to another point (x,y)(x', y'), called the image of (x,y)(x, y). Translation, rotation, and scaling are the most common transformations for changing the position, orientation, and size of planar figures.

Applying a transformation ff to a planar figure SS produces another planar figure f(S)f(S), defined as:

f(S)={f(p)pS}f(S) = \{ f(p) \mid p \in S \}

that is, the set of all points of SS transformed by ff. The figure f(S)f(S) is called the image of SS through ff.

#Translation

To move the star to a different position on the plane, we can add an offset vector (Δx,Δy)(\Delta x, \Delta y) to the coordinates of its vertices:

@vertex
fn vs(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
let dX = 0.3;
let dY = 0.2;
return vec4f(
star[index] + vec2f(dX, dY),
0.0,
1.0
);
}

In formula:

fT(x,y)=(x+Δx,y+Δy)f_T(x, y) = (x + \Delta x, y + \Delta y)

Below is an interactive widget showing the effect of translation based on the offset:

x y
Modifica i parametri di trasformazione con gli slider. La stella tratteggiata è la forma originale.

A positive offset moves the star to the right (X axis) or upward (Y axis), while a negative offset moves it to the left or downward.

If the center of the figure in object space coincides with the origin, then the translation vector (Δx,Δy)(\Delta x, \Delta y) directly indicates where the center of the transformed figure will be positioned. In other words, translating the figure is equivalent to deciding where we want its center to be.

#Scaling

To change the size of the star, we can multiply the coordinates of its vertices by a scale factor (sx,sy)(s_x, s_y):

@vertex
fn vs(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
let sX = 0.5;
let sY = 0.5;
return vec4f(
star[index] * vec2f(sX, sY),
0.0,
1.0
);
}

In formula:

fS(x,y)=(xsx,ysy)f_S(x, y) = (x \cdot s_x, y \cdot s_y)

Below is an interactive widget showing how the star changes shape and size as the scale factor varies:

x y
Modifica i parametri di trasformazione con gli slider. La stella tratteggiata è la forma originale.

A scale factor less than 1 reduces the star’s size, while a factor greater than 1 enlarges it. If the scale factor is negative, the star is reflected along the corresponding axis. It is also possible to apply a non-uniform scale using different values for sX and sY, thus changing the star’s size independently on both axes.

If the figure in object space is contained within a unit square, then applying a scale factor (sx,sy)(s_x, s_y) means that the scaled figure will be contained in a rectangle of dimensions sxs_x and sys_y.

#Rotation

The following code implements rotation:

@vertex
fn vs(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
let theta = radians(45.0);
let cosT = cos(theta);
let sinT = sin(theta);
let x = star[index].x;
let y = star[index].y;
let rotated = vec2f(
x * cosT - y * sinT,
x * sinT + y * cosT
);
return vec4f(rotated, 0.0, 1.0);
}

The rotation is counterclockwise, pivots around the origin, and uses an angle θ\theta expressed in radians. The corresponding formula is:

fR(x,y)=(xcos(θ)ysin(θ),xsin(θ)+ycos(θ))f_R(x, y) = (x \cdot \cos (\theta) - y \cdot \sin (\theta), x \cdot \sin (\theta) + y \cdot \cos (\theta))

A positive angle rotates counterclockwise, while a negative angle rotates in the opposite direction. Below is an interactive widget showing how the star rotates based on the specified angle:

x y
Modifica i parametri di trasformazione con gli slider. La stella tratteggiata è la forma originale.

To derive the rotation formula, let’s start by considering a point P(x,y)P(x, y) and its rotation Pθ(xθ,yθ)P_\theta(x_\theta, y_\theta) around the origin:

θ P θ P y θ y x θ x x ^ y ^
Il punto P viene ruotato di 25° per ottenere Pθ

The coordinates (x,y)(x, y) and (xθ,yθ)(x_\theta, y_\theta) are relative to the axes x^=(1,0)\hat{x} = (1, 0), y^=(0,1)\hat{y} = (0, 1). We can make this dependency explicit by writing:

P=xx^+yy^Pθ=xθx^+yθy^\begin{align*} P &= x \cdot \hat{x} + y \cdot \hat{y} \\ P_\theta &= x_\theta \cdot \hat{x} + y_\theta \cdot \hat{y} \end{align*}

Let’s now consider the axes xθ^\hat{x_\theta} and yθ^\hat{y_\theta} obtained by applying the rotation to the axes x^,y^\hat{x}, \hat{y}:

θ P θ P x θ ^ y θ ^ x ^ y ^
La rotazione applicata agli assi anzichè ai punti.

We can observe that the coordinates of PθP_\theta with respect to the rotated axes xθ^\hat{x_\theta} and yθ^\hat{y_\theta} are equal to the coordinates of PP with respect to the original axes x^\hat{x} and y^\hat{y}, that is:

Pθ=xxθ^+yyθ^P_\theta = x \cdot \hat{x_\theta} + y \cdot \hat{y_\theta}

At this point, it suffices to compute xθ^,yθ^\hat{x_\theta}, \hat{y_\theta} by rotating the axes x^=(1,0),y^=(0,1)\hat{x} = (1, 0), \hat{y} = (0, 1).

Recalling that cos(θ)\cos (\theta) and sin(θ)\sin (\theta) are, by definition, the x and y coordinates of the point (1,0)(1, 0) rotated by θ\theta radians counterclockwise around the origin, we immediately have:

xθ^=(cos(θ),sin(θ))\hat{x_\theta} = ( \cos (\theta), \sin (\theta) )

To compute yθ^\hat{y_\theta}, we exploit the fact that it must be perpendicular to xθ^\hat{x_\theta}. In 2D, to find a vector orthogonal to a given one, it suffices to swap its components and negate one. Applying this rule, we obtain:

yθ^=(sin(θ),cos(θ))\hat{y_\theta} = (-\sin (\theta), \cos (\theta))

Having the coordinates of the rotated axes, we can finally compute the coordinates of the rotated point:

Pθ=xxθ^+yyθ^=x(cos(θ),sin(θ))+y(sin(θ),cos(θ))=(xcos(θ)ysin(θ),xsin(θ)+ycos(θ))\begin{align*} P_\theta &= x \cdot \hat{x_\theta} + y \cdot \hat{y_\theta} \\ &= x \cdot ( \cos (\theta), \sin (\theta) ) + y \cdot ( -\sin (\theta), \cos (\theta) ) \\ &= (x \cdot \cos (\theta) - y \cdot \sin (\theta), x \cdot \sin (\theta) + y \cdot \cos (\theta) ) \end{align*}

which is the formula implemented by the shader.

#Shearing

Shearing tilts the star along one of the axes, altering the internal angles of the figure without modifying its area. The effect is achieved by adding a term proportional to the coordinates of one axis to the coordinates of the other:

@vertex
fn vs(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
let shX = 0.5;
let shY = 0.2;
let x = star[index].x;
let y = star[index].y;
return vec4f(
x + shX * y,
y + shY * x,
0.0,
1.0
);
}

In formula:

fSH(x,y)=(x+shxy,y+shyx)f_{SH}(x, y) = (x + sh_x \cdot y, y + sh_y \cdot x)

Below is an interactive widget showing the shearing effect:

x y
Modifica i parametri di trasformazione con gli slider. La stella tratteggiata è la forma originale.

A positive factor tilts the figure in the corresponding direction, while a negative factor tilts it in the opposite direction. Shearing can be applied on only one axis or both. An interesting aspect is that geometric rotation can be obtained by composing a horizontal shear with a vertical one.

#Composing transformations

In practice, a single transformation is rarely sufficient to position or modify an object as desired. It is often necessary to apply a series of transformations in sequence. For example, we might want to scale an object to resize it and then rotate it around its center to orient it correctly. The combination of geometric transformations is achieved by applying them one after another. To illustrate this process, let’s look at a shader that first applies scaling, then rotation, and finally translation:

@vertex
fn vs(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
// Step 1: Scale
let sX = 0.5;
let sY = 0.5;
var transformed = vec2f(
star[index].x * sX,
star[index].y * sY
);
// Step 2: Rotation
let theta = radians(45.0);
let cosT = cos(theta);
let sinT = sin(theta);
transformed = vec2f(
transformed.x * cosT - transformed.y * sinT,
transformed.x * sinT + transformed.y * cosT
);
// Step 3: Translation
let tX = 0.2;
let tY = -0.1;
transformed += vec2f(tX, tY);
return vec4f(transformed, 0.0, 1.0);
}

You can find the complete source code for this example on GitHub.

Below is an interactive widget that allows varying the translation, scale, and rotation parameters:

x y
Modifica i parametri di trasformazione con gli slider. La stella tratteggiata è la forma originale.

The order in which transformations are applied is fundamental, as it affects the final result. For example, rotating and then translating a figure can give a different result than translating and then rotating.

x y
Una stella prima ruota e poi trasla, mentre l'altra esegue le stesse trasformazioni nell'ordine inverso.

#Conclusions and next steps

In this article, we defined the most common geometric transformations. In the next one, we will see how to use matrices and homogeneous coordinates to handle all the examined transformations in a flexible way.