四元數旋轉矩陣


四元數與旋轉的關係,就結論而言,若想繞著指定的特定軸來旋轉頂點,軸的部份使用單位向量 u = (x', y', z') 來表示,旋轉角度為 θ 的話:

四元數旋轉矩陣

若有個四元數 q = w + ai + bj + ck,其中 w、a、b、c 有以下的關係:

s = sin(θ/2)
w = cos(θ/2)
a = s * x'
b = s * y'
c = s * z'

那麼旋轉矩陣會是:

四元數旋轉矩陣

你可以找到許多關於四元數的導證說明,然而,大家最常問的是,四元數到底是什麼?上頭的四元數在 θ 變動時,能不能具體地呈現出來?

就結論而言,四元數是來描述四維空間中的數(這個四維空間有一個實數軸,三個虛數軸),由於大腦在長久演化過程中,已經讓人類的眼睛習於感知、區分三維的資訊,更高維度的資訊在視覺上要能感知、區分有難度(故且別管有的人能通靈這類的事)。

然而,若將三維空間的物件,看成是四維空間的投影,四元數是四維空間中的一個點,投影在三維空間中也只是一個點,就像三維空間中的一個點投影在平面上是個點,平面上的點投影在某軸上也只是個點,沒什麼好畫的!

不過,有如上描述關係的四元數 q = w + ai + bj + ck,在 θ 變動時,其在四維空間中的軌跡,投影在三維世界的資訊會是什麼呢?會是在一個複數平面上旋轉角度 θ 的圓弧,也就是下圖中綠色箭頭的部份:

四元數旋轉矩陣

如上圖中紅色部份表示,這時 θ/2 處是實部軸,與之正交的是虛部軸,也就是說,繞指定軸轉動,可以看成是在與指定軸正交的複數平面上轉動。

(別忘了,這個投影軌跡會是個圓弧,是因為上頭限制了 w、a、b、c 間的關係,不同限制投影出來的軌跡就不會是這樣的圓弧。)

雖然畫不出四元數在四維空間中的軌跡,不過可以畫出它投影在三維空間中的軌跡,這就像是三維空間中的圓弧,投影在 xz 平面上的軌跡會是個橢圓弧,而這個橢圓弧投影在 x 軸上會是個線軌跡。

想像一下自己生活在 xz 平面好了,因為從未生活在三維,就會無法想像三維空間中畫圓弧是什麼概念,然而可以看得到這個圓投影在 xz 平面的樣子。

在這邊我會這麼描繪具有上述關係的四元數,在 θ 變動時軌跡於三維上的投影,是因為四元數為複數的擴充,而複數可以看成是複數平面上的點,複數平面具有一個實部軸與一個些虛部軸,如果能描繪四維空間,那麼四元數所在的空間,會有一個實部軸與三個虛部軸。

大家在學習複數,應該都學過複數的加減乘除等運算,複數為實數的擴充,複數運算不能違反實數的運算,也就是複數間的運算方式,在虛部都為 0 時,必須符合實數的運算;同理,四元數為複數的擴充,在定義複數的運算性質時,像是 Hamilton 刻在石頭上的式子:

四元數旋轉矩陣

以及維基百科上 Quaternion 裏描述的那些運算(上圖出自該條目中的附圖),都不能違反複數(以及實數)的運算。

當複數描繪在複數平面,複數就可以表示為複數平面上的點或向量,四元數將這個概念擴充到三維,既然如此,想要理解四元數與旋轉的關係,自然就得先去瞭解複數在複數平面中旋轉的關係。

那麼為何要用複數來計算旋轉?如果使用直角座標計算,有些幾何上的運算會產生複雜的過程或結果,像是一大堆三角函式的轉換之類,這時可以試著轉換為複數來運算,複數本身包含了角度與長度資訊,而複數運算的一些特性,在處理角度與長度資訊時,可以避開直角座標運算時會遇上的複雜問題。

(這不單只是運用在幾何上,像電路分析時,也會將試著將某個待解的問題轉為複數計算,運算完成後再從複數轉換回想要的答案形式。)

如果還是不太能理解,可以想想看,三維中的一個點,除了可以用直角座標來描述外,也可以用極座標來描述,有的問題用直角座標可以簡單解決,然而有的問題用極座標會比較適合。

如果對於複數不那麼熟悉,想要從它開始認識,並且希望能看懂四元數甚至於導證出旋轉矩陣,可以參考〈四元数与三维旋转〉,在能看懂複數並認識四元數的基本運算性質之後,結合對〈Rodrigues 旋轉公式〉的導證,就可以試著去看懂如何導證旋轉矩陣了。

回到一開始的結論,既然知道了旋轉矩陣,那麼就可以實作出以下的矩陣:

const mat4 = {
    ...

    quatRotation(axis, rad) {
        const s = Math.sin(rad / 2);
        // q = xi + yj + zk + w
        const x = s * axis[0];
        const y = s * axis[1];
        const z = s * axis[2];
        const w = Math.cos(rad / 2);

        const x2 = x + x;
        const y2 = y + y;
        const z2 = z + z;

        const xx = x * x2;
        const yx = y * x2;
        const yy = y * y2;
        const zx = z * x2;
        const zy = z * y2;
        const zz = z * z2;
        const wx = w * x2;
        const wy = w * y2;
        const wz = w * z2;
        return [
            1 - yy - zz, yx + wz, zx - wy, 0,
            yx - wz, 1 - xx - zz, zy + wx, 0,
            zx + wy, zy - wx, 1 - xx - yy, 0,
            0, 0, 0, 1
        ];
    }
};

例如,若有個方塊一開始是在 x 軸上 0.25 位置,繞著底下這個單位向量旋轉:

const z = 1 / Math.sqrt(2);
const axis = [z / Math.sqrt(2), z / Math.sqrt(2), z];

可以如下撰寫:

let i = 0;
function drawCube() {
    i++;                
    const rotation = mat4.quatRotation(axis, 0.025 * i);
    renderer.uniformMatrix4fv('transformation', mat4.translate(rotation, 0.25, 0, 0));

    renderer.clear();
    renderer.bindBuffer(GL.ARRAY_BUFFER, vertBuffer);
    renderer.bufferSubData(GL.ARRAY_BUFFER, 0, geometry.verteices);
    renderer.render(cube);

    requestAnimationFrame(drawCube);                
}

可以察看範例網頁來觀看效果以及完整的程式碼。