本章开始学习shader的使用,以前大家常使用OpenGL固定管线来做一些程序,shader相对来说使用较少,而现代gpu编程,shader应用少不了,虽然使用shader编程,代码多一点,但是却更灵活。
OpenGL的shader管线框图如下,注意,少了tessellation的部分,而OpenGL中的tessellation和d3d11中的是一样的,只是每个阶段名字叫的不一样而已,后面教程中会提到。
第一步是vertex processor(vs),第二步是geometry processor(gs),第三步是clip操作(还应该包括PA, primitive assembly),最后是光栅化以及fragment processor(ps)。
可以看下D3D11教程的介绍,来和opengl的管线比较一下,其实除了名字,本质都是一样的,因为都是相同的硬件。
<a href="http://www.cnblogs.com/mikewolf2002/archive/2012/03/24/2415141.html">http://www.cnblogs.com/mikewolf2002/archive/2012/03/24/2415141.html</a>
1. 顶点处理(vertex processor),该阶段主要是对每个顶点执行shader操作,顶点数量在draw函数中指定。顶点shader中没有任何体元语义的内容,仅是针对顶点的操作,比如坐标空间变化,纹理坐标变化等等。每个顶点都必须执行顶点shader,不能跳过该阶段,执行完顶点shader后,顶点进入下一个阶段。
2. 几何处理(geometry processor),在该阶段,顶点的邻接关系以及体元语义都被传入shader,在几何shader中,不仅仅处理顶点本身,还要考虑很多附加的信息。几何shader甚至能改变输出体元语义类型,比如输入体元是一系列单独的点(point list体元语义),而输出体元则是三角形或者两个三角形组成的四边形等等, 甚至我们还能在几何shader中输入多个顶点,对于每个顶点输出不同语义的体元。
3. clipper阶段,或者称作裁剪阶段,这是一个固定管线模块,它将用裁剪空间的6个面对体元进行裁剪操作,裁剪空间外的部分将会被移去,这时可能会产生新的顶点和新的三角形,用户也可以定义自己的裁剪平面进行裁剪操作,裁剪后的三角形将会被传到光栅化阶段。[注:在clipper之前,会有PA阶段,就是顶点shader或几何shader处理过的顶点被重新装配成三角形]
4. 光栅化阶段和片元操作阶段,clipper后的三角形会先被光栅化,产生很多的fragment(片元),接着执行片元shader,在片元shader中,可能会装入纹理,从而产生最终的像素颜色。 【fragment可以理解为带sample、深度信息的像素】
不同于D3D11的管线,OpenGL的顶点shader、几何shader以及片元shader都是可选的,如果没有指定它们,则会执行缺省的功能。
shader管理类似于创建C/C++程序,首先写shader代码,把代码放在一个文本文件或者一个字符串中,然后编译该shader代码,把编译后的shader代码放到各个shader对象中,接着把shader对象链接到程序中,最后把shader送到GPU中去。
链接shader时候,driver可能会优化shader代码,比如你的顶点shader可能会输出一个法向的参数,但后面的片元shader并没有使用它,可能driver就会进行优化操作,去掉该参数的输出,从而加快顶点shader的执行。
在opengl中编写shader程序,主要有以下步骤:
首先创建一个shader程序对象,然后分别创建相应的shader对象,比如顶点shader对象, 片元shader对象,并把它们连接到shader程序对象。在创建shader对象时候,需要装入shader源码,编译shader,链接到shader程序对象几个步骤。
下面是主要的代码:
<code>GLuint ShaderProgram = glCreateProgram();</code>
我们首先创建一个shader程序对象,并把所有shader都链接到这个shader程序对象。
<code>GLuint ShaderObj = glCreateShader(ShaderType);</code>
我们用上面的函数创建2个shader对象,其中一个shader类型是GL_VERTEX_SHADER,另一个是 GL_FRAGMENT_SHADER,指定shader源代码和编译shader的函数对于这两种类型的shader来说是一样的。
<code>const GLchar* p[1]; p[0] = pShaderText; GLint Lengths[1]; Lengths[0]= strlen(pShaderText); glShaderSource(ShaderObj, 1, p, Lengths);</code>
在编译shader之前,我们首先要通过glShaderSource函数指定shader的源代码,该函数可以通过字符指针数组(实际上是二维指针const GLchar ** ,每个元素都是一个字符指针,指向相应的源代码)指定多个shader源代码。该函数第一个参数是shader对象,第二个参数是个整数,指定字符指针数组中元素的个数,即多少个源代码,第三个参数为字符指针数组地址,第四个参数是个整数指针数组,和shader字符指针数组对应,它指定每个shader源代码的字符数量。为了使程序简单,我们在glShaderSource中,字符指针数组元素只有一个,即只有一个slot来放源代码。
<code>glCompileShader(ShaderObj);</code>
上面这个函数用来编译shader对象。
<code>GLint success; glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &success); if (!success) { GLchar InfoLog[1024]; glGetShaderInfoLog(ShaderObj, sizeof(InfoLog), NULL, InfoLog); fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog); }</code>
通常编译shader的时候可能会碰到各种错误,这时候我们可以通过上面的代码得到编译状态并打印出错误信息,便于调试shader代码。
<code>glAttachShader(ShaderProgram, ShaderObj);</code>
最终把编译的shader对象和shader程序对象绑定起来。
<code>glLinkProgram(ShaderProgram);</code>
绑定之后是链接操作,链接操作之后,我们可以通过函数glDeleteShader释放中间shader对象。
<code>glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &Success); if (Success == 0) { glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog); fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog); }</code>
通过上面的代码,我们来检测shader链接时候是否有错误。注意检测shader链接错误的代码和检测shader编译的代码有些不同(最大的不同就是使用不一样的函数)。
<code>glValidateProgram(ShaderProgram);</code>
验证shader程序对象的有效性。这儿可能大家有点疑惑:既然前面已经链接成功,干嘛还要再次在验证有效性?其实它们之间是有点区别的,链接检测基于shader绑定,而验证有效性则是验证程序能否在现在的管线上执行。在复杂的应用中,有多个shader,多个状态,在每个draw之前,最好都做一次验证有效性的操作。在我们程序中,只做了一次验证。当然,为了减少验证的开销,我们可以只在debug阶段进行验证操作,而最终的release阶段不需要这些操作,从而减少程序开销,提高性能。
<code>glUseProgram(ShaderProgram);</code>
最终,我们通过上面这个函数把链接好的程序对象送到shader管线。这个shader将对随后的所有draw有效,除非你用另一个shader程序对象代替它或者通过设置glUseProgram(NULL)禁止它(此时会打开固定管线功能)。如果你只指定一个shader对象,比如只有vertex shader,那么fragment shader则使用固定管线的功能。
前面我们已经了解了shader管理的功能,下面看看顶点和片元shader的代码: (分别包含在 'pVS' 和 'pFS' 变量中,在我修改的代码中,会分别放在color.vs和color.ps中,并通过一个gclFile类来读写shader代码文件,这样便于调试shader代码)。
<code>#version 400</code>
这告诉编译器使用4.0版本的GLSL,如果编译器不支持,则会产生一个错误信息。
<code>layout (location = 0) in vec3 Position;</code>
这个声明指定顶点shader中顶点的属性值是一个三维坐标向量,它的属性名字是Position,对于GPU中每个执行顶点shader的顶点,该属性值都有效,即意味着顶点buffer中的值被解释为位置。 layout (location = 0)在顶点buffer和顶点属性名字之间创建了一个绑定关系。
location指定该属性在顶点buffer中的位置,特别是顶点包含多个属性时候(比如位置、法向、纹理坐标等等),我们必须要让编译器知道,顶点的那个属性和顶点buffer中的那个位置对应起来,通常有2种方法实现这个,一个就像我们在shader中指定的location = 0,在这种情况下,在cpp源代码中我们通过硬编码的方式,指定shader属性,比如glVertexAttributePointer(0),另外一种情况下,我们可以在shader代码中简单声明in vec3 Position,在应用程序中,通过函数glGetAttribLocation在运行时查找属性位置,这时我们要利用glGetAttribLocation的返回值,而不是硬编码。在本篇教程程序中,我们采用第一种方式,对于复杂的应用,可以采用第二种方式,让编译器决定属性索引,然后在运行时查询它,这样可以方便把多个shader源文件集成在一起,而不必用它们匹配我们自己定义的缓冲layout。
<code>void main()</code>
我们能够把多个shader对象链接在一起形成最终的shader,但是对每种类型shader(VS,GS,FS)来说,代码中只能有一个main函数,这个函数作为该shader的执行入口点,例如,我们能够创建一个shader库文件,其中包含计算光照的一些函数,然后把它链接到没有main函数的shader中去。
<code>gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);</code>
这儿,我们通过硬编码来改变顶点位置,x和y的值减半,z值保持不变。'gl_Position'是一个内置的特殊变量,用来存储顶点在齐次裁剪空间的坐标。光栅化模块将会查找这个变量的值,用它作为屏幕空间的值(其实不准确,视口变换在PA里面做的,所以vs里面的点并不是对应屏幕空间)。x和y减半意味着渲染出的三角形是上篇教程中三角形的1/4,注意:这儿我们设置w=1.0,这是很重要的,否则的话,渲染结果可能并不正确。
把物体从三维投影到二维,需要两个阶段:第一个阶段,把所有的顶点乘以投影矩阵,之后GPU在光栅化前会执行透视除法,就是x、y、z分量的值等于各自的值除以w的值,而w的值为1,透视除法是GPU的固定模块完成,通常是在PA里面。本教程中,我们直接设置w=1.0,透视除法后的值和没除前一样。
如果程序运行正常,将会有三个顶点(-0.5, -0.5), (0.5, -0.5) 和(0.0, 0.5)到达光栅化模块。本程序中clipper不需要做任何事情,因为我们所有的顶点都是在归一化的裁剪空间盒子内,之后,顶点的值被映射到屏幕空间(视口变化操作),并在光栅化中模块对三角形体元执行光栅化操作,光栅化后的每个片元随后会执行片元shader操作。
下面的shader是片元shader的代码:
<code>out vec4 FragColor;</code>
片元shader的功能就是输出片元最终的颜色值,在片元shader里面,也可以放弃一些片元(像素),或者改变它们的z值(注意:这将影响硬件early z的功能)。输出颜色最终通过宣布的out变量FragColor来完成,FragColor向量的四个分量分别表示R,G,B,A值,该值最终被写入framebuffer。
<code>FragColor = vec4(1.0, 0.0, 0.0, 1.0);</code>
上篇教程中,没有shader操作,最终屏幕画的是默认的白色,现在我们指定红色,最终将会在屏幕上画一个红色的三角形。
程序执行后效果如下: