跳至主要内容

5. 物體的變換

我們經常會對物體做一些變換,比如放大縮小,移動位置。其中例如原地旋轉,尺寸縮放,甚至歪斜這類不改變物體原點的變換,被叫做線性變換。線性變換都可以用矩陣乘上原式數據來計算。

比如在平面上,我們要把一組數據 (x,y)(x, y) 放大 3/2 倍,我們可以這樣表示:

[3/2003/2][xy]=[3/2x3/2y]\begin{bmatrix} 3/2 & 0\\ 0 & 3/2 \end{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix}= \begin{bmatrix} 3/2x\\ 3/2y \end{bmatrix}

而旋轉的變換可以用三角函數相關的矩陣來表示,比如我們想要旋轉 θ\theta 度:

[cosθsinθsinθcosθ][xy]=[xcosθysinθxsinθ+ycosθ]\begin{bmatrix} \cos\theta & -\sin\theta\\ \sin\theta & \cos\theta \end{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix}= \begin{bmatrix} x\cos\theta-y\sin\theta\\ x\sin\theta+y\cos\theta \end{bmatrix}

如果我們想要將這些線性變換組合在一起,我們可以把這些矩陣相乘。比如我們先放大 3/2 倍,然後再旋轉 θ\theta 度:

[cosθsinθsinθcosθ][3/2003/2][xy]=[3/2xcosθ3/2ysinθ3/2xsinθ+3/2ycosθ]\begin{bmatrix} \cos\theta & -\sin\theta\\ \sin\theta & \cos\theta \end{bmatrix} \begin{bmatrix} 3/2 & 0\\ 0 & 3/2 \end{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix}= \begin{bmatrix} 3/2x\cos\theta-3/2y\sin\theta\\ 3/2x\sin\theta+3/2y\cos\theta \end{bmatrix}

像平移這樣的變換,就不是用矩陣乘來表示,而是用加法:

[xy]+[txty]=[x+txy+ty]\begin{bmatrix} x\\ y \end{bmatrix}+ \begin{bmatrix} t_x\\ t_y \end{bmatrix}= \begin{bmatrix} x+t_x\\ y+t_y \end{bmatrix}

這樣我們就把 (x,y)(x,y) 平移了 (tx,ty)(t_x,t_y)。這類不是用乘法的變換則是非線性變換。

如果此時,我對物品的一系列變換的組合感興趣,其中包含線性或非線性。比如我想要對一個物體放大、平移、旋轉,再放大,再平移:

[xy]=[3/2003/2][cosθsinθsinθcosθ]([3/2003/2][xy]+[txty])+[txty]\begin{bmatrix} x'\\ y' \end{bmatrix}= \begin{bmatrix} 3/2 & 0\\ 0 & 3/2 \end{bmatrix} \begin{bmatrix} \cos\theta & -\sin\theta\\ \sin\theta & \cos\theta \end{bmatrix} \bigg( \begin{bmatrix} 3/2 & 0\\ 0 & 3/2 \end{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix}+ \begin{bmatrix} t_x\\ t_y \end{bmatrix} \bigg)+ \begin{bmatrix} t_x'\\ t_y' \end{bmatrix}

僅僅是這樣,我們的表達式已經開始變得混亂。但如果這樣的操作要嵌套幾十層,那麼我們的表達式會複雜的難以想象。我們希望盡可能的只用乘法來表示所有的變換,這樣我們的式子就能簡潔很多。

齊次坐標

對於矩陣加法來說,我們可以寫成:

[xy]+[txty]=[1[txty]][[xy]1]\begin{bmatrix} x\\ y \end{bmatrix}+ \begin{bmatrix} t_x\\ t_y \end{bmatrix}= \begin{bmatrix} 1& \begin{bmatrix} t_x\\ t_y \end{bmatrix} \end{bmatrix} \begin{bmatrix} \begin{bmatrix} x\\ y \end{bmatrix}\\ 1 \end{bmatrix}

如果我們把它寫開變成 3 維:

[10tx01ty001][xy1]=[x+txy+ty1]\begin{bmatrix} 1&0&t_x\\ 0&1&t_y\\ 0&0&1 \end{bmatrix} \begin{bmatrix} x\\ y\\ 1 \end{bmatrix}= \begin{bmatrix} x+t_x\\ y+t_y\\ 1 \end{bmatrix}

