webgl笔记(一) ——— 如何使用WebGL

410 阅读10分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情 >>

摘要

本系列将整理一些在学习WebGL过程中遇到的概念,以及需要注意的点;第一篇文章将简单介绍一下WebGL,以及通过讲述如何绘制一个点,来分析WebGL开发的基本流程。

webgl的一些基本概念

  • webgl是基于opengl在web端的实现,为web端提供了操作GPU的能力,使用webgl进行的绘制都是在GPU上进行的
  • webgl依赖于canvas元素,传统canvas绘图方法是通过canvas.getcontext('2d')实现的,而webgl是通过canvas.getcontext('webgl')实现的,该方法返回的对象是浏览器基于webgl规范实现的实例,在上面绑定了webgl绘图所需要的方法
  • 并非所有浏览器都支持webgl,可以根据canvas.getcontext('webgl')返回来判断是否支持,同时需要注意一旦一个canvas元素,调用了getcontext('webgl')后,再调用getcontext('2d')将会报错,同理调用getcontext('2d')后再调用getcontext('webgl')也会报错,即一旦决定使用传统canvas方法或者webgl后,后续该canvas的绘制,都必须使用该方法继续绘制,而不能中途转换方法
  • webgl的开发将会使用到一种名为glsl的语言,其写法有点类似C语言,具体如何使用后续会提及,这里先留意一个细节,glsl语言与js不同,是静态语言类型,也就是说,他的变量类型声明后便是固定的,这点与js动态语言极为不同,在编写glsl程序时需要时刻记住,例如下面程序在glsl里将无法通过编译,因为a是浮点型,无法与整数型做对比
float a = 1.0;
if (a == 1) a = 2.0;
  • webgl的开发流程
    • 编写shader程序(glsl程序)
    • 通过js将shader程序编译成可执行程序
    • 通过js声明shader程序中定义的顶点数据,矩阵数据,以及纹理数据,将如何取值
    • 通过js调用绘制方法
  • 接着当运行js程序的时候,webgl将按照之前声明的方式进行取值,并将值赋予shader中的变量,同时执行shader程序,绘制图案
  • shader即glsl程序,分为两类:顶点shader以及片元shader
    • 顶点shader,顾名思义,与绘制形状的顶点有关,其用来定义所绘制图案的顶点位置信息,另外可以通过顶点shader向片元shader传递信息
    • 片元shader,上一步中传入的顶点信息,会根据绘制图形(点、线、三角形)的不同,组合成一个个面,这些面最终会组合成绘制的图形,但在此之前,这些面将进行光栅化,即转换成屏幕上的一个个像素,而这些像素使用的颜色由片元shader决定
  • 顶点shader的执行次数,等于绘制图形所设定的顶点个数;而片元shader的执行次数则等于栅格化后像素的个数,故片元shader的执行次数远大于顶点shader
  • 直观上,一个片元就是一个像素,但实际上,他们有以下区别
    • 像素由片元经过如深度测试等处理,并且进行alpha混合后渲染而来
    • 片元个数远大于像素,因为有些片元由于没有通过深度测试,而没有被渲染,多个片元也可以通过混合成为一个像素,即同一个位置上可能绘制了多个图案,其中某些图案具有透明度,则该像素位置上有多个片元,他们将共同决定这个像素的颜色
    • 由于上述原因,片元除了坐标以及颜色之外,比像素还多了许多额外的信息,如深度,纹理坐标等

通过webgl绘制一个点

下面通过演示如何利用webgl画一个点,来说明webgl的开发流程

编写shader程序

