我正在参加「掘金·启航计划」
摘要
上一篇文章介绍了webgl的坐标系,以及如何利用webgl并与屏幕进行交互,从而绘制点和线;这篇文章在此基础上将记录如何绘制三角形,以及利用三角形的绘制,与webgl中的特殊变量纹理来进行图片的绘制。
三角形绘制
- 沿用上一篇文章的shader,我们只需要改变
gl.drawArrays中的参数,即可绘制三角形 - 首先是
gl.drawArrays中的第一个参数,其中代表三角形绘制的值可以为以下三种:gl.TRIANGLESgl.TRIANGLE_STRIPgl.TRIANGLE_FAN
TRIANGLES代表每三个点,组成一个三角形,每个点使用完后,不再重复使用TRIANGLE_STRIP代表遍历所有点,以遍历到的点以及跟着他后面的两个点,组成一个三角形,即4个点可以画两个三角形,这种情况下,数组中的点可能使用超过一次TRIANGLE_FAN代表将第一个点作为接下来绘制的所有三角形的一个顶点,然后遍历剩余的点,每个点以及其后的点与第一个点共同组成一个三角形,这种情况下,每个点也是会被使用多次- 除了
TRIANGLES模式,可以画出分离的三角形,其余两种模式绘制的三角形,必然都是相连的
function drawTriangle (array, type, start, count) {
// array,绘制所用到的点数据
// type,上面三角形绘制模式之一
// start,从数组的那个数据开始绘制,默认是0
// count,绘制了多少个点
const posData = new Float32Array(array);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, posData, gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
const colorData = new Float32Array(pointColorArr);
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colorData, gl.STATIC_DRAW);
const aColor = gl.getAttribLocation(program, 'a_color');
gl.enableVertexAttribArray(aColor);
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0);
gl.drawArrays(type, start, count);
}
- 上一篇文章中,我们介绍了如何通过点击屏幕并通过变换,记录点击屏幕的点,以及将之转换为webgl中的坐标,这里我们依然通过这个方法,实现点击屏幕,绘制三角形,首先记录点击屏幕的点
canvas.addEventListener('click', (e) => {
addPoints(e.clientX, e.clientY);
});
function addPoints (x, y) {
pointArr.push(x, y);
pointColorArr.push(Math.random(), Math.random(), Math.random());
}
- 记录屏幕点的同时,生成一个随机的颜色值,代表这个点的颜色
- 数组有了,颜色也有了,直接调用绘制方法就可以了
canvas.addEventListener('click', (e) => {
addPoints(e.clientX, e.clientY);
drawTriangle(pointArr, gl.TRIANGLES, 0, pointArr.length / 2);
// drawTriaxngle(pointArr, gl.TRIANGLE_STRIP, 0, pointArr.length / 2)
// drawTriangle(pointArr, gl.TRIANGLE_FAN, 0, pointArr.length / 2)
});
- 三种绘制方法的效果如下:
图片的绘制
- 图片的绘制是在三角形绘制的基础上进行的,因为三角形可以组合成面,而在这个面里,我们只需要将图片对应的像素,代替随机生成的颜色值,填充进去,就实现了图片的绘制,而这个
图片对应的像素,可以利用webgl中的一个特殊变量,纹理来实现
纹理变量
- shader中有专门的变量用来表示纹理,
sampler2D - 用
sampler2D声明的变量,可以简单的理解为一张存放在webgl内存里的图片,他包含整张图片的像素 - 在shader中,读取纹理的像素要用到
texture2D方法,该方法接受两个参数,一个是纹理变量,一个是纹理坐标 - 结合上面的介绍,我们可以写出下面的
fragment shader
precision mediump float;
varying vec2 v_texCoord; // 纹理坐标,从vertex shader传入
uniform sampler2D u_image; // 纹理变量
void main() {
vec4 texture = texture2D(u_image, v_texCoord); // 根据纹理坐标读取像素
gl_FragColor = texture; // 赋值
}
vertex shader
attribute vec2 a_position;
uniform mat4 u_proj;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main(void) {
gl_Position = u_proj * vec4(a_position, 0, 1.0);
v_texCoord = a_texCoord; // 将纹理坐标传给fragment shader
}
纹理变量赋值
- 类似于
attribute变量,纹理的赋值,需要先将图片存到内存里,再声明变量从内存中取
// 1. 创建一个纹理的内存空间
const texture = gl.createTexture();
// 2. 绑定该纹理到当前的处理对象
// 这里同时有个隐藏逻辑,将texture绑定到0号纹理
// webgl可以在内存中保存多张纹理,以index区分
// 如果想将纹理绑定到0号纹理以外的纹理
// 需要在bindTexture,使用gl.activeTexture(gl.TEXTURE1)
// 这样当前的处理对象,就会变成1号纹理,后续bindTexture会将纹理对象绑定到1号纹理
gl.bindTexture(gl.TEXTURE_2D, texture);
// 3. 设置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// 4. 获取shader中的纹理变量
const uImage = gl.getUniformLocation(program, 'u_image');
// 5. 设置shader中的纹理变量从哪取数
// 第二个参数代表从‘0’号纹理取数
gl.uniform1i(uImage, 0);
// 6. 设置纹理
// img为图片资源,gl.TEXTURE_2D说明它是一个二维图像
// 第三参数表示webgl保存像素的格式,第四个参数表示传入图片的格式是怎样的
// gl.UNSIGNED_BYTE为图片数据的字节格式
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
- 以上就将一个图片保存到webgl内存中了,导入多张纹理时,可以通过
activeTexture来指定不同level(几号)的纹理进行保存;
纹理坐标
- 上一篇文章里有提及,与webgl坐标系(原点在中心)以及canvas坐标系(原点左上角)不同,纹理有自己的坐标系,其坐标的原点在左下角,往上y轴递增,往左x轴递增,x、y范围均为
0~1如下: - 这就意味着如果图片长宽并不相等,则
sampler2D声明的纹理,x、y坐标的密度将会不一样,但范围依旧还是0~1,但比如0.1,其代表的长和宽映射到像素上,长度是不一致的 - 然而,由于图像的储存是从左往右,从上往下的,所以(0, 0)取到的像素实际是左上角,计算机中纹理坐标实际上与canvas方向相同
绘制图片
- 要绘制图片,需要确定两组顶点,一是canvas上的绘制位置,二是图片中的取数范围
- 先确定绘制范围
// 图像的大小
const { width, height } = img;
// canvas的大小
const { clientWidth, clientHeight } = canvas;
// 以高度为基准,调整宽度,居中绘制图案
const fixWidth = width * clientHeight / height;
// gl.TRIANGLE_STRIP绘制长方形,需要顶点呈‘Z’字形分布
const positionArr = [
clientWidth / 2 - fixWidth / 2, 0, // 左上角
clientWidth / 2 + fixWidth / 2, 0, // 右上角
clientWidth / 2 - fixWidth / 2, clientHeight, // 左下角
clientWidth / 2 + fixWidth / 2, clientHeight // 右下角
];
- 再确定图片的取数范围,假设取整张图片
// 需要和上面绘制的顶点坐标一一对应
const texCoord = [
0, 0, // 左上
1, 0, // 右上
0, 1, // 左下
1, 1, // 右下
];
// 如果想按左下角为原点来指定纹理坐标
// 可以在得到‘gl’后调用一遍gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
// gl.pixelStorei会影响‘texImage2D’,在向webgl写入图片时,程序会先对图像数据做一遍翻转,从而使原点变为左下角
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)可以使图像坐标系原点变为左下角,原理是程序写入数据时先做一遍翻转- 最后通过上一篇文章介绍过的方法对shader中的顶点赋值,再调用
drawArrays绘制即可画出图像,最终结果如下
总结
本文简单介绍了利用webgl绘制三角形,以及基于三角形绘制图片,并且介绍了纹理的使用;目前webgl最基本的用法都介绍完了,下一篇文章将绘制多个不同图案,介绍webgl的离屏buffer,以及如何利用离屏buffer做拾取判断,判断是否点中图案,并进行移动。