图形系统是如何绘图的?

首先,我们来说说计算机图形系统的主要组成部分,以及它们在绘图过程中的作用。知道了这些,我们就能很容易理解计算机图形系统绘图的基本原理了。一个通用计算机图形系统主要包括 6 个部分,分别是输入设备、中央处理单元、图形处理单元、存储器、帧缓存和输出设备。

  • 光栅(Raster):几乎所有的现代图形系统都是基于光栅来绘制图形的,光栅就是指构成图像的像素阵列。
  • 像素(Pixel):一个像素对应图像上的一个点,它通常保存图像上的某个具体位置的颜色等信息。
  • 帧缓存(Frame Buffer):在绘图过程中,像素信息被存放于帧缓存中,帧缓存是一块内存地址。
  • CPU(Central Processing Unit):中央处理单元,负责逻辑计算。
  • GPU(Graphics Processing Unit):图形处理单元,负责图形计算。

Webgl 基础介绍 - 图1

首先,数据经过 CPU 处理,成为具有特定结构的几何信息。然后,这些信息会被送到 GPU 中进行处理。在 GPU 中要经过两个步骤生成光栅信息。这些光栅信息会输出到帧缓存中,最后渲染到屏幕上。
Webgl 基础介绍 - 图2
这个绘图过程是现代计算机中任意一种图形系统处理图形的通用过程。它主要做了两件事,一是对给定的数据结合绘图的场景要素(例如相机、光源、遮挡物体等等)进行计算,最终将图形变为屏幕空间的 2D 坐标。二是为屏幕空间的每个像素点进行着色,把最终完成的图形输出到显示设备上。这整个过程是一步一步进行的,前一步的输出就是后一步的输入,所以我们也把这个过程叫做渲染管线(RenderPipelines)。

如何用 WebGL 绘制三角形?

浏览器提供的 WebGL API 是 OpenGL ES 的 JavaScript 绑定版本,它赋予了开发者操作 GPU 的能力。这一特点也让 WebGL 的绘图方式和其他图形系统的“开箱即用”(直接调用绘图指令或者创建图形元素就可以完成绘图)的绘图方式完全不同,甚至要复杂得多。我们可以总结为以下 5 个步骤:

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

步骤一:创建 WebGL 上下文

创建 WebGL 上下文这一步和 Canvas2D 的使用几乎一样,我们只要调用 canvas 元素的 getContext 即可,区别是将参数从’2d’换成’webgl’。

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

不过,有了 WebGL 上下文对象之后,我们并不能像使用 Canvas2D 的上下文那样,调用几个绘图指令就把图形画出来,还需要做很多工作。别着急,让我们一步一步来。

步骤二:创建 WebGL 程序接下来

我们要创建一个 WebGL 程序。你可能会觉得奇怪,我们不是正在写一个绘制三角形的程序吗?为什么这里又要创建一个 WebGL 程序呢?实际上,这里的 WebGL 程序是一个 WebGLProgram 对象,它是给 GPU 最终运行着色器的程序,而不是我们正在写的三角形的 JavaScript 程序。好了,解决了这个疑问,我们就正式开始创建一个 WebGL 程序吧!首先,要创建这个 WebGL 程序,我们需要编写两个着色器(Shader)。着色器是用 GLSL 这种编程语言编写的代码片段,这里我们先不用过多纠结于 GLSL 语言,我们只需要理解绘制三角形的这两个着色器的作用就可以了。

  1. const vertex = `
  2. attribute vec2 position;
  3. void main() {
  4. gl_PointSize = 1.0;
  5. gl_Position = vec4(position, 1.0, 1.0);
  6. }
  7. `;
  8. const fragment = `
  9. precision mediump float;
  10. void main()
  11. {
  12. gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  13. }
  14. `;

那我们为什么要创建两个着色器呢?这就需要我们先来理解顶点和图元这两个基本概念了。在绘图的时候,WebGL 是以顶点和图元来描述图形几何信息的。顶点就是几何图形的顶点,比如,三角形有三个顶点,四边形有四个顶点。图元是 WebGL 可直接处理的图形单元,由 WebGL 的绘图模式决定,有点、线、三角形等等。

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

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

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

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

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