原本我們在 2 維時的非線性平移變換,增加一個維度變成 3 維後,就可以用矩陣乘法來表示了。這樣的增加維度的方法,就是齊次坐標。我們可以把 2 維的點 (x,y)(x,y) 變成 3 維的點 (x,y,1)(x,y,1),這樣我們就可以用矩陣乘法來表示平移了。

這種通過增加一個維度的方法就是齊次坐標。我們規定,三維中一個點 (x,y,z)(x,y,z) 的齊次坐標是 (x,y,z,1)(x,y,z,1)。而如果我們要把一個齊次坐標 (x,y,z,w)(x,y,z,w) 變回普通坐標,我們可以這樣做:

[xyzw]    [x/wy/wz/w]\begin{bmatrix} x\\ y\\ z\\ w \end{bmatrix}\implies \begin{bmatrix} x/w\\ y/w\\ z/w \end{bmatrix}

也就是說,在齊次坐標中,a0\forall a \neq 0

[axayazaw]=[x/wy/wz/w]\begin{bmatrix} ax\\ ay\\ az\\ aw \end{bmatrix}= \begin{bmatrix} x/w\\ y/w\\ z/w \end{bmatrix}

而當 w=0w=0 時,我們就認為這個點是無窮遠的。

當我們想要做任何變換時,只需要把原本的坐標轉成齊次坐標,變換後再把齊次坐標轉回普通坐標。這樣我們就可以用矩陣乘法來表示所有的變換了。

我們可以把上次透視投影的變換用齊次坐標來表示。可以發現,x 和 y 的變換是一樣的,都是除上 1z/c1-z/c。根據齊次坐標的轉換規則,我們會希望第四個維度表示成 1z/c1-z/c。所以我們可以把透視投影的變換寫成:

[100001000010001/c1][xyz1]=[xyz1z/c]    [x/(1z/c)y/(1z/c)z/(1z/c)]\begin{bmatrix} 1&0&0&0\\ 0&1&0&0\\ 0&0&1&0\\ 0&0&-1/c&1 \end{bmatrix} \begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix}= \begin{bmatrix} x\\ y\\ z\\ 1-z/c \end{bmatrix}\implies \begin{bmatrix} x/(1-z/c)\\ y/(1-z/c)\\ z/(1-z/c) \end{bmatrix}

這裡我們同樣保留 z 軸的深度信息。

let mut projection = Matrix::identity(4);
projection[(3, 2)] = -1.0 / camera.z;
let screen_coords = [
world_to_screen(m2v(&projection * &v2m(ver[0]))),
world_to_screen(m2v(&projection * &v2m(ver[1]))),
world_to_screen(m2v(&projection * &v2m(ver[2]))),
];

其中 m2vv2m 齊次坐標和普通坐標的轉換函數。

fn v2m(v: &Vertex<f32>) -> Matrix {
let mut m = Matrix::new(4, 1);
m[(0, 0)] = v.x;
m[(1, 0)] = v.y;
m[(2, 0)] = v.z;
m[(3, 0)] = 1.0;
m
}

fn m2v(m: Matrix) -> Vertex<f32> {
Vertex {
x: m[(0, 0)] / m[(3, 0)],
y: m[(1, 0)] / m[(3, 0)],
z: m[(2, 0)] / m[(3, 0)],
}
}

坐標系變換

單獨一個物體的數據,大多是以自身的坐標系為基礎的。但我們把一個物體放到另一個空間中時,就需要根據坐標的改變來對物體的數據進行變換。

在歐式空間中,向量可以由原點和基底(base)來表示。假如 P 點在坐標系 (O,i,j,k) 中的坐標是 (x,y,z),那麼 OP 向量可以表示為:

OP=ix+jy+kz[ijk][xyz]\vec{OP}=\vec{i}x+\vec{j}y+\vec{k}z \begin{bmatrix} \vec{i} & \vec{j} & \vec{k} \end{bmatrix} \begin{bmatrix} x\\ y\\ z \end{bmatrix}

