對於要繪製的對象,先平移再將之旋轉,與先旋轉將再之平移,結果是不同的:
因此,為了要能達到想要的轉換結果,就要留意轉換操作的順序,例如,在〈轉換矩陣〉中最後的範例,想要看到方塊逆時針轉動,必須先平移方塊,再將方塊繞著 Z 軸轉動,範例的程式片段是這麼撰寫的:
...
transformation = mat4.translate(transformation, 0.25, 0, 0); // 先平移
let i = 0;
function drawCube() {
i++;
renderer.uniformMatrix4fv('transformation',
mat4.zRotate(transformation, 0.025 * i) // 再旋轉
);
...
}
不過,程式碼可以使用這樣的順序來撰寫,是因為 translate
、scale
、xRotate
、yRotate
、zRotate
是這麼撰寫的:
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 的矩陣處理程式庫,慣例上不會採取前乘的方式。
因為以方才的 translate
、scale
等實作方式而言,相當於方才的範例直接用 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
,想要進行轉換操作時,是將下一個轉換操作矩陣會是放在後面,或者說是右邊。
如果打算採用後乘的慣例,就目前我們完成的程式庫來說,translate
、scale
、xRotate
、yRotate
、zRotate
可以改寫為:
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);
}
可以點選完整的範例網頁來看看結果,如果你將 translate
、scale
等展開為使用 multiply
,就程式碼的外觀而言,就像是 Rz * T * Rx * Ry
,就像是在寫矩陣公式。
從另一個角度來看,你也可以將改寫後的 translate
、scale
等,看成是改變全域座標系統,以上例來說,你是在原點畫出一個方塊:
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 的慣例是後乘,在必須與其他工具或程式庫結合時,而它們採用的不是後乘的慣例,會遇上些困擾就是了。