WebGl入门 | 小册免费学

590 阅读12分钟

图形系统如何绘图

一个通用计算机图形系统主要包括 6 个部分,分别是输入设备、中央处理单元、图形处理 单元、存储器、帧缓存和输出设备。虽然我下面给出了绘图过程的示意图,不过这些设备在 可视化中的作用,我要再跟你多啰嗦几句。

光栅(Raster):几乎所有的现代图形系统都是基于光栅来绘制图形的,光栅就是指构 成图像的像素阵列。

像素(Pixel):一个像素对应图像上的一个点,它通常保存图像上的某个具体位置的颜 色等信息。

帧缓存(Frame Buffer):在绘图过程中,像素信息被存放于帧缓存中,帧缓存是一块 内存地址。

CPU(Central Processing Unit):中央处理单元,负责逻辑计算。 GPU(Graphics Processing Unit):图形处理单元,负责图形计算。

GPU(Graphics Processing Unit):图形处理单元,负责图形计算。

image-20210325201714038

首先,数据经过 CPU 处理,成为具有特定结构的几何信息。然后,这些信息会被送到 GPU 中进行处理。在 GPU 中要经过两个步骤生成光栅信息。这些光栅信息会输出到帧缓 存中,最后渲染到屏幕上。

image-20210325201756601

这个绘图过程是现代计算机中任意一种图形系统处理图形的通用过程。

它主要做了两件事

  • 一是对给定的数据结合绘图的场景要素(例如相机、光源、遮挡物体等等)进行计算,最终 将图形变为屏幕空间的 2D 坐标。
  • 二是为屏幕空间的每个像素点进行着色,把最终完成的图形输出到显示设备上。

这整个过程是一步一步进行的,前一步的输出就是后一步的输入,所 以我们也把这个过程叫做渲染管线(RenderPipelines)。

图片渲染为什么是用GPU 而不是CPU呢

因为渲染图片实际上相当于进行一个个小像素块的位置计算,处理一张图片相当于处理上万个像素点的任务。CPU具有并行能力,但是限制于其管道运行的设计,单个CPU并行如此多的线程需要高额的代价。而GPU则不一样,相比CPU它功能没有那么强大,但是胜在数量多,因此 我们对图片的处理一般都放置在CPU上去运行了。

WebGl的绘图方式

浏览器提供的 WebGL API 是 OpenGL ES 的 JavaScript 绑定版本,它赋予了开发者操作 GPU 的能力。但是这个特性让WebGl的操作复杂了很多,具体步骤为:

  1. 创建 WebGL 上下文
  2. 创建 WebGL 程序(WebGL Program)
  3. 将数据存入缓冲区
  4. 将缓冲区数据读取到 GPU
  5. GPU 执行 WebGL 程序,输出结果

我们以绘制三角形为例子,来详细看一下步骤

1.创建WebGL上下文

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

2.创建WebGl程序

这里的 WebGL 程序 是一个 WebGLProgram 对象,它是给 GPU 最终运行着色器的程序,而不是我们正在写的 绘制图形的 JavaScript 程序

要创建这个 WebGL 程序,我们需要编写两个着色器(Shader)。着色器是用 GLSL 这种编程语言编写的代码片段。

在说着色器之前,就要先看一下顶点和图元这两个基本概念了

在绘图的时候,WebGL 是以顶点和图元来描述图形几何信息的。顶点就是几何图形的顶点,比如,三角形有三个顶点,四边形有四个顶点。图元是 WebGL 可直接处理的图形单元,由 WebGL 的绘图模式决定,有点、线、三角形等等。

顶点和图元是绘图过程中必不可少的。因此,WebGL 绘制一个图形的过程,一般需 要用到两段着色器,一段叫顶点着色器(Vertex Shader)负责处理图形的顶点信息,另一 段叫片元着色器(Fragment Shader)负责处理图形的像素信息。

顶点着色器和片段着色器,都可以通过GLSL来书写,基本上它们算是一个组合。着色器的内部,必须要定义一个main函数,在这个函数里面添加自己的处理。而且,要从WebGL一侧向着色器传递数据的时候,需要用到一些特殊的修饰符所定义的变量。

要向着色器传递各个顶点的不同的信息的时候,使用attribute修饰符声明变量,要向着色器传递对所有顶点来说都一样的信息的时候,使用uniform修饰符声明变量。

另外,从顶点着色器向片段着色器传递数据的时候,使用varying修饰符声明变量。

顶点着色器中内置的变量gl_Position必须赋值,而片段着色器的内置变量gl_FragColor虽然不是必须赋值的,但是一般情况下都会赋值。

顶点着色器

