Canvas 简介

在 HTML5 出现之前,如果想要在网页上显示图像,只能使用 HTML 提供的 <img> 标签。用这个标签虽然简单,但是只能显示静态的图片,不能进行实时绘制和渲染,所以后来出现了一些第三方的解决方案,比如 Flash Player 等。

Canvas 中文译为“画布”,作为画布我们可以使用 HTMLCanvasElement.getContext(contextType) 获取 Canvas 的上下文,然后使用上下文提供的 API 在画布区域绘制图案,接下来分别介绍怎么使用二维和三维的方法绘制矩形和点。

contextType 的取值如下:

  1. "2d" 建立一个 CanvasRenderingContext2D 对象,代表一个二维渲染上下文。
  2. "webgl""experimental-webgl" 这将创建一个 WebGLRenderingContext 代表三维渲染上下文对象 (OpenGL ES 2.0)。

二维绘制矩形

首先要写一个 canvas 元素,然后获取二维渲染的上下文:

1
<canvas id="gl" width="300" height="300"></canvas>

绘制二维矩形很简单,只需要调用上下文的 ctx.fillRect(x, y, width, height) 方法即可,语法如下:

参数表述
x矩形起始点的 x 轴坐标
y矩形起始点的 y 轴坐标
width矩形的宽度
height矩形的高度

在调用之前首先使用 ctx.fillStyle 设置要填充的颜色,这里我们填充的是蓝色,具体绘制矩形的代码如下:

1
2
3
const ctx = document.getElementById('gl').getContext('2d');
ctx.fillStyle = 'rgba(0, 0, 255, 1.0)';
ctx.fillRect(0, 0, 300, 300);

运行效果为一个蓝色的矩形 点击运行

三维绘制矩形

和二维绘制矩形一样,在三维绘制一个矩形也是很简单的,首先要获取 WebGLRenderingContext 的对象,其次是调用 gl.clearColor(red, green, blue, alpha) 设置背景色,语法如下:

参数表述
red指定清除缓冲时的红色值。默认值:0
green指定清除缓冲时的绿色值。默认值:0
blue指定清除缓冲时的蓝色值。默认值:0
alpha指定清除缓冲时的不透明度。默认值:0

下面列出的是一些常用的颜色:

色值描述
(1.0, 0.0, 0.0, 1.0)红色
(0.0, 1.0, 0.0, 1.0)绿色
(0.0, 0.0, 1.0, 1.0)蓝色
(1.0, 1.0, 0.0, 1.0)黄色
(1.0, 0.0, 1.0, 1.0)紫色
(0.0, 1.0, 1.0, 1.0)青色
(1.0, 1.0, 1.0, 1.0)白色

然后在调用 gl.clear(mask) 用背景色清空 canvas 绘图区域,mask 参数为指定待清空的缓冲区,可以使用位操作符 OR(|) 来指定多个缓冲区,可以取的常量如下:

常量表述
gl.COLOR_BUFFER_BIT指定颜色缓冲区
gl.DEPTH_BUFFER_BIT指定深度缓冲区
gl.STENCIL_BUFFER_BIT指定模版缓冲区

这里我们设置的背景色为黑色,最后的代码如下,运行后我们想要的矩形就出来了,这个也是最短的 WebGL 程序。

1
2
3
const ctx = document.getElementById('gl').getContext('webgl');
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(this.gl.COLOR_BUFFER_BIT);

运行效果为一个黑色矩形 点击运行

三维绘制点

前边我们分别使用三行代码分别绘制了二维与三维的矩形,我们知道,在二维世界坐标是 (x, y),在三维世界比二维多了一个 z (深度),坐标也就是 (x, y, z),然后推测根据二维的代码多加一个 z 坐标应该就能画三维中的矩形,修改代码如下:

1
2
3
const ctx = document.getElementById('gl').getContext('webgl');
gl.drawColor(0.0, 0.0, 0.0, 1.0);
gl.drawPoint(0, 0, 0, 10); // 点的位置和大小

然而事情并没有那么简单,当我们运行代码发现出错了,实际的三维绘图要比二维复杂的多,WebGL 依赖于一种新的称为 着色器 (shader) 的绘图机制,上一篇 WebGL 三维教程 - WebGL 介绍(一) 我们简单介绍了 WebGL,其中有提及到 GLSL (OpenGL Shading Language),那么 GLSL 到底是什么呢?GLSL 为着色语言,用来编写 shader 脚本,着色器分为 顶点着色器 (Vertex shader) 与 片元着色器 (Fragment shader) 两部分,着色器提供了灵活而强大的绘制二维或三维图形的方法,正是因为它的强大,因此也带来了使用上的复杂度,接下来的教程中,我们将一步步去深入研究和理解它。

