2.1 使用缓冲区对象
2.1.1 什么是缓冲区对象
缓冲区对象是WebGL系统中的一块内存区域,可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用。
缓冲区对象解决的实际上就是多个顶点绘制的问题。
2.1.2 缓冲区对象流程
2.1.3 缓冲区执行过程
2.1.4 使用缓冲区对象
1、创建一组顶点数据
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5
]);
在webgl中,需要处理大量的相同类型数据,所以引入类型化数组,这样程序就可以预知到数组中的数据类型,提高性能。这个类型化数组有点类似于js使用ts来定义类型。
2、创建缓冲区对象
使用gl的createBuffer方法来创建一个内存区域,用来存储顶点数据。
// 创建缓冲区对象
const buffer = gl.createBuffer();
3、webgl关联缓冲区对象
内存区域创建成功之后,就可以通过bindBuffer(target, buffer)将缓冲区对象绑定到webgl上。
(1)target:
1.gl.ARRAY_BUFFER:表示缓冲区存储的是顶点的数据
2.gl.ELEMENT_ARRAY_BUFFER:表示缓冲区存储的是顶点的索引值
(2)buffer:已经创建好的缓冲区对象
// webgl关联缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
4、顶点数据写入缓冲区对象
使用gl的bufferData(target,data,type)方法来写入顶点数据。
(1)target:类型同gl.bindBuffer中的target
(2)data:写入缓冲区的顶点数据,如程序中的points
(3)type:表示如何使用缓冲区对象的数据,分为以下几类
// 顶点数据写入缓冲区对象
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
5、attribute变量赋值
目前缓冲区的一系列操作已经准备就绪,但由于顶点渲染需要执行顶点着色器的main函数,也就是说需要给attribute变量赋值才能生效。
之前使用的是gl.vertexAttrib系列的方法进行赋值,这次使用的是gl的vertexAttribPointer(location,size,type,normalized,stride,offset)方法。
(1)location:attribute变量的存储位置
(2)size:指定每个顶点所使用数据的个数
(3)type:指定数据格式,也就是数据类型
(4)normalized:表示是否将数据归一化到[0,1][-1,1]这个区间
(5)stride:两个相邻顶点之间的字节数
(6)offset:数据偏移量
const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE);
const aPosition = gl.getAttribLocation(program, 'aPosition');
// 创建一组顶点数据
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5
]);
// 创建缓冲区对象
const buffer = gl.createBuffer();
// webgl关联缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 顶点数据写入缓冲区对象
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
// 给attribute变量赋值
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
解析:
1.attribute变量名叫aPosition,它作为第一个参数
2.创建一组顶点数据的时候,用的是new Float32Array的方式传入了6个值,它没有区分每个顶点的数据,而是一次性把所有顶点的数据都写入进去。所以需要指定一个顶点需要读取Float32Array的多少个值,现在一个顶点由两个值就可以确定,所以第二个参数传2;
3.Float32Array里的每个数据都是浮点型,因此第三个参数为gl.FLOAT;
4.顶点数据已经在区间内,因此第四个参数为false;
5.相邻顶点字节数为0,第五个参数为0;
6.因为在Float32Array创建的顶点数据中,里面有6个数据,正好指定每个顶点读取2个数据,没有产生偏移量,第六个参数为0。
6、激活/禁用attribute变量状态
attribute变量目前属于未激活的状态,需要通过gl.enableVertexAttribArray(location)激活才能使用。禁用的方法是gl.disableVertexAttribArray(location)
7、修改绘制数量
gl.drawArrays的3个参数分别是绘制的图形、开始绘制的顶点下标、绘制多少个顶点,现在要绘制3个顶点,因此要把第三个参数设置为3。
gl.drawArrays(gl.POINTS, 0, 3);
2.1.5 代码示例
2.2 多缓冲区和数据偏移
我们可以通过缓冲区绘制多个点,如果想控制不同的属性,例如3个点的大小分别为10、20、30,我们同样可以通过缓冲区来实现。
2.2.1 创建多个缓冲区
1、顶点着色器使用多个变量
之前顶点着色器使用aPosition变量控制点坐标,现在也可以设置aPointSize变量来控制点大小。
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float aPointSize;
void main() {
// 要绘制的点的坐标
gl_Position = aPosition; // vec4(0.0,0.0,0.0,1.0)
// 点的大小
gl_PointSize = aPointSize;
}
`;
2、创建并使用控制顶点大小的缓冲区
这里创建和使用缓冲区的流程,跟上文”2.1.4使用缓冲区对象”完全一致。
const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE);
const aPosition = gl.getAttribLocation(program, 'aPosition');
const aPointSize = gl.getAttribLocation(program, 'aPointSize');
// 创建一组顶点数据
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5
]);
// 创建顶点坐标缓冲区对象
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPosition);
// 创建一组顶点大小数据
const size = new Float32Array([
10.0,
20.0,
30.0
]);
// 创建顶点大小缓冲区对象
const sizeBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
gl.bufferData(gl.ARRAY_BUFFER, size, gl.STATIC_DRAW);
gl.vertexAttribPointer(aPointSize, 1, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPointSize);
gl.drawArrays(gl.POINTS, 0, 3);
2.2.2 多缓冲区流程
2.2.3 整合数据并共用缓冲区
目前我们声明了两个数据,创建了两个缓冲区,相当于控制一个属性就得创建一套缓冲区流程,如果控制的属性很多,那么代码会变得非常冗余。
我们可以将两组数据整合在一起,共用一个缓冲区对象,通过数据偏移的方式区分读取的数据。
1、整合两组数据
把顶点位置和大小这两组数据整合在一起。
// 创建一组顶点数据
const points = new Float32Array([
-0.5, -0.5, 10,
0.5, -0.5, 20,
0.0, 0.5, 30
]);
2、共用同一个缓冲区
只需要创建一个缓冲区,然后各自对自己的attribute属性赋值和激活即可。
const aPosition = gl.getAttribLocation(program, 'aPosition');
const aPointSize = gl.getAttribLocation(program, 'aPointSize');
// 创建一组顶点数据
const points = new Float32Array([
-0.5, -0.5, 10,
0.5, -0.5, 20,
0.0, 0.5, 30
]);
// 创建缓冲区对象
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPointSize, 1, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPointSize);
gl.drawArrays(gl.POINTS, 0, 3);
3、设置顶点数据间距
gl.vertexAttribPointer方法的第5个参数,代表的是两个数据之间的间距(字节数)是多少,简单的理解为多少个数据是一组的。
// 创建一组顶点数据
const points = new Float32Array([
-0.5, -0.5, 10,
0.5, -0.5, 20,
0.0, 0.5, 30
]);
例如这里有3组数据,-0.5,-0.5,10是第一组,0.5,-0.5,20是第二组,0.0, 0.5, 30是第三组,那么一组由3个数据组成的,因此间距就是3,但是它这里需要传入的是字节数,因此先获取字节数,再用字节数 * 3即可。
// 获取数据对应的字节数
const BYTES = points.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, BYTES * 3, 0);
gl.vertexAttribPointer(aPointSize, 1, gl.FLOAT, false, BYTES * 3, 0);
4、设置偏移量
gl.vertexAttribPointer方法的第6个参数,代表的是数据偏移量,简单的理解为在组内从第几个数据开始读取。
例如顶点数据是-0.5,-0.5,10,顶点坐标是-0.5,-0.5,顶点大小是10,所以顶点坐标从组内的第一个数据开始读取,它没有产生偏移量,而顶点大小是从组内的第三个数据开始读取,因此它的偏移量为2,因为它要略过前面两个数据。同样它这里需要传入的是字节数,因此传入字节数 * 偏移量即可。
// 获取数据对应的字节数
const BYTES = points.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, BYTES * 3, 0);
gl.vertexAttribPointer(aPointSize, 1, gl.FLOAT, false, BYTES * 3, BYTES * 2);
2.2.4 数据偏移执行过程
2.2.5 代码示例
2.3 实现多种图形绘制
2.3.1 线段系列
1、LINES:线段
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5
]);
gl.drawArrays(gl.LINES, 0, 3);
注意:LINES类型它只接收两个点的数据,超过两个它会自动忽略,像这里传了3也只会读取前两个。
2、LINE_STRIP:折线
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5
]);
gl.drawArrays(gl.LINE_STRIP, 0, 3);
3、LINE_LOOP:多边形
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5
]);
gl.drawArrays(gl.LINE_LOOP, 0, 3);
2.3.2 三角形系列
三角形系列的顶点数至少需要有3个。
1、TRIANGLES:三角形
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
0.5, 0.5
]);
gl.drawArrays(gl.TRIANGLES, 0, 4);
注意:TRIANGLES类型它可以绘制多个三角形,但顶点数必须是3的倍数,否则不生效,例如以下传入6个顶点数据。
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
0.5, 0.5,
0.5, 0.8,
0.8, 0.5
]);
gl.drawArrays(gl.TRIANGLES, 0, 6);
2、TRIANGLES_FAN:飘带状三角形
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
0.5, 0.5
]);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
3、TRIANGLES_STRIP:条带状三角形
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
0.5, 0.5
]);
gl.drawArrays(gl.TRIANGLES_STRIP, 0, 4);
注意:可以通过TRIANGLES_STRIP类型来绘制一个正方形,不过这个正方形实际上是由两个三角形拼接起来的。
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
-0.5, 0.5,
0.5, 0.5
]);
gl.drawArrays(gl.TRIANGLES_STRIP, 0, 4);
2.3.3 图形总结
1、顶点着色器的gl_PointSize只有在渲染点的时候才生效,绘制图形的时候是不生效的
2、绘制的图形都以三角形为基础
(1)三角形是最简单的多边形,顶点数量最少
(2)三角形可以作为很多多边形的基础图形,可以理解为任何的多边形都可以拆分成不同的三角形组合
(3)三个顶点能确定唯一的三角形,而且属于唯一的平面
(4)三角形的内外状态十分明确,可以通过它的一些计算来确定某一个点对三角形的内外情况
(5)确定了三角形的三个顶点,可以容易计算出从一个顶点逐渐变化到另一个顶点的过程
2.3.4 代码示例
2.4 图形平移-着色器
之前我们可以通过不停地修改它的位置数据来实现图形的平移,如果是多个顶点就得通过循环来实现,还要判断数据里有哪些是x轴的数据,有哪些是y轴的数据,这样会让平移的操作变得非常的复杂。
以下使用着色器的方式来实现图形的平移。
1、创建attribute变量代表偏移量
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float aTranslate; // 偏移量
void main() {
// 要绘制的点的坐标
gl_Position = aPosition; // vec4(0.0,0.0,0.0,1.0)
// 点的大小
gl_PointSize = 10.0;
}
`;
2、修改gl_Position的赋值方式
因为平移需要改变坐标的x、y、z的其中一项,因此要先将gl_Position变成aPosition的x、y、z分别传入到vec4()里,方便后续计算。
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float aTranslate; // 偏移量
void main() {
// 要绘制的点的坐标
gl_Position = vec4(aPosition.x, aPosition.y, aPosition.z, 1.0);
// 点的大小
gl_PointSize = 10.0;
}
`;
3、坐标添加偏移量的变量值
在哪个方向需要平移就对应在aPosition的x/y/z上加上aTranslate,这里实现在x轴平移,那么坐标的x轴坐标即为aPosition.x + aTranslate。
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float aTranslate; // 偏移量
void main() {
// 要绘制的点的坐标
gl_Position = vec4(aPosition.x + aTranslate, aPosition.y, aPosition.z, 1.0);
// 点的大小
gl_PointSize = 10.0;
}
`;
4、添加定时器改变偏移值
在哪个方向需要平移就对应在aPosition的x/y/z上加上aTranslate,这里实现在x轴平移,那么坐标的x轴坐标即为aPosition.x + aTranslate。
const aTranslate = gl.getAttribLocation(program, 'aTranslate');
let x = -1;
setInterval(() => {
x += 0.01;
if (x > 1) {
x = -1;
}
gl.vertexAttrib1f(aTranslate, x);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}, 60);
5、代码示例
2.5 图形缩放-着色器
跟图形平移类似,图形的缩放也可以使用attribute变量,通过坐标与缩放变量相乘来实现。
1、创建attribute变量代表缩放比例
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float aScale; // 缩放比例
void main() {
// 要绘制的点的坐标
gl_Position = vec4(aPosition.x, aPosition.y, aPosition.z, 1.0);
// 点的大小
gl_PointSize = 10.0;
}
`;
2、坐标与缩放变量相乘
在哪个方向需要缩放就对应在aPosition的x/y/z上乘以aScale,这里实现在x轴缩放,那么坐标的x轴坐标即为aPosition.x * aScale。
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float aScale;
void main() {
// 要绘制的点的坐标
gl_Position = vec4(aPosition.x * aScale, aPosition.y, aPosition.z, 1.0);
// 点的大小
gl_PointSize = 10.0;
}
`;
3、添加定时器改变缩放比例
在哪个方向需要平移就对应在aPosition的x/y/z上加上aTranslate,这里实现在x轴平移,那么坐标的x轴坐标即为aPosition.x + aTranslate。
const aScale = gl.getAttribLocation(program, 'aScale');
let x = -1;
setInterval(() => {
x += 0.01;
if (x > 1) {
x = -1;
}
gl.vertexAttrib1f(aScale, x);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}, 60);
4、代码示例
2.6 图形旋转-着色器
之前我们可以通过不停地修改它的位置数据来实现图形的平移,如果是多个顶点就得通过循环来实现,还要判断数据里有哪些是x轴的数据,有哪些是y轴的数据,这样会让平移的操作变得非常的复杂。
以下使用着色器的方式来实现图形的平移。
1、创建attribute变量代表旋转角度
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float deg;
void main() {
// 要绘制的点的坐标
gl_Position = vec4(aPosition.x, aPosition.y, aPosition.z, 1.0);
// 点的大小
gl_PointSize = 10.0;
}
`;
2、设置旋转公式
这里需要根据旋转的角度,并运用三角函数的计算来实现。这里实现围绕z轴进行旋转,那么坐标的x轴和y轴坐标都要套入计算公式。
关于旋转的计算公式,在2.9.1会详细介绍。
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
attribute float deg;
void main() {
gl_Position.x = aPosition.x * cos(deg) - aPosition.y * sin(deg);
gl_Position.y = aPosition.x * sin(deg) + aPosition.y * cos(deg);
gl_Position.z = aPosition.z;
gl_Position.w = aPosition.w;
}
`;
3、添加定时器改变旋转角度
定时器不需要判断条件,因为当角度超过360的时候就等于回到了原点。
const deg = gl.getAttribLocation(program, 'deg');
let x = 1;
setInterval(() => {
x += 0.1;
gl.vertexAttrib1f(deg, x);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}, 60);
4、使用requestAnimationFrame代替setInterval
使用requestAnimationFrame来实现动画,比setInterval要流畅得多。
let x = 1;
function amination() {
x += 0.01;
gl.vertexAttrib1f(deg, x);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(amination)
}
amination();
5、代码示例
2.7 图形平移-平移矩阵
2.7.1 矩阵简介
矩阵就是纵横排列的数据表格(m行n列),它就类似于二维数组,作用是把一个点转换到另一个点。
矩阵可以分为行主序和列主序,行主序就是元素按行来存储,列主序按列来存储。
2.7.2 平移矩阵
1、使用矩阵表示转换关系
如果把黄色的三角形平移到虚线的三角形,它需要改变x、y、z三个坐标。如果使用4*4的伪矩阵来代表它们的转换关系:
2、转换公式
(1)ax + by + cz + d = x + x1:只有当a = 1, b = c = 0, d = x1的时候,等式左右两边成立
(2)ex + fy + gz + h = y + y1:只有当f = 1, e = g = 0, h = y1的时候,等式左右两边成立
(3)ix + jy + kz + l = z + z1:只有当k = 1, i = j = 0, l = z1的时候,等式左右两边成立
(4)mx + ny + oz + p = 1:只有当m = n = o = 0, p = 1的时候,等式左右两边成立
3、获得平移矩阵
代入公式之后得到左边的矩阵,它是按行存储的,但webgl是按列存储的,因此需要将它转换为列存储。这个矩阵是通过计算得到的,然后可以将它运用到程序里面来实现图形的平移。
2.7.3 通过矩阵实现图形平移
1、声明矩阵变量
首先声明一个接收矩阵的变量,而且使用uniform来声明,数据类型是mat4。uniform变量是针对的顶点生效的,因为我们平移三角形是需要平移所有顶点的,所以用uniform。
声明变量之后,就可以把当前的矩阵应用在位置信息上,我们就通过mat * aPosition实现矩阵对于位置坐标的转换。
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
uniform mat4 mat;
void main() {
gl_Position = mat * aPosition
gl_PointSize = 10.0;
}
`;
2、获取矩阵变量
可以通过getUniformLocation(program, location)方法获取矩阵变量。
const mat = gl.getUniformLocation(program, 'mat');
3、创建平移矩阵函数
这个矩阵函数,返回的数据其实就是上述的平移矩阵的数据,需要我们手动将x、y、z的参数传入,它们这3个参数默认值为0,也就是说这3个方向的偏移量统一为0。
function getTranslateMatrix(x = 0, y = 0, z = 0) {
return 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,
x, y, z, 1
])
}
4、实现动画函数
(1)获取矩阵
通过矩阵函数来获得矩阵,这里实现x和y轴的方向平移,因此传入x和y的值作为参数。
let x = -1;
function amination() {
x += 0.01;
if (x > 1) {
x = -1;
}
const matrix = getTranslateMatrix(x, x);
gl.uniformMatrix4fv(mat, false, matrix);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(amination)
}
amination();
(2)对矩阵变量赋值
之前对vec2/3/4类型用的是vertexAttrib1/2/3/4f方法进行赋值。目前对mat4类型进行赋值,使用gl.uniformMatrix4fv()方法。
let x = -1;
function amination() {
x += 0.01;
if (x > 1) {
x = -1;
}
const matrix = getTranslateMatrix(x, x);
gl.uniformMatrix4fv(mat, false, matrix);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(amination)
}
amination();
2.7.4 代码示例
2.8 图形缩放-缩放矩阵
2.8.1 缩放矩阵
1、使用矩阵表示转换关系
如果把蓝色的三角形缩放到虚线的三角形,它跟平移矩阵的区别在于,平移是相加,缩放是相乘。如果使用4*4的伪矩阵来代表它们的转换关系,这个矩阵的映射公式跟平移矩阵是一样的:
2、转换公式
(1)ax + by + cz + d = Tx * x:只有当a = Tx, b = c = d = 0的时候,等式左右两边成立
(2)ex + fy + gz + h = Ty * y:只有当f = Ty, e = g = h = 0的时候,等式左右两边成立
(3)ix + jy + kz + l = Tz * z:只有当k = Tz, i = j = l = 0的时候,等式左右两边成立
(4)mx + ny + oz + p = 1:只有当m = n = o = 0, p = 1的时候,等式左右两边成立
3、获得缩放矩阵
代入公式之后得到左边的矩阵,因为它是对称的,所以不管是行还是列主序都是一样的。
2.8.2 通过矩阵实现图形缩放
第1步声明矩阵变量和第2步获取矩阵变量,跟平移矩阵是一样的,可以参考2.7.3。
3、创建缩放矩阵函数
这个矩阵函数,返回的数据其实就是上述的缩放矩阵的数据,需要我们手动将x、y、z的参数传入,它们这3个参数默认值为1,也就是说这3个方向的缩放比例统一为1。
function getScaleMatrix(x = 1, y = 1, z = 1) {
return new Float32Array([
x, 0.0, 0.0, 0.0,
0.0, y, 0.0, 0.0,
0.0, 0.0, z, 0.0,
0.0, 0.0, 0.0, 1
])
}
4、实现动画函数
步骤跟平移矩阵完全一样,可以参考2.7.3。在x的初始值和范围稍作调整,从0.1开始到1.5,也就是缩放比例从0.1倍到1.5倍。
let x = 0.1;
function amination() {
x += 0.01;
if (x > 1.5) {
x = 0.1;
}
const matrix = getScaleMatrix(x, x);
gl.uniformMatrix4fv(mat, false, matrix);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(amination)
}
amination();
2.8.3 代码示例
2.9 图形旋转-旋转矩阵
2.9.1 旋转矩阵
1、三角公式
图形旋转,以蓝色三角形旋转到虚线三角形为例。假设旋转前的顶点A的坐标为(x,y,z),旋转后的顶点A’的坐标为(x’,y’,z’):
(1)顶点A的坐标
x = R * cos(α)
y = R * sin(α)
z = 0
(2)顶点A’的坐标
x’ = R * cos(α + β)
y’ = R * sin(α + β)
z’ = 0
根据三角公式得知:
cos(α + β) = cos(α) * cos(β) – sin(α) * sin(β)
sin(α + β) = sin(α) * cos(β) + cos(α) * sin(β)
顶点A’的坐标代入三角公式为:
x’ = R * (cos(α) * cos(β) – sin(α) * sin(β)) = R*cos(α)cos(β) – Rsin(α)*sin(β)
y’ = R * (sin(α) * cos(β) + cos(α) * sin(β)) = R*sin(α)cos(β) + Rcos(α)*sin(β)
z’ = 0
(3)顶点A的公式代入顶点A’
x’ = x * cos(β) – y * sin(β)
y’ = y * cos(β) + x * sin(β)
z’ = z
2、矩阵推导
矩阵的映射公式跟平移/缩放矩阵是一样的:
3、转换公式
(1)ax + by + cz + d = x * cos(β) – y * sin(β):只有当a = cos(β), b = -sin(β),c = d = 0的时候,等式左右两边成立
(2)ex + fy + gz + h = y * cos(β) + x * sin(β):只有当e = sin(β), f = cos(β),g = h = 0的时候,等式左右两边成立
(3)ix + jy + kz + l = Tz * z:只有当k = 1, i = j = l = 0的时候,等式左右两边成立
(4)mx + ny + oz + p = 1:只有当m = n = o = 0, p = 1的时候,等式左右两边成立
3、获得旋转矩阵
代入公式之后得到以下的旋转矩阵:
2.9.2 通过矩阵实现图形旋转
第1步声明矩阵变量和第2步获取矩阵变量,跟平移矩阵是一样的,可以参考2.7.3。
3、创建旋转矩阵函数
这个矩阵函数只需要接收deg角度参数即可。
function getRotateMatrix(deg) {
return new Float32Array([
Math.cos(deg), Math.sin(deg), 0.0, 0.0,
-Math.sin(deg), Math.cos(deg), 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1
])
}
4、实现动画函数
步骤跟平移矩阵完全一样,可以参考2.7.3。变量x不需要加判断,因为到达360度就等于回到原点,一直累加即可。
let x = 0;
function amination() {
x += 0.01;
const matrix = getRotateMatrix(x);
gl.uniformMatrix4fv(mat, false, matrix);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(amination)
}
amination();
2.9.3 代码示例
2.10 图形复合变换-矩阵组合
图形复合变换指的是它包含平移、缩放、旋转中的两项以上。
2.10.1 三个矩阵共同控制图形变换
1、声明三个矩阵变量
声明三个矩形变量分别对应平移、缩放、旋转,然后将这3个变量与坐标变量aPosition相乘,实现复合变换。
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
uniform mat4 translateMatrix;
uniform mat4 scaleMatrix;
uniform mat4 rotationMatrix;
void main() {
gl_Position = translateMatrix * scaleMatrix * rotationMatrix * aPosition;
gl_PointSize = 10.0;
}
`;
2、获取三个矩阵变量
const translateMatrix = gl.getUniformLocation(program, 'translateMatrix');
const scaleMatrix = gl.getUniformLocation(program, 'scaleMatrix');
const rotationMatrix = gl.getUniformLocation(program, 'rotationMatrix');
3、调整动画函数
动画函数里要获取这3个矩阵,然后分别定义对应的变量作为参数传入平移、缩放、旋转的矩阵函数。
let deg = 0;
let translateX = -1;
let scaleX = 0.1;
function amination() {
deg += 0.01;
translateX += 0.01;
scaleX += 0.01;
if (translateX > 1) {
translateX = -1;
}
if (scaleX > 1.5) {
scaleX = 0.1;
}
const translate = getTranslateMatrix(translateX);
const scale = getScaleMatrix(scaleX);
const rotate = getRotateMatrix(deg);
gl.uniformMatrix4fv(translateMatrix, false, translate);
gl.uniformMatrix4fv(scaleMatrix, false, scale);
gl.uniformMatrix4fv(rotationMatrix, false, rotate);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(amination);
}
amination();
2.10.2 组合矩阵
上述的方式是可以实现图形复合的,但是它需要声明3个uniform变量,然后传入3个矩阵,然后改变3个变量值。这样的方式显得比较麻烦而且代码比较臃肿。
可以通过将3个矩阵组合在一起,也一样能实现图形变换。
1、矩阵相乘
矩阵相乘的执行过程是矩阵A的列乘以矩阵B的行。
注意:矩阵的乘法是不符合乘法交换律的,也就是说矩阵A * 矩阵B,是不等于矩阵B * 矩阵A的。
2.10.3 通过组合矩阵实现图形复合
1、声明组合矩阵变量
使用组合矩阵之后,就不需要声明3个uniform变量了,只需要声明一个uniform变量来代表组合矩阵即可。
const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
uniform mat4 mat;
void main() {
gl_Position = mat * aPosition
gl_PointSize = 10.0;
}
`;
2、获取组合矩阵变量
const mat = gl.getUniformLocation(program, 'mat');
3、创建矩阵组合函数
function mixMatrix(A, B) {
const result = new Float32Array(16);
for (let i=0; i<4; i++) {
result[i] = A[i] * B[0] + A[i + 4] * B[1] + A[i + 8] * B[2] + A[i + 12] * B[3];
result[i + 4] = A[i] * B[4] + A[i + 4] * B[5] + A[i + 8] * B[6] + A[i + 12] * B[7];
result[i + 8] = A[i] * B[8] + A[i + 4] * B[9] + A[i + 8] * B[10] + A[i + 12] * B[11];
result[i + 12] = A[i] * B[12] + A[i + 4] * B[13] + A[i + 8] * B[14] + A[i + 12] * B[15];
}
return result;
}
4、优化动画函数
动画函数里不再需要分别获取3个矩阵,只需要获取一个组合矩阵即可。
let deg = 0;
let translateX = -1;
let scaleX = 0.1;
function amination() {
deg += 0.01;
translateX += 0.01;
scaleX += 0.01;
if (translateX > 1) {
translateX = -1;
}
if (scaleX > 1.5) {
scaleX = 0.1;
}
const translate = getTranslateMatrix(translateX);
const scale = getScaleMatrix(scaleX);
const rotate = getRotateMatrix(deg);
// 组合矩阵控制图形复合
const matrix = mixMatrix(mixMatrix(translate, scale), rotate);
gl.uniformMatrix4fv(mat, false, matrix);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(amination);
}
amination();