构成三维模型的基本图形是三角形,所以接下来就从如何绘制一个三角形开始,之后涉及到图形的变换和动画。
1. 图形绘制
先回顾以下绘制单个点的方式:通过gl.getAttribLocati on()
获得了GLSL
中的Vertex着色器的属性值,并利用gl.vertexArrib[1234]f()
方法簇给着色器属性赋值,并将值传递给GLSL
的内置变量gl_Position
,之后调用gl.drawArrays(gl.POINT, 0, 1)
的方法绘制点。
如果要绘制多个点怎么办?当然,我们可以设置多次调用gl.vertexAttrib[1234]f
和gl.drawArrays(gl.POINT, 0, 1)
的方式来实现绘制多个点,例如:
html:
<canvas id="glCanvas" width="640" height="480"></canvas>
Javascript:
const canvas = document.querySelector('#glCanvas');
const gl = canvas.getContext('webgl');
// 着色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
void main() {
gl_Position = a_Position;
gl_PointSize = a_PointSize;
}
`;
const FSHADER_SOURCE = `
precision mediump float;
uniform vec4 u_FragColor;
void main() {
gl_FragColor = u_FragColor;
}
`
let program = init(gl, VSHADER_SOURCE, FSHADER_SOURCE);
// 获取顶点位置的属性
let a_Position = gl.getAttribLocation(program, 'a_Position');
if (a_Position < 0) {
console.log('Cant find the position');
return;
}
// 设置顶点点的尺寸
let a_PointSize = gl.getAttribLocation(program, 'a_PointSize');
if (a_PointSize < 0) {
console.log('Cant find the pointsize');
return;
}
gl.vertexAttrib1f(a_PointSize, 10.0);
// 设置顶点颜色
let u_FragColor = gl.getUniformLocation(program, 'u_FragColor');
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 设置并绘制多个顶点
drawPoint();
// 绘制多个点的方法
function drawPoint(gl, a_Position) {
gl.vertexAttrib3f(a_Position, -0.5, -0.5, 0.0);
gl.drawArrays(gl.POINT, 0, 1);
gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0);
gl.drawArrays(gl.POINT, 0, 1);
gl.vertexAttrib3f(a_Position, 0.5, -0.5, 0.0);
gl.drawArrays(gl.POINT, 0, 1);
}
这样反复调用绘制的方案明显效率低下(反复重绘整个画布),另外如果我们想绘制其他图形,建立点之间点连接关系,似乎就没有办法了(因为每次绘制的点都是独立的)。为了解决着两个问题,就需要用到WebGL提供的缓冲区来一次性存储多种信息(不仅仅存储顶点坐标,还可以一次性存入顶点颜色等信息,后面再讨论)
1.1 利用缓冲区绘制多个点
WebGL中要使用缓冲区,主要有以下五个步骤:
- 利用
gl.createBuffer()
创建缓冲区对象- 利用
gl.bindBuffer()
将创建的缓冲区对象和WebGL中的内置对象gl.ARRAY_BUFFER
进行绑定- 利用
gl.bufferData()
给gl.ARRAY_BUFFER
内置对象传递数据(数据不能直接传递给缓冲区对象,要通过gl.ARRAY_BUFFER
来进行)- 利用
gl.vertexAttribPointer()
将缓冲区数据分配给GLSL
中的变量- 最后利用
gl.enableVertexAttribArray()
开启缓冲区对变量的使用,开启后gl.vertexAttrib[1234]f()
方法簇的赋值将失效
完成上述操作后,调用gl.drawArrays()
来就可以一次绘制缓冲区数据了,这时只要将第三个参数变为所需要绘制点的数目就可以了。根据前一个例子,这里将drawPoint()
进行调整为使用缓冲区
function initPoint(gl, a_Position) {
let pointData = new Float32Array([
-0.5, -0.5,
0, 0.5,
0.5, -0.5
]);
// 创建缓冲区对象
let buffer = gl.createBuffer();
// 绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 将数据传递到缓冲区
gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
// 将缓冲区数据传递给顶点着色器attribute属性
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 激活顶点和缓冲区到连接
gl.enableVertexAttribArray(a_Position);
// 一次性绘制多个点
gl.drawArrays(gl.POINT, 0, 3);
}
PS:代码中使用了Float32Array
来创建点的数据对象,它是Javascript
提供的一种类型化数组,目的是为了说明数组中的所有数据都是同一种数据类型的特殊数组,使处理数组效率更快(可能不用做类型判断和转化了),其中类型化数组提供了很多方法和属性,尤其BYTES_PER_ELEMENT
属性在之后会有很大用处的。
1.2 利用mode控制图形绘制
在使用缓冲区的基础上,绘制图形就很简单了,只需要改变gl.drawArrays()
中的第一个参数就可以了
WebGL的第一个参数mode,提供了7种值:
gl.POINT
: 绘制点gl.LINES
: 绘制线段gl.LINE_STRIP
:绘制连续线段,例如传入[A0, A1, A2, A3]四个坐标信息,那么绘制结果为[A0, A1], [A1, A2], [A2, A3]gl.LINE_LOOP
:首位两个点会连接起来gl.TRIANGLES
:绘制三角形gl.TRIANGLE_STRIP
:绘制一系列三角形,例如传入[A0, A1, A2, A3, A4, A5]五个坐标信息,那么绘制结果为[A0, A1, A2], [A2, A1, A3], [A2, A3, A4], [A4, A3, A5](webGL中绘制是按照逆时针方式进行绘制的)gl.TRIANGLE_FAN
:以第一个点为所有三角形顶点,绘制三角扇
2. 图形变换
我们经常会将绘制的图形进行平移,旋转,缩放的操作,这些操作统一被称为仿射变化。
2.1 基本变换
2.1.1 平移
平移要实现的其实是方向坐标上的位移:
x' = x + ux;
y' = y + uy;
因为GLSL中矢量可以直接进行加减运算,所以修改顶点着色器程序:
// 着色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
void main() {
// 矢量可以直接进行运算
gl_Position = a_Position + vec4(0.1, 0.1, 0.0, 0.0);
gl_PointSize = a_PointSize;
}
`;
如果想要自定义平移距离,那么可以在顶点着色器程序中新增一个uniform
变量(使用uniform
是因为变量本身与顶点无关)
// 着色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
unifrom vec4 u_Translate;
void main() {
// 矢量可以直接进行运算
gl_Position = a_Position + u_Translate;
gl_PointSize = a_PointSize;
}
`;
// 设置平移距离
let u_Translate = gl.getUniformLocation(program, 'u_Translate');
gl.uniform4f(u_Translate, 0.1, 0.1, 0.0, 0.0);
PS:要注意因为是使用齐次坐标,所以最后一个变量值要传递为0.0(因为默认齐次坐标的第四个变量为1.0,矢量计算后最后一个变量仍然应该为1.0)
2.1.2 缩放
缩放要实现的是方向坐标上的比例变化
x' = ux;
y' = uy;
实现缩放的关键是要获取坐标在某些方向上的分量,可以通过a_Position.x
和a_Position.y
来分别获取x
和y
方向上的分量,修改着色器程序:
// 着色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
void main() {
// 分别设置四个方向上的分量信息
gl_Position.x = a_Position.x * 0.5;
gl_Position.y = a_Position.y * 0.5;
gl_Position.z = a_Position.z;
gl_Position.w = 1.0;
gl_PointSize = a_PointSize;
}
`;
2.1.3 旋转
旋转相比起来就复杂的多了,旋转必须指明三个要素,旋转轴,旋转方向和旋转角度。
WebGL中旋转正方向是逆时针方向,遵循右手旋转法则,也就是大拇指朝向轴的正方向,四指方向就是旋转正方向。
以仅绕z轴旋转为例,那么如果坐标系中的点的原角度为a(与x轴正方向角度),转动角度为b,我们可以利用三角函数得到
x = r*cos(a);
y = r*sin(a);
// 利用三角函数和角公式进行变化
x' = r*cos(a+b) = r*cos(a)*cos(b) - r*sin(a)*sin(b) = x*cos(b) - y* sin(b);
y' = r*sin(a+b) = r*cos(a)*sin(b) + r*sin(a)*cos(b) = x*sin(b) + y*cos(b)
于是同样通过获取分量的方式,将顶点着色器程序进行修改
// 着色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
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;
gl_PointSize = a_PointSize;
}
`;
// 转动30度
let rad = 90 / 180 * Math.PI;
let u_Sinb = gl.getUniformLocation(program, 'u_Sinb');
gl.uniform1f(u_Sinb, Math.sin(rad));
let u_Cosb = gl.getUniformLocation(program, 'u_Cosb');
gl.uniform1f(u_Cosb, Math.cos(rad));
2.2 矩阵变换
所有的变换其实都是平移,旋转,缩放的叠加,但是按照之前的方式来进行那么在变换叠加的时候,计算过程就变得麻烦了,例如,我们要先旋转后平移再缩放。。。由于WebGL支持矩阵运算,所以变换可以使用矩阵变化来处理。
具体矩阵运算可以参见矩阵(其实也就是将方程转换为了矩阵的方式)
在WebGL中,要使用变换矩阵主要要进行四步骤:
- 顶点着色器程序中增加矩阵变量
uniform mat4 u_Matrix
- 顶点着色器程序中修改
gl_Position = u_Matrix * a_Position
(注意矩阵计算时的顺序)- 创建矩阵变换的数组
new Float32Array()
- 使用
gl.unifromMatrix[1234]fv
的方法设置矩阵变换uniform变量的值
以旋转变化为例,那么:
// 着色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
uniform mat4 u_Matrix;
void main() {
gl_Position = u_Matrix * a_Position;
gl_PointSize = a_PointSize;
}
`;
let rad = 90 / 180 * Math.PI;
let u_Matrix = gl.getUniformLocation(program, 'u_Matrix');
let matrix = new Float32Array([
Math.cos(rad), Math.sin(rad), 0.0, 0.0,
-Math.sin(rad), Math.cos(rad), 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
])
gl.uniformMatrix4fv(u_Matrix, false, matrix);
PS:特别注意,数组中存储二维数组有两种顺序,按列主序和按行主序,WebGL和OpenGL中都是按列主序,所以,注意数组中的数据和真是矩阵中数据位置存在转置(gl.uniformMatrix4fv
的第二个参数可以实现矩阵转置,但是WebGL中并没有实现转置操作所以始终默认为false
)
3. 动画
3.1 利用requestAnimationFrame进行动画
动画基本上就是基于图形的各种变换的持续执行过程,于是在webGL中要实现动画,实际上就是在图形变换的基础上,调用了动画函数进行循环调用,不断重新绘制图形。可以使用setInterval
函数也可以使用requestAnimationFrame
来实现动画的循环调用,不过建议使用后者,因为后者只有在浏览器tab页激活的时候才会执行,而前者会一直执行。还是以旋转为例,下面是持续旋转的例子:
// 着色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
uniform mat4 u_Matrix;
void main() {
gl_Position = u_Matrix * a_Position;
gl_PointSize = a_PointSize;
}
`;
let start = Date.now();
let rad = 0;
animation();
// 旋转动画
function animation() {
let now = Date.now();
let offsetTime = now - start;
start = now;
// 假设每秒钟转动30度
rad += offsetTime * 30 / 360 / 1000 * Math.PI;
let u_Matrix = gl.getUniformLocation(program, 'u_Matrix');
let matrix = new Float32Array([
Math.cos(rad), Math.sin(rad), 0.0, 0.0,
-Math.sin(rad), Math.cos(rad), 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
])
gl.uniformMatrix4fv(u_Matrix, false, matrix);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.LINE_LOOP, 0, 3);
requestAnimationFrame(animation);
}
}
- 总结
一开始我总觉得自己在旋转的过程中图形形状发生了变化,很奇怪,最后花了很多事件才发现原来demo中的canvas不是正方形。。。所以在进行旋转操作的时候,x轴和y轴并不是相同的单位比例。 - 参考
《WebGL编程指南》
网友评论