目录
变换矩阵:旋转
变换矩阵:平移
4×4的旋转矩阵
示例代码:
gl.uniformMatrix4fv()规范
平移:相同的策略
变换矩阵:缩放
变换矩阵:旋转
对于简单的变换,你可以使用数学表达式来实现。但是当情形逐渐变得复杂时,你很快就会发现利用表达式运算实际上相当繁琐。比如,下图显示了一个“旋转后平移”的过程,如果使用数学表达式,我们就需要两种变换的等式叠加,获得一个新的等式,然后在顶点着色器中实现。
但是如果这样做,每次都需要进行一次新的变换,我们就需要重新求取一个新的等式,然后实现一个新的着色器,这当然很不科学。好在我们可以使用另一个数学工具——变换矩阵来完成这项工作。变换矩阵非常适合操作计算机图形。
如下图所示,矩阵是一个矩形的二维数组,数字按照行(水平方向)和列(垂直方向)排列,数字两侧的方括号表示这些数字是一个整体(一个矩阵)。我们将使用矩阵来表示前面的计算过程。
在解释如何使用变换矩阵来替代数学表达式之前,你需要理解矩阵和矢量的乘法。矢量就是由多个分量组成的对象,比如顶点的坐标(0.0,0.5,1.0)。
矩阵和矢量的乘法可以写成如下等式一的形式(虽然乘号“×”通常被忽略不写,但是为了强调,本书中我们总是明确地将这个符号写出来)。可见,将矩阵(中间)和矢量(右边)相乘,就获得了一个新的矢量(左边)。注意矩阵的乘法不符合交换律,也就是说,A×B和B×A并不相等。
上式中的这个矩阵具有3行3列,因此又被称为3×3矩阵。矩阵右侧是一个由x、y、z组成的矢量(为了与矢量相乘,矢量被写成列的形式,其仍然表示点的坐标)。矢量具有3个分量,因此被称为三维矢量。再次说明,数字两侧的方括号表示这些数字是一个整体(一个矢量)。
在本例中,矩阵与矢量相乘得到的新矢量,其三个分量为x'、y'、z',其值如下等式二所示。注意,只有在矩阵的列数与矢量的行数相等时,才可以将两者相乘。
x'=ax+by+cz
y'=dx+ey+fz
z'=gx+hy+iz
现在,为了理解矩阵是如何代替数学表达式的,下面将矩阵等式与数学表达式(如下等式三,即WebGL非矩阵变换_山楂树の的博客-CSDN博客 中的等式R4)进行比较。
等式三:
x' = x cosβ - y sinβ
y' = x sinβ + y cosβ
z' = z
与比较关于x'的表达式进行比较:
x'=ax+by+cz
x'=x cos β-y sin β
这样的话,如果设a=cosβ,b=-sinβ,c=0,那么这两个等式就完全相同了。再来看一下y':
y'=dx+ey+fz
y'=x sin β+y cos β
这样的话,设d=sinβ,e=cosβ,f=0,两个等式也就完全相同了。最后的关于z'的等式更简单,设g=0,h=0,i=1即可。
接下来,将这些结果代入到等式一中,得到等式四:
这个矩阵就被称为变换矩阵(transformation matrix),因为它将右侧的矢量(x,y,z)“变换”为了左侧的矢量(x',y',z')。上面这个变换矩阵进行的变换是一次旋转,所以这个矩阵又可以被称为旋转矩阵
可以看到,等式三中矩阵的元素都是等式二中的系数。一旦你熟悉这种矩阵表示法,进行变换就变得非常简单了。变换矩阵的概念在三维图形学中非常重要。
变换矩阵在三维计算机图形学中应用得如此广泛,以致于着色器本身就实现了矩阵和矢量相乘的功能。但是,在我们修改着色器代码以采用矩阵之前,先来快速浏览一遍(除了旋转矩阵的)其他几种变换矩阵。
变换矩阵:平移
显然,如果我们使用变换矩阵来表示旋转变换,我们就也应该使用它来表示其他变换,比如平移。比较一下等式二和平移的数学表达式,如下所示:
这里第二个等式的右侧有常量项Tx,第一个等式中没有,这意味着我们无法通过使用一个3×3的矩阵来表示平移。为了解决这个问题,我们可以使用一个4×4的矩阵,以及具有第4个分量(通常被设为1.0)的矢量。也就是说,我们假设点p的坐标为(x,y,z,1),平移之后的点p'的坐标为(x',y',z',1),如等式等式五所示:
该矩阵的乘法的结果如下等式六:
x'=ax+by+cz +d
y'=ex+fy+gz+h
z'=ix+jy+kz + l
1=mx+ny+oz+ p
根据最后一个式子1=mx+ny+oz+p,很容易求算出系数m=0,n=0,o=0,p=1。这些方程都有常数项d、h、l和p,看上去比较适合平移等式(因为平移等式也有常数项)。平移等式如下所示,我们将它与等式六进行比较:
x'=x+Tx
y'=y+Ty
z'=z+Tz
比较x',可知a=1,b=0,c=0,d=Tx;类似地,比较y',可知e=0,f=1,g=0,h=Ty;比较z',可知i=0,j=0,k=1,l=Tz。这样,你就可以写出表示平移的矩阵,又称为平移矩阵,如等式七所示:
4×4的旋转矩阵
至此,我们已经成功地创建了一个旋转矩阵和一个平移矩阵,这两个矩阵的作用与此前示例程序中的数学表达式的作用是一样的,那就是计算变换后的顶点坐标。在“先旋转再平移”的情形下,我们需要将两个矩阵组合起来(你应该记得,这也是我们使用矩阵的初衷),然而旋转矩阵(3×3矩阵)与平移矩阵(4×4矩阵)的阶数不同。我们不能把两个阶数不一样的矩阵组合起来,所以得使用某种手段,使这两个矩阵的阶数一致。
将旋转矩阵从一个3×3矩阵转变为一个4×4矩阵,只需要将等式三和等式六比较一下即可。
x'=x cos β-y sin βy'=x sin β+y cos β
z'=z
x'=ax+by+cz+d
y'=ex+fy+gz+h
z'=ix+jy+kz+l
1=mx+ny+oz+p
例如,当你通过比较x'=x cosβ- y sinβ与x'=ax+by+cz+d时,可知a=cosβ,b=-sinβ,c=0,d=0。以此类推,求得y'和z'等式中的系数,最终得到4×4的旋转矩阵,如等式八所示:
这样,我们就可以使用相同阶数(4×4)的矩阵来表示平移和旋转,实现了最初的目标!
示例代码:
在创建了4×4的旋转矩阵之后,我们使用旋转矩阵来重写之前的示例程序,令三角形绕Z轴逆时针旋转90度。例如下显示了本例的代码,其运行结果与 WebGL非矩阵变换_山楂树の的博客-CSDN博客 的旋转实例完全一致。
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_xformMatrix;\n' +
'void main() {\n' +
' gl_Position = u_xformMatrix * a_Position;\n' +
'}\n';
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
'}\n';
var ANGLE = 90.0;
// var tx = 0.5, ty = 0.5, tz=0 平移矩阵所用
function main() {
var canvas = document.getElementById('webgl');
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
var radian = Math.PI * ANGLE / 180.0; // Convert to radians
var cosB = Math.cos(radian), sinB = Math.sin(radian);
// 旋转矩阵
var xformMatrix = new Float32Array([
cosB, sinB, 0.0, 0.0,
-sinB, cosB, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
]);
// 平移矩阵
// var xformMatrix = new Float32Array([
// 1.0, 0.0, 0.0, 0.0,
// 0.0, 1.0, 0.0, 0.0,
// 0.0, 0.0, 1.0, 0.0,
// tx, ty, tz, 1.0
// ]);
var u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix');
if (!u_xformMatrix) {
console.log('Failed to get the storage location of u_xformMatrix');
return;
}
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, n);
}
function initVertexBuffers(gl) {
var vertices = new Float32Array([
0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n = 3; // The number of vertices
var vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
console.log('Failed to create the buffer object');
return false;
}
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position');
return -1;
}
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
return n;
}
首先来看看顶点着色器:
u_xformMatrix变量表示等式八中的旋转矩阵,a_Position变量表示顶点的坐标(即等式八中右侧的矢量),二者相乘得到变换后的顶点坐标,与等式八中相同。
示例程序中,你可以在一行代码中完成矢量相加的运算(gl_Position=a_Position+u_Translate)。同样,你也可以在一行代码中完成矩阵与矢量相乘的运算(gl_Position=u_xformMatrix * a_Position)。这时因为着色器内置了常用的矢量和矩阵运算功能,这种强大特性正是专为三维计算机图形学而设计的。
由于变换矩阵是4×4的,GLSL ES需要知道每个变量的类型,所以我们将u_xformMatrix定义为mat4类型。如你所料,mat4类型的变量就是4×4的矩阵。
JavaScript按照等式八计算旋转矩阵,然后将其传给u_xformMatrix。
这段代码首先计算了90度的正弦值和余弦值这两个值需要被用来构建旋转矩阵;之后创建了Float32Array类型的xformMatrix变量表示旋转矩阵。与GLSL ES不同,JavaScript并没有专门表示矩阵的类型,所以你需要使用类型化数组Float32Array。我们在数组中存储矩阵的每个元素,但问题是:矩阵是二维的,其元素按照行和列进行排列,而数组是一维的,其元素只是排成一行。这里,我们可以按照两种方式在数组中存储矩阵元素:按行主序(row major oder)和按列主序(column major order),如下图所示。
WebGL和OpenGL一样,矩阵元素是按列主序存储在数组中的。比如,图3.27所示的矩阵存储在数组中就是这样的:[a, e, i, m, b, f, j, n, c, g, k, o, d, h, l, p] 。本例中,旋转矩阵也是按照这样的顺序存储在Float32Array类型的数组中的。
最后,我们使用gl.uniformMatrix4fv()函数,将刚刚生成的数组传给u_xformMatrix变量。注意,函数名的最后一个字母是v,表示它可以向着色器传输多个数据值。
gl.uniformMatrix4fv()规范
平移:相同的策略
如你所见,4×4的矩阵不仅可以用来表示平移,也可以用来表示旋转。不管是平移还是旋转,你都使用如下形式来进行矩阵和矢量的运算以完成变换:<新坐标>=<变换矩阵> * <旧坐标>,比如在着色器中:
这意味着,如果我们改变数组xformMatrix中的元素,使之成为一个平移矩阵,那么就可以实现平移操作,其效果就和之前使用数学表达式进行的平移操作一样。
因此,修改4x4旋转矩阵代码,将旋转角度修改为与平移相关的变量:
我们还需重写创建矩阵的代码,记住,矩阵是按列主序存储的。虽然xformMatrix现在是一个平移矩阵了,但我们仍使用这个变量名。因为对于着色器而言,旋转矩阵和平移矩阵其实是一回事。最后,你不会用到ANGLE变量,把与旋转相关的代码注释掉:
变换矩阵:缩放
最后,我们来学习缩放变换矩阵。仍然假设最初的点p,经过缩放操作之后变成了p'。
假设在三个方向X轴,Y轴,Z轴的缩放因子S
x'=S× x
y'=Sy y
z'=Sz z
将上式与等式六作比较,可知缩放操作的变换矩阵:
和之前的例子一样,我们只要将缩放矩阵传给xformMatrix变量,就可以直接使用4x4旋转矩阵中的着色器对三角形进行缩放操作了。下面这个示例程序会将三角形在垂直方向上拉伸到1.5倍,如图所示。