目标:对三角形进行平移、旋转、缩放
现在,你已经掌握了绘制图形的方法。让我们更进一步,尝试移动、旋转和缩放三角形,然后在屏幕上绘制出来。这样的操作称为变换或仿射变换。
首先,让我们编写第一个示例程序 TranslatedTriangle,该程序将上一节中的三角形向右和向上各移动了0.5个单位。
考虑一下,为了平移一个三角形,你需要对它的每一个顶点做怎样的操作?答案是你需要对顶点坐标的每个分量(x和y),加上三角形在对应轴(如X轴和Y轴)上平移的距离。比如,将点p(x, y, z)平移到p’(x’, y’, z’),在X轴、Y轴、Z轴三个方向上平移的距离分别为 Tx, Ty, Tz,其中 Tz 为0,如下图所示:
那么在坐标的对应分量上,直接加上这些T值,就可以确定 p’ 的坐标了,如下表:
我们只需要着色器为顶点坐标的每个分量加上一个常量就可以实现上面的等式。显然,这是一个逐顶点操作而非逐片元操作,上述修改因当发生在顶点着色器,而不是片元着色器。
一旦你理解了这一点,修改代码就很简单了,将平移距离Tx、Ty、Tz的值传入顶点着色器,然后分别加在顶点坐标的对应分量上,再赋值给gl_Postion。
//顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'uniform vec4 u_Translation;'+
'void main(){'+
'gl_Position=a_Position + u_Translation;'+
'}';
//片元着色器程序
var FSHADER_SOURCE=
'void main(){'+
'gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);'+
'}';
var Tx = 0.5, Ty = 0.5, Tz = 0.0;
function main() {
//获取canvas元素
var canvas = document.getElementById("webgl");
if(!canvas){
console.log("Failed to retrieve the );
return;
}
//获取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 initialize shaders.");
return;
}
//设置顶点位置
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
//将平移距离传输给顶点着色器
var u_Translation = gl.getUniformLocation(gl.program, 'u_Translation');
if(u_Translation < 0){
console.log("Failed to get the storage location of u_Translation");
return;
}
gl.uniform4f(u_Translation, Tx, Ty, Tz, 0.0);
//指定清空
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//清空
gl.clear(gl.COLOR_BUFFER_BIT);
//绘制三个点
gl.drawArrays(gl.TRIANGLES, 0, n);
}
function initVertexBuffers(gl) {
var vertices = new Float32Array([
0.0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n=3; //点的个数
//创建缓冲区对象
var vertexBuffer = gl.createBuffer();
if(!vertexBuffer){
console.log("Failed to create thie buffer object");
return -1;
}
//将缓冲区对象保存到目标上
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;
}
//将缓冲区对象分配给a_Postion变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
//连接a_Postion变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
return n;
}
首先,main()函数中定义了正三角形在各轴方向上的平移距离:
var Tx = 0.5, Ty = 0.5, Tz = 0.0;
因为Tx、Ty、Tz对于所有顶点来说是固定的,所以我们使用 uniform 变量 u_Translation 来表示三角形的平移距离。首先,获取 uniform 变量的存储位置,然后将数据传给着色器:
//将平移距离传输给顶点着色器
var u_Translation = gl.getUniformLocation(gl.program, 'u_Translation');
if(u_Translation < 0){
console.log("Failed to get the storage location of u_Translation");
return;
}
gl.uniform4f(u_Translation, Tx, Ty, Tz, 0.0);
注意,gl.uniform4f()函数需接受齐次坐标,所以我们把最后一个参数设为0.0。这么做的具体原因将在稍后讨论。
现在来看一下修改后的顶点着色器:如你所见,我们新定义了 uniform 变量 u_Translation,用来接受了三角形在各轴方向上的评议距离。该变量的类型是vec4,这样它就可以与 vec4 类型的顶点坐标 a_Position 直接相加,然后赋值给同样是 vec4 类型的 gl_Position。
'attribute vec4 a_Position;'+
'uniform vec4 u_Translation;'+
'void main(){'+
'gl_Position=a_Position + u_Translation;'+
'}';
在做完准备工作之后,我们就直奔主题:在顶点着色器中,为 a_Position 变量的每个分量(x, y, z)加上 u_Translation 变量中对应的平移距离(Tx, Ty, Tz),并赋值给 gl_Postion。
因为 a_Position 和 u_Translation 变量都是vec4类型的,所以你可以直接使用 + 号,两个的矢量的对应分量会被同时相加,如下图所示:
最后,解释一下齐次坐标矢量的最后一个分量w。如第2章所述,gl_Position是其次坐标,具有4个分量。如果齐次坐标的最后一个分量是 1.0,那么它的前三个分量就可以表示一个点的三维坐标。在本例中,平移后点坐标第4分量 w1+w2 必须是1.0,而 w1 是1.0,所以平移矢量本身的第4分量 w2 只能是0.0,这就是为什么 gl.uniform4f()的最后一个参数为0.0。
最后调用 gl.drawArrays(gl.TRIANGLES, 0, n)执行顶点着色器,每次执行都会进行以下3步:
旋转比平移稍微复杂一些,因为描述一个旋转本身就比描述一个平移复杂。为了描述一个旋转,你必须指明:
在不节中我们这样来旋转操作:绕Z轴,逆时针旋转了β角度。这种表述方式同样适用于绕X轴和Y轴的情况。
在旋转中,关于“逆时针”的约定是:如果 β 是正值,观察在Z轴正半轴某处,实现沿着Z轴负方向进行观察,那么看到的物体是逆时针旋转的,如下图所示。这种情况又称为 正旋转。我们也可以使用右手来确认旋转方向:右手握拳,大拇指伸直并使其指向旋转轴的正方向,那么右手其余几个手指就指明了旋转方向,因此证旋转又可以成为 右手法则旋转。
之前我们计算了平移的数学表达式,现在来看旋转的数学表达式。如下图,假设点p(x, y, z)旋转 β 交付之后变成了点p’(x’, y’, z’):首先旋转是绕Z轴进行的,所以z坐标不会变,可以直接忽略;然后x坐标和y坐标的情况有一些复杂。
在上图中,r 是从原点到点 p 的距离,而 α 是 X 轴旋转到 p 的角度。用着两个变量计算出点p的坐标:
类似的,也可以使用r、α、β 来表示点 p’ 的坐标:
利用三角函数两角和公式,可得:
最后,可得
我们可以把 sinβ 和 cosβ 的值传给顶点着色器,然后在着色器中根据等式计算旋转后的点坐标,就可以实现旋转这个点的效果了。
RotatedTriangle.js
//顶点着色器程序
var VSHADER_SOURCE =
//x' = x cosb - y sinb;
//y' = x sinb - y cosb
//z' = z
'attribute vec4 a_Position;'+
'uniform float u_CosB, u_SinB;'+
'void main(){'+
'gl_Position.x = a_Position.x * u_CosB - a_Position.y * u_SinB;'+
'gl_Position.y = a_Position.x * u_SinB - a_Position.y * u_CosB;'+
'gl_Position.z = a_Position.z;'+
'gl_Position.w = 1.0;'+
'}';
//片元着色器程序
var FSHADER_SOURCE=
'void main(){'+
'gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);'+
'}';
//旋转角度
var ANGLE = 90.0;
function main() {
//获取canvas元素
var canvas = document.getElementById("webgl");
if(!canvas){
console.log("Failed to retrieve the );
return;
}
//获取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 initialize shaders.");
return;
}
//设置顶点位置
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
//将旋转图形所需的数据传输给顶点着色器
var radian = ANGLE * Math.PI / 180.0;
var cosB = Math.cos(radian);
var sinB = Math.sin(radian);
var u_CosB = gl.getUniformLocation(gl.program, 'u_CosB');
if(u_CosB < 0){
console.log("Failed to get the storage location of u_CosB");
return;
}
var u_SinB = gl.getUniformLocation(gl.program, 'u_SinB');
if(u_SinB < 0){
console.log("Failed to get the storage location of u_SinB");
return;
}
gl.uniform1f(u_CosB, cosB);
gl.uniform1f(u_SinB, sinB);
//指定清空
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//清空
gl.clear(gl.COLOR_BUFFER_BIT);
//绘制三个点
gl.drawArrays(gl.TRIANGLES, 0, n);
}
function initVertexBuffers(gl) {
var vertices = new Float32Array([
0.0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n=3; //点的个数
//创建缓冲区对象
var vertexBuffer = gl.createBuffer();
if(!vertexBuffer){
console.log("Failed to create thie buffer object");
return -1;
}
//将缓冲区对象保存到目标上
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;
}
//将缓冲区对象分配给a_Postion变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
//连接a_Postion变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
return n;
}
对于简单的变换,你可以使用数学表达式来实现。但是当情形逐渐变得复杂时,你很快就会发现利用表达式运算实际上相当繁琐。
如果这样做,每次进行一次新的变换卖我么就要重新求取一个新的等式,然后实现一个新的着色器,这当然很不科学。好在我们可以使用两一个数学工具——变换矩阵来完成这项工作。变换矩阵非常适合操作计算机图形。
如下图所示,矩阵是一个矩形的二维数组,数字按照行和列排列,数字两侧的方括号表示这些数字是一个整体。我们将使用矩阵来表示前面的计算过程。
在解释如何利用变换矩阵来替代数学表达式之前,你需要理解矩阵和矢量的乘法。矢量就是由多个分量组成的对象,比如顶点的坐标(0.0, 0.5, 1.0)。
矩阵和矢量的乘法可以写成下表的形式。可见,将矩阵(中间)和矢量(右边)相乘,就得到了一个新的矢量(左边)。注意,矩阵的乘法不符合交换律。
上式中的这个矩阵具有3行3列,因此又被称为 3x3矩阵。矩阵右侧是一个由x、y、z组成的矢量。矢量具有3个分量,因此被称为三维矢量。
在本例中,矩阵与矢量相乘得到的新矢量,其三个分量为x’、y’、z’,其值如下等式所示。注意,只有在矩阵的列数与矢量的行数相等时,才可以将两者相乘。
现在,为了理解矩阵是如何代替数学表示式的,下面将矩阵等式与数学表达式进行比较:
与比较关于 x’ 的表达式进行比较:
这样的话,如果设a = cosβ,b = -sinβ, c = 0,那么这两个等式就完全相同了。再来看一下y’:
这样的话,设d = sinβ,e = cosβ, f = 0,两个等式也就完全相同了。最后的关于 z’ 的等式更简单,设 g = 0, h = 0, i = 1即可。
接下来,将这些结果代入上面的等式中,得到:
这个矩阵就被称为变换矩阵,因为它将右侧的矢量(x, y, z)“变换”为了左侧的矢量(x’, y’, z’)。上面这个变换矩阵进行的变换时一次旋转,所以这个矩阵又可以被称为旋转矩阵。
变换矩阵在三维计算机图形学中应用的如此广泛,以至于着色器本身就实现了矩阵和矢量相乘的功能。但是,在我们修改着色器代码以采用矩阵之前,先来快速浏览一遍其他几种变换矩阵。
显然,如果我们使用变换矩阵来表示旋转变换,我们就应使用它来表示其他变换,比如平移:
这里第二个等式的右侧有常量项Tx,第一个等式中没有,这意味着我们无法通过使用一个3x3的矩阵来表示平移,为了解决这个问题,我们可以使用一个4x4的矩阵,以及具有第4个分量的矢量。也就是说,我们假设点p的坐标为(x, y, z, 1),平移之后的点p’的坐标为(x’, y’, z’, 1):
该矩阵的乘法的结果如下所示:
根据最后一个式子 1 = mx + ny+ oz + p,很容易求算出系数 m = 0, n = 0, o = 0, p = 1。这些方程都有常数项 d、h、1和p。我们把他与下面的式子进行比较:
比较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。这样,你就可以写出表示平移的矩阵又称为平移矩阵,如:
此外,我们已经成功地创建了一个旋转矩阵和平移矩阵,这两个矩阵的作用与此前示例程序中的数学表达式的作用是一样的,那就是计算变换后的顶点坐标。在“先旋转再平移”的情形下,我们需要将两个矩阵组合起来,然后旋转矩阵与平移矩阵的阶数不同。我们不能把两个阶数不一样的矩阵组合起来,所以使用某种手段,使者两个矩阵的阶数一致。
将旋转矩阵从一个3x3矩阵转变为4x4矩阵,先比较以下方程:
例如,当你通过比较 x’ = x cosβ - ysinβ 与 x’ = ax + by +cz + d 时,可知 a = cosβ, b = -sinβ, c = 0, d = 0。以此类推,最终得到4x4矩阵:
这样,我们就可以使用相同阶数(4x4)的矩阵来表示平移和旋转,实现了最初的目标!
RotatedTriangle_Matrix.js
//顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'uniform mat4 u_xformMatrix;'+
'void main(){'+
'gl_Position = a_Position * u_xformMatrix;'+
'}';
//片元着色器程序
var FSHADER_SOURCE=
'void main(){'+
'gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);'+
'}';
//旋转角度
var ANGLE = 90.0;
function main() {
//获取canvas元素
var canvas = document.getElementById("webgl");
if(!canvas){
console.log("Failed to retrieve the );
return;
}
//获取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 initialize shaders.");
return;
}
//设置顶点位置
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
//创建旋转矩阵
var radian = ANGLE * Math.PI / 180.0;
var cosB = Math.cos(radian);
var 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, 1.0,
0.0, 0.0, 0.0, 1.0
]);
//将旋转矩阵传输给顶点着色器
var u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix');
if(u_xformMatrix < 0){
console.log("Failed to get the storage location of u_xformMatrix");
return;
}
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
//指定清空
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//清空
gl.clear(gl.COLOR_BUFFER_BIT);
//绘制三个点
gl.drawArrays(gl.TRIANGLES, 0, n);
}
function initVertexBuffers(gl) {
var vertices = new Float32Array([
0.0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n=3; //点的个数
//创建缓冲区对象
var vertexBuffer = gl.createBuffer();
if(!vertexBuffer){
console.log("Failed to create thie buffer object");
return -1;
}
//将缓冲区对象保存到目标上
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;
}
//将缓冲区对象分配给a_Postion变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
//连接a_Postion变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
return n;
}
首先来看看顶点着色器:
//顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;'+
'uniform mat4 u_xformMatrix;'+
'void main(){'+
'gl_Position = a_Position * u_xformMatrix;'+
'}';
u_xformMatrix 变量表示等式中的旋转矩阵,a_Position 变量表示顶点的坐标,二者相乘得到变换后的顶点坐标。
在示例程序 TranslatedTriangle 中,你可以在一行代码中完成矢量相加的运算。同样,你也可以在一行代码中完成矩阵与矢量相乘的运算(gl_Position = u_xforMatrix * a_Position)。这时因为着色器内置了常用的矢量和矩阵运算功能,这种强大特性正式专为三维计算机图形学而设计的。
由于变换矩阵是 4x4的,GLSL ES 需要知道每个变量的类型,所以我们将 u_xformatrix 定义为 mat4类型。如你所料,mat4 类型的变量就是 4x4 的矩阵。
//创建旋转矩阵
var radian = ANGLE * Math.PI / 180.0;
var cosB = Math.cos(radian);
var 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, 1.0,
0.0, 0.0, 0.0, 1.0
]);
//将旋转矩阵传输给顶点着色器
var u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix');
if(u_xformMatrix < 0){
console.log("Failed to get the storage location of u_xformMatrix");
return;
}
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
这段代码首先计算了90度的正弦值和余弦值,这两个值需要被用来构建旋转矩阵,之后创建了 Float32Array 类型的 xforMatrix 变量宝石旋转矩阵。与GLSL ES 不同,JS 并没有专门表示矩阵的类型,所以你需要使用类型化数组 Float32Array。我们在数组中存储矩阵的每个元素,但问题是:矩阵式二维的,其元素按照行和列进行排序,而数组是一维的,其元素只能排成一行。这里,我们可以按照两种方式在数组中存储矩阵元素:按行主序和按列主序,如图所示:
WebGL 和 OpenGL 一样,矩阵元素是按列主序存储在数组中。比如上图所示的矩阵存储在数组中就是这样的:[a, e, i, m, b, f, j, n, c, g, k, o, d, n, l, p]。本例中,旋转矩阵也是按照这样的顺序存储在 Float32Array 类型的数组中的。
最后,我们使用 gl.uniforMatrix4fv()函数,将刚刚生成的数组传给 u_xformMatrix 变量。注意,函数名的最后一个字母是 v,表示它可以向着色器传输多个数据值。
最后运算结果和前面的旋转一样
如你所见,4x4 的矩阵不仅可以用来表示平移,也可以用来表示旋转。不管是评议还是旋转,你都使用如下形式来进行矩阵和矢量的运算以完成变换:<新坐标> = <变换矩阵> * <旧坐标>,比如在着色器中:
'gl_Position = a_Position * u_xformMatrix;'+
这意味着,如果我们改变数组 xformMatrix 中的元素,使之成为一个平移矩阵,那么就可以实现平移操作,其效果就和之前使用数学表达式进行的平移操作一样。
因此,修改 RotatedTriangle_Matrix.js,将旋转角度改为与平移相关的变量:
我们还需重写创建矩阵的代码,记住,矩阵是按列主序存储的,虽然 xformMatrix 现在是一个平移矩阵了,但我们仍使用这个变量名。因为对于着色器而言,旋转矩阵和平移矩阵其实是一回事。最后,你不会用到 ANGLE 变量,把与旋转相关的代码注释掉。
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,
0.5, 0.5, 0.0, 1.0
]);
最后,我们来学习缩放变化矩阵。仍然假设最初的点p,经过缩放操作之后变成了p’。
假设在三个方向X轴,Y轴,Z轴的缩放因子 Sx, Sy, Sz 不想管,那么有:
缩放操作的变换矩阵:
和之前的例子一样,我们只要将缩放矩阵传给 xformMatrix 变量,就可以直接使 RotatedTriangle_Matrix.js 中的着色器对三角形进行缩放操作了。下面这个示例程序将三角形在垂直方向上拉伸了1.5倍。
var Sx = 1.0, Sy = 1.5, Sz = 1.0;
......
var xformMatrix = new Float32Array([
Sx, 0.0, 0.0, 0.0,
0.0, Sy, 0.0, 0.0,
0.0, 0.0, Sz, 0.0,
0.0, 0.0, 0.0, 1.0
]);