目录
前言
初始化着色器的7个步骤
创建着色器对象(gl.createShader())
gl.createShader()规范
gl.deleteShader()规范
指定着色器对象的代码(gl.shaderSource())
gl.shaderSource()规范
编译着色器(gl.compileShader())
gl.compileShader()规范编辑
gl.getShaderParameter()规范
gl.getShaderInfoLog()规范
创建程序对象(gl.createProgram())
gl.createProgram()规范
为程序对象分配着色器对象(gl.attachShader())
gl.attachShader()规范
gl.detachShader()规范
连接程序对象(gl.linkProgram())
gl.getProgramPara-meters()规范
gl.getProgramInfoLog()规范
告知WebGL系统所使用的程序对象(gl.useProgram())
gl.usePorgram()规范
initShaders()函数的内部流程
initShaders()函数
createProgram()函数
loadShader()函数
本章来研究一下以前一直使用的辅助函数initShaders()。以前的所有程序都使用了这个函数,它隐藏了建立和初始化着色器的细节。但如果你确实很想知道WebGL原生API是如何将字符串形式的GLSL ES代码编译为显卡中运行的着色器程序,那么这一节的内容将大大满足你的好奇心。
initShaders()函数的作用是,编译GLSL ES代码,创建和初始化着色器供WebGL使用。具体地,分为以下7个步骤:
1. 创建着色器对象(gl.createShader())。
2. 向着色器对象中填充着色器程序的源代码(gl.shaderSource())。
3. 编译着色器(gl.compileShader())。
4. 创建程序对象(gl.createProgram())。
5..为程序对象分配着色器(gl.attachShader())。
6. 连接程序对象(gl.linkProgram())。
7. 使用程序对象(gl.useProgram())。
虽然每一步看上去都比较简单,但是放在一起显得复杂了,我们将逐条讨论。首先,你需要知道这里出现了两种对象:着色器对象(shader object)和程序对象(program object)。
着色器对象
着色器对象管理一个顶点着色器或一个片元着色器。每一个着色器都有一个着色器对象。
程序对象
程序对象是管理着色器对象的容器。WebGL中,一个程序对象必须包含一个顶点着色器和一个片元着色器。
着色器对象和程序对象的关系图
下面就来逐个讨论上述7个步骤。
所有的着色器对象都必须通过调用gl.createShader()来创建。
gl.createShader()函数根据传入的参数创建一个顶点着色器或者片元着色器。如果不再需要这个着色器,可以使用gl.deleteShader()函数来删除着色器。
注意,如果着色器对象还在使用(也就是说已经使用gl.attachShader()函数使之附加在了程序对象上,我们马上就会讨论该函数),那么gl.deleteShader()并不会立刻删除着色器,而是要等到程序对象不再使用该着色器后,才将其删除。
通过gl.shaderSource()函数向着色器指定GLSL ES源代码。在JavaScript程序中,源代码以字符串的形式存储
向着色器对象传入源代码之后,还需要对其进行编译才能够使用。GLSL ES语言和JavaScript不同而更接近C或C++,在使用之前需要编译成二进制的可执行格式,WebGL系统真正使用的是这种可执行格式。使用gl.compileShader()函数进行编译。注意,如果你通过调用gl.shaderSource(),用新的代码替换掉了着色器中旧的代码,WebGL系统中的用旧的代码编译出的可执行部分不会被自动替换,你需要手动地重新进行编译。
当调用gl.compileShader()函数时,如果着色器源代码中存在错误,那么就会出现编译错误。可以调用gl.getShaderParameter()函数来检查着色器的状态。
调用gl.getShaderParameter()并将参数pname指定为gl.COMPILE_STATUS,就可以检查着色器编译是否成功。
如果编译失败,gl.getShaderParameter()会返回false,WebGL系统会把编译错误的具体内容写入着色器的信息日志(information log),我们可以通过gl.getShaderInfoLog()来获取之。
虽然日志信息的具体格式依赖于浏览器对WebGL的实现,但大多数WebGL系统给出的错误信息都会包含代码出错行的行号。比如,如果你试图编译如下这样的一个着色器:
代码的第2行出错了(应该是gl_而不是gl.),Chrome浏览器给出的编译错误信息如图9.11所示(cuon-utils.js将错误信息字符串打印在控制台上):
可见,错误信息告诉我们:第2行的变量gl未被定义。
cuon-utils.js:88表示这条错误是在JavaScript文件cuon-utils.js的第88行,即定义initShaders()函数之处检测到的,该函数调用了gl.getShaderInfolog()函数。
如前所述,程序对象包含了顶点着色器和片元着色器,可以调用gl.createProgram()来创建程序对象。事实上,之前使用程序对象,gl.getAttribLocation()函数和gl.getUniformLocation()函数的第1个参数,就是这个程序对象。
类似地,可以使用gl.deleteProgram()函数来删除程序对象。
WebGL系统要运行起来,必须要有两个着色器:一个顶点着色器和一个片元着色器。可以使用gl.attachShader()函数为程序对象分配这两个着色器。
着色器在附给程序对象前,并不一定要为其指定代码或进行编译(也就是说,把空的着色器附给程序对象也是可以的)。类似地,可以使用gl.detachShader()函数来解除分配给程序对象的着色器。
在为程序对象分配了两个着色器对象后,还需要将(顶点着色器和片元)着色器连接起来。使用gl.linkProgram()函数来进行这一步操作。
程序对象进行着色器连接操作,目的是保证:(1)顶点着色器和片元着色器的varying变量同名同类型,且一一对应;(2)顶点着色器对每个varying变量赋了值;(3)顶点着色器和片元着色器中的同名uniform变量也是同类型的(无需一一对应,即某些uniform变量可以出现在一个着色器中而不出现在另一个中);(4)着色器中的attribute变量、uniform变量和varying变量的个数没有超过着色器的上限,等等。
在着色器连接之后,应当检查是否连接成功。通过调用gl.getProgramPara-meters()函数来实现。
如果程序已经成功连接,我们就得到了一个二进制的可执行模块供WebGL系统使用。如果连接失败了,也可以通过调用gl.getProgramInfoLog()从信息日志中获取连接出错信息。
最后,通过调用gl.usePorgram()告知WebGL系统绘制时使用哪个程序对象。
这个函数的存在使得WebGL具有了一个强大的特性,那就是在绘制前准备多个程序对象,然后在绘制的时候根据需要切换程序对象。
这样,建立和初始化着色器的任务就算完成了。如你所见,initShaders()函数隐藏了大量的细节,我们可以放心地使用该函数来创建和初始化着色器,而不必考虑这些细节。本质上,在该函数顺利执行后,顶点着色器和片元着色器就已经就位了,只需要调用gl.drawArrays()或gl.drawElements()来使整个WebGL系统运行起来。
现在,你对上述诸多WebGL原生API函数已经有了不错的理解,下面来看一下cuon-utils.js中initShaders()函数的内部流程。
initShaders()函数将调用createProgram()函数,后者负责创建一个连接好的程序对象;createProgram()函数则又会调用loadShader()函数,后者负责创建一个编译好的着色器对象;这3个函数被依次定义在cuon-utils.js文件中。initShaders()函数定义在该文件的顶部,如下所示
function initShaders(gl, vshader, fshader) {
// 创建程序对象
var program = createProgram(gl, vshader, fshader);
if (!program) {
console.log('Failed to create program');
return false;
}
gl.useProgram(program);
gl.program = program;
return true;
}
initShaders()函数本身很简单,首先调用createProgram()函数创建一个连接好的程序对象,然后告诉WebGL系统来使用这个程序对象,最后将程序对象设为gl对象的program属性。
createProgram()函数通过调用loadShader()函数,创建顶点着色器和片元着色器的着色器对象(第31和32行)。loadShader()函数返回的着色器对象已经指定过源码并已经成功编译了。
createProgram()函数自己负责创建程序对象(第38行),然后将前面创建的顶点着色器和片元着色器分配给程序对象(第44和45行)。
接着,该函数连接程序对象(第48行),并检查是否连接成功(第51行)。如果连接成功,就返回程序对象(第60行)。
最后看一下loadShader()函数,如下所示。该函数被createProgram()函数调用。
loadShader()函数首先创建了一个着色器对象(第72行),然后为该着色器对象指定源代码(第79行),并进行编译(第82行),接着检查编译是否成功(第85行),如果成功编译,没有出错,就返回着色器对象。