三维世界的坐标系统

WebGL 中的坐标是由 (x, y, z) 组成,下图描述了 WebGL 坐标系。z 轴表示深度,正值 z 表示对象在屏幕/查看器附近,而负值 z 表示该对象不在屏幕上。同样,x 的正值表示对象是屏幕右侧,负值表示对象是左侧。同样,y 的正值和负值表示对象位于屏幕顶部或底部的底部。

顶点与片元着色器介绍

  • 顶点你着色器是用来描述顶点特征(如位置、颜色等)的程序。顶点 是指二维或三维空间中的一个点,将这些顶点连接起来,可以形成线或三角形,WebGL 中最常用的是三角形。
  • 片元着色器是进行逐片元处理过程的程序,如处理光照,片元史 WebGL 术语,可以理解为像素。

创建着色器步骤

(一)、使用 gl.createProgram() 方法用于创建和初始化一个 WebGLProgram 对象。
(二)、使用 gl.createShader(type) 创建一个 WebGLShader 着色器对象。

参数表述
typegl.VERTEX_SHADER 为顶点着色器,gl.FRAGMENT_SHADER 为片元着色器

(三)、使用 gl.shaderSource(shader, source) 挂接 source (GLSL) 源代码到 shader 上。

参数表述
shader包含顶点着色器和片元着色器的 WebGLShader 对象
source一坨字符串,也就是着色器 (GLSL) 语言代码

(四)、使用 gl.compileShader(shader) 编译一个 GLSL 着色器,使其成为为二进制数据,然后就可以被 WebGLProgram 对象所使用。

参数表述
shader一个片元或顶点着色器

(五)、使用 gl.attachShader(program, shader) 方法把着色器 shader 添加到 program

参数表述
program包含顶点着色器和片元着色器的 WebGLProgram 对象
shader一个类型为片段或者顶点的 WebGLShader 对象

(六)、使用 gl.linkProgram(program)gl.useProgram(program) 分别链接和使用 WebGLProgram 对象。

综上六个步骤就可以创建一个顶点着色器或片元着色器,为了方便这里把创建着色器封装成了一个 createShader(type, source) 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point {

createShader(type, source) {
// 创建一个`WebGLShader`着色器对象,`type`为`gl.VERTEX_SHADER`(顶点着色器)或`gl.FRAGMENT_SHADER`片元着色器
const shader = this.gl.createShader(type);

// 挂接`source`(GLSL)源代码到`shader`上
this.gl.shaderSource(shader, source);

// 编译`shader`(GLSL)为二进制文件,以便被`WebGLProgram`使用
this.gl.compileShader(shader);

// 添加一个片元着色器或者顶点着色器
this.gl.attachShader(this.shaderProgram, shader);

return shader;
}

}

初始化顶点和片元着色器

接下来在添加一个 constructor()initShader() 方法,用来获取 WebGLRenderingContext 对象和初始化顶点着色器和片元着色器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Point {

constructor() {
this.gl = self.gl = document.getElementById('gl').getContext('webgl');
this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);
}

...

initShader() {
// 创建一个`WebGLProgram`
this.shaderProgram = this.gl.createProgram();

// 创建顶点着色器
this.createShader(this.gl.VERTEX_SHADER, `
attribute vec4 a_Position;

void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`);

// 创建片元着色器
this.createShader(this.gl.FRAGMENT_SHADER, `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`);

// 分别链接和使用
this.gl.linkProgram(this.shaderProgram);
this.gl.useProgram(this.shaderProgram);
}

...

}

上边程序中第 16-21 行编写了顶点着色器,26-28 行编写了片元着色器,从这两块的程序中可以看出,GLSL 语言是跟 C 语言非常类似的,都必须包含一个 main() 函数,main() 前边的关键字 void 是表示这个函数不会有返回值,还有不能为 main() 函数指定参数。

在顶点着色器和中使用了:gl_Positiongl_Position 和片元着色器中的: gl_FragColor,这些分别为内置的变量,我们把相应的值赋值给相应的内置变量就可以得到相应的渲染结果,它们的功能如下:

类型表述
gl_Position表示顶点位置
gl_Position表示点的尺寸
gl_FragColor指定片元颜色(RGBA)