我们可以把顶点着色器理解为处理顶点的 GPU 程序代码。它可以改变顶点 的信息(如顶点的坐标、法线方向、材质等等),从而改变我们绘制出来的图形的形状或者 大小等等。

顶点处理完成之后,WebGL 就会根据顶点和绘图模式指定的图元,计算出需要着色的像素 点,然后对它们执行片元着色器程序。简单来说,就是对指定图元中的像素点着色。

片元着色器

WebGL 从顶点着色器和图元提取像素点给片元着色器执行代码的过程,就是我们前面说的 生成光栅信息的过程,我们也叫它光栅化过程。所以,片元着色器的作用,就是处理光栅化后的像素信息。

举个例子来看,我们可以将图元设为线段,那么片元着色器就会处理顶点之间的线段上的像素点信息,这样画出来的图形就是空心的。而如果我们把图元设为三角形,那么片元着色器就会处理三角形内部的所有像素点,这样画出来的图形就是实心的。

这里你要注意一点,因为图元是 WebGL 可以直接处理的图形单元,所以其他非图元的图 形最终必须要转换为图元才可以被 WebGL 处理。举个例子,如果我们要绘制实心的四边 形,我们就需要将四边形拆分成两个三角形,再交给 WebGL 分别绘制出来。

同时,片元着色器的着色过程是并行的,也就是无论多少像素点,都可以同时处理。

那么来一起绘制一个三角形吧

首先需要声明两个着色器

//在 GLSL 中,attribute 表示声明变量,vec2 是变量的类型,它表示一个二维向量,position 是变量名。接下来我们将 buffer 的数据绑定给顶点着色器的 position 变量。
const vertex = `
  attribute vec2 position;

  void main() {
    gl_PointSize = 1.0;
    gl_Position = vec4(position, 1.0, 1.0);
  }
`;


const fragment = `
  precision mediump float;

  void main()
  {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); //定义片元着色器中绘制时候像素点颜色
  }    
`;

然后需要来创建一个WebGL程序,并把刚才创建的着色器绑定在其中

const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertex);
gl.compileShader(vertexShader);


const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragment);
gl.compileShader(fragmentShader);

然后来创建 WebGLProgram 对象,并将这两个 shader 关联到这个 WebGL 程序上。WebGLProgram 对象的创建过程主要是添加 vertexShader 和 fragmentShader,然后将这个 WebGLProgram 对象链接到 WebGL 上下文对象上。

const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);

最后来应用这个WebGl对象

gl.useProgram(program);

至此,已经创建并完成 WebGL 程序的配置。接下来, 我们只要将三角形的数据存入缓冲区,也就能将这些数据送入 GPU 了。

WebGl是右手坐标系,默认是三维的 我们需要一个平面三角形 直接使z轴为0即可。

默认以整个视窗左上角为(0,0)点

我们通过使用Float32Array的格式来定义一个数组

const points = new Float32Array([
  -1, -1,
  0, 1,
  1, -1,
]);

然后需要将定义好的数据写入 WebGL 的缓冲区。这个过程我们可以简单总结为三步,分别是创建一个缓存对象,将它绑定为当前操作对象,再把当前的数据写入缓存对象。这三个步骤主要是利用 createBuffer、bindBuffer、bufferData 方法来实现的

const bufferId = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);

现在我们已经把数据写入缓存了,但是我们的 shader 现在还不能读取这个数据,还需要把数据绑定给顶点着色器中的 position 变量。

const vPosition = gl.getAttribLocation(program, 'position');//获取顶点着色器中的position变量的地址
gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0);//给变量设置长度和类型
gl.enableVertexAttribArray(vPosition);//激活这个变量

经过这样处理,在顶点着色器中,我们定义的 points 类型数组中对应的值,就能通过变量 position 读到了。

最后一步!绘制就可以!

我们把数据传入缓冲区以后,GPU 也可以读取绑定的数据到着色器变量了。接下来,我们只需要调用绘图指令,就可以执行着色器程序来完成绘制了。

我们先调用 gl.clear 将当前画布的内容清除,然后调用 gl.drawArrays 传入绘制模式。这里我们选择 gl.TRIANGLES 表示以三角形为图元绘制,再传入绘制的顶点偏移量和顶点数量,WebGL 就会将对应的 buffer 数组传给顶点着色器,并且开始绘制。代码如下:

gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, points.length / 2);

gl.drawArrays(gl.LINE_LOOP, 0, points.length/2) //这样可以绘制空心的三角形

关于着色器的补充

不论这个三角形是大还是小,有几十个像素点还是上百万个像素点,GPU 都是同时处理每个像素点的。也就是说,图形中有多少个像素点,着色器程序在 GPU 中就会被同时执行多少次。

