我們經常會對物體做一些變換,比如放大縮小,移動位置。其中例如原地旋轉,尺寸縮放,甚至歪斜這類不改變物體原點的變換,被叫做線性變換。線性變換都可以用矩陣乘上原式數據來計算。
比如在平面上,我們要把一組數據 ( x , y ) (x, y) ( x , y ) 放大 3/2 倍,我們可以這樣表示:
[ 3 / 2 0 0 3 / 2 ] [ x y ] = [ 3 / 2 x 3 / 2 y ] \begin{bmatrix}
3/2 & 0\\
0 & 3/2
\end{bmatrix}
\begin{bmatrix}
x\\
y
\end{bmatrix}=
\begin{bmatrix}
3/2x\\
3/2y
\end{bmatrix} [ 3/2 0 0 3/2 ] [ x y ] = [ 3/2 x 3/2 y ]
而旋轉的變換可以用三角函數相關的矩陣來表示,比如我們想要旋轉 θ \theta θ 度:
[ cos θ − sin θ sin θ cos θ ] [ x y ] = [ x cos θ − y sin θ x sin θ + y cos θ ] \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} [ cos θ sin θ − sin θ cos θ ] [ x y ] = [ x cos θ − y sin θ x sin θ + y cos θ ]
如果我們想要將這些線性變換組合在一起,我們可以把這些矩陣相乘。比如我們先放大 3/2 倍,然後再旋轉 θ \theta θ 度:
[ cos θ − sin θ sin θ cos θ ] [ 3 / 2 0 0 3 / 2 ] [ x y ] = [ 3 / 2 x cos θ − 3 / 2 y sin θ 3 / 2 x sin θ + 3 / 2 y cos θ ] \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} [ cos θ sin θ − sin θ cos θ ] [ 3/2 0 0 3/2 ] [ x y ] = [ 3/2 x cos θ − 3/2 y sin θ 3/2 x sin θ + 3/2 y cos θ ]
像平移這樣的變換,就不是用矩陣乘來表示,而是用加法:
[ x y ] + [ t x t y ] = [ x + t x y + t y ] \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 ] + [ t x t y ] = [ x + t x y + t y ]
這樣我們就把 ( x , y ) (x,y) ( x , y ) 平移了 ( t x , t y ) (t_x,t_y) ( t x , t y ) 。這類不是用乘法的變換則是非線性變換。
如果此時,我對物品的一系列變換的組合感興趣,其中包含線性或非線性。比如我想要對一個物體放大、平移、旋轉,再放大,再平移:
[ x ′ y ′ ] = [ 3 / 2 0 0 3 / 2 ] [ cos θ − sin θ sin θ cos θ ] ( [ 3 / 2 0 0 3 / 2 ] [ x y ] + [ t x t y ] ) + [ t x ′ t y ′ ] \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} [ x ′ y ′ ] = [ 3/2 0 0 3/2 ] [ cos θ sin θ − sin θ cos θ ] ( [ 3/2 0 0 3/2 ] [ x y ] + [ t x t y ] ) + [ t x ′ t y ′ ]
僅僅是這樣,我們的表達式已經開始變得混亂。但如果這樣的操作要嵌套幾十層,那麼我們的表達式會複雜的難以想象。我們希望盡可能的只用乘法來表示所有的變換,這樣我們的式子就能簡潔很多。
齊次坐標
對於矩陣加法來說,我們可以寫成:
[ x y ] + [ t x t y ] = [ 1 [ t x t y ] ] [ [ x y ] 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} [ x y ] + [ t x t y ] = [ 1 [ t x t y ] ] [ x y ] 1
如果我們把它寫開變成 3 維:
[ 1 0 t x 0 1 t y 0 0 1 ] [ x y 1 ] = [ x + t x y + t y 1 ] \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} 1 0 0 0 1 0 t x t y 1 x y 1 = x + t x y + t y 1
原本我們在 2 維時的非線性平移變換,增加一個維度變成 3 維後,就可以用矩陣乘法來表示了。這樣的增加維度的方法,就是齊次坐標。我們可以把 2 維的點 ( x , y ) (x,y) ( x , y ) 變成 3 維的點 ( x , y , 1 ) (x,y,1) ( x , y , 1 ) ,這樣我們就可以用矩陣乘法來表示平移了。
這種通過增加一個維度的方法就是齊次坐標。我們規定,三維中一個點 ( x , y , z ) (x,y,z) ( x , y , z ) 的齊次坐標是 ( x , y , z , 1 ) (x,y,z,1) ( x , y , z , 1 ) 。而如果我們要把一個齊次坐標 ( x , y , z , w ) (x,y,z,w) ( x , y , z , w ) 變回普通坐標,我們可以這樣做:
[ x y z w ] ⟹ [ x / w y / w z / w ] \begin{bmatrix}
x\\
y\\
z\\
w
\end{bmatrix}\implies
\begin{bmatrix}
x/w\\
y/w\\
z/w
\end{bmatrix} x y z w ⟹ x / w y / w z / w
也就是說,在齊次坐標中,∀ a ≠ 0 \forall a \neq 0 ∀ a = 0
[ a x a y a z a w ] = [ x / w y / w z / w ] \begin{bmatrix}
ax\\
ay\\
az\\
aw
\end{bmatrix}=
\begin{bmatrix}
x/w\\
y/w\\
z/w
\end{bmatrix} a x a y a z a w = x / w y / w z / w
而當 w = 0 w=0 w = 0 時,我們就認為這個點是無窮遠的。
當我們想要做任何變換時,只需要把原本的坐標轉成齊次坐標,變換後再把齊次坐標轉回普通坐標。這樣我們就可以用矩陣乘法來表示所有的變換了。
我們可以把上次透視投影的變換用齊次坐標來表示。可以發現,x 和 y 的變換是一樣的,都是除上 1 − z / c 1-z/c 1 − z / c 。根據齊次坐標的轉換規則,我們會希望第四個維度表示成 1 − z / c 1-z/c 1 − z / c 。所以我們可以把透視投影的變換寫成:
[ 1 0 0 0 0 1 0 0 0 0 1 0 0 0 − 1 / c 1 ] [ x y z 1 ] = [ x y z 1 − z / c ] ⟹ [ x / ( 1 − z / c ) y / ( 1 − z / c ) z / ( 1 − z / 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} 1 0 0 0 0 1 0 0 0 0 1 − 1/ c 0 0 0 1 x y z 1 = x y z 1 − z / c ⟹ x / ( 1 − z / c ) y / ( 1 − z / c ) z / ( 1 − z / c )
這裡我們同樣保留 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 ] ) ) ) , ] ;
其中 m2v
和 v2m
齊次坐標和普通坐標的轉換函數。
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 向量可以表示為:
O P ⃗ = i ⃗ x + j ⃗ y + k ⃗ z [ i ⃗ j ⃗ k ⃗ ] [ x y z ] \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} OP = i x + j y + k z [ i j k ] x y z
如果我們有另一套坐標系 (O',i',j',k'),P 點在這套坐標系中的坐標是 (x',y',z'),如果我們還知道 O' 相對於 O 的位置,即在 O 坐標系中 OO' 向量:
那麼我們可以把 OP 向量重新表示為:
O P ⃗ = O O ′ ⃗ + O ′ P ⃗ = [ i ⃗ j ⃗ k ⃗ ] [ O x ′ O x ′ O z ′ ] + [ i ′ ⃗ j ′ ⃗ k ′ ⃗ ] [ x ′ y ′ z ′ ] \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} OP = O O ′ + O ′ P = [ i j k ] O x ′ O x ′ O z ′ + [ i ′ j ′ k ′ ] x ′ y ′ z ′
因為 ( i , j , k ) (i,j,k) ( i , j , k ) 和 ( i ′ , j ′ , k ′ ) (i',j',k') ( i ′ , j ′ , k ′ ) 都是基底,是從原點出發的向量。所以存在一個矩陣 M M M ,使得:
[ i ′ ⃗ j ′ ⃗ k ′ ⃗ ] = [ i ⃗ j ⃗ k ⃗ ] M \begin{bmatrix}
\vec{i'} & \vec{j'} & \vec{k'}
\end{bmatrix}=
\begin{bmatrix}
\vec{i} & \vec{j} & \vec{k}
\end{bmatrix}
M [ i ′ j ′ k ′ ] = [ i j k ] M
因此
O P ⃗ = [ i ⃗ j ⃗ k ⃗ ] ( [ O x ′ O x ′ O z ′ ] + M [ x ′ y ′ z ′ ] ) \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) OP = [ i j k ] ( O x ′ O x ′ O z ′ + M x ′ y ′ z ′ )
也就是說
[ x y z ] = [ O x ′ O x ′ O z ′ ] + M [ x ′ y ′ z ′ ] ⟹ [ x ′ y ′ z ′ ] = M − 1 ( [ x y z ] − [ O x ′ O x ′ O z ′ ] ) \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) x y z = O x ′ O x ′ O z ′ + M x ′ y ′ z ′ ⟹ x ′ y ′ z ′ = M − 1 ( x y z − O x ′ O x ′ O z ′ )
移動相機
如果我們想要觀察一個物體或者世界的不同角度,我們可以移動相機。但我們也可以反過來移動物體,讓相機固定在原地。比如我想要通過相機觀察物體的右側,那我可以把物體向相反的方向,也就是左側移動。
我們假設相機的位置在 e e e 點,並且面向 c c c 點,而 u ⃗ \vec{u} 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
是歸一化的,這是為了方便我們計算 M − 1 M^{-1} M − 1 ,因為歸一化的正交矩陣的逆矩陣就是它的轉置矩陣。
我們讓 tr
矩陣的第四個元素是相機坐標的相反數,因為我們的新坐標系是以相機為原點的。
t r [ x y z 1 ] = [ 1 0 0 − e x 0 1 0 − e y 0 0 1 − e z 0 0 0 1 ] [ x y z 1 ] = [ x − e x y − e y z − e z 1 ] 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} t r x y z 1 = 1 0 0 0 0 1 0 0 0 0 1 0 − e x − e y − e z 1 x y z 1 = x − e x y − e y z − e z 1