最后,本章来研究一下以前一直使用的辅助函数 initShaders()。以前的所有程度都使用了这个函数,它隐藏了建立和初始化着色器的细节。本书故意将这一部分内容留到最后,是为了确保你在学习 initShaders()函数中的复杂细节时,对 WebGL 已经有了比较深入的交接。掌握这部分内容并不是必须的,直接使用 initShaders()函数也能够编写处相当不错的 WebGL 程序,但如果你确实很想知道 WebGL 原生 API 是如何将字符串形式的 GLSL ES 代码编译为显卡中运行的着色器程序,那么这一节的内容将大大满足你的好奇心。
initShaders()函数的作用是,编译 GLSL ES 代码,创建和初始化着色器供 WebGL 使用。具体地,分为以下7个步骤:
- 创建着色器对象(gl.createShader())
- 向着色器对象中填充着色器程序的源代码(gl.shaderSource())
- 编译着色器(gl.compileShader())
- 创建程序对象(gl.createProgram())
- 为程序对象分配着色器(gl.attachShader())
- 连接程序对象(gl.linkProgram())
- 使用程序对象(gl.useProgram())
虽然每一步看上去都比较简单,但放在一起显得复杂了,我们将逐条讨论。首先,你需要知道这里出现了两种对象:着色器对象和程序对象。
着色器对象:着色器对象管理一个顶点着色器或一个片元着色器。每一个着色器都有一个着色器对象。
程序对象:程序对象是管理着色器对象的容器。WebGL 中,一个程序对象必须包含一个顶点着色器和一个片元着色器。
着色器对象和程序对象间的关系:
下面来逐个讨论上述7个步骤。
创建着色器对象(gl.createShader())
所有的着色器对象都必须通过调用 gl.createShader()来创建。
gl.createShader()函数根据传入的参数创建一个顶点着色器或者片元着色器。如果不再需要这个着色器,可以调用 gl.deleteShader()函数来删除着色器。
注意,如果着色器对象还在使用,那么 gl.deleteShader()并不会立刻删除着色器,而是要等到程序对象不再使用该着色器后,才将其删除。
指定着色器对象的代码(gk.shaderSource())
通过 gl.shaderSource()函数向着色器制动 GLSL ES 源代码。在 JS 程序中,源代码以字符串的形式存储。
编译着色器(gl.compileShader())
向着色器对象传入源代码之后,还需要对其进行编译才能够使用。GLSL ES 语言和 JS 不同而更接近 C 或 C++,在使用之前需要编译成二进制的可执行格式,WebGL 系统真正使用的是这种可执行格式。使用 gl.compileShader()函数进行编译。注意,如果你通过调用 gl.shaderSource(),用新的代码替换掉了着色器中旧的代码,WebGL 系统中的用旧的代码编译处可执行部分不会被自动替换,你需要手动地重新进行编译。
当调用 gl.compileShader()函数时,如果着色器代码中存在错误,那么就会出现编译错误。可以调用 gl.getShaderParameter()函数来检查着色器的状态。
调用 gl.getShaderParameter()并将参数 pname 指定为 gl.COMPILE_STATUS,就可以检查着色器编译是否成功。
如果编译失败,gl.getShaderParameter()会返回 false,WebGL 系统会把编译错误的具体内容写入着色器的信息日志,我们可以通过 gl.getShaderInfoLog()来获取之。
虽然日志信息的具体格式依赖于浏览器对 WebGL 的实现,但大多数 WebGL 系统给出的错误信息都会包含代码出错航的嗲吗。比如,如果你试图编译如下这样一个着色器:
代码的第2行出错了,Chrome 浏览器给出的编译错误信息如下图所示:
可见,错误信息告诉我们:第2行的变量 gl 未被定义。
创建程序对象(gl.createProgram())
如前所述,程序对象包含了顶点着色器和片元着色器,可以调用 gl.createProgram()来创建程序对象。事实上,之前使用程序对象,gl.getAttribLocation()函数和 gl.getUniformLocation()函数的第1个参数,就是这个程序对象。
类似地,可以使用 gl.deleteProgram()函数来删除程序对象。
一旦程序对象被创建之后,需要向程序附上两个着色器。
为程序对象分配着色器对象
WebGL 系统要运行起来,必须要有两个着色器:一个顶点着色器和一个片元着色器。可以使用 gl.attachShader()函数为程序对象分配这两个着色器。
着色器在附给程序对象前,并不一定要为其指定代码或进行编译。也就是说,把空的着色器赋给程序对象也是可以的。类似的,可以使用 gl.derachShader()函数来解除分配给程序对象的着色器。
连接程序对象(gl.linkProgram())
在为程序对象分配了两个着色器对象后,还需要将着色器连接起来。使用 gl.linkProgram()函数来进行这一步操作。
程序对象进行着色器连接操作,目的是保证:
- 顶点着色器和片元着色器的 varying 变量同名同类型,且一一对应;
- 顶点着色器对每个varying 变量赋了值;
- 顶点着色器和片元着色器的同名uniform 变量也是同类型的,无需一一对应,即某些 uniform 变量可以出现在一个着色器中而不出现在另一个中;
- 着色器中的 attribute 变量、uniform 变量和 varying 变量的个数没有超过着色器的上学,等等。
在着色器连接之后,应当检查是否连接成功。通过调用 gl.getProgramParameters()函数来实现。
如果程序已经成功连接,我们就得到了一个二进制的可执行模块供 WebGL 系统使用。如果连接失败了,也可以通过调用 gl.getProgramInfoLog()从信息日志中获取连接错误的信息。
告知 WebGL 系统所使用的程序对象(gl.useProgram())
最后,通过调用 gl.useProgram()告知 WebGL 系统绘制时使用哪个程序对象。
这个函数的存在使得 WebGL 具有了一个强大的特性,那就是在会之前准备多个程序对象,然后在绘制的时候根据需要切换程序对象。
这样,建立和初始化着色器的任务就算完成了。如你所见,initShaders()函数隐藏了大量的细节,我们可以放心地使用该函数来创建和初始化着色器,而不必考虑这些细节。本质上,在该函数顺利执行后,顶点着色器和偏远着色器就已经就为了,只需要调用 gl.drawArryas()或 gl.drawElements()来使整个 WebGL 系统运行起来。
现在,你对上述诸多 WebGL 原生 API 函数已经有了不错的理解。下面来看以下 cuon-utils.js 中 initShaders()函数的内部流程。
initShaders()函数的内部流程
initShaders() 函数将调用 createProgram()函数,后者负责创建一个连接好的程序对象;createProgram()函数则又会调用 loadShader()函数,后者负责创建一个编译好的着色器对象;这3个函数被一次定义在 cuon-utils.js 文件中。initShaders() 函数定义在该文件的顶部,注意该文件中每个函数前面的注释是按照 JavaDoc 的格式编写,它们可以用来自动化地生成文档。
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 属性。
function createProgram(gl, vshader, fshader) {
// Create shader object
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
}
// Create a program object
var program = gl.createProgram();
if (!program) {
return null;
}
// Attach the shader objects
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// Link the program object
gl.linkProgram(program);
// Check the result of linking
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log('Failed to link program: ' + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
return program;
}
createProgram()函数通过调用 loadShader()函数,创建顶点着色器和片元着色器的着色器对象。loadShader()函数返回的着色器对象已经制定过源码并已经成功编译了。
createProgram()函数自己负责创建程序对象,然后将前面创建的顶点着色器和偏远着色器分配给程序对象。
接着,该函数连接程序对象,并检查是否连接成功。如果连接成功,就会返回程序对象。
最后来看一下 loadShader()函数:
function loadShader(gl, type, source) {
// Create shader object
var shader = gl.createShader(type);
if (shader == null) {
console.log('unable to create shader');
return null;
}
// Set the shader program
gl.shaderSource(shader, source);
// Compile the shader
gl.compileShader(shader);
// Check the result of compilation
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
console.log('Failed to compile shader: ' + error);
gl.deleteShader(shader);
return null;
}
return shader;
}
loadShader()函数首先创建了一个着色器对象,然后为该着色器对象指定源代码,并进行编译,接着检查编译是否成功,如果成功编译,没有出错,就返回着色器对象。