Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Affine Transformations in 3D Using Homogeneous Coordinates

Welcome. This course begins with the mathematical and PyTorch foundations you will use to work with 3D data, then builds toward point clouds, meshes, equivariant models, and research practice. Today’s distinction is small but consequential: a point is a location, while a displacement vector describes an offset between locations. You will learn to transform both with the same matrix without accidentally translating a displacement.

This lesson should take about 40 minutes, including the video, reading, and code.


Why add a fourth coordinate?

A linear transformation of 3D coordinates has the form , where is a matrix. It can rotate, scale, reflect, or shear, but it cannot move the origin: . To include a translation , we need an affine transformation:

The image shows the 2D version. Notice that translation occupies a separate column of each matrix; the same arrangement becomes a matrix in 3D.

Translation, scaling, rotation, and shear applied to a square. The \(3\times3\) matrices use a trailing coordinate of \(1\) for 2D points; 3D points use analogous \(4\times4\) matrices.

Watch only the opening of the following video. Its game-engine setting is incidental; focus on why the extra coordinate makes translation part of matrix multiplication.

Euler Angles & Homogeneous Coordinates - Vulkan Game Engine Tutorial 12

Watch “Euler Angles & Homogeneous Coordinates” by Brendan Galea for a visual introduction to points, offsets, and the extension from 2D to 3D.

Watch the extra coordinate to see why an ordinary linear matrix cannot translate. In combining transforms, notice why the order of operations matters. The key distinction is in points and offsets. Finish with the 3D extension; you can stop there.

For a 3D point, append ; for now, think of it as a switch that allows the translation column to contribute:

The bottom row keeps the last coordinate equal to . Here can be any linear transformation; it need not be a rotation. For the affine matrices in this lesson, there is no need to divide the resulting coordinates by the last entry. That division becomes relevant later for perspective projection.


Points move; displacements do not translate

Suppose and are two locations. Their difference is a displacement. If both locations undergo the same affine transformation, their new difference is

Translation cancels. The displacement may still rotate, scale, or shear because acts on it.

Homogeneous coordinates encode this distinction by appending to a displacement:

The translation column is multiplied by zero. Do not divide a displacement’s coordinates by its last entry: it is zero. Instead, read the first three entries as the transformed displacement.

This gives a useful consistency rule. A point plus a displacement is another point, and subtracting two points gives a displacement. Their homogeneous last entries behave accordingly: and . In point-cloud work, this rule prevents a common error: if you reposition an entire cloud, the offsets between its points must not acquire the cloud’s translation.


Compose transformations in the order you intend

Homogeneous matrices are useful because several affine operations can be combined into one matrix. With the column-vector convention used here, the rightmost matrix acts first. Read the worked example in LearnOpenGL before following the 3D calculation below.

LearnOpenGL - Transformations

Read the “Combining matrices” section for its example of scaling first and translating second.

In “Combining matrices,” start at the sentence beginning “The true power from using matrices for transformations” and read the worked example through the explanation of why reversing the order changes the result. Stop before “Running the final transformation matrix on our vector.” Focus on the effect of the matrix nearest the point, rather than treating matrix order as a memorized rule.

Consider a scale by in the -direction, followed by a rotation about the -axis, followed by translation by . With column vectors, the combined matrix is . Its upper-left block is

For , the transformed point is

But the displacement , despite having the same three numbers, becomes . Its meaning—and therefore its fourth coordinate—is different.

Order matters for points. If you translated before applying , the result would be , not . The earlier translation would itself be rotated and scaled.

An affine transformation preserves straight lines and parallelism, but an arbitrary need not preserve lengths or angles. The scale in this example changes some distances; a rotation followed by translation, without scaling or shear, would preserve them.


When the matrix describes a change of coordinates

So far, we have treated as an operation that moves points. The same matrix form can also describe a different task: expressing an unchanged physical point in a different coordinate frame. The arithmetic looks identical, so labels matter.

Suppose frame has its origin at in world frame , and its three basis vectors, expressed in world coordinates, form the columns of . Then

For the point, . For the displacement, : the position of frame ’s origin is irrelevant to an offset. If is invertible, its inverse converts world coordinates back to frame coordinates:

For example, let ’s axes be rotated about the world -axis, and let its origin be . The local point is the world point . A local displacement , however, is the world displacement .

A matrix that moves a point in a fixed frame and one that changes the coordinates used to describe that point can have the same numerical entries. Before multiplying, state what the input coordinates are and what the output coordinates mean. This habit will be especially important when you encounter camera frames.


Apply and check the rule in PyTorch

The following small batch implements the scale–rotation–translation example. Rows store points, so the code transposes the batch while applying the column-vector matrix convention established above.

import torch

dtype = torch.float64

scale = torch.eye(4, dtype=dtype)
scale[:3, :3] = torch.diag(torch.tensor([2., 1., 1.], dtype=dtype))

rotate = torch.eye(4, dtype=dtype)
rotate[:2, :2] = torch.tensor([[0., -1.], [1., 0.]], dtype=dtype)

translate = torch.eye(4, dtype=dtype)
translate[:3, 3] = torch.tensor([3., -1., 2.], dtype=dtype)

T = translate @ rotate @ scale  # scale first, translate last

def transform_rows(x, last_coordinate):
    w = torch.full((x.shape[0], 1), last_coordinate, dtype=x.dtype)
    x_h = torch.cat((x, w), dim=1)
    return (T @ x_h.T).T[:, :3]

points = torch.tensor([[1., 2., 0.], [2., 4., 0.]], dtype=dtype)
offset = points[1:] - points[:1]

new_points = transform_rows(points, 1.)
new_offset = transform_rows(offset, 0.)

print(new_points)  # [[ 1., 1., 2.], [-1., 3., 2.]]
print(new_offset)  # [[-2., 2., 0.]]

assert torch.allclose(new_points[1:] - new_points[:1], new_offset)

The assertion checks a geometric fact, not just a pair of expected numbers: transforming the difference between points must agree with taking the difference after transforming the points. It would fail if you appended to the offset and inadvertently translated it. The matrix used here is invertible, so torch.linalg.inv(T) can also undo its effect on points; a zero scaling factor would make that impossible.


You now have the core rule: use a affine matrix with a trailing for a point and for a displacement. The block acts on both; the translation column acts only on points. Keep track of multiplication order and coordinate-frame labels, because correct arithmetic with the wrong interpretation still produces the wrong geometry.

Next, you will use PyTorch to compute batched pairwise distances between points in a cloud—an operation whose behavior under translation and rotation you can now reason about.

Can't find a good explanation? Sign up and we'll make it for you

Sign up