3D Transformations & Matrices — Translation, Rotation, Scaling and Projection
In this tutorial, you'll learn about 3d transformations & matrices. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
3D transformation matrices are 4x4 linear algebra structures that encode translation, rotation, and scaling operations, enabling objects to be positioned, oriented, and projected through a sequence of coordinate spaces in the graphics pipeline.
What You'll Learn & Why It Matters
In this tutorial, you will learn how 3D transformations work mathematically. You will build translation, rotation, and scaling matrices, combine them into model-view-projection (MVP) matrices, and understand coordinate spaces from local to screen space.
Real-world use: Every 3D object in a game or film is positioned and animated through matrix transformations. Computer Graphics engines like Unity and Unreal multiply millions of matrices per frame for character animation, camera control, and physics simulation.
Prerequisites
- Linear algebra fundamentals
- C++ or Python basics
- Rasterization Pipeline (previous)
Learning Path
flowchart LR A[Rasterization Pipeline] --> B[3D Transformations & Matrices] B --> C[Shader Programming] B --> D[Animation & Rigging] B --> E[OpenGL Guide] C --> F[Lighting Models] B:::current classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
The 4x4 Transformation Matrix
In homogeneous coordinates, a 3D point (x, y, z) becomes (x, y, z, w). A 4x4 matrix can represent translation, rotation, and scaling in a single multiplication:
#include <array>
#include <cmath>
#include <iostream>
struct Mat4 {
std::array<float, 16> m = {0};
Mat4() { m[0] = m[5] = m[10] = m[15] = 1.0f; } // Identity
static Mat4 identity() { return Mat4(); }
static Mat4 translate(float tx, float ty, float tz) {
Mat4 mat;
mat.m[12] = tx;
mat.m[13] = ty;
mat.m[14] = tz;
return mat;
}
static Mat4 scale(float sx, float sy, float sz) {
Mat4 mat;
mat.m[0] = sx;
mat.m[5] = sy;
mat.m[10] = sz;
return mat;
}
static Mat4 rotateX(float angleDeg) {
float rad = angleDeg * M_PI / 180.0f;
float c = cos(rad), s = sin(rad);
Mat4 mat;
mat.m[5] = c; mat.m[6] = -s;
mat.m[9] = s; mat.m[10] = c;
return mat;
}
static Mat4 rotateY(float angleDeg) {
float rad = angleDeg * M_PI / 180.0f;
float c = cos(rad), s = sin(rad);
Mat4 mat;
mat.m[0] = c; mat.m[2] = s;
mat.m[8] = -s; mat.m[10] = c;
return mat;
}
static Mat4 rotateZ(float angleDeg) {
float rad = angleDeg * M_PI / 180.0f;
float c = cos(rad), s = sin(rad);
Mat4 mat;
mat.m[0] = c; mat.m[1] = -s;
mat.m[4] = s; mat.m[5] = c;
return mat;
}
Mat4 operator*(const Mat4& rhs) const {
Mat4 result;
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
float sum = 0;
for (int k = 0; k < 4; k++)
sum += m[row * 4 + k] * rhs.m[k * 4 + col];
result.m[row * 4 + col] = sum;
}
}
return result;
}
};
int main() {
Mat4 T = Mat4::translate(2.0f, 0.0f, 0.0f);
Mat4 R = Mat4::rotateY(45.0f);
Mat4 S = Mat4::scale(0.5f, 0.5f, 0.5f);
Mat4 transform = T * R * S;
std::cout << "TRS matrix created (translate x=2, rotate y=45, scale 0.5)" << std::endl;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++)
std::cout << transform.m[i * 4 + j] << " ";
std::cout << std::endl;
}
return 0;
}
The MVP Matrix Pipeline
Every 3D object passes through four coordinate spaces: local, world, view, and clip (projection):
import numpy as np
def create_perspective(fov_deg, aspect, near, far):
fov_rad = np.radians(fov_deg)
f = 1.0 / np.tan(fov_rad / 2.0)
return np.array([
[f / aspect, 0, 0, 0],
[0, f, 0, 0],
[0, 0, (far + near) / (near - far), (2 * far * near) / (near - far)],
[0, 0, -1, 0]
], dtype=np.float32)
def create_look_at(eye, target, up):
fwd = (target - eye) / np.linalg.norm(target - eye)
right = np.cross(fwd, up)
right = right / np.linalg.norm(right)
up = np.cross(right, fwd)
view = np.eye(4, dtype=np.float32)
view[0, :3] = right
view[1, :3] = up
view[2, :3] = -fwd
view[:3, 3] = -right.dot(eye), -up.dot(eye), fwd.dot(eye)
return view
eye = np.array([3.0, 2.0, 5.0])
target = np.array([0.0, 0.0, 0.0])
up = np.array([0.0, 1.0, 0.0])
view = create_look_at(eye, target, up)
proj = create_perspective(45.0, 16.0/9.0, 0.1, 100.0)
model = np.eye(4, dtype=np.float32)
model[:3, :3] = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
model[:3, 3] = [0.0, 0.0, -2.0]
mvp = proj @ view @ model
point_local = np.array([0.5, 0.5, 0.5, 1.0])
point_clip = mvp @ point_local
point_ndc = point_clip[:3] / point_clip[3]
print(f"Local point: {point_local[:3]}")
print(f"Clip space: {point_clip[:3]}")
print(f"NDC: {point_ndc}")
Expected output:
Local point: [0.5 0.5 0.5]
Clip space: [ ... values ... ]
NDC: [ ... normalized values between -1 and 1 ... ]
Quaternions for Rotation
Quaternions avoid gimbal lock and provide smooth interpolation (SLERP) for animation:
#include <cmath>
#include <iostream>
struct Quaternion {
float w, x, y, z;
Quaternion(float w, float x, float y, float z) : w(w), x(x), y(y), z(z) {}
static Quaternion fromAxisAngle(float ax, float ay, float az, float angleDeg) {
float rad = angleDeg * M_PI / 180.0f;
float s = sin(rad / 2.0f);
return Quaternion(cos(rad / 2.0f), ax * s, ay * s, az * s);
}
Quaternion operator*(const Quaternion& q) const {
return Quaternion(
w * q.w - x * q.x - y * q.y - z * q.z,
w * q.x + x * q.w + y * q.z - z * q.y,
w * q.y - x * q.z + y * q.w + z * q.x,
w * q.z + x * q.y - y * q.x + z * q.w
);
}
float norm() const { return sqrt(w*w + x*x + y*y + z*z); }
Quaternion normalized() const {
float n = norm();
return Quaternion(w/n, x/n, y/n, z/n);
}
static Quaternion slerp(const Quaternion& a, const Quaternion& b, float t) {
float dot = a.w*b.w + a.x*b.x + a.y*b.y + a.z*b.z;
if (dot < 0) { dot = -dot; /* use negated b */ }
if (dot > 0.9995f) return a; // nearly parallel, use linear
float theta = acos(dot);
float sinTheta = sin(theta);
float wa = sin((1-t) * theta) / sinTheta;
float wb = sin(t * theta) / sinTheta;
return Quaternion(wa*a.w + wb*b.w, wa*a.x + wb*b.x, wa*a.y + wb*b.y, wa*a.z + wb*b.z);
}
};
int main() {
Quaternion q1 = Quaternion::fromAxisAngle(0, 1, 0, 0);
Quaternion q2 = Quaternion::fromAxisAngle(0, 1, 0, 90);
Quaternion interpolated = Quaternion::slerp(q1, q2, 0.5f);
std::cout << "SLERP(0deg, 90deg, t=0.5): w=" << interpolated.w
<< " (angle = " << acos(interpolated.w) * 2 * 180 / M_PI << " deg)" << std::endl;
return 0;
}
flowchart TD A[Local Space] -->|Model Matrix| B[World Space] B -->|View Matrix| C[View/Camera Space] C -->|Projection Matrix| D[Clip Space] D -->|Perspective Divide| E[NDC] E -->|Viewport Transform| F[Screen Space] style A fill:#4ae,color:#fff style F fill:#f90,color:#fff
Common Errors & Mistakes
1. Matrix Multiplication Order
Mistake: Applying transformations in the wrong order (e.g., translate then rotate instead of rotate then translate), causing the object to orbit instead of rotate in place.
Fix: Use transform = T * R * S (scale first, then rotate, then translate). Column-major multiplication reads right-to-left: the last operation happens first in local space.
2. Perspective Divide in Wrong Space
Mistake: Performing the perspective divide (w division) in vertex shader instead of letting the GPU handle it.
Fix: The GPU automatically performs the perspective divide after the vertex shader. Output homogeneous clip coordinates from the vertex shader and let the rasterizer handle normalization.
3. Degenerate Matrices
Mistake: Creating a projection matrix with near or far plane at zero, or a view matrix with zero-length look direction, producing NaN or inverted results.
Fix: Validate near/far values (near > 0, far > near). Ensure the look-at direction has non-zero length.
4. Gimbal Lock
Mistake: Using Euler angles for hierarchical rotation, causing one axis to lose a degree of freedom.
Fix: Use quaternions for interpolation and hierarchical rotations. Convert to a matrix only at the end for the shader.
Practice Questions
Question 1
Why are 4x4 matrices used instead of 3x3 for 3D transformations?
Show answer
A 4x4 matrix in homogeneous coordinates allows translation (which is an affine transformation) to be represented as a matrix multiplication. With 3x3 matrices, translation would require a separate vector addition, breaking the composability of matrix chains.Question 2
What is the difference between model, view, and projection matrices?
Show answer
The model matrix transforms from local object space to world space. The view matrix transforms from world space to camera space. The projection matrix transforms from camera space to clip space, applying perspective or orthographic projection.Question 3
How does a quaternion avoid gimbal lock?
Show answer
Gimbal lock occurs when using Euler angles (rotations around X, Y, Z axes sequentially) and two axes align, losing a degree of freedom. Quaternions represent rotation as a single rotation around an arbitrary axis, avoiding sequential axis dependencies.Challenge
Implement a hierarchical transformation system for a robot arm with shoulder, elbow, and wrist joints. Use matrix stacks for parent-child transformations and animate each joint independently with interpolated rotations.
FAQ
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Author: DodaTech | Last updated: June 23, 2026
DodaTech tutorials are built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — security tools used by millions worldwide.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro