从这一章开始,我们将学习如何初始化 WebGL 程序,通过 Canvas 2D 例子来更好地理解 WebGL,最后通过着色器代码绘制点线面图形。
由于 WebGL 被浏览器原生支持,所以无需在系统中安装额外插件,而且原生的 JavaScript 代码即可驱动 WebGL 程序,在应用层也无需引用额外 JavaScript 库。当然,市面上已存在很多的 WebGL 相关 JavaScript 库,它们的目的是封装了很多实用函数以及屏蔽实现细节以提升开发效率,作为初学者,推荐从最基础的代码写起,以更好地了解底层原理。
我们以绘制一个点为目标,分别通过 Canvas 2D 和 WebGL 来实现,让读者更好地理解两者差异。
HTML 只需要一个 Canvas 节点无需赘述,我们直接看 JS 代码:
// 获取 canvas 节点
var canvas = document.getElementById('canvas')
// 若获取不到 canvas 节点,则退出
if (!canvas) {
console.log("Failed to retrieve <canvas> element")
return
}
// 获取 2D 渲染上下文
var context = canvas.getContext('2d')
// 清空画布 & 绘制黑色底
context.clearRect(0, 0, 380, 160)
context.fillRect(0, 0, 380, 160)
// 绘制一个点(5x5 的正方形)
context.fillStyle = 'rgba(255, 255, 255, 1)'
context.fillRect(190, 80, 5, 5)
通过获取 2D 渲染上下文,即可调用 JavaScript API 进行绘制(非常傻瓜,无需关注具体绘制细节),下面是 Live Demo(后续所有 Demo 都将通过 Codepen 等在线编辑器展示,你可以在当前页面通过嵌入的控件查看源码,或者点击右上角进入 Codepen 浏览):
See the Pen 1. Draw Point - Canvas by shaderfans (@shaderfans) on CodePen.
如果想通过 WebGL 来绘制一个点,就需要依托顶点着色器和片元着色器可以操作像素的能力了,不妨先直接看看效果(效果上其实没有差异):
See the Pen 2. Draw Point - WebGL by shaderfans (@shaderfans) on CodePen.
代码量一下子起来了,接下来逐行分析代码,看看和 Canvas 2D 绘制一个点有什么差异:
在前面的内容可以得知:WebGL1.0 的标准在 2011.3 发布,在标准发布前,由于标准未定变化较多,浏览器厂商都会对草案进行实验性支持(所以会存在一些前缀,如 experimental-webgl),随着标准的指定,浏览器的底层实现也经过不断迭代,逐渐普及标准写法。为了更好的兼容不同的浏览器以及不同版本的浏览器,我们需要在代码层面做兼容,但随着时间的发展,最终的写法一定是以标准的为主:
// 除了 webgl,还存在 experimental-webgl, webkit-3d, moz-webgl 等兼容写法
var gl = canvas.getContext('webgl')
// webgl2 则不需要考虑前缀,直接使用标准写法
// var gl = canvas.getContext('webgl2')
在 Demo 中,我们以字符串的形式,通过两个变量来存储 Shader 代码(后面会介绍通过 script 标签以及文件形式来存储 Shader 的方法):
// 定义顶点着色器
var VSHADER_SOURCE = `
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0); // 设置点的位置
gl_PointSize = 5.0; // 设置点的大小
}
`
// 定义片元着色器
var FSHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); // 设置顶点颜色
}
`
Shader 是一个以 C 语言为基础的高阶着色语言,它是由 OpenGL ARB 所建立,提供开发者对绘图管线更多的直接控制,而无需使用汇编语言或硬件规格语言。既然是以 C 语言为基础,自然具备了很多 C 语言的特点,建议读者收藏这份中文手册,可快速查询语法(本教程只会在用到的时候介绍):
Shader 着色器语言具备以下特点(WebGL 1.0):
Shader 使用一些特殊的内置变量与硬件进行沟通。它们大致分成两种:一种是 input 类型,负责向硬件(渲染管线)发送数据;另一种是 output 类型,负责向程序回传数据,以便编程时需要,gl_Position 和 gl_FragColor 都是 output 类型。
gl_Position = vec4(0.0, 0.0, 0.0, 1.0); // 设置点的位置
通过上述代码可以知道画布只有一个点,位置在 (0, 0, 0) 上(因为是 3D 的,所以需要 xyz 坐标来确定),而第四个数值 1.0 为齐次坐标,用于矩阵运算的,可以先略过,我们后面再讲。
这个时候问题来了,既然是在 (0, 0, 0) 的位置,为什么不应该在左上角呢?要知道 Canvas 2D 的坐标系就是基于左上角的。
如你所见,WebGL 使用的坐标系跟 Canvas 2D 存在差异,它以画布中央作为中心点,可以通过右手来确定方向 xyz 轴方向:
同时你会发现,在 Canvas 2D 中,XY 轴的数值是以实际像素为值,而 WebGL 中则是 -1~1 的范围。其实 WebGL 的坐标系是个比较大的专题,这里同样先略过。了解完坐标系差异之后,再看看片元着色器中唯一的代码的含义:
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); // 设置顶点颜色
其实不难理解,实际上四个值分别代表 r, g ,b, a,红绿蓝和透明度。这里需要注意的是原本 0~255 的色值在 Shader 中通过 0~1 来表示了,所以在设置时需要对它「归一化」处理。
剩下的代码就比较固定了,简单来说就是:设置着色器类型—>添加着色器—>编译着色器内容—>通过 program 绑定两个着色器—>添加到 WebGL—>使用这个 program。
// 编译顶点着色器代码
var vertexShader = gl.createShader(gl.VERTEX_SHADER); // 设置着色器类型
gl.shaderSource(vertexShader, VSHADER_SOURCE); // 设置顶点着色器内容
gl.compileShader(vertexShader); // 编译顶点着色器内容
// 编译片元着色器代码
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); // 设置着色器类型
gl.shaderSource(fragmentShader, FSHADER_SOURCE); // 设置顶点着色器内容
gl.compileShader(fragmentShader); // 编译顶点着色器内容
// 创建一个程序 Program
var program = gl.createProgram();
// 将顶点着色器和片元着色器绑定
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// 将绑定后的 Program 绑定到 WebGL 渲染上下文中
gl.linkProgram(program);
// 使用这个 Program
gl.useProgram(program);
随后就是清空画布了,WebGL 特别之处在于它除了可以指定清空的颜色以及透明度,还能指定清空不同的缓冲通道:
// 指定清空缓冲区的颜色
gl.clearColor(0, 0, 0, 1)
// 清空颜色通道缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
// 除了 gl.COLOR_BUFFER_BIT(颜色缓冲区) 之外
// 还有 gl.DEPTH_BUFFER_BIT(深度缓冲区)和 gl.STENCIL_BUFFER_BIT(模板缓冲区,很少用)
// 深度缓冲区的存在正是为了 3D 场景而准备的
最后,也是最最最为关键的一个函数,我们暂且理解它是以 gl.POINTS 的类型绘制顶点着色器中定义的那一个点(0, 0, 0),更详细的用法会在随后几节中介绍:
gl.drawArrays(gl.POINTS, 0, 1);
当然上面的代码有些繁琐了,通过引入第三方的库(左下角 resources 中可查引入资源,资源来自《WebGL 编程指南》),屏蔽了兼容细节以及重复性的代码,最终得到了更优雅的代码实现(留意 HTML,示意了如何通过 script 标签引入 Shader 代码):
See the Pen 1. Draw Point Simplify- WebGL by ShaderFans (@shaderfans) on CodePen.
了解完上面的一系列操作后,你是否能清晰的回答这个提问:WebGL 与 Canvas 的区别是什么?
Canvas 就是画布,只要浏览器支持,可以在 Canvas 上获取 2D 上下文和 3D 上下文,其中 3D 上下文就是 WebGL。
Canvas 是 HTML5 提供的一个特性,你可以把它当做一个载体,简单的说就是一张白纸。而 Canvas 2D 相当于获取了内置的二维图形接口,也就是二维画笔。Canvas 3D 是获取基于 WebGL 的图形接口,相当于三维画笔。你可以选择不同的画笔在上面作画。
OpenGL 是底层的驱动级的图形接口(与显卡有直接关系的),类似于 Direct3D。但是这种底层的 OpenGL 是寄生于浏览器的 JavaScript 无法触碰到的。为了让 Web 拥有更强大的图形处理能力,2010 年时候 WebGL 被推出来了。WebGL 允许工程师使用 JS 去调用部分封装过的 OpenGL ES 标准接口以提供硬件级别的 3D 图形加速功能,所以 GLSL 程序真正运行的时候还是跑在 OpenGL 驱动上的。这些驱动由不同平台的开发人员编写,因此可以针对性的进行一些优化,如从 2013 年起,Windows 平台下会使用 ANGLE(Almost Native Graphics Layer Engine)技术将 OpenGL ES 转成拥有更好驱动支持的 D3D 9.0c 或 D3D 11,而 iOS/MacOS 中也通过苹果自研的 Metal 框架来实现 WebGL 2.0 接口。