Book of Shaders 02 - 矩陣:二維仿射變換練習

knxfe發表於2020-09-17

0x00 一些廢話

如果要深入學習 CG (Computer Graphics,計算機圖形學),必然要學習相關的數學知識。CG 涉及到多個不同的領域,根據所研究領域的不同,也會涉及到不同的數學分支。但其中一定少不了線性代數的身影。凡涉及空間中的幾何表示與操作,都離不開線性代數的主要研究物件:矩陣。


上圖出自電影:The Matrix

以頂點著色器為例,頂點著色器如何完成座標變換的工作?如果知道矩陣的知識,就會有這樣的意識:座標變換用矩陣來計算是最合適不過的。我們可以利用矩陣簡潔地描述幾何體的變換,例如縮放、旋轉和平移。除此之外,還可以藉助矩陣將點或向量的座標在不同的標架之間進行轉換。即,座標變換。

本文不談論頂點著色器中座標變換的具體細節,也不談論有關矩陣的過多細節,是以快速回顧並熟悉矩陣的仿射變換為目的。

0x01 矩陣乘法

一個規模為 m x n 的矩陣 (matrix) M,是由 m 行 n 列實數所構成的矩形陣列。對於參與矩陣乘法的兩個矩陣 A 和 B,如果 A 的規模為 m x n,則 B 的規模須為 n x p。即,A 的行向量的維數與 B 的列向量的維數要一致。兩者乘積 AB 的結果是一個規模為 m x p 的矩陣 C。C 中第 i 行、第 j 列的元素,由矩陣 A 的第 i 個行向量與矩陣 B 的第 j 個列向量的點積求得。

例如,

\({\begin{bmatrix} 1 & 2 \\ 3 & 4 \\ 5 & 6 \end{bmatrix}}{\begin{bmatrix} a & b \\ c & d \end{bmatrix}}={\begin{bmatrix} A & B \\ C & D \\ E & F \end{bmatrix}}\)

上式等號右邊的矩陣,其中的 B 位於該矩陣的第 1 行、第 2 列。所以,應由乘式左邊向量的第 1 個行向量:\({\begin{pmatrix} 1 & 2 \end{pmatrix}}\),與乘式右邊向量的第 2 個列向量:\({\begin{pmatrix} b & d \end{pmatrix}}\),兩者的點積所得。即,\(B = 1 \times b + 2 \times d\)

需要注意的是,矩陣乘法不滿足交換律。因為,A 和 B 交換後,B 的行向量的維數 p 與 A 的列向量的維數 m 並不能保證相等。

0x02 仿射變換

仿射變換就是線性變換加上平移。

什麼是線性變換?線性變換需要滿足兩點:一是,直線在變換後仍然保持為直線,不能有所彎曲;二是,原點必須保持固定。線性變換包括:縮放、翻轉、錯切、旋轉。下面是這幾個操作的變換矩陣。這裡假設了所有操作都是在二維空間完成的上,變換前的點記為 \((x, y)\),變換後的點記為 \((x^{'}, y^{'})\)

縮放:\({\begin{bmatrix} x^{'} \\ y^{'} \end{bmatrix}}={\begin{bmatrix} s & 0 \\ 0 & s \end{bmatrix}}{\begin{bmatrix} x \\ y \end{bmatrix}}\)

翻轉:縮放的一種,當縮放值為負就可以達到翻轉的效果,

錯切:\({\begin{bmatrix} x^{'} \\ y^{'} \end{bmatrix}}={\begin{bmatrix} 1 & a \\ 0 & 1 \end{bmatrix}}{\begin{bmatrix} x \\ y \end{bmatrix}}\)

旋轉:\({\begin{bmatrix} x^{'} \\ y^{'} \end{bmatrix}}={\begin{bmatrix} \cos{\Theta} & -\sin{\Theta} \\ \sin{\Theta} & \cos{\Theta} \end{bmatrix}}{\begin{bmatrix} x \\ y \end{bmatrix}}\)

為什麼平移不屬於線性變換呢?原因是平移操作後,原點的位置會發生了改變。這種情況下,平移也無法寫成矩陣乘法的形式。

加法:\({\begin{bmatrix} x^{'} \\ y^{'} \end{bmatrix}}={\begin{bmatrix} x \\ y \end{bmatrix}}+{\begin{bmatrix} t_x \\ t_y \end{bmatrix}}\)

線性變換總是將原點對映到原點,因此無法用來呈現平移。不過,我們可以在原空間維度 n 的基礎上增加 1 個維度。這樣,原空間的平移操作,可以藉由更高維度空間中的切變操作來完成。

平移:\({\begin{bmatrix} x^{'} \\ y^{'} \\ w^{'} \end{bmatrix}}={\begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}}{\begin{bmatrix} x + t_x \\ y + t_y \\ 1 \end{bmatrix}}\)

根據矩陣乘法不滿足交換律的性質,我們還可以推斷出一個資訊:變換有時序。先平移再旋轉和先旋轉再平移,得到的結果是不同的。

0x03 變換練習

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform float u_time;

// 縮放
mat3 scale(vec2 scl) {
    return mat3(scl.x, 0.0, 0.0,
                0.0, scl.y, 0.0,
                0.0, 0.0, 1.0);
}

// 錯切
mat3 shear(vec2 shr) {
    return mat3(1.0, shr.y, 0.0,
                shr.x, 1.0, 0.0,
                0.0, 0.0, 1.0);
}

// 旋轉
mat3 rotate(float angle) {
    return mat3(cos(angle), -sin(angle), 0.0,
                sin(angle), cos(angle), 0.0,
                0.0, 0.0, 1.0);
}

// 平移
mat3 translate(vec2 pos) {
    return mat3(1.0, 0.0, 0.0,
                0.0, 1.0, 0.0,
                pos.x, pos.y, 1.0);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;

    vec3 color = vec3(0.0);
    vec3 p3 = vec3(st, 1.0);

    mat3 t = scale(vec2(2.0));
    t *= translate(vec2(0.25, 0.25));
    t *= shear(vec2(-0.5, 0.0));
    t *= rotate(u_time);
    t *= translate(vec2(-0.5, -0.5));
    st = (t * p3).xy;

    // 畫個正方形
    vec2 bl = step(vec2(0.0), st);
    vec2 tr = step(vec2(0.0), 1.0 - st);
    float p = bl.x * bl.y * tr.x * tr.y;
    color = vec3(p * st, 0.4);

    gl_FragColor = vec4(color, 1.0);
}

效果:

0x04 一些說明

需要注意的是 OpenGL 的矩陣以列為主。即:

mat2(a, b, //第一列
     c, d) //第二列

另外,在上面的變換練習中,是對整個座標系進行變換的,而非繪製的圖形本身。

參考資料:

相關文章