本文为读书笔记第四章
总目录链接:https://blog.csdn.net/floating_heart/article/details/124001572
第4章 高级变换与动画基础
本章较为简单,基本没有涉及新的WebGL的内容,而是展示了在已学习内容的基础上如何灵活运用。
用书中的开篇作为笔记的开始,承上启下
在第3章“绘制与变换三角形”中,你已经了解了如何利用缓冲区对象绘制三角形,通过数学表达式学习了图形变换(平移、旋转、缩放)的原理,了解了如何使用矩阵来简化变换操作(涉及到复合变换时,使用表达式变换图形非常繁琐)。在这一章中,我们将进一步研究变换矩阵,并在此基础上制作一些简单的动画效果。具体地,我们将:
- 学习使用一个矩阵变换库,该库封装了矩阵运算的数学细节。
- 快速上手使用该矩阵库,对图形进行复合变换。
- 在该矩阵库的帮助下,实现简单的动画效果。
本章将要介绍的技术,是复杂的WebGL程序的基础。在后面几章中,几乎所有的示例程序都会用到这些技术。
矩阵变换库:cuon-matrix.js
小结:介绍了矩阵函数库cuon-matrix.js的大致功能
虽然平移、旋转、缩放等变换操作都可以用一个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
示例会变得更加简单,新的示例命名为RotatedTriangle_Matrix4.js
,二者区别如下:
- 矩阵的创建方式
// 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)
...
- 传输矩阵数据
// 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
属性的内容传输给着色器。
setRotate()
与OpenGL中glRotatef()
函数类似,接收的参数如下:
- 旋转角—角度制,逆时针为正
- 旋转轴—x,y,z,旋转绕着从原点(0,0,0)指向(x,y,z)的轴进行的
从源码中可以看到,函数内部也是通过圆周率PI来换算角度为弧度:
angle = Math.PI * angle / 180;
s = Math.sin(angle);
c = Math.cos(angle);
函数内部通过"==="操作符判断旋转轴是否为三个主轴,例如旋转轴是x轴的判断条件为:
if (0 !== x && 0 === y && 0 === z) {
// Rotation around X axis
if (x < 0) {
s = -s;
}
...
}
非主轴的旋转轴则通过下面的方式构建旋转矩阵:
// Rotation around another axis
len = Math.sqrt(x*x + y*y + z*z);
if (len !== 1) {
rlen = 1 / len;
x *= rlen;
y *= rlen;
z *= rlen;
}
nc = 1 - c;
xy = x * y;
yz = y * z;
zx = z * x;
xs = x * s;
ys = y * s;
zs = z * s;
e[ 0] = x*x*nc + c;
e[ 1] = xy *nc + zs;
e[ 2] = zx *nc - ys;
e[ 3] = 0;
e[ 4] = xy *nc - zs;
e[ 5] = y*y*nc + c;
e[ 6] = yz *nc + xs;
e[ 7] = 0;
e[ 8] = zx *nc + ys;
e[ 9] = yz *nc - xs;
e[10] = z*z*nc + c;
e[11] = 0;
e[12] = 0;
e[13] = 0;
e[14] = 0;
e[15] = 1;
如果没有这个函数,我们也需要同样的操作才能构建出所需的变换矩阵,应用函数库可以帮助我们把精力放在更重要的事情上。
书中罗列了Matrix4
对象所支持的方法和属性,此处也记录在此处,方便查找:
方法和属性名称 | 描述 |
---|---|
Matrix4.setIdentity() | 将Matrix4实例初始化为单位阵。 |
Matrix4.setTranslate(x,y,z) | 将Matrix4实例设置为平移变换矩阵,在x轴上平移的距离为x,在y轴上平移的距离为y,在z轴上平移的距离为z。 |
Matrix4.setRotate(angle,x,y,z) | 将Matrix4实例设置为旋转变换矩阵,旋转的角度为angle,旋转轴为(x,y,z)。旋转轴(x,y,z)无须归一化(见第8章“光照”)。 |
Matrix4.setScale(x,y,z) | 将Matrix4实例设置为缩放变换矩阵,在三个轴上的缩放因子分别为x、y和z。 |
Matrix4.translate(x,y,z) | 将Matrix4实例乘以一个平移变换矩阵(该平移矩阵在x 轴上平移的距离为x,在y轴上平移的距离是y,在z轴上平移的距离是z),所得的结果还存储在Matrix4中。 |
Matrix4.rotate(angle,x,y,z) | 将Matrix4实例乘以一个旋转变换矩阵(该旋转矩阵旋转的角度为angle,旋转轴为(x,y,z)。旋转轴(x,y,z)无须归一化),所得的结果还存储在Matrix4中(见第8章)。 |
Matrix4.scale(x,y,z) | 将Matrix4实例乘以一个缩放变换矩阵(该缩放矩阵在三个轴上的缩放因子分别为x, y和z),所得的结果还存储在 Matrix4中 |
Matrix4.set(m) | 将Matrix4实例设置为m,m必须也是一个Matrix4实例 |
Matrix4.elements | 类型化数组(Float32Array)包含了Matrix4实例的矩阵元素 |
*单位阵在矩阵乘法中的行为,就像数字1在乘法中的行为一样。将一个矩阵乘以 单位阵,得到的结果和原矩阵完全相同。在单位阵中,对角线上的元素为1.0,其 余的元素为0.0。
*笔者简单阅读源码之后发现:Matrix4在不提供数据直接初始化的情况下,其挂载的element元素就是一个单位阵,之后大部分操作都是对element元素进行的。函数库源码并不长,建议读者简单浏览一下源码,会对上述操作有更准确的认识。当然,只遵循手册来使用也可以,如果手册编写足够细致的话。
从上表可见,Matrix4对象有两种方法:
- 一种方法的名称中含有前缀set,这一类方法会根据参数计算出变换矩阵,然后将变换矩阵写入自身;
- 另一种方法的名称中不含set前缀,这一类方法在计算出变换矩阵后,会将自身与计算出的变换矩阵相乘,最终结果写入Matrix4对象。
这些方法基本适应了目前的内容。
不含set前缀的方法实现比较巧妙,它通过set的方法获得变换矩阵,再把挂载变换矩阵的Matrix4对象作为参数,调用矩阵相乘的函数进行乘法操作。4阶矩阵的乘法操作通过代码来完成,乘法的顺序为:原有矩阵*新的变换矩阵。示例如下:
Matrix4.prototype.rotate = function(angle, x, y, z) { return this.concat(new Matrix4().setRotate(angle, x, y, z)); };
/** * Multiply the matrix from the right. * @param other The multiply matrix * @return this */ Matrix4.prototype.concat = function(other) { var i, e, a, b, ai0, ai1, ai2, ai3; // Calculate e = a * b e = this.elements; a = this.elements; b = other.elements; // If e equals b, copy b to temporary matrix. if (e === b) { b = new Float32Array(16); for (i = 0; i < 16; ++i) { b[i] = e[i]; } } for (i = 0; i < 4; i++) { ai0=a[i]; ai1=a[i+4]; ai2=a[i+8]; ai3=a[i+12]; e[i] = ai0 * b[0] + ai1 * b[1] + ai2 * b[2] + ai3 * b[3]; e[i+4] = ai0 * b[4] + ai1 * b[5] + ai2 * b[6] + ai3 * b[7]; e[i+8] = ai0 * b[8] + ai1 * b[9] + ai2 * b[10] + ai3 * b[11]; e[i+12] = ai0 * b[12] + ai1 * b[13] + ai2 * b[14] + ai3 * b[15]; } return this; }; Matrix4.prototype.multiply = Matrix4.prototype.concat;
注:WebGL中矩阵是按列主序保存的,在看源码的时候需要注意。
复合变换RotatedTranslatedTriangle.js
相关内容:采用Matirx4对象,运用矩阵乘法模拟符合变换。
相关函数:Matrix4.setRotate(), Matrix4.translate()注意:矩阵乘法的先后顺序与模型变换的先后顺序有关,不能随意变化(矩阵乘法不满足乘法交换律)。
如名称所示,本小节示例尝试将之前平移和旋转的操作组合起来,即先进性一次平移,再进行一次旋转,使用Matrix4对象方便计算。
坐标变换的过程如下所示:
<
平
移
后
的
坐
标
>
=
<
平
移
矩
阵
>
×
<
原
始
坐
标
>
<
平
移
后
旋
转
后
的
坐
标
>
=
<
旋
转
矩
阵
>
×
<
平
移
后
的
坐
标
>
<平移后的坐标>=<平移矩阵>\times<原始坐标>\\ <平移后旋转后的坐标>=<旋转矩阵>\times<平移后的坐标>\\
<平移后的坐标>=<平移矩阵>×<原始坐标><平移后旋转后的坐标>=<旋转矩阵>×<平移后的坐标>
所以:
<
平
移
后
旋
转
后
的
坐
标
>
=
<
旋
转
矩
阵
>
×
(
<
平
移
矩
阵
>
×
<
原
始
坐
标
>
)
<
平
移
后
旋
转
后
的
坐
标
>
=
(
<
旋
转
矩
阵
>
×
<
平
移
矩
阵
>
)
×
<
原
始
坐
标
>
<平移后旋转后的坐标>=<旋转矩阵>\times(<平移矩阵>\times<原始坐标>)\\ <平移后旋转后的坐标>=(<旋转矩阵>\times<平移矩阵>)\times<原始坐标>
<平移后旋转后的坐标>=<旋转矩阵>×(<平移矩阵>×<原始坐标>)<平移后旋转后的坐标>=(<旋转矩阵>×<平移矩阵>)×<原始坐标>
于是,我们在JavaScript中计算出<旋转矩阵>×<平移矩阵>,再将得到的矩阵传递给顶点着色器即可。
一个模型可能经过了多次变换,将这些变换全部复合成一个等效的变换,就得到了模型变换(model transformation),或称建模变换(modeling transformation),相应的,模型变换的矩阵称为模型矩阵(model matrix)。
示例程序整体流程与之前仿射变换的各个示例相同,与RotatedTriangle_Matrix4.js
示例和RotatedTriangle_Matrix.js
示例的不同之处如下:
- 命名习惯,在顶点着色器中命名uniform变量(mat4)为u_ModelMatrix,取模型矩阵之意。
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_ModelMatrix;\n' +
'void main(){\n' +
' gl_Position = u_ModelMatrix * a_Position;\n' +
'}\n'
- 构建矩阵时,采用Matrix4对象,先设置对象为旋转矩阵,再和平移矩阵相乘。
// 创建Matrix4对象
let modelMatrix = new Matrix4()
// 旋转角度
let ANGLE = 90.0
// 平移距离
let Tx = 0.5
// 设置模型矩阵为旋转矩阵
modelMatrix.setRotate(ANGLE, 0, 0, 1)
// 将模型矩阵乘以平移矩阵
modelMatrix.translate(Tx, 0, 0)
// 将模型矩阵传输给顶点着色器
let u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix')
if (!u_ModelMatrix) {
console.log('Failed to get the storage location of u_ModelMatrix')
}
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
示例效果如下:
示例程序RotatedTranslatedTriangle.js
的完整代码如下:
// RotatedTranslatedTriangle.js
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_ModelMatrix;\n' +
'void main(){\n' +
' gl_Position = u_ModelMatrix * a_Position;\n' +
'}\n'
// 片元着色器
var FSHADER_SOURCE =
'void main(){\n' + ' gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' + '}\n'
// 主函数
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
}
// 创建Matrix4对象
let modelMatrix = new Matrix4()
// 旋转角度
let ANGLE = 90.0
// 平移距离
let Tx = 0.5
// 设置模型矩阵为旋转矩阵
modelMatrix.setRotate(ANGLE, 0, 0, 1)
// 将模型矩阵乘以平移矩阵
modelMatrix.translate(Tx, 0, 0)
// 将模型矩阵传输给顶点着色器
let u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix')
if (!u_ModelMatrix) {
console.log('Failed to get the storage location of u_ModelMatrix')
}
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
// 设置背景色
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.3, -0.3, -0.3, 0.3, -0.3])
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
}
动画-RotatingTriangle.js
相关内容:通过JavaScript灵活设计WebGL系统,通过反复变换和重绘图形生成动画效果;setInterval()系列函数和requestAnimationgFrame()系列函数的异同。
相关函数:setInterval(), requestAnimationFrame(), cancelAnimationFrame()
所谓动画,实际是灵活地使用矩阵变换图形,形成动画的效果。
这一小节的目的是将矩阵变换运用到动画图形中,具体一点就是创建一个示例程序RotatingTriangle,程序能够以恒定的速度(45度/秒)旋转三角形,要实现这一效果也比较直观:只需要不断擦除和绘制三角形。下图是三个不同时刻呈现的图形:
为了生成上述动画,书中提出了两个关键机制:
**机制一:**在t0、t1、t2、t3等时刻反复调用同一个函数来绘制三角形。
**机制二:**每次绘制之前,清除上次绘制的内容,并使三角形旋转相应的角度。
基于此,该实例程序与前面的示例有以下三点区别:
- 实现反复调用绘制函数的机制(机制一)
- 定义绘制函数,在绘制函数中包括清空绘图区、向着色器传值、绘制三步
- 由于程序需要反复绘制,所以在一开始就指定了背景色。
示例程序的完整代码如下:
// RotatedTranslatedTriangle.js
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_ModelMatrix;\n' +
'void main(){\n' +
' gl_Position = u_ModelMatrix * 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_STEP = 45.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
}
// 设置背景色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
// 获取u_ModelMatrix变量存储位置
let u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix')
if (!u_ModelMatrix) {
console.log('Failed to get the storage location of u_ModelMatrix')
}
// 三角形当前旋转角度
let currentAngle = 0.0
// 模型矩阵,Matrix4对象
let modelMatrix = new Matrix4()
// 开始绘制三角形
let tick = function () {
currentAngle = animate(currentAngle) // 更新旋转角
draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix)
requestAnimationFrame(tick) // 请求浏览器调用tick
}
tick()
}
function initVertexBuffers(gl) {
// 设置类型化数组和顶点数
let vertices = new Float32Array([0.0, 0.3, -0.3, -0.3, 0.3, -0.3])
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
}
function draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix) {
// 设置旋转矩阵
modelMatrix.setRotate(currentAngle, 0, 0, 1)
// 旋转前加个平移
// modelMatrix.translate(0.5, 0, 0)
// 将旋转矩阵传输给顶点着色器
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
// 清空绘图区
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, n)
}
// 记录上一次调用函数的时刻
var g_last = Date.now()
// 更新旋转角
function animate(angle) {
// 计算距离上次调用经过多长时间
let now = Date.now()
let elapsed = now - g_last // 毫秒
g_last = now
// 根据上次调用的时间,更新当前旋转角度
let newAngle = angle + (ANGLE_STEP * elapsed) / 1000.0
return (newAngle %= 360)
}
示例程序给出后,整个思路就一目了然了。我们还是将其中的重要步骤进行适当解释,不外乎以下几点:
- 重复调用绘制函数的方法
- 旋转角度的获取
- 绘制函数的设计
重复调用函数的方法:
笔者本以为动画是用WebGL自动生成的一系列图形,实际上还是用JavaScript的方法重复调用WebGL系统的功能,不断擦除、绘制形成的,所以此处重复调用函数的方法还是挂载在Window对象下的相关方法,示例中采用的是requestAnimationFrame()
方法,这一部分代码如下:
// 开始绘制三角形
let tick = function () {
currentAngle = animate(currentAngle) // 更新旋转角
draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix) // 绘制三角形
requestAnimationFrame(tick) // 请求浏览器调用tick
}
tick()
从示例功能的角度看,tick()函数思路如下:
书中注明:你可以使用上述的基本步骤来实现各种动画,这也是3D图形编程的关键技术!
提到JavaScript重复执行某个特定任务(函数),的时候,我们常常会想到同为Window下的方法setInterval()
,该函数规范如下:
setInterval(func, delay):
每隔delay时间间隔,调用func函数。
参数:
func: 指定需要多次调用的函数。
delay: 指定时间间隔(以毫秒为单位)。
**返回值:**Time id
上一个方法只要把时间间隔设置为和浏览器或屏幕刷新频率一致即可。但该方法是一个诞生较早的方法,其出现时浏览器还没有支持多标签页,所以在现代浏览器中,不论标签页是否被激活,该方法都会反复调用func,如果标签页较多,就会增加浏览器的负荷。
示例主要依赖于requestAnimationFrame()
方法来完成重复调用,通过名称猜测,该方法专门用于制作动画,其规范如下:
requestAnimationFrame(func):
请求浏览器在将来某时刻回调函数func。我们应当在回调函数最后再次发起该请求。
参数:
func: 指定将来某时刻调用的函数,函数将会接收到一个time参数,用来表明此次调用的时间戳。(很明显,示例中没有使用这一时间戳进行任何操作。)
**返回值:**Request id
这一方法只有当标签页处于激活状态时才会生效,基于此也无法指定重复调用的时间间隔。从函数的规范来看,requestAnimationFrame()
更像setTimeOut()
方法,在时间到来时调用回调函数,所以也需要在回调函数中再次发出下次调用的请求,如示例中在tick()函数最后再次发出requestAnimationFrame(tick)
请求下次调用tick()。(书中提到,requestAnimationFrame()
方法时新引入的方法,还没有实现标准化,不知道现在实现了么。)
如果想要取消请求,需要使用cancelAnimationFrame()。
cancelAnimationFrame(Request id):
取消由requestAnimationFrame()发起的请求。
参数:
Request id: 指定requestAnimationFrame()的返回值
返回值: Request id
更新旋转角度:
tick()函数使用requestAnimationFrame()方法请求浏览器在适当的时机调用参数函数,所以调用的时间间隔不是固定的,浏览器会根据自身状态决定何时调用,所以更新旋转角度的操作会以调用时间间隔为依据,稍稍复杂一点。
tick()函数的第一步就是更新旋转角的操作
currentAngle = animate(currentAngle) // 更新旋转角
整个旋转角度更新的过程如下。
main()函数中规定了当前的旋转角度,初始为0.0:
// 三角形当前旋转角度
let currentAngle = 0.0
animate()是更新旋转角的主要部分,该函数配合当前旋转角currentAngle变量、全局变量ANGLE_STEP变量(旋转速度)和g_last(上一次调用函数的时刻)使用,基本思路是:根据本次调用与上次调用之间的时间间隔来决定这一帧的旋转角度比上一帧大出多少,具体如下。
...
// 旋转速度(度/秒)
var ANGLE_STEP = 45.0
...
// 记录上一次调用函数的时刻
var g_last = Date.now()
// 更新旋转角
function animate(angle) {
// 计算距离上次调用经过多长时间
let now = Date.now()
let elapsed = now - g_last // 毫秒
g_last = now
// 根据上次调用的时间,更新当前旋转角度
let newAngle = angle + (ANGLE_STEP * elapsed) / 1000.0
return (newAngle %= 360)
}
- g_last变量记录了上一次调用函数的时刻,它在初次加载html页面时,按照加载顺序初始化为当时的时间,在每次调用animate()函数时都会计算相差的时间并更新为调用的时间。
- animate()函数每次调用都会计算当前时间和上次调用函数时间的差值,根据旋转速度计算出旋转的角度,加上初始角度获得现在角度,返回现在的角度。
- tick()函数中将返回的角度赋值给currentAngle变量,更新当前旋转角,当然角度要除以360取余以保证其小于360度。
最终,每次调用animate()方法,当前旋转角度currentAngle和上次调用的时间g_last都会更新,currentAngle用于后面绘制图形。
绘制函数的设计:
draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix) // 绘制三角形
绘制函数draw()在tick()中第二行被调用,函数代码如下:
/**
* 绘制三角形
* @param {WebGLRenderingContext} gl 上下文对象
* @param {number} n 顶点数量 int
* @param {number} currentAngle 当前旋转角度 float
* @param {Martix4} modelMatrix 根据当前的旋转角度计算出的旋转矩阵,存储在Martix4对象中
* @param {number} u_ModelMatrix 顶点着色器中同名的uniform变量的存储位置 uint
*/
function draw(gl, n, currentAngle, modelMatrix, u_ModelMatrix) {
// 设置旋转矩阵
modelMatrix.setRotate(currentAngle, 0, 0, 1)
// 将旋转矩阵传输给顶点着色器
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
// 清空绘图区
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, n)
}
代码中的注释说明了函数的参数和函数的步骤,每次gl.drawArrays()之前都需要gl.clear()。
参数说明是笔者自行添加的,类型注释可有可无,许多参数是其他函数的返回值。
看到参数说明很想吐槽JavaScript的弱类型性质,全是number对使用有很大的麻烦,但不写number又不符合语言要求,故在后面又加上了int之类的标志,水平和时间有限,此处int没有区分无符号还是有符号。
小结
在这一章中,我们研究了如何使用矩阵库来对图形进行变换,如何将多个基本变换组合成一个复杂的变换,以及如何产生动画。这一章有两点很关键:第一,复杂变换的矩阵可以通过一系列基本变换的矩阵相乘得到;第二,通过反复变换和重绘图形可以生成动画效果。
在第五章中,“颜色与纹理”是介绍基础技术的最后一章,我们将研究如何使用颜色和纹理。一旦掌握了这些知识,你就可以开始编写自己的WebGL程序,我们将在之后的章节中继续学习WebGL的那些高级功能。