如果我們有另一套坐標系 (O',i',j',k'),P 點在這套坐標系中的坐標是 (x',y',z'),如果我們還知道 O' 相對於 O 的位置,即在 O 坐標系中 OO' 向量:

那麼我們可以把 OP 向量重新表示為:

OP=OO+OP=[ijk][OxOxOz]+[ijk][xyz]\vec{OP} = \vec{OO'} + \vec{O'P} = \begin{bmatrix} \vec{i} & \vec{j} & \vec{k} \end{bmatrix} \begin{bmatrix} O_x'\\ O_x'\\ O_z' \end{bmatrix} + \begin{bmatrix} \vec{i'} & \vec{j'} & \vec{k'} \end{bmatrix} \begin{bmatrix} x'\\ y'\\ z' \end{bmatrix}

因為 (i,j,k)(i,j,k)(i,j,k)(i',j',k') 都是基底,是從原點出發的向量。所以存在一個矩陣 MM,使得:

[ijk]=[ijk]M\begin{bmatrix} \vec{i'} & \vec{j'} & \vec{k'} \end{bmatrix}= \begin{bmatrix} \vec{i} & \vec{j} & \vec{k} \end{bmatrix} M

因此

OP=[ijk]([OxOxOz]+M[xyz])\vec{OP}= \begin{bmatrix} \vec{i} & \vec{j} & \vec{k} \end{bmatrix} \bigg( \begin{bmatrix} O_x'\\ O_x'\\ O_z' \end{bmatrix}+ M \begin{bmatrix} x'\\ y'\\ z' \end{bmatrix} \bigg)

也就是說

[xyz]=[OxOxOz]+M[xyz]    [xyz]=M1([xyz][OxOxOz])\begin{bmatrix} x\\ y\\ z \end{bmatrix}= \begin{bmatrix} O_x'\\ O_x'\\ O_z' \end{bmatrix}+ M \begin{bmatrix} x'\\ y'\\ z' \end{bmatrix} \implies \begin{bmatrix} x'\\ y'\\ z' \end{bmatrix}= M^{-1}\bigg( \begin{bmatrix} x\\ y\\ z \end{bmatrix}- \begin{bmatrix} O_x'\\ O_x'\\ O_z' \end{bmatrix} \bigg)

移動相機

如果我們想要觀察一個物體或者世界的不同角度,我們可以移動相機。但我們也可以反過來移動物體,讓相機固定在原地。比如我想要通過相機觀察物體的右側,那我可以把物體向相反的方向,也就是左側移動。

我們假設相機的位置在 ee 點,並且面向 cc 點,而 u\vec{u} 向量則是指向相機正上方。那麼 (c, x',y',z') 坐標就時我們屏幕的坐標系。而物體的數據是在 (O, x,y,z) 坐標系中的。因此我們需要計算把 (O, x,y,z) 坐標系變換到 (c, x',y',z') 坐標系。而我們要給的參數則是在 (O, x,y,z) 坐標系中的相機的位置、渲染的坐標系原點、以及指向相機上方向量。

fn lookat(eye: &Vertex<f32>, center: &Vertex<f32>, up: &Vertex<f32>) -> Matrix {
let z = (eye - center).normalize();
let x = (*up ^ z).normalize();
let y = (z ^ x).normalize();
let mut m = Matrix::identity(4);
let mut tr = Matrix::identity(4);
for i in 0..3 {
m[(i, 0)] = x[i];
m[(i, 1)] = y[i];
m[(i, 2)] = z[i];
tr[(0, 3)] = -center.x;
}
let m_inv = m.transpose();
m_inv * tr
}

因為我們假定 up 向量並不與視線垂直,它只是一個大致的方位。因此我們需要計算出一組互相垂直的向量 x,y,z ,來構成新的坐標系。這裡的 ^ 是向量的外積運算符。我們在計算時還要求 x,y,z 是歸一化的,這是為了方便我們計算 M1M^{-1},因為歸一化的正交矩陣的逆矩陣就是它的轉置矩陣。

我們讓 tr 矩陣的第四個元素是相機坐標的相反數,因為我們的新坐標系是以相機為原點的。

tr[xyz1]=[100ex010ey001ez0001][xyz1]=[xexyeyzez1]tr \begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix}= \begin{bmatrix} 1&0&0&-e_x\\ 0&1&0&-e_y\\ 0&0&1&-e_z\\ 0&0&0&1 \end{bmatrix} \begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix}= \begin{bmatrix} x-e_x\\ y-e_y\\ z-e_z\\ 1 \end{bmatrix}