从画一个点入手学习 WebGL

3,973 阅读8分钟

WebGL 简介

WebGL 是一种 3d 绘图标准,也是一种 js api。它把 js 和 OpenGL ES 2.0(OpenGL ES 是专为嵌入式系统设计的图形 api,是 OpenGL 的子集) 结合在一起,通过增加 OpenGL ES 2.0 的绑定,使得 webgl 可以为 h5 的 canvas 提供硬件 3d 加速渲染,从而使前端人员可以借助显卡(GPU),在浏览器展示 3d/2d 图形。WebGL 的 GL 即 Graphics Library,图形库的意思。

CPU 与 GPU

平常我们写的 js 代码,都是由 CPU 处理的,它有 3 个主要的单元:

控制单元(Control):对流程进行控制;

计算单元(ALU):涉及到计算的工作,比如可以简单地认为,4 核处理器就是有 4 个计算单元;

缓存单元(Cache):如果计算量过大,来不及计算的就放在缓存单元。

image.png

如果我们要在页面上绘制一个 3d 图像,需要计算成千上万个像素点的位置和颜色信息。此时若是通过 CPU 来处理,靠着那 4 个 ALU 就显得力不从心。所以图形的处理会交给 GPU,它有非常多的 ALU 单元,更加胜任需要相对简单但是数量众多的计算工作:

image.png

webgl 绘制 3d 图形就会结合使用 CPU 和 GPU。

WebGL 的工作原理

我们先来说说 webgl 的工作原理。webgl 的工作方式和工厂流水线类似,将绘制过程分成多个步骤,每个步骤只会对前一个步骤的结果进行处理,再将处理结果传递给下一个步骤,最终在屏幕上渲染出图形。这种渲染方式被称为图形管线渲染管线。示意图如下,蓝色块表示我们可以通过代码控制的步骤:

image.png

  • 在顶点着色器阶段,利用 GPU 并行计算的优势对顶点的位置信息进行逐个计算;
  • 图元装配阶段是对顶点进行装配,组装成图形。请注意,webgl 可绘制的图元,只有点、线段和三角形,任何复杂的 3d 图形都是由它们组成的;
  • 光栅化则是将图形用不包含颜色信息的像素进行填充;
  • 片元着色器阶段则是为像素进行着色,最终显示在屏幕上。

绘制点

webgl 的这些 api 都是在 <canvas> 元素中使用的,关于 <canvas> 的一些细节知识,可参见《canvas 实现卫星绕月动画》,本文不再赘述。要使用 webgl 画一个点,我们可以新建一个 html 文件,在里面放上一个 <canvas>,然后使用 js 去获取 canvas 元素,再通过 getContext 传入 'webgl' 得到 gl,即 WebGLRenderingContext 接口 —— 正是它提供了基于 OpenGL ES 2.0 的绘图上下文,我们才能在 <canvas> 中绘图:

<body>
  <canvas id="canvas"></canvas>
  <script>
    const canvas = document.getElementById('canvas')
    const gl = canvas.getContext('webgl')
  </script>
</body>

重置画布颜色

如果现在打开浏览器查看代码的运行效果,看到的会是一片空白。如果想看到 canvas 画布,之前我们都是直接给 <canvas> 设置 css 样式 background-color 来实现,有了 gl, 我们就可以使用它的 clearColor()clear() 方法来重置画布的颜色:

gl.clearColor(0.5, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
  • gl.clearColor() 用于设置清空颜色缓冲时的颜色值,传入的 4 个参数分别代表 r(红)g(绿)b(蓝)和 a(透明度),注意它们的取值区间为 0 ~ 1,而不是在 css 中涉及颜色时 rgb 常用的 0 ~ 255;
  • gl.clear() 方法使用预设值来清空缓冲,传入 gl.COLOR_BUFFER_BIT 表示清空颜色缓冲区。

效果如下:
image.png

着色器(shaders)

使用 webgl 绘制任何图形,都需要使用被称为“着色器”的玩意来实现。它是一种类似 c/c++ 的绘制图形的语言,由 GLSL 编写。也就是说,webgl 的代码,由 js + GLSL 组合而成。

着色器可以分为顶点着色器和片元着色器:

顶点着色器(vertex shader)

顶点着色器用于输入顶点数据(顶点也就是二/三维空间中的一个个点,或者说坐标),然后进行平移、旋转等变换计算,最后将变换后的顶点坐标赋值给用于存储当前顶点位置的 gl_Position,作为顶点着色器的输出,传递给渲染管线的下一个功能单元。

顶点着色器源码
// 顶点着色器源码
const vsSource = `
  void main() {
    // 点的坐标
    gl_Position = vec4(0, 0, 0, 1);
    // 点的大小
    gl_PointSize = 24.0;
  }
`

vsSource 就是我们自己编写的着色器源码,其在 js 中以字符串的形式存在,使用的是着色器语言:

着色器语言(GLSL,OpenGL Shading Language)
  • GLSL 通过 main 函数作为程序的入口,void 表示该函数没有返回值,语句末尾的分号不可省略;
  • vec4() 是矢量的构造函数,另外还有 vec2()vec3()。此处传入的 4 个参数代表的是点的 x、y、z 坐标以及齐次坐标 w,函数返回值为具有 4 个浮点数元素的矢量。注意,GLSL 是一种强类型语言,其默认基础数据类型有:
    • int:整型;
    • float:单精度浮点数;
    • boolean:布尔值

所以在赋值点的大小时,传的是 24.0,而不是 24。传递给 vec4() 的值也需要是浮点数,照理应该写成 0.01.0 这样,但是 vec4 内部进行了转换,类似 float(1)

  • GLSL 中的注释和 js 是一样的,单行注释使用 //,多行注释使用 /**/

除了上面这种写法,还可以将着色器源码写在 <script> 中:

<script type="notjs" id="vsSource">
  void main() {
    gl_Position = vec4(0, 0, 0, 1);
    gl_PointSize = 24.0;
  }
</script>

当省略 type 属性或定义 type="javascript"type="text/javascript" 时,<script> 标签内默认放置的是 js 代码,而我们让 typenotjs 或是其它的什么诸如 glsl,浏览器就会忽略 script 标签的内容。

gl_Positiongl_PointSize 都是顶点着色器的内置变量,类型分别为 vec4float,一个描述顶点的位置,一个描述顶点的尺寸(像素数)。gl_Position 是必须赋值的,而 gl_PointSize 如果不设置,就会为默认值 1.0

片元着色器(fragment shader )

片元可以理解为一个个像素,类比顶点着色器,片元着色器的作用就是计算出一个个像素的颜色信息,然后赋值给内置的gl_FragColor 变量 。

片元着色器源码
<script type="notjs" id="fsSource">
  void main() {
    // 点的颜色
    gl_FragColor = vec4(1, 1, 0, 1);
  }
</script>

gl_FragColor 为片元着色器的内置变量,用于指定片元颜色(rgba 格式),赋值时也用到了 vec4(),但是传入的 4 个参数代表的是颜色值的 rgba。

创建着色器

有了着色器源码,接着我们就可以创建着色器了。对于顶点着色器或片元着色器的创建,步骤是类似的,所以可以定义一个函数 createShader 来实现:

// 创建着色器
function createShader(gl, type, source) {
  const shader = gl.createShader(type)
  // 给着色器指定源码
  gl.shaderSource(shader, source)
  // 编译着色器
  gl.compileShader(shader)

  // 获取编译是否成功
  const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS)
  if (success) {
    return shader
  }
  // 编译失败则打印信息
  console.log(gl.getShaderInfoLog(shader))
}

