关键词:
- 顶点缓冲区
一、一次绘制多个点
在第二章的ClickedPoints例子中,只绘制了一个点。对那些由多个顶点组成的图形,比如三角形、矩形和立方体来说,你需要一次性地将图形的顶点全部传入顶点着色器,然后才能把图形画出来。
WebGL提供了一种很方便的机制,即缓冲区对象(buffer object),它可以一次性地向着色器传入多个顶点的数据。缓冲区对象是WebGL系统中的一块内存区域,我们可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用。
//MultiPoint.js
function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('webgl');
// Get the rendering context for WebGL
var gl = getWebGLContext(canvas);
if (!gl) {
console.log('Failed to get the rendering context for WebGL');
return;
}
// Initialize shaders
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to intialize shaders.');
return;
}
// Write the positions of vertices to a vertex shader
var n = initVertexBuffers(gl);
if (n < 0) {
console.log('Failed to set the positions of the vertices');
return;
}
// Specify the color for clearing <canvas>
gl.clearColor(0, 0, 0, 1);
// Clear <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
// Draw three points
gl.drawArrays(gl.POINTS, 0, n);
}
initVertexBuffers创建了顶点缓冲区对象,并将多个顶点的数据保存在缓冲区中,然后将缓冲区传给顶点着色器。函数返回值是待绘制顶点的数量,保存在变量n当中。使用drawArrays函数,传入的第三个参数是n。WebGL系统并不知道缓冲区中有多少个顶点的数据,即使它知道也不能确定是否要全部画出,所以我们要显式地告诉它要绘制多少个顶点。
function initVertexBuffers(gl) {
var vertices = new Float32Array([
0.0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n = 3; // The number of vertices
// Create a buffer object
var vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
console.log('Failed to create the buffer object');
return -1;
}
// Bind the buffer object to target
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// Write date into the buffer object
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;
}
// Assign the buffer object to a_Position variable
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// Enable the assignment to a_Position variable
gl.enableVertexAttribArray(a_Position);
return n;
}
这里向缓冲区对象写入的顶点坐标是一种特殊的JS类型化数组(Float32Array),创建的唯一方法是使用new运算符,不能使用[]运算符(那样创建的就是普通数组)。关于JS的Float32Array可以参考jsmpeg系列一 基础知识 字符处理 ArrayBuffer TypedArray
1.缓冲区
缓冲区对象是WebGL系统中的一块存储区,我们可以在缓冲区对象中保存想要绘制的所有顶点的数据。如下图所示:
使用缓冲区对象向顶点着色器传入多个顶点的数据,需要遵循以下五个步骤。处理其他对象,如纹理对象(第4章)、帧缓冲区对象(第8章 光照)时的步骤也比较类似:
- 创建缓冲区对象 gl.createBuffer
- 绑定缓冲区对象 gl.bindBuffer
- 将数据写入缓冲区对象 gl.bufferData
- 将缓冲区对象分配给一个attribute变量 gl.vertexAttribPointer
- 开启attribute变量 gl.enableVertexAttribArray
2.var vertexBuffer = gl.createBuffer();
相应的,也有gl.deleteBuffer(buffer)用来删除创建的缓冲区对象。
创建缓冲区前后WebGL系统的变化如下图所示:执行后WebGL系统中多出了“缓冲区对象”
3. gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
创建缓冲区对象后,需要将其绑定到WebGL系统中已经存在的“目标”(target)上。“目标”表示缓冲区对象的用途(示例中为向顶点着色器提供传给attribute变量的数据),这样WebGL才能够正确处理其中内容。
绑定缓冲区对象采用的函数为gl.bindBuffer(),函数规范如下:
gl.bindBuffer(target, buffer):允许使用buffer表示的缓冲区对象并将其绑定到target表示的目标上。
参数:
- target参数可以是以下中的一个:
- gl.ARRAY_BUFFER 表示缓冲区对象中包含了顶点的数据
- gl.ELEMENT_ARRAY_BUFFER 表示缓冲区对象中包含了顶点的索引值(参见第6章着色器语言GLSL ES)
- buffer:待删除的缓冲区对象
- 返回值: 无
- 错误:
INVALID_ENUM target不是上述值之一,这时将保持原有的绑定情况不变
示例程序中,我们将创建的缓冲区对象绑定到gl.ARRAY_BUFFER目标上,代码执行完毕后,WebGL系统内如下图所示:
3.gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
这一步将vertices中的数据写入到绑定在gl.ARRAY_BUFFER目标的缓冲区对象。此处不能直接向缓冲区写入数据,而是依据target写入数据,所以在此之前target下需要有绑定的缓冲区对象。
gl.bufferData()的函数规范如下:
gl.bufferData(target, data, usage):开辟存储空间,向绑定在target上的缓冲区对象中写入数据。
参数:target:gl.ARRAY_BUFFER 或 gl.ELEMENT_ARRAY_BUFFER
data:写入缓冲区对象的数据(类型化数组)
-
usage:表示程序将如何使用存储在缓冲区对象中的数据。该参数将帮助WebGL优化操作,但是就算你传入了错误的值,也不会终止程序(仅仅是降低程序的效率)
- gl.STATIC_DRAW 只会向缓冲区对象中写入一次数据,但需要绘制很多次(many times)
- gl.STREAM_DRAW 只会向缓冲区对象中写入一次数据,然后绘制若干次(at most a few times)
- gl.DYNAMIC_DRAW 会向缓冲区对象中多次写入数据,并绘制很多次(many times)
返回值: 无
错误:
INVALID_ENUM target不是上述值之一,这时将保持原有的绑定情况不变
写入数据后,WebGL系统如下:
4.gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
缓冲区对象准备好之后,需要获取attribute变量地址,再向attribute变量传递参数。第二章中使用了gl.vertexAttrib[1234]f[v]系列函数来传递数据,但此方法一次只能传递一个值,此时需要一次传递多个值,示例中采用gl.vertexAttribPointer()方法。
该函数的规范如下:
gl.vertexAttribPointer(location, size, type, normalized, stride, offset):
将绑定到gl.ARRAY_BUFFER的缓冲区对象(实际上是其引用或指针)分配给由location指定的attribute变量。
参数:
- location:指定待分配attribute变量的存储位置
- size:指定缓冲区中每个顶点的分量个数(1到4)。若size比attribute变量需要的分量数小,缺失分量将按照与gl.vertexAttrib[1234]f()相同的规则补全。比如,如果size为1,那么第2、3分量自动设为0,第4分量为1。
- type:用以下类型之一来指定数据格式
- gl.UNSIGNED_BYTE 无符号字节,Uint8Array
- gl.SHORT 短整型,Int16Array
- gl.INT 整型,Int32Array
- gl.UNSIGNED_INT 无符号整型,UInt32Array
- gl.FLOAT 浮点型,Float32Array
- normalize:传入true或false,表明是否将非浮点型的数据归一化到[0,1]或[-1,1]区间(正则化)
- stride:指定相邻两个顶点间的字节数,默认为0(参见第5章)
- offset:指定缓冲区对象中的偏移量(以字节为单位),即attribute变量从缓冲区中的何处开始存储。如果是从起始位置开始的,offset设为0。
- 返回值: 无
- 错误:
- INVALID_OPERATION 不存在当前程序对象
- INVALID_VALUE location大于等于attribute变量的最大数目(默认为8)。或者stride或offset是负值。
执行完毕后,gl.ARRAY_BUFFER缓冲区对象被分配给attribute变量,此时缓冲区对象还不可用,如下图所示:
5.gl.enableVertexAttribArray(a_Position);
虽然前一步已经将attribute变量指向了缓冲区对象,但此时着色器还不能访问缓冲区内的数据,需要使用先开启attribute变量。
相关函数的函数规范如下:
gl.enableVertexAttrirbArray(location):
开启location指定的attribute变量(实际处理对象是缓冲区)。
参数:
- location:指定attribute变量的存储位置
- 返回值: 无
- 错误:
- INVALID_VALUE location大于等于attribute变量的最大数目(默认为8)。
开启attribute变量之后,缓冲区对象和attribute变量之间的连接就真正建立起来了,如下图所示:
开启attribute变量之后,我们就不能通过gl.vertexAttrib[1234]f()来向该变量传递数据了,实际上也不应该同时通过两种方式传递数据。
于是,我们也可以通过gl.disableVertexAttribArray()来关闭分配,函数规范如下:
gl.enableVertexAttrirbArray(location):
关闭location指定的attribute变量。
参数:
- location:指定attribute变量的存储位置
- 返回值: 无
- 错误:
- INVALID_VALUE location大于等于attribute变量的最大数目(默认为8)。
6.开始绘制
通过上面的函数,我们已经配置好了缓冲区和着色器,可以开始绘制:
// 设置背景色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制
gl.drawArrays(gl.POINTS, 0, n)
绘制同样采用gl.drawArrays()方法,该方法函数规范在第二章已经给出,此处给出函数语法作为提示:
gl.drawArrays(mode, first, count)
示例中n=3,count为3,所以顶点着色器实际执行了3次。
在建立attribute变量和缓冲区联系时,函数gl.vertexAttribPointer()中的参数size为2,表示缓冲区每个顶点有2个分量值。所以每次着色器运行前,gl_Position都被提供了两个分量(通过attribute变量a_Position),其他值按照规则填充为0.0和1.0。
绘制过程如下图所示:
二、绘制三角形
与上面绘制多点的MultiPoint例子相比,只改了两处:
// MultiPoint.js (c) 2012 matsuda
// Vertex shader program
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' +
' gl_PointSize = 10.0;\n' +
'}\n';
// HelloTriangle.js (c) 2012 matsuda
// Vertex shader program
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' +
'}\n';
第一处,将gl_PointSize=10.0删去了,该语句只有在绘制单个点的时候才起作用。
// Draw three points
gl.drawArrays(gl.POINTS, 0, n);
// Draw the rectangle
gl.drawArrays(gl.TRIANGLES, 0, n);
第二处,drawArrays的第一个参数发生了变化,这个参数提供了7种mode:
- gl.POINTS 一系列点
- gl.LINES 线段
- gl.LINE_STRIP 线条
- gl.LINE_LOOP 回路
- gl.TRIANGLES 三角形
- gl.TRIANGLE_STRIP 三角带
- gl.TRIANGLE_FAN 三角扇
这7种基本图形是WebGL绘制其他更加复杂的图形的基础,正如本章开头所说,从球体到立方体,再到游戏中的三维角色,都可以由小的三角形组成。
1.用三角形绘制矩形(HelloQuad)
既然任何东西都可以由基本图形构成,那么我们来绘制一个矩形,体会这种“构成”的方式。
矩形可以由两个三角形组成,绘制方式可以采用gl.TRAINGLES、gl.TRIANGLE_STRIP、gl.TRIANGLE_FAN三种方法,第一种方法需要用到6个顶点,后两种需要4个顶点,每种方法的顶点顺序都不相同。此处采用gl.TRIANGLE_STRIP方法进行绘制,相比于上个示例,此处改动如下:
let vertices = new Float32Array([-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5]) ;
let n = 4 // 点的个数
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n)
此处如果把HelloQuad中gl.drawArrays()的mode参数改为gl.TRIANGLE_FAN,会出现:
三、移动、旋转和缩放
基础概念部分,可以参考
图形学笔记一 仿射变换和齐次坐标
图形学笔记二 正交矩阵、转置矩阵和旋转
相关内容:1.表达式方式进行仿射变换;2.变换矩阵进行仿射变换;3.将矩阵传递给uniform变量;4.按列主序
相关函数:gl.uniformMatrix4fv()
小结:
本节通过表达式展示了仿射变换的坐标转换过程,主要为了引出变换矩阵。变换矩阵是常用的对图形处理的方法,多种变换能够糅合到一个矩阵中进行操作,对于代码编写非常遍历,当然也需要一定数学基础。许多语言对于矩阵运算都有额外的设计,这些设计大大提高了矩阵运算的效率,WebGL中也支持矩阵和矢量的运算,这大大提高了三维图像处理的效率。
本节用到的方法多为前面已经讲述过的内容,包括着色器中attribute变量和uniform变量、缓冲区对象的使用。关于矩阵的运算可以直接写在着色器语言中,不需要额外函数,构建矩阵除了类型化数组之外,还需要注意WebGL(OpenGL)中按列主序来保存内容。本节提到了一个新的函数来向unform传递矩阵数据。
从数据传递的过程可以看出,WebGL系统要求接收的数据一般需要严格定义类型,在传输的时候还需要告诉该系统使用方式,很多函数都是如此设计,以最新的函数gl.uniformMatrix4fv()为例,单看函数名,它传递的数据为float型数组,数组元素共4×4个,它告诉WebGL以4×4矩阵的方式使用该数组并将其赋值给uniform变量,传递数据的类型(数组)和使用数据的类型(矩阵)并不一致。
这一节有所局限的地方在于,所有的变换都是单一类型仅仅一步的变换,之后多半会提及的复杂类型的变换可能还涉及变换矩阵之间的运算,敬请期待。
现在,你已经掌握了绘制图形的方法。让我们再进一步,尝试平移、旋转和缩放三角形,这样的操作称为变换(transformations)或仿射变换(affine transformations)。
1.平移
我们只需要着色器为顶点坐标的每个分量加上一个常量就可以实现平移,显然这是一个逐顶点操作,发生在顶点着色器中。
// TranslatedTriangle.js (c) 2012 matsuda
// Vertex shader program
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform vec4 u_Translation;\n' +
'void main() {\n' +
' gl_Position = a_Position + u_Translation;\n' +
'}\n';
// Fragment shader program
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
'}\n';
// The translation distance for x, y, and z direction
var Tx = 0.5, Ty = 0.5, Tz = 0.0;
function main() {
...
// Pass the translation distance to the vertex shader
var u_Translation = gl.getUniformLocation(gl.program, 'u_Translation');
if (!u_Translation) {
console.log('Failed to get the storage location of u_Translation');
return;
}
gl.uniform4f(u_Translation, Tx, Ty, Tz, 0.0);
...
在第二章中有提到:
- attribute变量 传输的是那些与顶点相关的数据
- uniform变量 传输那些对所有顶点都相同(或与顶点无关)的数据
所以这里使用了uniform变量u_Translation来表示三角形的平移距离。因为a_Position和u_Translation都是vec4类型的,所以可以直接使用+号,两个矢量的对应分量会被同时相加。
下面解释一下齐次坐标的第4分量。在第2章中有提到,如果齐次坐标第4分量是1.0,那么它的前三个分量就可以表示一个点的三维坐标。在本例中,平移后点坐标第4分量w1+w2必须是1.0,因为点的位置坐标平移之后还是一个点位置坐标,而w1是1.0(它是平移前点坐标第4分量),所以w2只能是0.0,这就是为什么gl.uniform4f()的最后一个参数为0.0
2.旋转
x = r cos α
y = r sin α
x' = r cos(α+β)
y' = r sin(α+β)
利用三角函数公式可得
x' = x cos β - y sin β
y' = x sin β + y cos β
在RotatedTriangle.js中,实现了一个三角形绕Z轴逆时针旋转90度的例子。
// RotatedTriangle.js (c) 2012 matsuda
// Vertex shader program
var VSHADER_SOURCE =
// x' = x cosβ - y sinβ
// y' = x sinβ + y cosβ Equation 3.3
// z' = z
'attribute vec4 a_Position;\n' +
'uniform float u_CosB, u_SinB;\n' +
'void main() {\n' +
' gl_Position.x = a_Position.x * u_CosB - a_Position.y * u_SinB;\n' +
' gl_Position.y = a_Position.x * u_SinB + a_Position.y * u_CosB;\n' +
' gl_Position.z = a_Position.z;\n' +
' gl_Position.w = 1.0;\n' +
'}\n';
// Fragment shader program
var FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
'}\n';
// The rotation angle
var ANGLE = 90.0;
...
// // Pass the data required to rotate the shape to the vertex shader
var radian = Math.PI * ANGLE / 180.0; // Convert to radians
var cosB = Math.cos(radian);
var sinB = Math.sin(radian);
var u_CosB = gl.getUniformLocation(gl.program, 'u_CosB');
var u_SinB = gl.getUniformLocation(gl.program, 'u_SinB');
if (!u_CosB || !u_SinB) {
console.log('Failed to get the storage location of u_CosB or u_SinB');
return;
}
gl.uniform1f(u_CosB, cosB);
gl.uniform1f(u_SinB, sinB);
...
也可以将旋转的角度传入顶点着色器,并在着色器中计算正弦值和余弦值。但是,实际上所有顶点旋转的角度是一样的,在JS中算好正弦值和余弦值,再传递进去,只需要计算一次,效率更高。
在进行平移变换时,齐次坐标的4个分量是作为整体进行加法运算的。而旋转操作,需要单独访问每个分量。
对于简单的变换,可以用上述方式来实现。但是当情形逐渐复杂时,比如一个旋转后再平移,就不能每次变换都用一个新的着色器,此时就需要变换矩阵。
x'=ax+by+cz
y'=dx+ey+fz
z'=gx+hy+iz
与上述公式对比
x' = x cosβ - y sinβ
y' = x sinβ + y cosβ
z' = z
可知让a=cosβ,b=-sinβ,c=0即可。
完整矩阵如上,它将右侧的矢量(x,y,z)变换成了左侧的(x',y',z'),可以看作是一个旋转矩阵。
x'=ax+by+cz
x'=x+Tx
对比上述变换,发现无法通过3*3的矩阵来表示平移,所以我们需要一个4*4的矩阵。平移矩阵推导方式类似:
下面先给出RotatedTriangle_Matrix.js的完整代码:
// RotatedTriangle_Matrix.js
// 顶点着色器
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
// 主函数
function main() {
// 获取canvas元素
let canvas = document.getElementById('webgl')
// 获取WebGL上下文
let 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
}
// 设置顶点位置
let n = initVertexBuffers(gl)
if (n < 0) {
console.log('Failed to set the positions of the vertices')
return
}
// 创建旋转矩阵
let radian = (Math.PI * ANGLE) / 180.0 // 转换为弧度制
let cosB = Math.cos(radian)
let sinB = Math.sin(radian)
// 注意WebGL中矩阵是列主序的
let 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,
])
// 将旋转图形所需数据传输给顶点着色器
let u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix')
if (!u_xformMatrix) {
console.log('Failed to get the storage location of u_xformMatrix')
}
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) {
// 设置类型化数组和顶点数
let vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5])
let n = 3
// 创建缓冲区对象
let vertexBuffer = gl.createBuffer()
if (!vertexBuffer) {
console.log('Failed to create the buffer object')
return -1
}
// 绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
// 缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW)
let 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
}
// 将缓冲区分配给attribute变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
// 开启attribute变量(连接)
gl.enableVertexAttribArray(a_Position)
return n
}
以下为顶点着色器代码:
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_xformMatrix;\n' +
'void main(){\n' +
' gl_Position = u_xformMatrix * a_Position;\n' +
'}\n'
- mat4类型的变量是4×4的矩阵。
- WebGL顶点着色器中的’*'操作符支持矩阵乘法。着色器内置了常用的矢量和矩阵运算功能,我们可以用基本的操作符,如示例中出现过的‘+’和‘*’直接完成矢量和矩阵的运算,这种强大特性正是专为三维计算机图形学而设计的。
概念参考行主序 列主序
// 创建旋转矩阵
let radian = (Math.PI * ANGLE) / 180.0 // 转换为弧度制
let cosB = Math.cos(radian)
let sinB = Math.sin(radian)
// 注意WebGL中矩阵是列主序的
let 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,
])
向uniform变量传递矩阵
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix)
该函数规范如下:
gl.uniformMatrix4fv(location, transpose, array):
将array表示的4×4矩阵分配给由location指定的uniform变量。
- 参数:
- location:uniform变量的存储位置。
- transpose:是否转置矩阵,在WebGL中没有转置矩阵的方法,必须指定为false。
- array:待传输的类型化数组,4×4矩阵按列主序存储在其中。
- 返回值: 无
- 错误:
- INVALID_OPERATION 不存在当前程序对象
- INVALID_VALUE transpose不为false,或者数组的长度小于16。
3.平移 矩阵
// 创建平移矩阵
let 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,
])
4.缩放 矩阵
// 创建缩放矩阵
let 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,
])
四、第四章 高级变换与动画基础
虽然平移、旋转、缩放等变换操作都可以用一个4×4的矩阵表示,但在写图像处理程序时,手动计算每一个矩阵很耗费时间,大多数的开发者都使用矩阵操作函数库来简化矩阵有关的操作。
OpenGL提供了一系列有用的函数来帮助我们创建变换矩阵。比如,通过调用glTranslate()函数并传入X、Y、Z轴上的平移的距离,就可以创建一个平移矩阵,如下图所示:
可惜的是,WebGL没有提供类似的矩阵函数。当然,目前有很多开源的矩阵库可以使用,本书使用的是书的作者编写的JavaScript函数库cuon-matrix.js,为了学习此书,我们首先要了解一下函数库中的内容。
在看书中内容之前,我们可以大致浏览一下函数库源码。可以发现,函数库主要创建了一个Matrix4类(构造函数),在该类原型函数下绑定了众多方法,许多函数之前都留有注释,我们主要看一下代码最前面的注释:
/**
* This is a class treating 4x4 matrix.
* This class contains the function that is equivalent to OpenGL matrix stack.
* The matrix after conversion is calculated by multiplying a conversion matrix from the right.
* The matrix is replaced by the calculated result.
*/
根据注释可知,该函数库主要处理4×4的矩阵,对标OpenGL中矩阵处理函数。函数库提供了一个名为Matrix4的对象(构造函数),我们可以通过new方法创建它的实例,对象内部挂载了许多关于矩阵计算的方法。
以上一章RotatedTriangle_Matrix.js示例改造如下:
1.创建矩阵
// RotatedTriangle_Matrix.js
...
// 创建旋转矩阵
let radian = (Math.PI * ANGLE) / 180.0 // 转换为弧度制
let cosB = Math.cos(radian)
let sinB = Math.sin(radian)
// 注意WebGL中矩阵是列主序的
let 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,
])
...
改造之后
// RotatedTriangle_Matrix4.js
...
// 创建旋转矩阵
// 为旋转矩阵创建Matrix4对象
let xformMatrix = new Matrix4()
// 将xformMatrix设置为旋转矩阵
xformMatrix.setRotate(ANGLE, 0, 0, 1)
...
2.传输矩阵数据
// RotatedTriangle_Matrix.js
...
// 将旋转图形所需数据传输给顶点着色器
let u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix')
if (!u_xformMatrix) {
console.log('Failed to get the storage location of u_xformMatrix')
}
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix)
...
改造之后
// RotatedTriangle_Matrix4.js
...
let u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix')
if (!u_xformMatrix) {
console.log('Failed to get the storage location of u_xformMatrix')
}
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix.elements)
...
可见,在新的RotatedTriangle_Matrix4.js中,首先创建Matrix4实例,调用实例的setRotate()方法,在实例下elements属性上创建了变换矩阵,最后把element属性的内容传输给着色器。
3.复合变换和动画
对上述知识的一个综合应用,参见原书,或者【《WebGL编程指南》读书笔记-绘制和变换三角形】