後乘?右乘?


對於要繪製的對象,先平移再將之旋轉,與先旋轉將再之平移,結果是不同的:

後乘?右乘?

因此,為了要能達到想要的轉換結果,就要留意轉換操作的順序,例如,在〈轉換矩陣〉中最後的範例,想要看到方塊逆時針轉動,必須先平移方塊,再將方塊繞著 Z 軸轉動,範例的程式片段是這麼撰寫的:

...
transformation = mat4.translate(transformation, 0.25, 0, 0); // 先平移

let i = 0;
function drawCube() {
    i++;
    renderer.uniformMatrix4fv('transformation', 
        mat4.zRotate(transformation, 0.025 * i) // 再旋轉
    );

    ...
}

不過,程式碼可以使用這樣的順序來撰寫,是因為 translatescalexRotateyRotatezRotate 是這麼撰寫的:

translate(m, tx, ty, tz) {
    return this.multiply(this.translation(tx, ty, tz), m);
},

scale(m, sx, sy, sz) {
    return this.multiply(this.scaling(sx, sy, sz), m);
},

xRotate(m, radians) {
    return this.multiply(this.xRotation(radians), m);
},

yRotate(m, radians) {
    return this.multiply(this.yRotation(radians), m);
},

zRotate: function(m, radians) {
    return this.multiply(this.zRotation(radians), m);
}                          

也就是,對於一個舊有的轉換矩陣 m,想要進行轉換操作時,是將下一個轉換操作矩陣乘上 m,這種寫法稱為前乘(Pre-Multiplication)或左乘(Left-Multiplication),也就是下個轉換操作矩陣在前,或者說是左邊。

這麼做的話,如果程式碼撰寫時是 translatezRotate,就會如上頭的說明的方式進行,然而,前乘並不是 OpenGL 的慣例,因為 WebGL 衍生自 OpenGL,WebGL 的矩陣處理程式庫,實際上也不會是採取前乘的方式。

WebGL 的矩陣處理程式庫,慣例上會依循 OpenGL,也就是採用後乘(Post-Multiplication)或右乘(Right-Multiplication),也就是對於一個舊有的轉換矩陣 m,想要進行轉換操作時,是將下一個轉換操作矩陣會是放在後面,或者說是右邊。

由於矩陣乘法具有結合律,就數學公式本身的演算來說,例如 S * R * T * V,前乘或後乘,不過是從後面往前算,或者是從前面往後算,結果都是相同的。

只不過,撰寫程式碼時總究需要個前後順序,而採用前乘時,操作頂點的順序正好符合程式碼上呼叫函式的順序,如〈轉換矩陣〉中的範例就是如此。

然而,如果打算採用後乘的慣例,translatescalexRotateyRotatezRotate 就必須改寫為:

translate(m, tx, ty, tz) {
    return this.multiply(m, this.translation(tx, ty, tz));
},

scale(m, sx, sy, sz) {
    return this.multiply(m, this.scaling(sx, sy, sz));
},

xRotate(m, radians) {
    return this.multiply(m, this.xRotation(radians));
},

yRotate(m, radians) {
    return this.multiply(m, this.yRotation(radians));
},

zRotate: function(m, radians) {
    return this.multiply(m, this.zRotation(radians));
}   

然而,這會造成一個結果,操作頂點的順序與程式碼撰寫上的順序是反過來的,例如想要完成同樣是逆時針轉動的話,就必須寫為:

let zRotation = mat4.create();

function drawCube() {                
    zRotation = mat4.zRotate(zRotation, 0.025);

    let transformation = mat4.translate(zRotation, 0.25, 0, 0);
    transformation = mat4.xRotate(transformation, 0.5);
    transformation = mat4.yRotate(transformation, 0.5);
    renderer.uniformMatrix4fv('transformation', transformation);

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

    requestAnimationFrame(drawCube);                
}

可以點選完整的範例網頁來看看結果,乍看之下撰寫程式碼時,順序似乎很不直覺,其實這是觀點不同,這時在建立轉換矩陣的過程,心智模型應想著數學公式上的右乘順序,不是頂點操作順序,也就是說,心裡想的要是 Rz * T * Rx * Ry,而程式碼的順序對照的是由左至右的順序。

(某些程度上,感覺這就像是函數式設計,畢竟 3D 運算就是數學,你要想著這邊要完成一個什麼轉換矩陣,而不是如何對頂點做操作,也就是函數式設計上常說的,宣告 What,而不是編寫 How。)

當然,若你堅持前乘而不是後乘,也不能說錯,只不過不符合 OpenGL/WebGL 的慣例下,在必須與其他工具或程式庫結合時,而它們採用的是後乘的慣例,會遇上些困擾就是了。