前言

我第一次真正「看懂」矩陣的時候,不是在線性代數的課堂上,而是在寫 shader 的某個深夜。我把一個 2×2 矩陣的四個數字接上滑桿,然後拖動它們,看著螢幕上的正方形被拉伸、旋轉、扭曲——突然間,矩陣不再是一張數字表格,而是一種「空間的變形」。

這篇文章想帶你建立同樣的直覺。我們會從最基本的 2D 變換矩陣開始,逐步推導旋轉、縮放、剪切的矩陣形式,並用 p5.js 把每一種變換的幾何意義動態展示出來。

矩陣是什麼?一種空間變換

在 2D 平面上,一個點 (x, y) 可以被一個 2×2 矩陣變換成新的點 (x’, y’):

| x' |   | a  b | | x |   | ax + by |
|    | = |      | |   | = |         |
| y' |   | c  d | | y |   | cx + dy |

這意味著:矩陣的第一列 (a, c) 決定了原本的 x 軸基向量被映射到哪裡,第二列 (b, d) 決定了原本的 y 軸基向量被映射到哪裡。

這個觀點非常重要。矩陣本質上就是在告訴你:原來的兩個基向量 (1,0) 和 (0,1) 被送到了哪裡。 理解了這一點,所有的變換矩陣都變得直覺了。

基本變換

縮放(Scaling)

最簡單的變換。把 x 方向放大 sx 倍,y 方向放大 sy 倍:

| sx  0  |
| 0   sy |

基向量 (1,0) 被映射到 (sx, 0),(0,1) 被映射到 (0, sy)。就像抓住一張圖片的邊角拉伸一樣。

當 sx 或 sy 為負數時,就是鏡射(flip)。例如 sx = -1, sy = 1 就是水平翻轉。

旋轉(Rotation)

這是最經典的推導。我們要把空間逆時針旋轉 θ 角。問自己:基向量 (1,0) 旋轉 θ 之後到哪了?答案是 (cos θ, sin θ)。那 (0,1) 旋轉 θ 之後呢?答案是 (-sin θ, cos θ)。

所以旋轉矩陣就是:

| cos θ   -sin θ |
| sin θ    cos θ |

就是這樣!不需要背誦,只需要想一想基向量被送到哪裡就好了。

剪切(Shear)

剪切是一種比較少被注意但很有趣的變換。水平剪切的矩陣:

| 1   k |
| 0   1 |

這讓 (1,0) 不動,但 (0,1) 被映射到 (k, 1)。效果就像把一疊撲克牌推歪——每一層水平偏移的量和它的高度成正比。

垂直剪切則是:

| 1   0 |
| k   1 |

p5.js 視覺化:觀察基向量的變化

以下是一個完整的程式,讓你用滑桿控制 2×2 矩陣的四個元素,即時觀察變換效果:

let sliders = [];

