最近正在接触WebGL,正好是个机会来重温一下OpenGL中的诸多概念和可编程渲染管线中的基本操作。只是来梳理下基础的东西,没有太多细节,就当是温习加概括。实际上,这也是一个机会,重新学习一下。
首先,什么是WebGL?它是一组在浏览器中使用的三维模型渲染接口,通过这组接口开发者可以直接操作显卡的硬件资源,从而开发出更真实、更丰富的渲染效果。从实现上来看,WebGL是OpenGL ES(OpenGL ES又是OpenGL的一个子集)的一个子集,是专门为HTML Canvas元素设计的一套渲染接口。从应用上来看,它变的越来越流行且被重视,因为基于云运算的Web应用正在成为趋势,由此也促使3D图形渲染从桌面软件到Web端的转移,于是WebGL的出现和发展,为这个目的提供了很好的技术支持。另外一个额外的好处是,基于Web的应用可以很快的嵌入移动客户端。
WebGL作为OpenGL的子集,也是一个有限状态机。
并且,系统创建时,会设置一组初始的状态。 WebGL(OpenGL)就是这样一组基于有限状态机的的软件接口。大部分的接口都是在操作系统的某个状态,比如当前使用的顶点属性数据,纹理对象,Shader等等,都是状态变量,一旦通过接口设置了它们的值,就会在下面的运行中一直起作用,直到被再一次改变。所以,WebGL(OpenGL)的接口可以大致分成这样几种类别,
WebGL(OpenGL)被设计成有限状态机是有其理由的,
WebGL (OpenGL) Context可以理解为一个对象,或者一个大的结构体,又或者是WebGL (OpenGL)实现的一个实例,它包含了所有的状态变量,提供了所有的接口 。
OpenGL早在1.0版本是没有对象概念的,那时的接口都是用来操作全局状态的。不过在接下来的版本里,OpenGL逐渐的加入了各种对象。比如,1.1引入了Texture Object,1.5引入了VBO,2.0引入了Shader,等等。 OpenGL对象就是一个OpenGL资源或者结构,它由一组状态变量表示。所有对象的操作都需要首先和当前活动的Context绑定,于是对象的状态就相应的映射为Context的状态,然后接下来对这些状态的改变都会保存到该对象中,并且使用这些状态的操作会直接从该对象中读取。这样的接口设计也正是OpenGL“状态机”理念的体现。 这里只看下WebGL中的对象资源,
从这一段开始,来看看要把3D模型渲染到网页中,都涉及了哪些WebGL的基本操作。在开始前,先确认浏览器 是否支持WebGL。 可以通过http://www.doesmybrowsersupportwebgl.com/ 来测试。
先创建一个HTML Canvas元素,作为WebGL渲染的画布。然后通过这个画布得到和它关联的WebGL context,
var canvas = document.getElementById("canvas_id");
gl = canvas.getContext("experimental-webgl");
使用“experimental-webgl”作为context type,是由其历史原因的,而且WebGL目前也是一直发展的状态。不过,如果浏览器同时也支持“webgl”字符串作为context type,那么这两者是完全一样的,会返回同一个context。 另外,当通过canvas得到context时候,浏览器应该做如下的操作,
模型数据传给GPU去渲染,需要设置相应的Buffer。常听到的两种用途的buffer,比如,Vertex Buffer Object和Index Buffer Object。简单来说就是,一个是用来传递所有顶点数据的,一个是用索引告诉WebGL哪些顶点在一起表示一个基本元素的(三角片,线或点)。基本步骤包括,
创建一个Buffer
vertexPositionBuffer = gl.createBuffer();
用模型的顶点数据,填充这个Buffer。任何Buffer操作的尝试,都需要先把它绑定到Context上,然后拷贝数据。
gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
WebGL需要用户提供Vertex和Fragment Shader。简单来说,
Vertex Shader,就是一段字符串表示的GLSL代码,它的输入是每个顶点的数据,经过代码处理,主要输出为标准化设备空间坐标(Normalized Device Coordinates)。一个最简单的示例,
<\script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}
</script>
Fragment Shader,也是一段字符串表示的GLSL代码,它作用在每一个像素上面,输入是Vertex Shader的输出但经过了光栅化处理的结果,然后经过代码处理,输出为每个像素的颜色值。一个最简单的示例,
<\script id="shader-fs" type="x-shader/x-fragment">
precision mediump float;
void main(void) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>
然后,使用以上的Shader涉及的WebGL调用包括,
创建Shader对象,设置Shader字符串格式的程序,然后编译。(可以想像这个过程就是,编译一个c程序模块)
fragmentshader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentshader, strfragmentshadersource);
gl.compileShader(fragmentshader);
vertextshader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexshader, strvertexshadersource);
gl.compileShader(vertexshader);
创建Program对象,关联上面的Shader到Program上,然后链接使用。(可以想象这个过程是,通过上面编译好的模块创建一个可执行程序。)
shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
使用创建好的Program,下面的WebGL调用会告诉context,使用这个program提供的shader程序。
gl.useProgram(shaderProgram);
设置好Program后,还需要启动Program里可以读取顶点数据的能力。
gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
上面传入的参数,以整数索引的方式指定了在Program里读取顶点坐标的变量,于是这个调用就是告诉context,这个program里读取顶点坐标的这个变量已经被启动,可以被program使用。
当顶点数据和Shader都准备好了,那么下一步就是要告诉Shader怎么使用顶点数据。
gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionBuffer);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,
vertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);
一样的,在操作buffer之前,要确定先绑定它到当前context中,然后通过vertexAttribPointer告诉Shader,索引为vertexPositionAttribute的变量从绑定buffer里以如下的格式读取数据,首先buffer的元素被解析为float类型,每个顶点对应了itemSize个元素,最后两个参数(0,0),表示从buffer的起点开始读数据,并且相邻两个顶点数据间没有间隔。最后,中间的那个参数表示如果当前buffer是以整形格式来存储的,但需要以float格式来访问,那么是否需要把数据标准化。如果需要,那么WebGL在读取前会把有符号整数映射到[-1,1]或者无符号整数映射到[0,1],如果不需要,那么直接读取这个整数作为浮点数来对待。简单来说,经过上面的设置后,在Shader程序里,顶点坐标就能正确的从这个buffer里取得。
到这里,大部分的数据已经设置好了,不过距离正确的渲染,还差点。下面给出了其他一些主要状态的设置。
设置viewport,这是告诉WebGL要渲染到给定画布的什么范围上,一般是充满整个画布。
gl.viewport(0, 0, width, height)
设置背景色,这是告诉WebGL渲染模型之前,先把画布涂成给定的颜色。
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
设置camera,这是告诉WebGL要渲染模型空间里的哪一部分到画布上。这里的空间就是由camera确定的视景体(frustum)来定义的。Camera可以分为透视投影(perspective)或者正交投影(orthographic),不管那种投影方式,最后都会给出一个投影矩阵,把模型投影到标准设备坐标系下(NDC),然后再被映射到画布对应的窗口坐标系下。一般的,这个矩阵会作为Vertex Shader的一个uniform 变量使用。
设置其他一些状态。好吧,其实要达到最终的渲染效果,还需要设置不少的状态值。不过,最基本的还需要打开深度测试,因为3D空间的物体映射到2D平面上时,是需要通过深度来确定正确的遮挡关系的。
gl.enable(gl.DEPTH_TEST);
最终,终于,到了实际调用渲染的步骤了。这就是状态机的使用方式,把该设置的都弄好了,WebGL才能调动GPU做正确的渲染。
如果只有顶点数据,那么渲染时调用
gl.drawArrays(gl.TRIANGLES, 0, numItems);
这个调用时告诉WebGL,用之前设置好的状态和数据,每三个顶点画一个三角片,从第0个开始一直到第numItems个。
如果还有索引数据(通过索引可以减少重复顶点),那么就使用
gl.drawelements(gl.TRIANGLES, numItems, GL_UNSIGNED_BYTE, offset);
类似的,每三个索引对应的顶点将组成一个三角片,一共有numItems个GL_UNSIGNED_BYTE类型的索引来使用,并且从offset指定的偏移开始。当然,使用索引数据也需要和定点数据差不多的操作,绑定到索引数据buffer上,然后指定如何解析。
到此为止,如果一切顺利,那么画布上就应该渲染出想要的图形和效果。另外需要注意的,上述的draw调用后,会马上在画布上展示效果,并且WebGL的帧缓存会被清空。如果要改变这以默认行为,可以在创建context的时候指定属性preserveDrawingBuffer为true。不过,这一改动可能会引起明显的性能下降。
真心感谢,这强大开放的网络世界,可以查阅各种资料和学习各种知识。
自己的一点感悟,当人们在接触一样新技术时候,定会感到很多的疑惑。而消除疑惑的一种很好的方式就是:了解其基本原理,弄清其体系结构,熟悉其常用业务流程。更进一步的,如果想成为专家,那就研读其使用手册。