牢记 WebGl是并行计算的特性

1.如果需要修改三角形周长怎么办?

常规的来说 需要重新设置点的坐标即可,但是在WEbGL中。因为每个点都是独立的并行的渲染出来的。

我们可以通过修改顶点着色器中position的值 来直接缩小

gl_Position = vec4(position, 1.0, 1.0); 
#修改为
gl_Position = vec4(position * 0.5, 1.0, 1.0);

2.点着色器像片着色器传数据

顶点着色器还可以将数据通过 varying 变量传给片元着色器。然后,这些值会根据片元着色器的像素坐标与顶点像素坐标的相对位置做线性插值

简单理解,就是可以做到渐变的效果

attribute vec2 position;
varying vec3 color;

void main() {
  gl_PointSize = 1.0;
  color = vec3(0.5 + position * 0.5, 0.0);
  gl_Position = vec4(position * 0.5, 1.0, 1.0);
}
image-20210326115152145

我们修改了顶点着色器,定义了一个 color 变量,它是一个三维的向量。我们通过数学技巧将顶点的值映射为一个 RGB 颜色值(关于顶点映射 RGB 颜色值的方法,在后续的课程中会有详细介绍),映射公式是 vec3(0.5 + position * 0.5, 0.0)。

这样一来,顶点[-1,-1]被映射为[0,0,0]也就是黑色,顶点[0,1]被映射为[0.5, 1, 0]也就是浅绿色,顶点[1,-1]被映射为[1,0,0]也就是红色。这样一来,三个顶点就会有三个不同的颜色值。

然后我们将 color 通过 varying 变量传给片元着色器。片元着色器中的代码如下:

precision mediump float;
varying vec3 color;

void main()
{
  gl_FragColor = vec4(color, 1.0);
}  

3.WebGl基础图元

webgl支持的图元类型有七种,

  • gl.POINTS(点),

  • gl.LINES(线段),

  • gl.LINE_STRIP(线条),

  • gl.LINE_LOOP(回路),

  • gl.TRIANGLES(三角形),

  • gl.TRIANGLE_STRIP(三角带),

  • gl.TRIANGLE_FAN(三角扇)

    如果我们需要把刚刚的三角形绘制成空心的话

    要绘制空心三角形,gl.LINE_STRIP(线条)、gl.LINES(线段)、 gl.LINE_LOOP(回路)都可以实现。 但是gl.LINES(线段)需要写入六个顶点([-1, -1, 0, 1, 0, 1, 1, -1, 1, -1,-1, -1]), gl.LINE_STRIP(线条)也需要写入四个顶点([-1, -1, 0, 1, 1, -1,-1, -1]),而gl.LINE_LOOP(回路),只需要是三个顶点([-1, -1, 0, 1, 1, -1]),因此gl.LINE_LOOP(回路)是最佳选择

总结

image-20210326115310283

GLSL学习

首先需要配置一下本地的开发环境

注意有一个安装包需要下载配置好 不然在vscode中安装GLSL-lint会报错

参考文章

顶点缓存

既然顶点的位置坐标是必须的,那么要如何定义这些信息呢?

顶点最终在画面上绘制的时候,要经过模型坐标变换,视图坐标变换和投影坐标变换,这个已经说过好多遍了。但是,在使用坐标信息之前,首先必须定义这些顶点的构成,否则就没有办法开始了。

顶点放到什么位置,就表现为坐标,一般叫做局部坐标。局部坐标就是模型的各个顶点相对于原点(x,y,z都为0)的坐标。

比如,一个局部坐标为(1.0,0.0,0.0)的顶点,x轴方向距离原点的距离是1.0。同样,各个顶点都依次定义了局域坐标,这样顶点的位置就形成了。

缓存(buffer),是表示数据保存空间的一般的计算机用语。WebGL中还有帧缓存,索引缓存等各种缓存,但是不管哪种缓存,你只需要把它想成保存数据的一块儿空间就行了。顶点缓存是其中的一种,就是用来保存顶点信息的,WebGL中的顶点缓存叫做VBO(vertex buffer object)。

顶点缓存的作用,不光是保存顶点的位置,位置以外跟顶点相关的信息都可以用顶点缓存来保存。

比如,顶点的法线,颜色,文理坐标等所有跟顶点相关的信息都可以用顶点缓存来保存和管理。但是需要注意一点的是,向顶点信息中追加信息的时候,需要使用相应的VBO。

顶点缓存相关的处理的具体流程如下。

・顶点的各种信息保存到数组里

・使用WebGL的方法生成VBO

・使用WebGL的方法将数组中的信息传给VBO

・顶点着色器中的attribute函数和VBO结合

本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情