好了,那让我们回到片元着色器对像素点着色的过程。你还要注意,这个过程是并行的。也就是说,无论有多少个像素点,片元着色器都可以同时处理。这也是片元着色器一大特点。
**
接下来,我们用顶点着色器和片元着色器。
首先,因为在 JavaScript 中,顶点着色器和片元着色器只是一段代码片段,所以我们要将它们分别创建成 shader 对象。代码如下所示:

  1. const vertexShader = gl.createShader(gl.VERTEX_SHADER);
  2. gl.shaderSource(vertexShader, vertex);
  3. gl.compileShader(vertexShader);
  4. const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
  5. gl.shaderSource(fragmentShader, fragment);
  6. gl.compileShader(fragmentShader);

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

  1. const program = gl.createProgram();
  2. gl.attachShader(program, vertexShader);
  3. gl.attachShader(program, fragmentShader);
  4. gl.linkProgram(program);

最后,我们要通过 useProgram 选择启用这个 WebGLProgram 对象。这样,当我们绘制图形时,GPU 就会执行我们通过 WebGLProgram 设定的 两个 shader 程序了。

  1. gl.useProgram(program);

好了,现在我们已经创建并完成 WebGL 程序的配置。接下来, 我们只要将三角形的数据存入缓冲区,也就能将这些数据送入 GPU 了。那实现这一步之前呢,我们先来认识一下 WebGL 的坐标系。

步骤三:将数据存入缓冲区

我们要知道 WebGL 的坐标系是一个三维空间坐标系,坐标原点是(0,0,0)。其中,x 轴朝右,y 轴朝上,z 轴朝外。这是一个右手坐标系。

Webgl 基础介绍 - 图4

假设,我们要在这个坐标系上显示一个顶点坐标分别是(-1, -1)、(1, -1)、(0, 1)的三角形,如下图所示。因为这个三角形是二维的,所以我们可以直接忽略 z 轴。下面,我们来一起绘图。
Webgl 基础介绍 - 图5

首先,我们要定义这个三角形的三个顶点。WebGL 使用的数据需要用类型数组定义,默认格式是 Float32Array。Float32Array 是 JavaScript 的一种类型化数组(TypedArray),JavaScript 通常用类型化数组来处理二进制缓冲区。
因为平时我们在 Web 前端开发中,使用到类型化数组的机会并不多,你可能还不大熟悉,不过没关系,类型化数组的使用并不复杂,定义三角形顶点的过程,你直接看我下面给出的代码就能理解。

  1. const points = new Float32Array([
  2. -1, -1,
  3. 0, 1,
  4. 1, -1,
  5. ]);

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

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

步骤四:将缓冲区数据读取到 GPU

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

还记得我们的顶点着色器是什么样的吗?它是按如下的形式定义的:

  1. attribute vec2 position;
  2. void main() {
  3. gl_PointSize = 1.0;
  4. gl_Position = vec4(position, 1.0, 1.0);
  5. }

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

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

提一句:
因为 WebGL 在执行片元着色器程序的时候,顶点着色器传给片元着色器的变量,会根据片元着色器的像素坐标对变量进行线性插值。利用线性插值可以让像素点的颜色均匀渐变这一特点,我们就能绘制出颜色更丰富的图形了。

绘制一个立方体:

其实,在目前的三维模型,其实是由大量的三角形进行拼接而成的,你所碰到的立方体和一些复杂的模型也是。这里我们打算绘制一个立方体:

1、首先,我们先绘制一个正方形,由2d向3d进行过渡

绘制一个正方形,我们需要计算正方形的4个顶点位置,这里,我们可以对正方形进行分割,由两个三角形拼接而成。
image.png
正方形的可以由(0, 1, 2)和(0,2, 3)这两个三角形组成。这里我引入一个ogl的框架,来简化我们的操作。代码如下:

  1. // vertex shader 顶点着色器
  2. attribute vec2 a_vertexPosition;
  3. attribute vec4 color;
  4. varying vec4 vColor;
  5. void main() {
  6. gl_PointSize = 1.0;
  7. vColor = color;
  8. gl_Position = vec4(a_vertexPosition, 1, 1);
  9. }
  1. // fragment shader 片元着色器
  2. #ifdef GL_ES
  3. precision highp float;
  4. #endif
  5. varying vec4 vColor;
  6. void main() {
  7. gl_FragColor = vColor;
  8. }