并且 GLSL 语言为强类型语言,如果把第 20 行的 gl_PointSize = 10.0 改为 gl_PointSize = 10 就会导致程序报错,上边使用到的类型介绍如下:

类型表述
float表示浮点数
vec4表示由四个浮点数组成的矢量,另外三种:vec1、vec2、vec3

其中 vec4 在这里有两种身份,它既能声明变量类型: attribute vec4 a_Position,又能作为 构造函数 (Constructor Functions) 使用:gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0),构造函数的名称和创建的变量的名称是一致的。最前边的关键 attribute 被称为 储存限定符 (Storage Qualifier),attribute 必须声明为全局变量,数据将从着色器外部传给该变量。

着色器初始化完成后就可以开始画点了,步骤如下:

(一)、使用 gl.getAttribLocation(program, name) 方法获取 attribute 变量的储存位置。

类型表述
program包含顶点着色器和片元着色器的 WebGLProgram 对象
name着色器 attribute 变量的名称

(二)、使用 gl.vertexAttrib3f(index, v0, v1, v2) 方法向 attribute 变量赋值。

类型表述
indexattribute 的储存位置
v0指定填充 attribute 变量第一个分量的值
v1指定填充 attribute 变量第二个分量的值
v2指定填充 attribute 变量第三个分量的值

(三)、使用 gl.drawArrays(mode, first, count) 方法进行绘制。

类型表述
mode绘制的方式,可以选择的常量:gl.POINTS、gl.LINES、gl.LINE_STRIP、gl.LINE_LOOP、gl.TRIANGLES、gl.TRIANGLE_STRIP、gl.TRIANGLE_FAN
first指定从哪个顶点开始绘制
count指定绘制需要多少个顶点

mode 可以绘制的基本图形解析:

mode图形表述
gl.POINTS一系列点,绘制在 v0、v1、v2 … 处
gl.LINES线段一系列单独的线段,绘制在 (v0, v1)、(v2, v3)、(v4, v5) … 处,如果点的个数是奇数,最后一个点将被忽略
gl.LINE_STRIP线条一系列连接的线段,被绘制在(v0,v1)、(v1,v2)、(v2,v3) … 处,第 1 个点是第1条线段的起点,第 2 个点是第 1 条线段的终点和第 2 条线段的起点 … 第 i(i>1) 个点是第 i-1 条线段的终点和第 i 条线段的起点,以此类推。最后一个点是最后一条线段的终点
gl.LINE_LOOP回路一系列连接的线段。与gl.LINE_STRIP 绘制的线条相比,增加了一条从最后一个点到第1个点的线段。因此,线段被绘制在 (v0, v1)、(v1, v2) … (vn, v0) 处,其中 vn 是最后一个点
gl.TRIANGLES三角形一系列单独的三角形,绘制在 (v0, v1, v2)、(v3, v4, v5) … 处,如果点的个数不是 3 的整数倍,最后剩下的一或两个点将被忽略
gl.TRIANGLE_STRIP三角带一系列条带状的三角形,前三个点构成了第 1 个三角形,从第 2 个点开始的三个点构成了第 2 个三角形(该三角形与前一个三角形共享一条边),以此类推。这些三角形被绘制在 (v0, v1, v2)、(v2, v1, v3)(v2, v3, v4) … 处
gl.TRIANGLE_FAN三角扇一系列三角形组成的类似于扇形的图形。前三个点构成了第 1 个三角形,接下来的一个点和前一个三角形的最后一条边组成接下来的一个三角形。这些三角形被绘制在 (v0, v1, v2)、(v0, v2, v3)、(v0, v3, v4) … 处

绘制三维世界中的点

添加一个 drawPoint() 方法进行画点,在运行前我们在 constructor() 方法中分别调用 this.initShader()this.drawPoint() 方法,最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point {

constructor() {

...

this.initShader();
this.drawPoint();
}

...

drawPoint() {
const a_Position = this.gl.getAttribLocation(this.shaderProgram, 'a_Position');
this.gl.vertexAttrib4f(a_Position, 0.0, 0.0, 0.0, 1.0);
this.gl.drawArrays(this.gl.POINTS, 0, 1);
}

}

预览效果 点击运行

小结

本文中出现了一些 WebGL 的一些概念,对于初学者来说,你不需要记住本文中的所有东西,在后续的教程中将一步步深入研究这些概念,再加上经常练习才能真正理解,所以,现在还不用太严肃,现在只需要享受阅读,等以后再回头看也不迟。