顶点shader:

  attribute vec2 a_position;

  void main(void) {
    gl_Position = vec4(a_position, 0, 1.0);
    gl_PointSize = 15.0;
  }
  • 以上是一个最基本的顶点shader
    • 后续我们将通过js告诉webgl顶点变量a_position将如何取值,这里只需要知道他代表一个点的坐标
    • gl_PointSize代表点的大小,绘制点时必须设置这个值,否则其默认值是undefined,为了使结果更明显,这里设置成15
    • vec2是glsl的内部变量,代表一个二维向量,一维向量是float,也就是浮点数,三维是vec3,最多是四维变量vec4
    • 可以看到后续一个点的坐标gl_Position是一个四维向量,其中前三个向量代表三维空间的xyz,最后一个维度是w,这是因为在计算机图形学里,绘制的是透视空间,是使用齐次坐标来表示的,而齐次坐标就是使用n个维度的向量去表示n-1维空间中的坐标,
    • w可以理解为透视分量(物体的远近),我们将xyz分别除以w,得到(x/w, y/w, z/w),当w是1的时候,这个坐标就是我们逻辑上理解的坐标,当w等于0的时候,我们得到了{∞, ∞, ∞},即这个坐标在当前坐标系的无穷远的地方
    • 当我们绘制二维空间的时候,可以理解为物体就在我们面前,w恒等于1,所以上面的shader中通过vec4(a_position, 0, 1.0)规定了一个顶点的位置
    • 关于齐次坐标的意义可以参考这篇文章

片元shader:

  precision mediump float;
  uniform vec4 u_color;

  void main() {
    gl_FragColor = u_color;
  }
  • 以上是一个最基本的片元shader
    • uniform是一个在webgl运行过程中恒定不变的值,gl_FragColor是一个webgl里内部规定的值,在渲染时会取这个值作为像素的颜色
    • u_color是一个具有4个分量的值,对应颜色的rgba
    • precision mediump float用于告诉webgl,将使用何种精度去计算浮点值,这里是mediump即中等精度,精度不光可以在片元shader中声明,也可以在顶点shader中声明,进一步了解精度声明可以参考这篇文章

编译shader

有了shader程序后,我们需要将其编译成可运行的程序,并通过webgl调用他们

// 1. 通过getcontext获取到webgl上下文
// 这里默认已经通过js获取到canvas元素,且浏览器支持webgl

const gl = canvas.getContext('webgl');

// 2. 创建一个空的shader,并指明其类型
// 假设顶点shader保存在变量vertexShaderSource中
// 片元shader保存在变量fragmentShaderSource中

const vertexShader = gl.createShader(gl.VERTEX_SHADER);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

// 3. 将shader变量与shader源码关联,并编译它
gl.shaderSource(vertexShader, vertexShaderSource);
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(vertexShader);
gl.compileShader(fragmentShader);

// 4. 创建一个webgl程序,目前是个空的程序,还不能运行
const program = gl.createProgram();

// 5. 将shader关联到这个program中,后续就可以通过该program设置shader的一些行为
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);

// 6. 创建一个可运行的程序,会在GPU中创建相应存储空间,以及初始化
gl.linkProgram(program);

shader变量赋值

这里我们只需要给顶点a_position以及颜色变量u_color赋值即可

  • 先看固定值u_color是怎么赋值的
// 1. 首先声明是在哪个program上进行的操作
gl.useProgram(program);

// 2. 声明一个数组,用于赋值
const color = [1, 0, 0, 1]; // 红色

// 3. 获取u_color的地址
const uColor = gl.getUniformLocation(program, 'u_color');

// 4. 赋值
gl.uniform4f(uColor, color[0], color[1], color[2], color[3]);
  • 这里用来赋值的方法是uniform4f,其他还有uniform3funiform4fv等等,用来代表不同变量的赋值方式,具体可以参考webgl规范,这里不展开,知道有这个赋值这个步骤即可;
  • 接下来看看a_position是怎么赋值的;
// 1. 同样先声明使用的是哪个program
gl.useProgram(program);

// 2. 声明用于赋值的变量
// [0, 0]代表原点,在原点画一个点
const posData = new Float32Array([0, 0]);

// 3. 告诉webgl怎么获取这个顶点值
// 3.1 为此首先我们要创建一个缓冲
const buffer = gl.createBuffer();

