後乘?右乘?


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

後乘?右乘?

因此,為了要能達到想要的轉換結果,就要留意轉換操作的順序,例如,在〈轉換矩陣〉中最後的範例,想要看到方塊逆時針轉動,必須先平移方塊,再將方塊繞著 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),也就是下個轉換操作矩陣在前,或者說是左邊。

前乘並不是 OpenGL 的慣例,因為 WebGL 衍生自 OpenGL,WebGL 的矩陣處理程式庫,慣例上不會採取前乘的方式。

因為以方才的 translatescale 等實作方式而言,相當於方才的範例直接用 multiply的話會是:

...
transformation = mat4.multiply(mat4.translation(0.25, 0, 0), transformation);

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

    ...
}        

就程式碼閱讀的順序是,一開始有個單位矩陣,接著乘上移動矩陣 T,再來乘上旋轉矩陣 R,也就是 T * R,然而,這個 OpenGL/WebGL 的設計上認為,這閱讀順序不符合實際上需要的矩陣公式 R * T

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

如果打算採用後乘的慣例,就目前我們完成的程式庫來說,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);                
}

可以點選完整的範例網頁來看看結果,如果你將 translatescale 等展開為使用 multiply,就程式碼的外觀而言,就像是 Rz * T * Rx * Ry,就像是在寫矩陣公式。

從另一個角度來看,你也可以將改寫後的 translatescale 等,看成是改變全域座標系統,以上例來說,你是在原點畫出一個方塊:

renderer.render(cube);

轉動整個座標系統的 y 軸得到每個頂點的新頂點座標:

transformation = mat4.yRotate(transformation, 0.5);
renderer.uniformMatrix4fv('transformation', transformation);

renderer.render(cube);

轉動整個座標系統的 x 軸得到每個頂點的新頂點座標:

transformation = mat4.xRotate(transformation, 0.5);
transformation = mat4.yRotate(transformation, 0.5);
renderer.uniformMatrix4fv('transformation', transformation);

renderer.render(cube);

移動整個座標系統得到每個頂點的新頂點座標:

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.render(cube);

接著再轉動整個座標系統的 z 軸得到每個頂點的新頂點座標:

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.render(cube);

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