webgl笔记(三) ——— 绘制图片

1,130 阅读6分钟

我正在参加「掘金·启航计划」

摘要

上一篇文章介绍了webgl的坐标系,以及如何利用webgl并与屏幕进行交互,从而绘制点和线;这篇文章在此基础上将记录如何绘制三角形,以及利用三角形的绘制,与webgl中的特殊变量纹理来进行图片的绘制。

三角形绘制

  • 沿用上一篇文章的shader,我们只需要改变gl.drawArrays中的参数,即可绘制三角形
  • 首先是gl.drawArrays中的第一个参数,其中代表三角形绘制的值可以为以下三种:
    • gl.TRIANGLES
    • gl.TRIANGLE_STRIP
    • gl.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)
});
  • 三种绘制方法的效果如下:

Kapture 2022-10-17 at 18.06.40.gif

图片的绘制

  • 图片的绘制是在三角形绘制的基础上进行的,因为三角形可以组合成面,而在这个面里,我们只需要将图片对应的像素,代替随机生成的颜色值,填充进去,就实现了图片的绘制,而这个图片对应的像素,可以利用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如下: image.png
  • 这就意味着如果图片长宽并不相等,则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绘制即可画出图像,最终结果如下

Kapture 2022-10-19 at 18.51.20.gif

总结

本文简单介绍了利用webgl绘制三角形,以及基于三角形绘制图片,并且介绍了纹理的使用;目前webgl最基本的用法都介绍完了,下一篇文章将绘制多个不同图案,介绍webgl的离屏buffer,以及如何利用离屏buffer做拾取判断,判断是否点中图案,并进行移动。