// 3.2 将刚刚创建的缓存绑定到webgl上
// gl.ARRAY_BUFFER是webgl的一个全局变量,代表缓冲的绑定点
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

// 3.3 将实际的顶点数据也绑定到绑定点上
// gl.STATIC_DRAW用来指代该缓冲中的内容是否会经常被修改
// 其他还有别的值,可以参考webgl文档,这个设置只是告诉webgl缓冲类型,好让其采取对应的优化策略,实际无论设置什么值都不影响绘制
gl.bufferData(gl.ARRAY_BUFFER, posData, gl.STATIC_DRAW);

// 3.4 获取a_position的地址,并激活他
// webgl中的顶点变量默认时关闭的,只有激活了才能被使用
const aPos = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(aPos);

// 3.5 告诉webgl如何取值
// 这里告诉webgl,a_postion取值时,从绑定的data中,每次取2个数,类型是浮点型,作为顶点数据
// 第四个参数是否归一化,当类型是UNSIGNED_BYTE时,会将0~255的数归一化到0~1,通常情况下,他都是false
// 第五、六个参数表示取值时的偏移量,一般都是0
// 具体可参考(https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/vertexAttribPointer)
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
  • 可以看到attribute的变量赋值与uniform的变量区别很大,这是因为uniform是固定不变的,但attribute在webgl运行过程中是变化的,顶点数据是逐点从data中获取的,所以uniform在赋值的瞬间,值就写进了内存里,而attribute的赋值,并没有真正把数值写进内存,而只是声明了,顶点着色器运行时,应该如何以及从哪里获取值
  • 同时也可以看出顶点数据的传递,是通过创建一个buffer进行的,由于顶点着色器运行时,js与其无法交互,所以需要先通过buffer,将需要用到的数据存起来;
  • gl.ARRAY_BUFFER可以理解为一个搬运工,将webgl buffer和js data都绑定到这上面的时候,js data的值,就会流向webgl buffer
  • 目前我们已经把数据拷贝到了缓冲中,下一步需要做的,就是把shader中的变量与缓冲关联起来,这个关联方法就是vertexAttribPointer,同时他也指定了顶点取数时,应该如何从buffer里取;
  • 至此,顶点shader里的a_position已经和创建的buffer关联了起来,即后续即使绑定别的缓冲或数据到gl.ARRAY_BUFFER上,也不会影响a_position的取数。

绘制

shader写好了,数据也准备好了,接下来就剩下绘制了,绘制非常简单

// webgl支持绘制点、线、三角形,这里绘制的是点gl.POINTS
// 第二个参数是指从哪个点开始绘制
// 第三个参数时指绘制图形需要用到多少个点
// 这里我们只绘制一个点,所以参数是0, 1
gl.drawArrays(gl.POINTS, 0, 1);

结果

ret.png 黑色的部分是canvas,红色的正方形就是我们绘制的点,他会根据顶点的位置,结合gl_PointSize,计算距离顶点xgl_PointSize远的x值,计算距离顶点ygl_PointSize远的y值,再将两者结合起来组合成的点,都进行着色,最后表现出来就是上述截图中的正方形

源码地址

总结

  • 本文整理了一些webgl相关的概念
  • 以及通过绘制一个点,演示了webgl的基本方法,后续绘制不同的图形,虽然调用的api可能不一样,但大体的流程都是一样的
  • 从结果可以看出,webgl绘制和直接调用canvas绘制,最终结果都是呈现在canvas的上,但有一个问题,canvas的坐标原点是在左上角的,但webgl的原点,从上述结果可以看出,是在canvas中心的
  • 那么webgl的坐标系和canvas以及整个html的坐标系有什么区别呢,下一篇文章将会整理这些相关的概念,以及实现通过点击屏幕绘制点,和拖动鼠标绘制线段
  • 最终这个系列的目标是实现一个图像编辑器,支持将导入图片灰度化后,可以通过鼠标或触屏进行局部涂色,还原图片局部彩色的效果,目前已实现了基本功能,可以先体验一下