Hello WebGL
1. 获取渲染上下文
现代的网页基本上是由html, css, javascript三种语言写成的。其中 html 用于描述一个页面都有什么元素。css 描述了各个元素应该长什么样子。 javascript,以后简称 js,它是一个脚本语言,用于控制页面中各个元素的行为逻辑。我们通过 WebGL 编写图形引擎,主要使用的是 js。
这里是一个十分简单的 html 页面,它没有加载任何 css 和 js 文件。 我使用的是 chrome 浏览器,可以通过 ctrl+shift+c 唤起它的调试界面,如下图所示。在 Elements 标签页中显示了网页的所有元素,把鼠标放上去就会在页面中高亮显示对应的元素。 这里我们有一个 id 为 "nishuosha" 的 canvas 元素,它将是我们后续的 3D 作图区域。
![](http://gaoyichao.com/Xiaotu//WebGL和图形引擎/img/HelloWebGL调试界面0.png)
就像 OpenGL 中需要一个窗口才能建立上下文一样,WebGL 需要一个 canvas 对象构建渲染上下文。如下图所示,我们在 Console 标签页里先后输入了四条语句,把 canvas 的背景刷成了黑色。 第一条语句是通过 id 获取 html 中的 canvas 对象,用变量 cv 标记。这里的 document 是浏览器中的一个特定变量,指的就是当前页面的 html。 接着就是通过 cv 获取一个 webgl 的渲染上下文 gl。然后通过 gl 设置了背景色为黑色,这里输入的四个参数 (0,0,0,1) 分别对应着 RGBA。最后清空了 WebGL 的颜色缓冲区。
![](http://gaoyichao.com/Xiaotu//WebGL和图形引擎/img/HelloWebGL调试界面1.png)
上述过程就是获取一个渲染上下文的全部过程,得到的 gl 类型为 WebGLRenderingContext。通过该接口我们就可以访问 WebGL 的各种 API 了。 为了方便后续例程的编写,这里对它做了一些封装,放到了XiaoTuWebglBox中。 如下图所示,我们可以在 html 文件中通过相对路径加载并使用它。 这个相对路径是相对于加载的 html 文件而言的,如左侧的目录树所示,hello_webgl_1.html 向上两级目录就是 Xiaotu, webgl.js 就在其两级子目录 js/XiaoTuWebglBox 下。所以,我们可以通过 "../../js/XiaoTuWebglBox/webgl.js" 找到封装的接口。 我们在这个 WebGLRenderer 的构造函数里重复了一遍获取上下文并清除颜色缓冲区的操作,同时还修改了画布的大小。详细的代码读者可以点击 webgl.js 查看,这里不在展开。
![](http://gaoyichao.com/Xiaotu//WebGL和图形引擎/img/HelloWebGL调试界面2.png)
2. 加载着色器
渲染的过程实际上就是将 3D 的模型转换到屏幕的 2D 像素图像的过程,在很多教程中,会将这个过程称为渲染管线。通常情况下,3D 模型是由一堆顶点和三角形描述的。 我们需要将这些顶点搬运到虚拟世界中,经过一系列的平移、旋转、缩放的操作,得到模型在虚拟世界中的位置、方向和大小。 然后根据三角形的关系,将这些顶点组成一个个的基础图元,经过视角椎的裁剪,深度和遮挡关系的处理,光栅化形成像素。 最后我们还需要描述每个像素的颜色值,填充完毕就得到了一幅 2D 的图像。 详细的工作原理可以参考教程。
在 WebGL 中,渲染的工作通常是在显卡中完成的,我们需要通过 API 完成数据的搬运工作。顶点的坐标换算需要通过顶点着色器 Vertex Shader 完成。 从图元的组装到光栅化的过程,基本上都是 WebGL 帮我们完成了。我们还需要做的事情就是提供一个片元着色器 Fragment Shader 来指引光栅化过程计算各个像素的颜色值。 着色器是现代 OpenGL 的核心概念,它是我们以后实现各种酷炫特效的基础工具。在 WebGL 中我们需要按照 GLSL 的语法写两个类似 C 语言的程序。
下面我们将定义一对极简的顶点着色器和片元着色器,来完成一个三角形的绘制, 参见例程。下面的截图中,我们封装了一个接口 createProgramFromUrl 来加载着色器。这个接口做了三件事情: ① 通过 GET 请求获取源文件 ② 编译源代码生成 WebGLShader 对象 ③ 连接两个着色器构成 WebGLProgram 对象。
![](http://gaoyichao.com/Xiaotu//WebGL和图形引擎/img/HelloWebGL调试界面3.png)
对于我们搬进 WebGL 中的每一个点,gl 都会运行一遍顶点着色器。在下面左侧的代码片段中,我们给出了一个极简的顶点着色器。 它只是将传入的顶点坐标 a_position 直接输出到 gl_Position 中。 关于着色器的语法可以参考教程。 变量 a_position 的类型为 vec4 是一个 4 维的向量,前面加了一个 attribute 的修饰符,表示我们可以通过接口 getAttribLocation 获取它的位置, 并把顶点数据搬运到 a_position 中。gl_Position 是 WebGL 内置的 vec4 类型的变量,它记录的是顶点在GL世界中的最终坐标。 至于为什么三维空间中的点要用一个四维的向量来表示, 可以参考刚体运动与齐次变换。
// 顶点着色器 hello_webgl_vertex.vs
attribute vec4 a_position;
void main() {
gl_Position = a_position;
}
// 片元着色器 hello_webgl_fragment.fs
precision highp float;
uniform vec4 uPixelColor;
void main() {
gl_FragColor = uPixelColor;
}
![](http://gaoyichao.com/Xiaotu//WebGL和图形引擎/img/HelloWebGL调试界面4.png)
上面右侧的代码片段是一个极简的片元着色器。它只是把面元对应的所有像素都涂成了相同的颜色 uPixelColor。 这里的 uPixelColor 也是一个 vec4 的向量,它的每个元素都是区间 \([0, 1]\)的,分别对应着 RGBA 四个颜色通道。 前面的修饰符 uniform 表示我们可以通过 uniform4f 系列的接口给这个变量赋值。 在我们的例程中,封装了三个函数 bindArrayBuffer, activateAttribute, uniform4fv 分别用来搬运数据,指定 attribute 变量,给 uniform 变量赋值。在一切顺利的情况下, 就可以看到一个黑色的背景上面有一个绿色的三角,如右图所示。在 WebGL 中输出的 2D 图像坐标的原点在画布的中心,如最右图所示, 横纵轴的取值范围都是 \([-1, 1]\)。
读者有兴趣也可以看看 OpenGL 相关的教程,很多概念是可以直接照搬到 WebGL 中的。
- lazyfoo.net,我学生时代的入门教程,不仅仅有 OpenGL 还有 SDL,以及游戏引擎的介绍,是个宝库。
- learningopengl,是一个相对新一点教程,页面风格也美观一些,就是开发环境是 windows。