WebGL 技术储备指南
Posted GISEarth
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WebGL 技术储备指南相关的知识,希望对你有一定的参考价值。
WebGL 是 html 5 草案的一部分,可以驱动 Canvas 渲染三维场景。WebGL 虽然还未有广泛应用,但极具潜力和想象空间。本文是我学习 WebGL 时梳理知识脉络的产物,花点时间整理出来与大家分享。
示例
WebGL 很酷,有以下 demos 为证:
寻找奥兹国
赛车游戏
划船的男孩(Goo
Engine Demo)
本文的目标
本文的预期读者是:不熟悉图形学,熟悉前端,希望了解或系统学习 WebGL 的同学。
本文不是 WebGL 的概述性文章,也不是完整详细的 WebGL 教程。本文只希望成为一篇供 WebGL 初学者使用的提纲。
Canvas
熟悉 Canvas 的同学都知道,Canvas 绘图先要获取绘图上下文:
var context = canvas.getContext(‘2d‘);
在context
上调用各种函数绘制图形,比如:
// 绘制左上角为(0,0),右下角为(50, 50)的矩形
context.fillRect(0, 0, 50, 50);
WebGL 同样需要获取绘图上下文:
var gl = canvas.getContext(‘webgl‘); // 或 experimental-webgl
但是接下来,如果想画一个矩形的话,就没这么简单了。实际上,Canvas 是浏览器封装好的一个绘图环境,在实际进行绘图操作时,浏览器仍然需要调用 OpenGL API。而 WebGL API 几乎就是 OpenGL API 未经封装,直接套了一层壳。
Canvas 的更多知识,可以参考:
- JS 权威指南的 21.4 节或 JS 高级程序设计中的 15 章
- W3CSchool
- 阮一峰的 Canvas 教程
矩阵变换
三维模型,从文件中读出来,到绘制在 Canvas 中,经历了多次坐标变换。
假设有一个最简单的模型:三角形,三个顶点分别为(-1,-1,0),(1,-1,0),(0,1,0)。这三个数据是从文件中读出来的,是三角形最初始的坐标(局部坐标)。如下图所示,右手坐标系。
模型通常不会位于场景的原点,假设三角形的原点位于(0,0,-1)处,没有旋转或缩放,三个顶点分别为(-1,-1,-1),(1,-1,-1),(0,1,-1),即世界坐标。
绘制三维场景必须指定一个观察者,假设观察者位于(0,0,1)处而且看向三角形,那么三个顶点相对于观察者的坐标为(-1,-1,-2),(1,-1,-2),(0,1,-2),即视图坐标。
观察者的眼睛是一个点(这是透视投影的前提),水平视角和垂直视角都是90度,视野范围(目力所及)为[0,2]在Z轴上,观察者能够看到的区域是一个四棱台体。
将四棱台体映射为标准立方体(CCV,中心为原点,边长为2,边与坐标轴平行)。顶点在 CCV 中的坐标,离它最终在 Canvas 中的坐标已经很接近了,如果把 CCV 的前表面看成 Canvas,那么最终三角形就画在图中橙色三角形的位置。
上述变换是用矩阵来进行的。
局部坐标 –(模型变换)-> 世界坐标 –(视图变换)-> 视图坐标 –(投影变换)–> CCV 坐标。
以(0,1,0)为例,它的齐次向量为(0,0,1,1),上述变换的表示过程可以是:
上面三个矩阵依次是透视投影矩阵,视图矩阵,模型矩阵。三个矩阵的值分别取决于:观察者的视角和视野距离,观察者在世界中的状态(位置和方向),模型在世界中的状态(位置和方向)。计算的结果是(0,1,1,2),化成齐次坐标是(0,0.5,0.5,1),就是这个点在CCV中的坐标,那么(0,0.5)就是在Canvas中的坐标(认为 Canvas 中心为原点,长宽都为2)。
上面出现的(0,0,1,1)是(0,0,1)的齐次向量。齐次向量(x,y,z,w)可以代表三维向量(x,y,z)参与矩阵运算,通俗地说,w 分量为 1 时表示位置,w 分量为 0 时表示位移。
WebGL 没有提供任何有关上述变换的机制,开发者需要亲自计算顶点的 CCV 坐标。
关于坐标变换的更多内容,可以参考:
比较复杂的是模型变换中的绕任意轴旋转(通常用四元数生成矩阵)和投影变换(上面的例子都没收涉及到)。
关于绕任意轴旋转和四元数,可以参考:
关于齐次向量的更多内容,可以参考。
着色器和光栅化
在 WebGL 中,开发者是通过着色器来完成上述变换的。着色器是运行在显卡中的程序,以 GLSL 语言编写,开发者需要将着色器的源码以字符串的形式传给 WebGL 上下文的相关函数。
着色器有两种,顶点着色器和片元(像素)着色器,它们成对出现。顶点着色器任务是接收顶点的局部坐标,输出 CCV 坐标。CCV 坐标经过光栅化,转化为逐像素的数据,传给片元着色器。片元着色器的任务是确定每个片元的颜色。
顶点着色器接收的是 attribute 变量,是逐顶点的数据。顶点着色器输出 varying 变量,也是逐顶点的。逐顶点的 varying 变量数据经过光栅化,成为逐片元的 varying 变量数据,输入片元着色器,片元着色器输出的结果就会显示在 Canvas 上。
着色器功能很多,上述只是基本功能。大部分炫酷的效果都是依赖着色器的。如果你对着色器完全没有概念,可以试着理解下一节 hello world 程序中的着色器再回顾一下本节。
关于更多着色器的知识,可以参考:
程序
这一节解释绘制上述场景(三角形)的 WebGL 程序。点这个链接,查看源代码,试图理解一下。这段代码出自WebGL Programming Guide,我作了一些修改以适应本文内容。如果一切正常,你看到的应该是下面这样:
解释几点(如果之前不了解 WebGL ,多半会对下面的代码困惑,无碍):
-
字符串 VSHADER_SOURCE 和 FSHADER_SOURCE 是顶点着色器和片元着色器的源码。可以将着色器理解为有固定输入和输出格式的程序。开发者需要事先编写好着色器,再按照一定格式着色器发送绘图命令。
-
Part2 将着色器源码编译为 program 对象:先分别编译顶点着色器和片元着色器,然后连接两者。如果编译源码错误,不会报 JS 错误,但可以通过其他 API(如gl.getShaderInfo等)获取编译状态信息(成功与否,如果出错的错误信息)。
// 顶点着色器 var vshader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vshader, VSHADER_SOURCE); gl.compileShader(vshader); // 同样新建 fshader var program = gl.createProgram(); gl.attachShader(program, vshader); gl.attachShader(program, fshader); gl.linkProgram(program);
-
program 对象需要指定使用它,才可以向着色器传数据并绘制。复杂的程序通常有多个 program 对 象,(绘制每一帧时)通过切换 program 对象绘制场景中的不同效果。
gl.useProgram(program);
-
Part3 向正在使用的着色器传入数据,包括逐顶点的 attribute 变量和全局的 uniform 变量。向着色器传入数据必须使用 ArrayBuffer,而不是常规的 JS 数组。
var varray = new Float32Array([-1, -1, 0, 1, -1, 0, 0, 1, 0])
-
WebGL API 对 ArrayBuffer 的操作(填充缓冲区,传入着色器,绘制等)都是通过 gl.ARRAY_BUFFER 进行的。在 WebGL 系统中又很多类似的情况。
// 只有将 vbuffer 绑定到 gl.ARRAY_BUFFER,才可以填充数据 gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer); // 这里的意思是,向“绑定到 gl.ARRAY_BUFFER”的缓冲区中填充数据 gl.bufferData(gl.ARRAY_BUFFER, varray, gl.STATIC_DRAW); // 获取 a_Position 变量在着色器程序中的位置,参考顶点着色器源码 var aloc = gl.getAttribLocation(program, ‘a_Position‘); // 将 gl.ARRAY_BUFFER 中的数据传入 aloc 表示的变量,即 a_Position gl.vertexAttribPointer(aloc, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(aloc);
-
向着色器传入矩阵时,是按列存储的。可以比较一下 mmatrix 和矩阵变换一节中的模型矩阵(第 3 个)。
-
顶点着色器计算出的 gl_Position 就是 CCV 中的坐标,比如最上面的顶点(蓝色)的 gl_Position 化成齐次坐标就是(0,0.5,0.5,1)。
-
向顶点着色器传入的只是三个顶点的颜色值,而三角形表面的颜色渐变是由这三个颜色值内插出的。光栅化不仅会对 gl_Position 进行,还会对 varying 变量插值。
-
gl.drawArrays()方法驱动缓冲区进行绘制,gl.TRIANGLES 指定绘制三角形,也可以改变参数绘制点、折线等等。
关于 ArrayBuffer 的详细信息,可以参考:
关于 gl.TRIANGLES 等其他绘制方式,可以参考下面这张图或这篇博文。
深度检测
当两个表面重叠时,前面的模型会挡住后面的模型。比如这个例子,绘制了两个交叉的三角形( varray 和 carray 的长度变为 18,gl.drawArrays 最后一个参数变为 6)。为了简单,这个例子去掉了矩阵变换过程,直接向着色器传入 CCV 坐标。
顶点着色器给出了 6 个顶点的 gl_Position ,经过光栅化,片元着色器获得了 2X 个片元(假设 X 为每个三角形的像素个数),每个片元都离散的 x,y 坐标值,还有 z 值。x,y 坐标就是三角形在 Canvas 上的坐标,但如果有两个具有相同 x,y 坐标的片元同时出现,那么 WebGL 就会取 z 坐标值较小的那个片元。
在深度检测之前,必须在绘制前开启一个常量。否则,WebGL 就会按照在 varray 中定义的顺序绘制了,后面的会覆盖前面的。
gl.enable(gl.DEPTH_TEST);
实际上,WebGL 的逻辑是这样的:依次处理片元,如果渲染缓冲区(这里就是 Canvas 了)的那个与当前片元对应的像素还没有绘制时,就把片元的颜色画到渲染缓冲区对应像素里,同时把片元的 z 值缓存在另一个深度缓冲区的相同位置;如果当前缓冲区的对应像素已经绘制过了,就去查看深度缓冲区中对应位置的 z 值,如果当前片元 z 值小,就重绘,否则就放弃当前片元。
WebGL 的这套逻辑,对理解蒙版(后面会说到)有一些帮助。
顶点索引
gl.drawArrays()是按照顶点的顺序绘制的,而 gl.drawElements()可以令着色器以一个索引数组为顺序绘制顶点。比如这个例子。
这里画了两个三角形,但只用了 5 个顶点,有一个顶点被两个三角形共用。这时需要建立索引数组,数组的每个元素表示顶点的索引值。将数组填充至gl.ELEMENT_ARRAY
,然后调用
gl.drawElements()。
var iarray = new Uint8Array([0,1,2,2,3,4]);
var ibuffer = gl.createBuffer(gl.ARRAY_BUFFER, ibuffer);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, iarray, gl.STATIC_DRAW);
纹理
attribute 变量不仅可以传递顶点的坐标,还可以传递其他任何逐顶点的数据。比如 HelloTriangle 程
以上是关于WebGL 技术储备指南的主要内容,如果未能解决你的问题,请参考以下文章