// 获取着色器源码
const vsSource = document.getElementById('vsSource').innerText
const fsSource = document.getElementById('fsSource').innerText
// 创建顶点着色器
const vShader = createShader(gl, gl.VERTEX_SHADER, vsSource)
// 创建片元着色器
const fShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource)
  • gl.createShader() 用于创建着色器对象,传入的参数 typegl.VERTEX_SHADERgl.FRAGMENT_SHADER,前者表示创建顶点着色器,后者表示创建片元着色器;
  • 有了着色器源码也有了着色器,就需要使用 gl.shaderSource() 将它们关联到一起,参数 shader 表示着色器,source 为着色器源码;
  • 因为着色器源码只是些字符串,所以还需要调用 gl.compileShader() 进行编译,将着色器编译成二进制数据,方便被接下去要创建的着色程序使用。

创建着色程序

有了着色器,就可以创建着色程序对象了,我们也封装一个函数 createProgram

// 创建程序对象
function createProgram(gl, vShader, fShader) {
  const program = gl.createProgram()
  // 关联程序对象和着色器
  gl.attachShader(program, vShader)
  gl.attachShader(program, fShader)
  // 连接着色器到程序对象
  gl.linkProgram(program)

  // 获取连接是否成功
  const success = gl.getProgramParameter(program, gl.LINK_STATUS)
  if (success) {
    return program
  }
  // 连接失败则打印信息
  console.log(gl.getProgramInfoLog(program))
}

const program = createProgram(gl, vShader, fShader)
  • 通过 gl.attachShader() 分别传入顶点着色器和片元着色器,以给着色程序对象分配 webgl 运行必须的着色器;
  • 使用 gl.linkProgram() 传入程序对象 program,让两个着色器与程序进行连接。

使用着色程序与绘制

有了着色程序 program 后,就可以使用它并绘制点了:

// 使用程序对象
gl.useProgram(program)

// 执行绘制
gl.drawArrays(gl.POINTS, 0, 1)
  • gl.useProgram() 告诉 webgl 在绘制时要使用哪个程序对象;

  • gl.drawArrays() 用于从向量数组中执行绘制:

    • 第 1 个参数用于图元装配的过程中指定绘制图元的方式,gl.POINTS 表示要绘制的是一系列点,如果要绘制线段则是 gl.LINES 等,绘制三角形则是 gl.TRIANGLES 等;
    • 第 2 个参数为整型数,传入 0 表示指定从第 1 个点开始绘制,第 1 个点的下标就是 0
    • 第 3 个参数为整型数,传入 1 表示绘制时需要使用到 1 个点。

效果演示

webgl 三维坐标系

在使用 canvas 绘制二维图形时,我们知道 canvas 本身的默认坐标空间是以画布左上角为原点,x 轴方向朝右,y 轴方向朝下的。但从上方的效果演示可以看到,当我们输入点的坐标的 x、y 和 z 都为 0.0 时,点是相对 canvas 画布居中显示的。这是因为 webgl 的三维坐标系的原点位于画布的中心点,x 轴方向朝右,y 轴方向朝上,z 轴方向则是朝向屏幕外(正交右手坐标系):

x、y 和 z 轴的取值区间都为 [-1, 1],与 canvas 的尺寸无关。即 x 轴最右边为 1,最左边为 -1;y 轴最上面为 1,最下边为 -1;z 轴则是朝屏幕外最远值为 1,朝屏幕里远值为 -1。

感谢.gif 点赞.png