衔接上一篇:第三章:绘制和变换图形
收获
当你学习完下面内容后你会有如下收获:
- 什么是复合变换、如何进行复合变换(矩阵相乘)。
- 使用矩阵库来对图形进行变换。
- 如何产生动画、动画的两个关键点。
前言
上一章你已经了解了如何利用缓冲区对象绘制三角形,通过数学表达式学习了图形变换(平移、旋转、缩放)的原理,了解了如何使用矩阵来简化变换操作(涉及到复合变换时,使用表达式变换图形非常繁琐)。在这一章中,我们将进一步研究变换矩阵。
1.复合变换
我们上一章我们只是对图形进行一次基础的变换,而这一章我们将对图形进行多次的有顺序的基础变换,这一过程称为复合变换。
1.1 问题思考
我们可以思考一下,下面的这个复合变换的例子该如何得到最后的新矢量。
例如:
- 我现在有一个初始点位A(x,y,z,1.0)。
- 先沿X轴移动Tx,沿Y轴移动Ty,沿Z轴移动Tz。
- 然后旋转β度。
到这里我们学习了矩阵与矢量的乘积,其实上面的例子可以通过平移矩阵 * 矢量
得到平移后的矢量,然后再用旋转矩阵 * 平移后矢量
就能够得到先平移后旋转的矢量了。但是这样太麻烦了,相信学过高等数学的应该知道矩阵乘法,就是矩阵与矩阵的相乘能够得到一个新的矩阵。那么我们这里就可以使用平移矩阵 * 旋转矩阵
得到一个复合矩阵,最后将这个复合矩阵与矢量相乘即可。
1.2 矩阵相乘
下面是两个2×2
矩阵相乘的过程,同样的3×3
或4×4
矩阵也是一样的计算步骤。
相信你已经发现了这一过程,就是第一个矩阵的行与第二个矩阵的列相乘后相加来得到的一个新矩阵。
1.3 实现代码
下面我们可以做一个练习,需求就是:将三角形沿Y轴向上移动0.7,然后旋转180度
相信你学习完矩阵相乘后,再结合上一章,这个需求对于你来说已经不成问题。
实现代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
margin: 0;
}
</style>
<script src="./utils.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
uniform mat4 u_RotateMatrix;
uniform mat4 u_TranslateMartix;
void main () {
// 先进行平移然后旋转所以是平移矩阵 * 旋转矩阵 * 矢量
gl_Position = u_TranslateMartix * u_RotateMatrix * a_Position;
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
void main () {
gl_FragColor = vec4(1.0, 1.0, 0, 1.0);
}
</script>
<script>
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const vsSource = document.getElementById('vertexShader').innerHTML;
const fsSource = document.getElementById('fragmentShader').innerHTML;
const gl = canvas.getContext('webgl');
const vertices = new Float32Array([0, 0.3, -0.1, 0, 0.1, 0]);
const ANGLE = 180;
const radian = (Math.PI * ANGLE) / 180;
const cosB = Math.cos(radian), sinB = Math.sin(radian);
// 旋转矩阵
const rotateMatrix = 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,
])
const Tx = 0.0, Ty = 0.7, Tz = 0.0;
// 平移矩阵
const translationMatrix = 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,
])
initShader(gl, vsSource, fsSource);
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
const u_RotateMatrix = gl.getUniformLocation(gl.program, 'u_RotateMatrix');
const u_TranslateMartix = gl.getUniformLocation(gl.program, 'u_TranslateMartix');
gl.uniformMatrix4fv(u_TranslateMartix, false, translationMatrix);
gl.uniformMatrix4fv(u_RotateMatrix, false, rotateMatrix);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>
</body>
</html>
这里的utils.js
在第二章:WebGL入门这篇文章中有写到,暂时不需要我们理解里面的内容,用就好了。
2.矩阵变换库
虽然平移、旋转、缩放等变换操作都可以用4×4矩阵表示,但是在写WebGL程序的时候,手动计算每个矩阵很耗费时间。为了提高开发的效率和简化编程,我们可以使用第三方的数学库来隐藏矩阵计算的细节。下面我们都将使用Three.js
的Martrix
来简化矩阵操作。
创建矩阵
我们也可以通过Martrix
来对象的set
方法来创建矩阵,如下:
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/0.151.3/three.min.js"></script>
<script>
// 创建旋转矩阵
const ANGLE = 180;
const radian = (Math.PI * ANGLE) / 180;
const cosB = Math.cos(radian), sinB = Math.sin(radian);
const rotateMatrix = new THREE.Matrix4().set(
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,
)
// 创建平移矩阵
const Tx = 0.0, Ty = 0.7, Tz = 0.0;
const translationMatrix = new THREE.Matrix4().set(
1.0, 0.0, 0.0, Tx,
0.0, 1.0, 0.0, Ty,
0.0, 0.0, 1.0, Tz,
0.0, 0.0, 0.0, 1.0,
)
</script>
我们可以通过rotateMatrix.elements
和translationMatrix.elements
就能访问矩阵数据。
console.log(translationMatrix.elements);
矩阵相乘
之前我们是将两个矩阵传入到顶点着色器中进行相乘的,这里我们还可以通过Martrix
对象的multiply
方法来将复合矩阵在JavaScript
中计算出来。然后直接将其传入着色器中。
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
// 复合矩阵
uniform mat4 u_Matrix;
void main () {
gl_Position = u_Matrix * a_Position;
}
</script>
// 注意先后顺序先平移旋转
const matrix4 = translationMatrix.multiply(rotateMatrix);
const u_Matrix = gl.getUniformLocation(gl.program, 'u_Matrix');
gl.uniformMatrix4fv(u_Matrix, false, matrix4.elements);
平移、旋转、缩放矩阵
我们还可以通过Martrix
对象的makeRotation[X|Y|Z]
、makeTranslation
、makeScale
这些方法得到对应变换后的矩阵。
旋转矩阵
得到绕Z轴旋转180度后的旋转矩阵
// const ANGLE = 180;
// const radian = (Math.PI * ANGLE) / 180;
// const cosB = Math.cos(radian), sinB = Math.sin(radian);
// const rotateMatrix = 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,
// ])
// 一行代码代替
const rotateMatrix = new THREE.Matrix4().makeRotationZ((Math.PI * 180) / 180);
console.log(rotateMatrix.elements)
平移矩阵
得到沿Y轴向上移动0.7后的平移矩阵
// const Tx = 0.0, Ty = 0.7, Tz = 0.0;
// const translationMatrix = 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,
// ])
// 一行代码代替
const translationMatrix = new THREE.Matrix4().makeTranslation(0.0, 0.7, 0.0);
console.log(translationMatrix.elements)
缩放矩阵
得到放大2倍的缩放矩阵
// const scale = 2.0
// const scaleMatrix = new Float32Array([
// scale, 0.0, 0.0, 0.0,
// 0.0, scale, 0.0, 0.0,
// 0.0, 0.0, scale, 0.0,
// 0.0, 0.0, 0.0, 1.0,
// ])
// 一行代码代替
const scaleMatrix = new THREE.Matrix4().makeScale(2, 2, 2);
console.log(scaleMatrix.elements);
练一练
到这里想必你已经感受到矩阵变换库的方便,接下来我们可以利用矩阵变换库来练练手。
需求:将三角形沿Y轴向上移动0.7,然后旋转180度,再放大两倍。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
margin: 0;
}
</style>
<script src="./utils.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/0.151.3/three.min.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
uniform mat4 u_Matrix4;
void main () {
gl_Position = u_Matrix4 * a_Position;
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
void main () {
gl_FragColor = vec4(1.0, 1.0, 0, 1.0);
}
</script>
<script>
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const vsSource = document.getElementById('vertexShader').innerHTML;
const fsSource = document.getElementById('fragmentShader').innerHTML;
const gl = canvas.getContext('webgl');
const vertices = new Float32Array([0, 0.3, -0.1, 0, 0.1, 0]);
// 平移、旋转、缩放矩阵
const translationMatrix = new THREE.Matrix4().makeTranslation(0.0, 0.7, 0.0);
const rotateMatrix = new THREE.Matrix4().makeRotationZ((Math.PI * 180) / 180);
const scaleMatrix = new THREE.Matrix4().makeScale(2, 2, 2);
// 复合矩阵
const matrix4 = translationMatrix.multiply(rotateMatrix).multiply(scaleMatrix);
initShader(gl, vsSource, fsSource);
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
const u_Matrix4 = gl.getUniformLocation(gl.program, 'u_Matrix4');
gl.uniformMatrix4fv(u_Matrix4, false, matrix4.elements);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>
</body>
</html>
3.动画
上面我们已经介绍了使用矩阵库中的函数进行矩阵变换操作的知识。下面我们将要将矩阵变换运用到动画图形中去。
接下来我们要让三角形绕Y轴旋转起来。
3.1 动画基础
为了能够让三角形转动起来,我们需要不断的擦除和重绘,并且在每次重绘时轻微的改变其角度。
下图显示了一个转动的三角形在 t0、t1 、t2、t3、t4时刻的情形。每一张都是静态的,但是你能看出每张图中三角形的角度略有不同。当你按照顺序快速连续地看到这些图的时候,你的大脑就会不自觉地对影像进行插值,形成流畅的动画,就像“翻书页”一样。当然,在绘制一个新的三角形之前,你还需要将上一个三角形擦除掉。在绘制之前,我们需要调用gl.clear(),这条规则不管是对2D图形还是3D对象都适用。
为了生成动画,我们需要两个关键机制:
机制一:在t0、t1、t2、t3等时刻反复调用同一个函数来绘制三角形。
机制二:每次绘制之前,清除上次绘制的内容,并使三角形旋转相应的角度。
3.2 实现代码
首先我们需要有一个东西能够不停的去擦除和重绘三角形。
这时候可能你会想到使用setInterval()
函数。但是使用它有一些缺点,现代的浏览器都支持多个标签页,每个标签页具有单独的JavaScript运行环境,但是自setInterval()函数诞生之初,浏览器还没有开始支持多标签页。所以在现代浏览器中,不管标签页是否被激活,其中的setInterval()函数函数都会反复调用func,如果标签页比较多,就会增加浏览器的负荷。所以后来,浏览器又引入了requestAnimation()
方法,也就是我们接下来要使用的方法。该方法只有当标签页处于激活状态时才会生效。
function tick() {
// 更新三角形的旋转角
console.log('更新旋转角');
// 重绘三角形
requestAnimationFrame(tick);
}
tick();
我们会发现tick()
函数在被不停的调用
所以我们就可以在该函数内不停的更新和重绘,我们可以写一个函数用来更新三角形旋转的角度。
// 每次转动一度
function updateAngle(angle) {
angle++;
return angle % 360;
}
上面这段代码是我最初的想法,其实这个函数逻辑没有什么问题。但是我们忽略了requestAnimationFrame()
我们不能指定调用时机,只有请求浏览器在适当的时机调用参数函数,那么浏览器就会根据自身状态来决定下一次是什么时候调用参数函数。如果使用上面的方法固定每次旋转的角度的话就会导致不可控制的加速或减速的旋转效果。
既然我们不能控制调用时机,那么我们是不是可以通过控制每次旋转的角度来实现呢?我们可以根据本次调用与上次调用的时间间隔来控制旋转角度的大小,间隔短旋转的角度就小,间隔大旋转的角度就大。
// 旋转速度45(度/秒)
let ANGLE_STEP = 45,
// 初始化为当前时间
let g_last = Date.now();
function updateAngle(angle) {
const now = Date.now();
// 计算时间间隔
const elapsed = now - g_last;
g_last = now;
// 除1000同一单位秒
let newAngle = angle + (ANGLE_STEP * elapsed) / 1000;
return (newAngle %= 360);
}
有了每次需要旋转的角度,接下来我们还需要有一个方法用来旋转图形、清除上一次绘制、重绘。
function draw(gl, currentAngle, modelMatrix, u_ModelMatrix) {
// 得到新的旋转矩阵
modelMatrix = modelMatrix.makeRotationY((Math.PI * currentAngle) / 180);
// 将旋转矩阵传递给顶点着色器
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);
// 清除<canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
margin: 0;
}
</style>
<script src="./utils.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/three.js/0.151.3/three.min.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
uniform mat4 u_ModelMatrix;
void main () {
gl_Position = u_ModelMatrix * a_Position;
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
void main () {
gl_FragColor = vec4(1.0, 1.0, 0, 1.0);
}
</script>
<script>
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const vsSource = document.getElementById('vertexShader').innerHTML;
const fsSource = document.getElementById('fragmentShader').innerHTML;
const gl = canvas.getContext('webgl');
const vertices = new Float32Array([0, 0.3, -0.1, 0, 0.1, 0]);
initShader(gl, vsSource, fsSource);
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
const u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix');
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
// 更新旋转角度
let g_last = Date.now(), ANGLE_STEP = 45, currentAngle = 0, modelMatrix = new THREE.Matrix4();
function updateAngle(angle) {
const now = Date.now();
const elapsed = now - g_last;
g_last = now;
let newAngle = angle + (ANGLE_STEP * elapsed) / 1000;
return (newAngle %= 360);
}
// 重绘
function draw(gl, currentAngle, modelMatrix, u_ModelMatrix) {
// 得到新的旋转矩阵
modelMatrix = modelMatrix.makeRotationY((Math.PI * currentAngle) / 180);
// 将旋转矩阵传递给顶点着色器
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);
// 清除<canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
function tick() {
// 更新三角形的旋转角
currentAngle = updateAngle(currentAngle);
draw(gl, currentAngle, modelMatrix, u_ModelMatrix);
requestAnimationFrame(tick);
}
tick();
</script>
</body>
</html>