这篇文章里讲一下在GLSL如何使用函数和子程序(subroutines)。
GLSL支持函数,它们的语法结构和C很相似。但是调用约定会有所不同。下面,我们以一个普通的ADS(ambient,diffuse,specular)shader为例,熟悉一下GLSL中函数的用法。
Vertex Shader:
#version 400 layout (location = 0)in vec3 VertexPosition; layout (location = 1)in vec3 VertexNormal; out vec3LightIntensity; struct LightInfo { vec4 Position; // Light position in eyecoords. vec3 La; // Ambient light intensity vec3 Ld; // Diffuse light intensity vec3 Ls; // Specular light intensity }; uniform LightInfoLight; struct MaterialInfo { vec3 Ka; // Ambient reflectivity vec3 Kd; // Diffuse reflectivity vec3 Ks; // Specular reflectivity float Shininess; // Specular shininessfactor }; uniform MaterialInfoMaterial; uniform mat4ModelViewMatrix; uniform mat3NormalMatrix; uniform mat4ProjectionMatrix; uniform mat4 MVP; void getEyeSpace( outvec3 norm, out vec4 position ) { norm = normalize( NormalMatrix *VertexNormal); position = ModelViewMatrix *vec4(VertexPosition,1.0); } vec3 phongModel( vec4position, vec3 norm ) { vec3 s = normalize(vec3(Light.Position -position)); vec3 v = normalize(-position.xyz); vec3 r = reflect( -s, norm ); vec3 ambient = Light.La * Material.Ka; float sDotN = max( dot(s,norm), 0.0 ); vec3 diffuse = Light.Ld * Material.Kd *sDotN; vec3 spec = vec3(0.0); if( sDotN > 0.0 ) spec = Light.Ls * Material.Ks * pow( max( dot(r,v), 0.0 ),Material.Shininess ); return ambient + diffuse + spec; } void main() { vec3 eyeNorm; vec4 eyePosition; // Get the position and normal in eye space getEyeSpace(eyeNorm, eyePosition); // Evaluate the lighting equation. LightIntensity = phongModel( eyePosition,eyeNorm ); gl_Position = MVP * vec4(VertexPosition,1.0); }
上面的shader略微有点长……没事,我们一点一点来看。
layout (location = 0)in vec3 VertexPosition; layout (location = 1)in vec3 VertexNormal; out vec3LightIntensity; struct LightInfo { vec4 Position; // Light position in eyecoords. vec3 La; // Ambient light intensity vec3 Ld; // Diffuse light intensity vec3 Ls; // Specular light intensity }; uniform LightInfoLight; struct MaterialInfo { vec3 Ka; // Ambient reflectivity vec3 Kd; // Diffuse reflectivity vec3 Ks; // Specular reflectivity float Shininess; // Specular shininessfactor };
上面代码的前两行使用了顶点属性来向shader传递信息,具体请见之前的文章:http://blog.csdn.net/candycat1992/article/details/8830894#t1
之后定义了两个结构体LightInfo和MaterialInfo,并各自声明了一个变量,Light和Material,来表示灯光信息和材质信息。这部分内容也请见之前的文章:http://blog.csdn.net/candycat1992/article/details/8830894#t4
uniform MaterialInfoMaterial; uniform mat4ModelViewMatrix; uniform mat3NormalMatrix; uniform mat4ProjectionMatrix; uniform mat4 MVP;
这几行代码也没什么好说的,就是使用了uniform变量来向shader传递数据。接下来,就是我们这次第一次看到的GLSL中的函数了。
void getEyeSpace( outvec3 norm, out vec4 position ) { norm = normalize( NormalMatrix *VertexNormal); position = ModelViewMatrix *vec4(VertexPosition,1.0); } vec3 phongModel( vec4position, vec3 norm ) { vec3 s = normalize(vec3(Light.Position -position)); vec3 v = normalize(-position.xyz); vec3 r = reflect( -s, norm ); vec3 ambient = Light.La * Material.Ka; float sDotN = max( dot(s,norm), 0.0 ); vec3 diffuse = Light.Ld * Material.Kd *sDotN; vec3 spec = vec3(0.0); if( sDotN > 0.0 ) spec = Light.Ls * Material.Ks * pow( max( dot(r,v), 0.0 ),Material.Shininess ); return ambient + diffuse + spec; }
学过C或其他计算机语言的人基本都可以看懂,和C的程序很像。这里只对它们之间的不同进行说明。在GLSL的函数中,参数都是按值传递的,也就是说传递的是对象的复制品。函数参数可以用限定词in和out,以及inout。对于输入参数(被标记为in或者inout),它们被复制给对应的参数;对于输出参数(被标志为out),在函数结束时,它们被复制给对应的参数。如果参数类型没有使用任何标记,那么它们默认的标记为in。
我们以上面那个较短的函数getEyeSpace()为例。它接受两个输出参数,norm和position。它在main函数中被这样调用:
vec3 eyeNorm; vec4 eyePosition; // Get the position and normal in eye space getEyeSpace(eyeNorm, eyePosition);
也就是说,当函数结束后,eyeNorm和eyePosition就会得到函数计算的结果。
当然,我们可以给in参数进行赋值,只是这样在函数结束后是没有任何效果的。
const标识符可用于只读参数(标识符in,而不是out或inout)。这意味着该参数在函数内部不可以被赋值。这点和C一样。
GLSL允许函数重载,这点和C也很类似,也就是说,两个同名的函数具有不同的参数类型、参数个数、以及返回值都是允许的。
GLSL支持这么做,但是我们应该知道GLSL中的函数是按值传递的,因此如果我们使用参数传递了一个非常大的数组或结构体,那么就会产生大量的赋值操作,而这很有可能是我们不希望看到的。因此,另一种比较好的方法是使用全局变量。
怎么,还是很简单的吧!下面我们来看一个更高级的GLSL语法。
有时候,我们可能希望根据某个变量的值来决定使用不同的函数调用。例如,当使用shadow mapping时,我们需要根据深度信息判断当前片段是否在阴影中,如果在就只使用环境光渲染,如果不在就使用正常的ADS渲染。在GLSL中,子程序(subroutines)就可以帮助我们实现这样的功能。它根据一个变量的值,将一个函数调用绑定到一系列函数定义上。这和C++中的函数指针很类似。这时,一个uniform变量被当成一个函数指针,并且可以被用于调用一个函数。它可以被OpenGL赋值,从而绑定到其中某一个函数上。所有的子程序不需要具有相同的函数名字,但是必须有相同的参数列表和函数返回值。
通过使用子程序,我们可以不需要动态更换shader,或者在shader用根据一个uniform的值使用if判断句。要知道,在shader中,性能是非常重要的。而一个判断语句或者shader更替是非常耗性能的。
下面就举例说明如何使用子程序。在下面的程序里,我们想要用两种方法渲染一个茶壶,一个程序使用正常的ADS渲染,一个只使用diffuse渲染。shader的主要部分和上面的程序基本一样,只是用到了子程序来选择渲染方式。
Vertex Shader:
#version 400 subroutine vec3shadeModelType( vec4 position, vec3 normal); subroutine uniformshadeModelType shadeModel; layout (location = 0)in vec3 VertexPosition; layout (location = 1)in vec3 VertexNormal; out vec3LightIntensity; struct LightInfo { vec4 Position; // Light position in eyecoords. vec3 La; // Ambient light intensity vec3 Ld; // Diffuse light intensity vec3 Ls; // Specular light intensity };uniform LightInfoLight; struct MaterialInfo { vec3 Ka; // Ambient reflectivity vec3 Kd; // Diffuse reflectivity vec3 Ks; // Specular reflectivity float Shininess; // Specular shininessfactor };uniformMaterialInfo Material; uniform mat4ModelViewMatrix; uniform mat3NormalMatrix; uniform mat4ProjectionMatrix; uniform mat4 MVP; void getEyeSpace( outvec3 norm, out vec4 position ) { norm = normalize( NormalMatrix *VertexNormal); position = ModelViewMatrix *vec4(VertexPosition,1.0); } subroutine(shadeModelType ) vec3 phongModel( vec4position, vec3 norm ) { // The ADS shading calculations go here(see: "Using // functions in shaders," and"Implementing // per-vertex ambient, diffuse and specular(ADS) shading") … } subroutine(shadeModelType ) vec3 diffuseOnly(vec4 position, vec3 norm ) { vec3 s = normalize( vec3(Light.Position -position) ); return Light.Ld * Material.Kd * max( dot(s,norm), 0.0 ); } void main() { vec3 eyeNorm; vec4 eyePosition; getEyeSpace(eyeNorm, eyePosition); // Evaluate the shading equation. This willcall one of // the functions: diffuseOnly orphongModel. LightIntensity = shadeModel(eyePosition, eyeNorm ); gl_Position = MVP *vec4(VertexPosition,1.0); }
首先,前两行定义了一个子程序类型,然后声明了一个子程序类型的uniform变量,并把它命名为shaderModel:
subroutine vec3shadeModelType( vec4 position, vec3 normal); subroutine uniformshadeModelType shadeModel;
和C程序中的函数声明很像,一个子程序类型声明包含了子程序类型名称、参数列表(可选)以及返回值。shaderModel被当成一个函数指针,并在之后的OpenGL代码中被赋值到其中一个函数上。
随后,我们定义了两个子函数,这是通过在它们的函数定义前添加前缀:
subroutine (shadeModelType )
使用这个前缀表明,下面的函数应当和子函数类型声明中的声明相匹配(包括参数列表以及返回值,名字是任意的)。然后,我们在main函数中使用shadeModel调用了其中一个函数。那么我们究竟在哪里指明该调用哪个函数呢?答案是,在我们的OpenGL代码里,通常也就是我们的C++代码里。下面是这个例子中使用的OpenGL代码:
GLuint adsIndex =glGetSubroutineIndex( programHandle, GL_VERTEX_SHADER,"phongModel" ); GLuint diffuseIndex =glGetSubroutineIndex(programHandle, GL_VERTEX_SHADER, "diffuseOnly"); glUniformSubroutinesuiv(GL_VERTEX_SHADER, 1, &adsIndex); ... // Render theleft teapot glUniformSubroutinesuiv(GL_VERTEX_SHADER, 1, &diffuseIndex); ... // Render theright teapot // Get the positionand normal in eye space
为了在OpenGL代码里给一个子程序uniform变量赋值,我们需要按照下面的步骤。
首先,使用glGetSubroutineIndex得到每个子程序的索引:
GLuint adsIndex =glGetSubroutineIndex( programHandle, GL_VERTEX_SHADER,"phongModel" );
函数的第一个参数是shader程序句柄,第二个参数是shader等级,因为这里我们是在vertex shader中定义的,因此使用GL_VERTEX_SHADER。第三个参数是子程序的名字。这样,我们就可以得到两个子程序的索引,并把它们存储在adsIndex和diffuseIndex中。
然后,为了给shadowModel赋值,我们调用glUniformSubroutinesuiv来选定使用的子程序:
glUniformSubroutinesuiv(GL_VERTEX_SHADER, 1, &adsIndex); ... // Render theleft teapot glUniformSubroutinesuiv(GL_VERTEX_SHADER, 1, &diffuseIndex); ... // Render theright teapot
这个函数被用于同时给多个子程序uniform变量赋值。函数的第一个参数是shader等级,这里仍然使用GL_VERTEX_SHADER。第二个参数是赋值的uniform变量数。第三个参数是一个数组的指针,它指向需要赋值的uniform变量的索引。因为这里我们只有一个子程序uniform变量,因此是需要对adsIndex和diffuseIndex取地址即可。但是,当我们真的有很多子程序uniform变量需要赋值时,就应该使用一个真正的数组。通常,数组的第i个值被赋给索引为i的子程序uniform变量。因为我们只提供一个值,因此我们设置子程序uniform索引为0。
但是,我们怎么知道我们的子程序uniform变量索引为0呢?在调用glUniformSubroutinesuiv之前,我们可没有查询它的索引!这是因为,我们默认OpenGL将会自动从0开始连续地为我们的子程序进行索引。如果我们有多个子程序uniform变量,我们可以(也应该)使用glGetSubroutineUniformLocation来查询它们的索引,然后再据此给我们的数组变量排序。
同一个子程序可用于多个子程序类型。我们只需要使用逗号隔开不同的子程序类型即可,即使用下面的标识符:
subroutine( type1,type2 )
这里补充一点,subroutine功能是在OpenGL 4.0 版本里才增加的,也就是说4.0以前版本,包括OpenGL 3.3都是不支持的。如果你发现你的程序报错说,需要支持扩展ARB_shader_subroutine,那么你就应该更新你的显卡了。唉,我好像我的更新不了了诶。
更多关于OpenGL发展历史,请详见Wikipedia。