1.3 WebGL原生API
WebGL入门指南
计算机图形学中的基本概念在过去的数年中从未发生过变化。但图形技术却不断进化,特别是最近几年,伴随着设备和操作系统的多样化。这些百花齐放的图形技术的根基则是诞生于20世纪80年代末期的OpenGL。OpenGL作为行业规范已经存在了非常长的时间,并且成功经受住了来自微软的DirectX的竞争考验,无可争辩的成为了3D图形编程的领衔者。
但是并非所有的OpenGL都是一样的。根据不同平台的不同特征,包括台式电脑、智能电视、手机和平板电脑等,人们开发了不同版本的OpenGL。OpenGL ES(Embedded System,即嵌入式系统)就是其中的一个版本,专门用于运行在较小的设备上,例如智能电视和手机。也许当初OpenGL ES的设计者们并未料到,正是它成为了WebGL的理想核心。OpenGL ES小而精悍,这并不只是说它足够小因此可以集成到浏览器中,还意味着不同的浏览器厂商都可以使用这套规范。这样的话,WebGL应用就可以顺利的运行在所有的不同浏览器中。
但所有这些高性能和便携性的背后都有代价。WebGL精悍的本质其实是将责任压在了应用开发者的肩上,他们必须自己去处理所有的模型、场景、显示列表和其他构架;当然,在资深图形开发者看来,这都是理所当然的。最值得关注的是,对于一般的Web开发者,WebGL的学习之路陡峭而崎岖,WebGL看起来好像天书。而好消息则是目前已经有很多开源的WebGL框架,可以大大简化WebGL开发的工作量。说个有些粗糙的比喻,这些框架就好像是jQuery和Prototype.js一样。在本书中我们会花几页的篇幅讨论其中一个框架Three.js。但在这之前,我们还是快速地浏览一下WebGL的底层原理。
1.3.1 WebGL应用结构剖析
从本质上来讲,WebGL只是一个绘制库——一个增强型的绘制库,使用WebGL你可以绘制出惊人的图形,并可以在当前大部分机器上充分利用强大的GPU硬件能力。但也可以把WebGL理解成另一种画布,类似于HTML5浏览器中的2D Canvas。实际上,WebGL也正是使用了HTML5中的 元素来在浏览器页面中显示3D图形。
想要使用WebGL把图形渲染到页面中,一个应用至少需要执行如下步骤。
1.创建一个画布元素。
2.获取画布的上下文。
3.初始化视口。
4.创建一个或多个包含渲染数据的数组(通常是顶点数组)。
5.创建一个或多个矩阵,将顶点数组变换到屏幕空间中。
6.创建一个或多个着色器来实现绘制算法。
7.使用参数初始化着色器。
8.绘制。
在本小节中我们会详细介绍上述的每一个步骤,各步骤的代码片段来源于一个完整的示例,在这个示例中我们用WebGL绘制了一个白色的正方形。你可以从文件Chapter 1/example1-1.html中找到完整的代码。
1.3.2 画布元素与绘制上下文
所有的WebGL渲染都发生在一个上下文(context)中,这是一个JavaScript DOM对象,可以提供完整的WebGL API。这个结构类似于HTML5中 元素的2D绘制上下文。想要使用WebGL,只需要创建一个元素,然后获取与它关联的DOM对象(使用document.getElementById()),最后再为其获得一个WebGL上下文即可。示例1-1展示了如何从画布的DOM对象中获得WebGL上下文。
示例1-1 从Canvas中获取WebGL上下文
function initWebGL(canvas) {
var gl;
try
{
gl = canvas.getContext("experimental-webgl");
}
catch (e)
{
var msg = "Error creating WebGL Context!: " + e.toString();
alert(msg);
throw Error(msg);
}
return gl;
}
图片 3 请注意示例代码中的try/catch这一部分。这一部分非常重要,因为目前一些浏览器依然不支持WebGL,或者即使支持了,但是用户并没有安装包含WebGL支持的最新版本的浏览器。另外,即使浏览器支持WebGL,但因为用户使用的是老旧的硬件,因此并不能成功的获得上下文。所以示例1-1中这一部分的检测代码将会帮助你做一个回滚,比如回滚到2D画布,或者至少能让你的应用礼貌地选择退出。
1.3.3 视口
一旦成功的从画布中获得了WebGL的绘制上下文,你就可以告诉它在哪里绘制矩形了。在WebGL中,这被称为视口(vewport),在WebGL中设置视口非常简单,只需要调用上下文的viewport()方法即可(参见示例1-2)。
示例1-2 设置WebGL视口
function initViewport(gl, canvas)
{
gl.viewport(0, 0, canvas.width, canvas.height);
}
让我们回忆一下,gl对象是在initWebGL()函数中创建的。在这个示例中,我们初始化的WebGL的视口占据了整个画布的显示区域。
1.3.4 Buffer、ArrayBuffer和类型化数组
现在我们获取到了上下文并设置好了视口,可以准备开始绘制了。这和2D Canvas还是很像的。
WebGL的绘制是由图元(primitive)组成的,图元的种类包括三角形(三角形数组)、三角形带(triangle strip,稍后我们会讲到)、点和线。图元使用的数据数组被称为Buffer,它定义了顶点的位置。示例1-3展示了如何创建一个大小为1×1的正方形的顶点数组。返回的JavaScript对象存储了顶点数组信息、数组中每个顶点所占的尺寸(在这个示例中,包含三个浮点数来存储x, y, z的值)、需要绘制的顶点的数量,以及用于绘制正方形的图元的类型。(在这个示例中,我们使用了三角形带。三角形带是一种基本的渲染图元,它是指使用前三个顶点画出第一个三角形,然后再用这前三个顶点中的后两个与下一个顶点结合再绘制一个三角形,依次类推。)
示例1-3 创建顶点数组
// 创建用于绘制正方形的顶点数据
function createSquare(gl) {
var vertexBuffer;
vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
var verts = [
.5, .5, 0.0,
-.5, .5, 0.0,
.5, -.5, 0.0,
-.5, -.5, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts),
gl.STATIC_DRAW);
var square = {buffer:vertexBuffer, vertSize:3, nVerts:4,
primtype:gl.TRIANGLE_STRIP};
return square;
}
请注意我们使用了一个叫做Float32Array的数据类型。这是一种为了WebGL而专门引入浏览器中的新数据类型。Float32Array是ArrayBuffer的一种,也被称为类型化数组(typed array),用于在JavaScript中存储二进制数据。在JavaScript中访问类型化数组可以使用与传统数组相同的语法,但是速度更快并且占用更少的内存。在对性能要求很高的场合下,使用它来处理二进制数据是非常理想的选择。类型化数组还可以用于很多其他的场合,但是它最开始是因为WebGL才被引入的。你可以在科纳斯组织的官方网站上找到类型化数组的规范全文(http://www.khronos.org/registry/ typedarray/specs/latest/)。
1.3.5 矩阵
在绘制正方形之前,我们必须建立两个矩阵。首先,我们需要一个矩阵来定义正方形在3D坐标系中相对于相机的位置。这个矩阵也称为模型视图矩阵(modelview matrix),因为它综合了模型的变换和相机之间的关系。在这个示例中,我们对模型的变换就是将正方形沿着负z轴进行平移(即远离相机-3.333个单位)。
我们需要的第二个矩阵就是投影矩阵(projection matrix),这个矩阵将被用于在着色器中将相机空间中模型的3D坐标转换为绘制的视口的2D坐标。在这个示例中,投影矩阵定义了一个45°的相机视野。投影矩阵非常难以想象,几乎没有人会手动编写计算投影矩阵的代码,而都是由框架来自动完成。在这里我推荐一个非常好用的开源框架,叫做glMatrix(https://github.com/toji/glmatrix),它专门用来在JavaScript中进行矩阵运算。glMatrix是由Brandon Jones编写的,他同时也是一些精彩的WebGL作品的作者,他复刻了Quake和其他几个著名游戏的WebGL版本。
示例1-4 设置模型视图矩阵和投影矩阵
function initMatrices()
{
// 正方形的变换矩阵——相对于相机沿着z轴稍微后移一些
modelViewMatrix = new Float32Array(
[1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, −3.333, 1]);
// 变换矩阵(45度视野)
projectionMatrix = new Float32Array(
[2.41421, 0, 0, 0,
0, 2.41421, 0, 0,
0, 0, −1.002002, −1,
0, 0, −0.2002002, 0]);
}
1.3.6 着色器
我们离最终的绘制越来越近了。最后还剩一个非常重要的步骤需要设置:着色器。像我们之前所说的一样,着色器其实就是一小段程序,是由高等级的类C语言编写而成,它定义了如何将3D物体的像素切实地绘制在屏幕上。WebGL要求开发者为每个需要绘制的物体提供着色器。一个着色器可以同时应用于多个物体。所以在实际情况下,我们经常只提供一个着色器来满足整个应用程序,然后每次用不同的参数多次复用这个着色器。
着色器是由两个部分组成的:顶点着色器(vertex shader)和片元着色器(fragment shader),后者也被称为像素着色器(pixel shader)。顶点着色器负责将物体的坐标转换到2D空间;片元着色器负责生成最后的颜色输出到每个转换后的顶点像素,而颜色的输入则可以是纯色、纹理、光源或者材质。在这个简单的示例中,顶点着色器综合了modelViewMatrix和projectionMatrix的值,通过计算转换了每一个顶点;而片元着色器则只是简单的输出了白色。
在WebGL中,设置着色器必须遵循一定的步骤,首先编译着色器的各个部分,然后将它们链接到一起。因为篇幅有限,我们在这里只展示两个简单的着色器代码(使用GLSL ES语言,参考示例1-5),而并不写出所有的设置代码。你可以在完整的示例中看到如何设置着色器。
示例1-5 顶点着色器和片元着色器
var vertexShaderSource =
" attribute vec3 vertexPos;\n" +
" uniform mat4 modelViewMatrix;\n" +
" uniform mat4 projectionMatrix;\n" +
" void main(void) {\n" +
" // 返回变换并投影后的顶点数据\n" +
" gl_Position = projectionMatrix modelViewMatrix \n" +
vec4(vertexPos, 1.0);\n" +
" }\n";
var fragmentShaderSource =
" void main(void) {\n" +
" // 返回像素颜色:永远输出白色\n" +
" gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);\n" +
"}\n";
1.3.7 绘制图元
现在我们终于万事俱备,可以开始绘制正方形了(参考示例1-6)。我们建立了上下文,设置了视口,顶点数组、矩阵和着色器也已经设置并初始化完成。我们定义了一个函数draw(),在这个函数中将接管WebGL上下文和我们之前建立的矩形对象。首先,函数会先清理一下画布并用黑色作为背景颜色。然后将顶点数组绑定到上下文中,使用着色器,并把顶点数组和矩阵作为输入传递给着色器。最后我们调用了WebGL的drawArrays()方法来绘制正方形。我们简单地告诉它,图元的类型和图元中有多少顶点;WebGL会自动根据我们在上下文中设置的其他值(顶点、矩阵、着色器)完成绘制。
示例1-6 绘制代码
function draw(gl, obj) {
// 用黑色清空背景
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 设置顶点数组
gl.bindBuffer(gl.ARRAY_BUFFER, obj.buffer);
// 设置着色器
gl.useProgram(shaderProgram);
// 设置着色器参数:点点坐标、投影矩阵和模型视图矩阵
gl.vertexAttribPointer(shaderVertexPositionAttribute,
obj.vertSize, gl.FLOAT, false, 0, 0);
gl.uniformMatrix4fv(shaderProjectionMatrixUniform, false,
projectionMatrix);
gl.uniformMatrix4fv(shaderModelViewMatrixUniform, false,
modelViewMatrix);
// 绘制物体
gl.drawArrays(obj.primtype, 0, obj.nVerts);
}
最终完成绘制的结果如图1-5所示。
图1-5 使用WebGL绘制的正方形