全部代码如下:

  1. const vertex = `
  2. // vertex shader 顶点着色器
  3. attribute vec2 a_vertexPosition;
  4. attribute vec4 color;
  5. varying vec4 vColor;
  6. void main() {
  7. gl_PointSize = 1.0;
  8. vColor = color;
  9. gl_Position = vec4(a_vertexPosition, 1, 1);
  10. }
  11. `;
  12. const fragment = `
  13. // fragment shader 片元着色器
  14. #ifdef GL_ES
  15. precision highp float;
  16. #endif
  17. varying vec4 vColor;
  18. void main() {
  19. gl_FragColor = vColor;
  20. }
  21. `;
  22. const canvas = document.querySelector('canvas');
  23. const renderer = new GlRenderer(canvas);
  24. const gl = renderer.gl;
  25. gl.clearColor(1, 1, 1, 1);
  26. console.log(renderer)
  27. const program = renderer.compileSync(fragment, vertex);
  28. renderer.useProgram(program);
  29. // 顶点信息
  30. renderer.setMeshData([
  31. {
  32. positions: [[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5],], // 设置顶点
  33. attributes: {color: [[1, 0, 0, 1], [1, 0, 0, 1], [1, 0, 0, 1], [1, 0, 0, 1],],}, // 设置顶点颜色
  34. cells: [[0, 1, 2], [0, 2, 3]], // 设置顶点顺序
  35. }
  36. ]);
  37. renderer.render();

立方体可以由六个面组成,那么,我们可以绘制6个这样面,进行拼接出立方体来。不过,之前我们传入的都是2维向量,立方体的话,就传入3位向量。具体实现的话,代码过长就不贴了。
Webgl 基础介绍 - 图7

绘制完一个立方体后,如果我们想要他变的动起来,那我们就可以对顶点的位置进行改变,可以使用仿射矩阵对顶点进行移动和旋转矩阵。
绘制 3D 图形与绘制 2D 图形有一点不一样,那就是我们必须要开启深度检测和启用深度缓冲区。在 WebGL 中,我们可以通过gl.enable(gl.DEPTH_TEST),来开启深度检测。
实际上,WebGL 默认的剪裁坐标的 z 轴方向,的确是朝内的。也就是说,WebGL 坐标系就是一个左手系而不是右手系。但是,基本上所有的 WebGL 教程,也包括我们前面的课程,一直都在说 WebGL 坐标系是右手系,这又是为什么呢?这是因为,规范的直角坐标系是右手坐标系,符合我们的使用习惯。因此,一般来说,不管什么图形库或图形框架,在绘图的时候,都会默认将坐标系从左手系转换为右手系。

关于相机:

一般来说,投影有两种方式,分别是正投影与透视投影。你可以结合我给出的示意图,来理解它们各自的特点。
首先是正投影,它又叫做平行投影。正投影是将物体投影到一个长方体的空间(又称为视景体),并且无论相机与物体距离多远,投影的大小都不变。
Webgl 基础介绍 - 图8
而透视投影则更接近我们的视觉感知。它投影的规律是,离相机近的物体大,离相机远的物体小。与正投影不同,正投影的视景体是一个长方体,而透视投影的视景体是一个棱台。
Webgl 基础介绍 - 图9
我们一般用透视投影相机即可。

总结:
在这一节课,我们讲了 WebGL 的绘图过程以及顶点着色器和片元着色器的作用。WebGL 图形系统与用其他图形系统不同,它的 API 非常底层,使用起来比较复杂。想要学好 WebGL,我们必须要从基础概念和原理学起。一般来说,在 WebGL 中要完成图形的绘制,需要创建 WebGL 程序,然后将图形的几何数据存入数据缓冲区,在绘制过程中让 WebGL 从缓冲区读取数据,并且执行着色器程序。WebGL 的着色器程序有两个。一个是顶点着色器,负责处理图形的顶点数据。另一个是片元着色器,负责处理光栅化后的像素信息。此外,我们还要牢记,WebGL 程序有一个非常重要的特点就是能够并行处理,无论图形中有多少个像素点,都可以通过着色器程序在 GPU 中被同时执行。WebGL 完整的绘图过程实在比较复杂,为了帮助你理解,这里一个流程图,供你参考。

Webgl 基础介绍 - 图10

你会发现,这一套下来流程很多。只画出来一个三角形。但是,对于webgl来说,这部分知识又不能不学。如果是画一个3D的图形。就行需要很多个三角形进行拼接,现在的3d图形,大部分都是由三角形凭借而且,你需要计算出3D图形的顶点,然后计算出顶点的连接顺序。所以,现在一般写的话,不会使用原生的写,都会借助例如 three.js 、ogl 等库。因为他们帮我们封装了很多事情。
对于现在我们看到模型,可以由3Dmax这类的建模软件导出模型,我们进行引入,即可实现简单的3D模型展示。后续再说。