function setup() { createCanvas(600, 600); // 矩陣的四個元素 [a, b, c, d] // 初始值為單位矩陣 let defaults = [1, 0, 0, 1]; let labels = ['a', 'b', 'c', 'd']; for (let i = 0; i < 4; i++) { sliders[i] = createSlider(-200, 200, defaults[i] * 100); sliders[i].position(20, 610 + i * 30); // 標籤文字可以用 createSpan 等方式加上 } }

function draw() { background(30); translate(width / 2, height / 2); scale(1, -1); // 讓 y 軸朝上

let a = sliders[0].value() / 100; let b = sliders[1].value() / 100; let c = sliders[2].value() / 100; let d = sliders[3].value() / 100;

// 畫網格(變換前,淡灰色) stroke(60); strokeWeight(0.5); for (let i = -5; i <= 5; i++) { line(i 50, -300, i 50, 300); line(-300, i 50, 300, i 50); }

// 畫變換後的網格 stroke(80, 80, 80, 120); strokeWeight(1); for (let i = -5; i <= 5; i++) { // 垂直線:x=i, y 從 -5 到 5 let x1 = (a i 50 + b (-5) 50); let y1 = (c i 50 + d (-5) 50); let x2 = (a i 50 + b 5 50); let y2 = (c i 50 + d 5 50); line(x1, y1, x2, y2);

// 水平線:y=i, x 從 -5 到 5 x1 = (a (-5) 50 + b i 50); y1 = (c (-5) 50 + d i 50); x2 = (a 5 50 + b i 50); y2 = (c 5 50 + d i 50); line(x1, y1, x2, y2); }

// 畫變換後的基向量 let scale_ = 100; // e1 = (a, c) stroke(255, 80, 80); strokeWeight(3); line(0, 0, a scale_, c scale_); drawArrow(a scale_, c scale_, 255, 80, 80);

// e2 = (b, d) stroke(80, 180, 255); line(0, 0, b scale_, d scale_); drawArrow(b scale_, d scale_, 80, 180, 255);

// 畫變換後的單位正方形 noFill(); stroke(255, 200, 0, 180); strokeWeight(2); beginShape(); vertex(0, 0); vertex(a scale_, c scale_); vertex((a + b) scale_, (c + d) scale_); vertex(b scale_, d scale_); endShape(CLOSE);

// 原點 fill(255); noStroke(); ellipse(0, 0, 8, 8);

// 顯示矩陣值(需要翻轉文字) scale(1, -1); fill(255); textSize(14); text(| ${nf(a,1,2)} ${nf(b,1,2)} |, -280, -260); text(| ${nf(c,1,2)} ${nf(d,1,2)} |, -280, -240); text(det = ${nf(a<em>d - b</em>c, 1, 3)}, -280, -210); }

function drawArrow(x, y, r, g, b) { fill(r, g, b); noStroke(); push(); translate(x, y); let angle = atan2(y, x); rotate(angle); triangle(0, 0, -12, 6, -12, -6); pop(); }

拖動滑桿時,你會看到:

  • 紅色箭頭是第一個基向量 (a, c) 的位置
  • 藍色箭頭是第二個基向量 (b, d) 的位置
  • 黃色四邊形是原本的單位正方形被變換後的形狀
  • 背景網格也跟著變形

旋轉矩陣的推導:用動畫理解

讓我們做一個旋轉矩陣的動畫驗證:

let angle = 0;

function setup() { createCanvas(500, 500); }

function draw() { background(30); translate(width / 2, height / 2); scale(1, -1);

// 原始正方形的頂點 let points = [ { x: 50, y: 50 }, { x: -50, y: 50 }, { x: -50, y: -50 }, { x: 50, y: -50 } ];

// 旋轉矩陣 let cosA = cos(angle); let sinA = sin(angle);

// 方法一:手動矩陣乘法 let transformed = points.map(p => ({ x: cosA p.x - sinA p.y, y: sinA p.x + cosA p.y }));

// 畫原始正方形(半透明) noFill(); stroke(100); beginShape(); for (let p of points) vertex(p.x, p.y); endShape(CLOSE);

// 畫旋轉後的正方形 stroke(255, 80, 80); strokeWeight(2); beginShape(); for (let p of transformed) vertex(p.x, p.y); endShape(CLOSE);

// 畫基向量 let scale_ = 80; stroke(255, 80, 80); line(0, 0, cosA scale_, sinA scale_); stroke(80, 180, 255); line(0, 0, -sinA scale_, cosA scale_);

// 顯示角度 scale(1, -1); fill(255); textSize(14); text("θ = " + nf(degrees(angle), 1, 1) + "°", -230, -220);

angle += 0.01; }

複合變換:順序很重要

矩陣的一個關鍵特性:乘法不可交換。也就是說,先旋轉再縮放和先縮放再旋轉,結果通常不同。

// 先縮放再旋轉
M1 = R * S

// 先旋轉再縮放 M2 = S * R

// 一般來說 M1 ≠ M2

在程式碼中,變換的套用順序是「後寫的先執行」。這在 p5.js 中非常直覺:

// p5.js 的變換是從下往上讀的
translate(200, 200);  // 第三步:平移到畫布中心
rotate(angle);        // 第二步:旋轉
scale(2, 1);          // 第一步:x 方向拉伸

rect(-25, -25, 50, 50); // 畫一個正方形

如果你交換 rotatescale 的順序,結果會完全不同。先拉伸再旋轉,你會得到一個旋轉的長方形;先旋轉再拉伸,你會得到一個傾斜的平行四邊形。

applyMatrix:完全掌控

p5.js 提供了 applyMatrix() 函數,讓你直接套用一個 3×3 的仿射變換矩陣(齊次座標)。這在某些情況下比 translate/rotate/scale 更方便。

仿射變換矩陣包含了平移,形式如下:

| a  b  tx |
| c  d  ty |
| 0  0  1  |

在 p5.js 中使用:

function draw() {
  background(30);

let angle = frameCount * 0.02; let cosA = cos(angle); let sinA = sin(angle);

// 平移到畫面中心 + 旋轉 // applyMatrix(a, b, c, d, tx, ty) — 注意 p5.js 的參數順序 applyMatrix(cosA, sinA, -sinA, cosA, width/2, height/2);

// 此時的座標系統已經被旋轉並平移了 fill(255, 80, 80); noStroke(); rectMode(CENTER); rect(0, 0, 100, 100);

// 繪製子物件(局部座標系統) fill(80, 180, 255); rect(80, 0, 30, 30); }

利用矩陣做骨骼動畫

複合變換最經典的應用就是骨骼動畫。每個關節有自己的局部旋轉,透過矩陣串聯就能算出末端的全域位置:

function draw() {
  background(30);
  translate(width / 2, height / 2);

let angle1 = sin(frameCount 0.03) 0.5; // 上臂擺動 let angle2 = sin(frameCount 0.05) 0.8; // 前臂擺動 let angle3 = sin(frameCount 0.07) 0.4; // 手掌擺動

// 上臂 rotate(angle1); stroke(255, 80, 80); strokeWeight(8); line(0, 0, 100, 0);

// 前臂 — 從上臂末端開始 translate(100, 0); rotate(angle2); stroke(80, 180, 255); line(0, 0, 80, 0);

// 手掌 translate(80, 0); rotate(angle3); stroke(255, 200, 0); line(0, 0, 40, 0);

// 手掌末端的圓點 translate(40, 0); fill(255); noStroke(); ellipse(0, 0, 12, 12); }

每一次的 translate + rotate 都相當於乘上一個新的局部變換矩陣。p5.js 會自動把所有變換累積起來,算出每個物件在全域座標中的位置。

行列式:面積的變化量

矩陣有一個重要的數值叫做行列式(determinant):

det(M) = ad - bc

行列式的絕對值代表面積的縮放比例。如果原本的單位正方形面積是 1,變換後的面積就是 |det(M)|。

  • det = 1:面積不變(旋轉就是這類)
  • det > 1:面積放大
  • det < 1:面積縮小
  • det < 0:空間被翻轉(鏡射),方向反了
  • det = 0:空間被壓扁到一條線或一個點(不可逆)

小結

矩陣變換是計算機圖學的基石。從 2D 的 CSS transform,到 3D 的 MVP 矩陣,到機器學習中的張量運算,矩陣無處不在。而理解它們的最好方式,就是「看」它們對空間做了什麼事。

記住這個核心觀點:矩陣的列就是基向量的新位置。 掌握了這個直覺,任何變換矩陣你都能一眼看懂。

延伸閱讀

  • 3Blue1Brown 的「Essence of Linear Algebra」系列 — 這是我看過最好的矩陣視覺化教學
  • The Book of Shaders 第二章(矩陣)
  • p5.js 官方文檔中的 applyMatrix() 說明
  • 試著把本文的 2D 矩陣擴展到 3D(3×3 矩陣),觀察旋轉在三維空間中的行為