本文为《WebGL编程指南》第九章下半部分读书笔记
总目录链接:https://blog.csdn.net/floating_heart/article/details/124001572
着色器(shader)和程序(program)对象:initShaders()
扮演的角色
在此之前,我们一直使用书中封装的initShaders()
代替我们执行着色器和着色器程序对象的初始化,在这里,基于之前的知识,我们已经能够理解initShaders()
每一步的作用了,在笔者看来,其与缓冲区存储数据分配变量有异曲同工之妙。本节,我们结合书中的内容和函数源码,逐渐了解过程的细节。
initShaders()
函数的作用是,编译GLSL ES代码,创建和初始化着色器供WebGL使用。具体分为以下7个步骤:
- 创建着色器对象(gl.createShader())
- 向着色器对象中填充着色器程序的源代码(gl.shaderSource())
- 编译着色器(gl.compileShader())
- 创建程序对象(gl.createProgram())
- 为程序对象分配着色器(gl.attachShader())
- 连接程序对象(gl.linkProgram())
- 使用程序对象(gl.useProgram())
在详细介绍前,需要了解着色器对象(shader object)和程序对象(program object)。
**着色器对象:**着色器对象管理一个顶点着色器或一个片元着色器。每一个着色器都有一个着色器对象。
**程序对象:**程序对象是管理着色器对象的容器。WebGL中,一个程序对象必须包含一个顶点着色器和一个片元着色器。
二者关系入下图所示:
第一步:创建着色器对象(gl.createShader())
使用gl.createShader()方法创建着色器对象,该函数规范如下:
gl.createShader(type):
创建由type指定的着色器对象。
参数:
type: 指定创建着色器的类型,gl.VERTEX_SHADER表示顶点着色器,gl.FRAGMENT_SHADER表示片元着色器。
返回值:
non-null: 创建的着色器
null: 创建失败
错误:
INVALID_ENUM: type参数既不是gl.VERTEX_SHADER也不是gl.FRAGMENT_SHADER
gl.createShader()函数根据传入的参数创建一个顶点着色器或片元着色器。类似的,可以使用gl.deleteShader()函数来删除着色器。
gl.deleteShader(shader):
删除shader指定的着色器对象。
参数:
shader: 待删除的着色器对象。
返回值: 无
错误: 无
如果着色器对象还在使用,那么gl.deleteShader()并不会立刻删除着色器,而是等到程序对象不再使用该着色器后,才将其删除。
第二步:指定着色器对象的代码(gl.shaderSource())
通过gl.shaderSource()
函数向着色器指定GLSL ES源代码。在JavaScript程序中,源代码以字符串的形式存储。
gl.shaderSource(shader, source):
将source指定的字符串形式的代码传入shader指定的着色器。如果之前已经向shader传入过代码了,旧的代码将会被替换掉。
参数:
shader: 指定需要传入代码的着色器对象。
source: 指定字符串形式的代码。
返回值: 无
错误: 无
第三步:编译着色器(gl.compileShader())
GLSL ES语言更接近于C或C++,在使用前需要编译成二进制的可执行文件,WebGL系统真正使用的是这种可执行格式。
gl.compileShader(shader):
编译shader指定的着色器中的源代码。
参数:
shader: 待编译的着色器。
返回值: 无
错误: 无
当调用gl.compileshader()
函数时,如果着色器源代码中存在错误,那么就会出现编译错误。可以调用gl.getShaderParameter ()
函数来检查着色器的状态。
**gl.getShaderParameter **(shader, pname):
获取shader指定的着色器中,pname指定的参数信息。
参数:
shader: 指定待获取参数的着色器。
pname: 指定待获取参数的类型,可以是gl.SHADER_TYPE、gl.DELETE_STATUS或者gl.COMPILE_STATUS
返回值:
根据pname的不同,返回不同的值
gl.SHADER_TYPE: 返回是顶点着色器(gl.VERTEX_SHADER)还是片元着色器(gl.FRAGMENT_SHADER)
gl.DELETE_STATUS: 返回着色器是否被删除成功(true或false)
gl.COMPILE_STATUS: 返回着色器是否被编译成功(true或false)
错误:
INVALID_ENUM: pname的值无效
调用gl. getShaderParameter ()
并将参数pname
指定为gl.COMPILE_STATUS
,就可以检查着色器编译是否成功。
如果编译失败,gl.getShaderParameter ()
会返回false
,WebGL系统会把编译错误的具体内容写入着色器的信息日志(information log),我们可以通过 gl.getShaderlnfoLog ()
来获取之。
gl.getShaderlnfoLog(shader):
获取shader指定的着色器的信息日志。
参数:
shader: 指定待获取信息日志的着色器。
返回值:
non-null: 包含日志信息的字符串
null: 没有编译错误
错误: 无
日志信息的具体格式依赖于浏览器对WebGL的实现,大多数WebGL系统给出的错误信息都会包含代码出错行的行号。比如我们常见的:
Failed to compile shader: ERROR: 0:4: 'uniformmat4' : syntax error
代表第四行’uniformmat4’出现问题。通过上面的介绍我们能够了解,日志是记录在着色器中的,只看日志不能区分是哪个着色器出现问题(需要知道日志属于哪个着色器)。
第四步:创建程序对象(gl.createProgram())
程序对象包含了顶点着色器和片元着色器,我们之前使用的获取存储地址的函数gl.getAttribLocation()
和gl.getUniformLocation()
的第一个参数就是程序对象。WebGL中使用gl.createProgram()
创建程序对象,函数规范如下:
gl.createProgram():
创建程序对象。
参数: 无
返回值:
non-null: 新创建的程序对象
null: 创建失败
错误: 无
类似地,可以使用gl.deleteProgram()
函数来删除程序对象
gl.deleteProgram(program):
删除program指定的程序对象,如果该程序对象正在被使用,则不立即删除,而是等它不再被使用后再删除。
参数:
program: 指定待删除的程序对象。
返回值: 无
错误: 无
第五步:为程序对象分配着色器(gl.attachShader())
WebGL系统要运行起来,必须要有两个着色器:一个顶点着色器和一个片元着色器。gl.attachShader()
将两个着色器分配给程序对象。
gl.attachShader(program, shader):
将shader指定的着色器对象分配给program指定的程序对象。
参数:
program: 指定程序对象
shader: 指定着色器对象
返回值: 无
错误:
INVALID_OPERATION: shader已经被分配给了program
着色器在附给程序对象前,并不一定要为其指定代码或进行编译(也就是说,把空的着色器附给程序对象也是可以的)。
类似地,可以使用gl.deleteShader ()
函数来解除分配给程序对象的着色器。
gl.detachShader(program, shader):
取消shader指定的着色器对象对program指定的程序对象的分配。
参数:
program: 指定程序对象
shader: 指定着色器对象
返回值: 无
错误:
INVALID_OPERATION: shader没有被分配给program
第六步:连接程序对象(gl.linkProgram())
在为程序对象分配了两个着色器对象后,还需要将顶点着色器和片元着色器连接起来。使用gl.linkProgram ()
函数来进行这一步操作。
gl.linkProgram(program):
连接program指定的程序对象中的着色器。
参数:
program: 指定程序对象
返回值: 无
错误: 无
程序对象对着色器进行连接,是为了:
- 顶点着色器和片元着色器的varying变量同名同类型,且一一对应;
- 顶点着色器对每个varying变量赋了值;
- 顶点着色器和片元着色器中的同名uniform变量也是同类型的(无需一一对应);
- 着色器中的attribute变量、uniform变量和varying变量的个数没有超过着色器的上限
在着色器连接之后,应当检查是否连接成功。
与着色器部分类似,通过调用gl.getProgramParameter()
函数来实现。
gl.getProgramParameter (program, pname):
获取program指定的程序对象中pname指定的参数信息。
参数:
program: 指定的程序对象。
pname: 指定待获取参数的类型,可以是g.DELETE_STATUS、 gl.LINK_STATUS、 gl.VALIDATE_STATUS、gl.ATTACHED_SHADERS、gl.ACTIVE_ATTRIBUTES或gl.ACTIVE_UNIFORMS
返回值:
根据pname的不同,返回不同的值
gl.DELETE_STATUS: 程序是否已被删除(true或false)
gl.LINK_STATUS: 程序是否已经成功连接(true或false)
gl.VALIDATE_STATUS: 程序是否已经通过验证(true或false)
gl.ATTACHED_SHADERS: 已被分配给程序的着色器数量
gl.ACTIVE_ATTRIBUTES: 顶点着色器中attribute变量的数量
gl.ACTIVE_UNIFORMS: 程序中uniform变量的数量
错误:
INVALID_ENUM: pname的值无效
如果程序已经成功连接,我们就得到了一个二进制的可执行模块供WebGL系统使用。如果连接失败了,也可以通过调用gl.getProgramlnfoLog()
从信息日志中获取连接出错信息。
gl.getProgramlnfoLog(program):
连接program指定的程序对象的信息日志。
参数:
program: 指定待获取信息日志的程序对象
返回值:
包含日志信息的字符串
错误: 无
程序对象即使连接成功了,也有可能运行失败,比如没有为取样器分配纹理单元。这些错误是在运行阶段而不是连接阶段产生的。在运行阶段进行错误检查的性能开销很大,所以通常只在调试程序时这样做。
第七步:使用程序对象(gl.useProgram())
或者说,告知WebGL系统所使用的程序对象。
经过之前的步骤,完整的程序对象已经准备好,最后,通过调用gl.useProgram()
告知WebGL系统绘制时使用哪个程序对象。
gl.useProgram(program):
告知WebGL系统绘制时使用program指定的程序对象。
参数:
program: 指定待使用的程序对象
返回值: 无
错误: 无
这个函数的存在使得WebGL具有了一个强大的特性,那就是在绘制前准备多个程序对象,然后在绘制的时候根据需要切换程序对象。
initShaders()函数的内部流程
我们了解了着色器和程序对象初始化的流程,本节看一下initShaders()
的组织方式。
initShaders ()
函数将调用createProgram ()
函数,后者负责创建一个连接好的程序对象;createProgram ()
函数则又会调用loadShader()
函数,后者负责创建一个编译好的着色器对象。这3个函数被依次定义在cuon-utils. js
文件中。
下面复制源码给出分析。
initShaders()
的主函数如下。
首先通过createProgram ()
创建连接好的程序对象,然后(告知WebGL)使用程序对象。将程序对象挂载在上下文中方便后续调用。
/**
* Create a program object and make current
* @param gl GL context
* @param vshader a vertex shader program (string)
* @param fshader a fragment shader program (string)
* @return true, if the program object was created and successfully made current
*/
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;
}
createProgram ()
函数内容如下
通过loadShader()
获得编译好的顶点着色器和片元着色器;创建程序对象,将两个着色器分配到程序对象中;连接程序对象(内部着色器):连接失败的话,打印程序对象信息日志,删除程序对象和两个着色器(删除顺序符合逻辑,delete操作立即生效),返回null;否则返回创建的程序对象。
/**
* Create the linked program object
* @param gl GL context
* @param vshader a vertex shader program (string)
* @param fshader a fragment shader program (string)
* @return created program object, or null if the creation has failed
*/
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;
}
loadShader()
函数如下
创建着色器;编译着色器;获取编译信息;编译失败时,打印日志信息,删除着色器,返回null;否则返回创建的着色器。
/**
* Create a shader object
* @param gl GL context
* @param type the type of the shader object to be created
* @param source shader program (string)
* @return created shader object, or null if the creation has failed.
*/
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;
}
流程总结
最后,用流程图的形式,展示着色器和程序对象初始化的过程,并与initShaders()
中的内容进行对照
https://www.processon.com/view/link/6264cce91e085332a3b7fae6
如果了解了着色器和程序对象的结构(本节开始所述),这样的初始化流程还是比较容易理解的。
着色器和程序对象的初始化,和缓冲区相关操作类似,同样是创建、加载数据、分配。不同的是,此处着色器需要编译和检查编译结果、程序对象需要连接和检查连接结果。WebGL系统的设计风格十分统一。