转自:http://blog.sina.com.cn/s/blog_9e5d42ee01012io6.html
【GLSL教程】(一)图形流水线
这是一些列来自 lighthouse3d 的GLSL教程,非常适合入门。我将边学习边翻译该教程的内容,同时记录在这里,方便以后查询。
流水线概述
下图描述了一个简化的图形处理流水线,虽然简略但仍然可以展示着色器编程(shader programming)的一些重要概念。
一个固定流水线包括如下功能:
顶点变换(Vertex Transformation)
这里一个顶点是一个信息集合,包括空间中的位置、顶点的颜色、法线、纹理坐标等。这一阶段的输入是独立的顶点信息,固定功能流水线在这一阶段通常进行如下工作:
·顶点位置变换
·为每个顶点计算光照
·纹理坐标的生成与变换
图元组合和光栅化(Primitive Assembly and Rasterization)
此阶段的输入是变换后的顶点和连接信息(connectivity information)。连接信息告诉流水线顶点如何组成图元(三角形、四边形等)。此阶段还负责视景体(view frustum)裁剪和背面剔除。
光栅化决定了片断(fragment),以及图元的像素位置。这里的片断是指一块数据,用来更新帧缓存(frame buffer)中特定位置的一个像素。一个片断除了包含颜色,还有法线和纹理坐标等属性,这些信息用来计算新的像素颜色值。
本阶段的输出包括:
·帧缓存中片断的位置
·在顶点变换阶段计算出的信息对每个片断的插值
这 个阶段利用在顶点变换阶段算出的数据,结合连接信息计算出片断的数据。例如,每个顶点包含一个变换后的位置,当它们组成图元时,就可以用来计算图元的片断 位置。另一个例子是使用颜色,如果多边形的每个顶点都有自己的颜色值,那么多边形内部片断的颜色值就是各个顶点颜色插值得到的。
片断纹理化和色彩化(Fragment Texturing and Coloring)
此阶段的输入是经过插值的片断信息。在前一阶段已经通过插值计算了纹理坐标和一个颜色值,这个颜色在本阶段可以用来和纹理元素进行组合。此外,这一阶段还可以进行雾化处理。通常最后的输出是片断的颜色值以及深度信息。
光栅操作(Raster Operations)
此阶段的输入:
·像素位置
·片断深度和颜色值
在这个阶段对片断进行一系列的测试,包括:
·剪切测试(scissor test)
·Alpha测试
·模版测试
·深度测试
如果测试成功,则根据当前的混合模式(blend mode)用片断信息来更新像素值。注意混合只能在此阶段进行,因为片断纹理化和颜色化阶段不能访问帧缓存。帧缓存只能在此阶段访问。
一幅图总结固定功能流水线(Visual Summary of the Fixed Functionality)
下图直观地总结了上述流水线的各个阶段:
取代固定的功能(Replacing Fixed Functionality)
现在的显卡允许程序员自己编程实现上述流水线中的两个阶段:
·顶点shader实现顶点变换阶段的功能
·片断shader替代片断纹理化和色彩化的功能
顶点处理器
顶点处理器用来运行顶点shader(着色程序)。顶点shader的输入是顶点数据,即位置、颜色、法线等。
下面的OpenGL程序发送数据到顶点处理器,每个顶点中包含一个颜色信息和一个位置信息。
- glBegin(...);
-
glColor3f(0.2,0.4,0.6); -
glVertex3f(-1.0,1.0,2.0); -
glColor3f(0.2,0.4,0.8); -
glVertex3f(1.0,-1.0,2.0); - glEnd();
·使用模型视图矩阵以及投影矩阵进行顶点变换
·法线变换及归一化
·纹理坐标生成和变换
·逐顶点或逐像素光照计算
·颜色计算
不一定要完成上面的所有操作,例如你的程序可能不使用光照。但是,一旦你使用了顶点shader,顶点处理器的所有固定功能都将被替换。所以你不能只编写法线变换的shader而指望固定功能帮你完成纹理坐标生成。
从上一节已经知道,顶点处理器并不知道连接信息,因此这里不能执行拓扑信息有关的操作。比如顶点处理器不能进行背面剔除,它只是操作顶点而不是面。
顶点shader至少需要一个变量:gl_Position,通常要用模型视图矩阵以及投影矩阵进行变换。顶点处理器可以访问OpenGL状态,所以可以用来处理材质和光照。最新的设备还可以访问纹理。
片断处理器
片断处理器可以运行片断shader,这个单元可以进行如下操作:
·逐像素计算颜色和纹理坐标
·应用纹理
·雾化计算
·如果需要逐像素光照,可以用来计算法线
片断处理器的输入是顶点坐标、颜色、法线等计算插值得到的结果。在顶点shader中对每个顶点的属性值进行了计算,现在将对图元中的每个片断进行处理,因此需要插值的结果。
如同顶点处理器一样,当你编写片断shader后,所有固定功能将被取代,所以不能使用片断shader对片断材质化,同时用固定功能进行雾化。程序员必须编写程序实现需要的所有效果。
片断处理器只对每个片断独立进行操作,并不知道相邻片断的内容。类似顶点shader,我们必须访问OpenGL状态,才可能知道应用程序中设置的雾颜色等内容。
一个片断shader有两种输出:
·抛弃片断内容,什么也不输出
·计算片断的最终颜色gl_FragColor,当要渲染到多个目标时计算gl_FragData。
还可以写入深度信息,但上一阶段已经算过了,所以没有必要。
需要强调的是片断shader不能访问帧缓存,所以混合(blend)这样的操作只能发生在这之后。
【GLSL教程】(二)在OpenGL中使用GLSL
设置GLSL
这一节讲述在OpenGL中配置GLSL,假设你已经写好了顶点shader和像素shader。如果你还没有准备好,可以从如下网址获得相关内容:
http://www.3dshaders.com/home/
http://www.opengl.org/sdk/tools/ShaderDesigner/
http://developer.amd.com/archive/gpu/rendermonkey/pages/default.aspx
在OpenGL中,GLSL的shader使用的流程与C语言相似,每个shader类似一个C模块,首先需要单独编译(compile),然后一组编译好的shader连接(link)成一个完整程序。
这里将忽略ARB扩展,只列举OpenGL2.0的代码。建议使用GLEW库:
下面的代码检查OpenGL 2.0是否可用:
- #include
<GL/glew.h> - #include
<GL/glut.h> -
- void
main(int argc, char **argv) - {
-
glutInit(&argc, argv); -
... -
glewInit(); -
-
if (glewIsSupported("GL_VERSION_2_0")) -
printf("Ready for OpenGL 2.0\n"); -
else -
{ -
printf("OpenGL 2.0 not supported\n"); -
exit(1); -
} -
setShaders(); -
-
glutMainLoop(); - }
下图显示了创建shader的必要步骤,函数的具体使用方法将在下面各小结描述:
创建shader
下图显示了创建shader的步骤:
首先创建一个对象作为shader的容器,这个创建函数将返回容器的句柄。
- GLuint
glCreateShader(GLenum shaderType); - 参数:
- ·shaderType
– GL_VERTEX_SHADER or GL_FRAGMENT_SHADER.
下一步将添加源代码。shader的源代码是一个字符串数组,添加的语法如下:
- void
glShaderSource(GLuint shader, int numOfStrings, const char **strings, int *lenOfStrings); - 参数:
- ·shader
– the handler to the shader. - ·numOfStrings
– the number of strings in the array. - ·strings
– the array of strings. - ·lenOfStrings
– an array with the length of each string, or NULL, meaning that the strings are NULL terminated.
- void
glCompileShader(GLuint shader); - 参数:
- •shader
– the handler to the shader.
创建程序
下图显示了获得一个可以运行的shader程序的步骤:
首先创建一个对象,作为程序的容器。此函数返回容器的句柄。
- GLuint
glCreateProgram(void);
你可以创建任意多个程序,在渲染时,可以在不同程序中切换,甚至在某帧返回固定功能流水线。比如你想用折射和反射shader绘制一个茶杯,然后回到固定功能生成立方体环境贴图(cube map)显示背景。
下面将把上一节编译的shader附加到刚刚创建的程序中。方法如下:
- void
glAttachShader(GLuint program, GLuint shader); - 参数:
- ·program
– the handler to the program. - ·shader
– the handler to the shader you want to attach.
你也可以把一个shader附加到多个程序,比如你想在不同程序中使用某个相同的shader。
最后一步是连接程序。方法如下:
- void
glLinkProgram(GLuint program); - 参数:
- ·program
– the handler to the program.
程序连接后,可以调用glUseProgram来使用程序。每个程序都分配了一个句柄,你可以事先连接多个程序以备使用。
- void
glUseProgram(GLuint prog); - 参数:
- ·prog
– the handler to the program you want to use, or zero to return to fixed functionality.
下面的代码包含了上面描述的所有步骤,参数p,f,v是全局的GLuint型变量。
- void
setShaders() - {
-
char *vs,*fs; -
-
v = glCreateShader(GL_VERTEX_SHADER); -
f = glCreateShader(GL_FRAGMENT_SHADER); -
-
vs = textFileRead("toon.vert"); -
fs = textFileRead("toon.frag"); -
-
const char *vv = vs; -
const char *ff = fs; -
-
glShaderSource(v, 1, &vv, NULL); -
glShaderSource(f, 1, &ff, NULL); -
-
free(vs);free(fs); -
-
glCompileShader(v); -
glCompileShader(f); -
-
p = glCreateProgram(); -
-
glAttachShader(p, v); -
glAttachShader(p, f); -
-
glLinkProgram(p); -
glUseProgram(p); - }
http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/glutglsl_2.0.zip
完整例子中包含了shader代码及文本文件读入程序。
错误处理
调试shader是很困难的。目前还没有像printf这样的东西,虽然未来可能出现有调试功能的开发工具。
编译阶段的状态可以用如下函数获得:
- void
glGetShaderiv(GLuint object, GLenum type, int *param); - 参数:
- ·object
– the handler to the object. Either a shader or a program - ·type
– GL_COMPILE_STATUS. - ·param
– the return value, GL_TRUE if OK, GL_FALSE otherwise.
- void
glGetProgramiv(GLuint object, GLenum type, int *param); - 参数:
- ·object
– the handler to the object. Either a shader or a program - ·type
– GL_LINK_STATUS. - ·param
– the return value, GL_TRUE if OK, GL_FALSE otherwise.
为了获得特定shader或程序的日志,可以使用如下程序:
- void
glGetShaderInfoLog(GLuint object, int maxLen, int *len, char *log); - void
glGetProgramInfoLog(GLuint object, int maxLen, int *len, char *log); - 参数:
- ·object
– the handler to the object. Either a shader or a program - ·maxLen
– The maximum number of chars to retrieve from the InfoLog. - ·len
– returns the actual length of the retrieved InfoLog. - ·log
– The log itself.
- void
glGetShaderiv(GLuint object, GLenum type, int *param); - void
glGetProgramiv(GLuint object, GLenum type, int *param); - 参数:
- ·object
– the handler to the object. Either a shader or a program - ·type
– GL_INFO_LOG_LENGTH. - ·param
– the return value, the length of the InfoLog.
下面的函数可以用来打印InfoLog的内容:
- void
printShaderInfoLog(GLuint obj) - {
-
int infologLength = 0; -
int charsWritten = 0; -
char *infoLog; -
-
glGetShaderiv(obj, GL_INFO_LOG_LENGTH,&infologLength); -
-
if (infologLength > 0) -
{ -
infoLog = (char *)malloc(infologLength); -
glGetShaderInfoLog(obj, infologLength, &charsWritten, infoLog); -
printf("%s\n",infoLog); -
free(infoLog); -
} - }
-
- void
printProgramInfoLog(GLuint obj) - {
-
int infologLength = 0; -
int charsWritten = 0; -
char *infoLog; -
-
glGetProgramiv(obj, GL_INFO_LOG_LENGTH,&infologLength); -
-
if (infologLength > 0) -
{ -
infoLog = (char *)malloc(infologLength); -
glGetProgramInfoLog(obj, infologLength, &charsWritten, infoLog); -
printf("%s\n",infoLog); -
free(infoLog); -
} - }
清理
前面的小节讲到了附加一个shader到一个程序中,这里的调用是将shader从程序中分离:
- void
glDetachShader(GLuint program, GLuint shader); - 参数:
- ·program
– The program to detach from. - ·shader
– The shader to detach.
- void
glDeleteShader(GLuint id); - void
glDeleteProgram(GLuint id); - 参数:
- ·id
– The hanuler of the shader or program to delete.
如果一个shader还附加在某个程序中,这个shader并不能真正删除,只能标记为删除。当这个shader从所有程序中分离之后,才会被最终删除。
【GLSL教程】(三)在OpenGL中向shader传递信息
引言一个OpenGL程序可以用多种方式和shader通信。注意这种通信是单向的,因为shader的输出只能是渲染到某些目标,比如颜色和深度缓存。
OpenGL的部分状态可以被shader访问,因此程序改变OpenGL某些状态就可以与shader进行通信了。例如一个程序想把光的颜色传给shader,可以直接调用OpenGL接口,就像使用固定功能流水线时做的那样。
不 过,使用OpenGL状态并不是设置shader中使用数据的直观方式。比如一个shader需要一个表示时间变化的变量来计算动画,在OpenGL状态 中就没有现成的变量可用。当然,你可以使用没有用到的“镜面光截止角度(cutoffangle)”这样一个变量表示时间,但显然让人难以接受。
幸运的是,GLSL允许用户自定义变量,实现OpenGL应用程序与shader通信。有了这个功能,你就可以命名一个叫做timeElapsed的变量表示经过的时间。
上文的讨论涉及到了GLSL提供的两种类型修饰符(更多的类型将在后面提到):
·一致变量(Uniform)
·属性(Attribute)
在shader中定义的变量如果用这两种类型修饰符,表示对shader来说,它们是只读的。下面将详细讲述怎样使用这些类型的变量。
还有一种将变量送给shader的方法:使用纹理。一个纹理不止可以表示一张图片,它还可以表示一个数组。事实上,你完全可以决定如何在shader中解释纹理数据,即使它真是一幅图片。
数据类型和变量
下面是GLSL中的基本数据类型:·float
·bool
·int
浮点类型与C中类似,布尔类型可以为true或false。这些基本类型可以组成2、3或4维向量,如下所示:
·vec{2,3,4} a vector of 2,3,or 4 floats
·bvec{2,3,4} bool vector
·ivec{2,3,4} vector of integers
GLSL还包括2×2、3×3或4×4型矩阵,因为这些矩阵类型在图形处理中很常用:
·mat2
·mat3
·mat4
此外,还有一组用来实现纹理访问的特殊类型,它们被称为采样器(sampler),在读取纹理值(也称为纹素texel)时用到。下面就是纹理采样用到的数据类型:
·sampler1D – for 1D textures
·sampler2D – for 2D textures
·sampler3D – for 3D textures
·samplerCube – for cube map textures
·sampler1DShadow – for shadow maps
·sampler2DShadow – for shadow maps
在GLSL中,可以像C一样声明和访问数组,但是不能在声明时初始化数组。GLSL还可以定义结构体:
- struct
dirlight - {
-
vec3 direction; -
vec3 color; - };
- float
a,b; // two vector (yes, the comments are like in C) - int
c = 2; // c is initialized with 2 - bool
d = true; // d is true
- float
b = 2; // incorrect, there is no automatic type casting - float
e = (float)2; // incorrect, requires constructors for type casting - int
a = 2; - float
c = float(a); // correct. c is 2.0 - vec3
f; // declaring f as a vec3 - vec3
g = vec3(1.0,2.0,3.0); // declaring and initializing g
- vec2
a = vec2(1.0,2.0); - vec2
b = vec2(3.0,4.0); - vec4
c = vec4(a,b) // c = vec4(1.0,2.0,3.0,4.0); - vec2
g = vec2(1.0,2.0); - float
h = 3.0; - vec3
j = vec3(g,h);
- mat4
m = mat4(1.0) // initializing the diagonal of the matrix with 1.0 - vec2
a = vec2(1.0,2.0); - vec2
b = vec2(3.0,4.0); - mat2
n = mat2(a,b); // matrices are assigned in column major order - mat2
k = mat2(1.0,0.0,1.0,0.0); // all elements are specified
下面的例子给出了初始化结构体的方法:
- struct
dirlight // type definition - {
-
vec3 direction; -
vec3 color; - };
- dirlight
d1; - dirlight
d2 = dirlight(vec3(1.0,1.0,0.0),vec3(0.8,0.8,0.4));
- vec4
a = vec4(1.0,2.0,3.0,4.0); - float
posX = a.x; - float
posY = a[1]; - vec2
posXY = a.xy; - float
depth = a.w
对于结构体来说,可以像在C语言中一样访问其成员。所以访问前面定义的结构体,可以使用如下的代码:
- d1.direction
= vec3(1.0,1.0,1.0);
修饰符给出了变量的特殊含义,GLSL中有如下修饰符:
·const – 声明一个编译期常量。
·attribute– 随不同顶点变化的全局变量,由OpenGL应用程序传给顶点shader。这个修饰符只能用在顶点shader中,在shader中它是一个只读变量。
·uniform– 随不同图元变化的全局变量(即不能在glBegin/glEnd中设置),由OpenGL应用程序传给shader。这个修饰符能用在顶点和片断shader中,在shader中它是一个只读变量。
·varying –用于顶点shader和片断shader间传递的插值数据,在顶点shader中可写,在片断shader中只读。
一致变量(Uniform Variables)
不同于顶点属性在每个顶点有其自己的值,一个一致变量在一个图元的绘制过程中是不会改变的,所以其值不能在glBegin/glEnd中设置。一致变量适合描述在一个图元中、一帧中甚至一个场景中都不变的值。一致变量在顶点shader和片断shader中都是只读的。
首先你需要获得变量在内存中的位置,这个信息只有在连接程序之后才可获得。注意,对某些驱动程序,在获得存储位置前还必须使用程序(调用glUseProgram)。
获取一个一致变量的存储位置只需要给出其在shader中定义的变量名即可:
- GLint
glGetUniformLocation(GLuint program, const char *name); - 参数:
- ·program
– the hanuler to the program - ·name
– the name of the variable
- void
glUniform1f(GLint location, GLfloat v0); - void
glUniform2f(GLint location, GLfloat v0, GLfloat v1); - void
glUniform3f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2); - void
glUniform4f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3); - 或者
- GLint
glUniform{1,2,3,4}fv(GLint location, GLsizei count, GLfloat *v); - 参数:
- ·location
– the previously queried location - ·v0,v1,v2,v3
– float values - ·count
– the number of elements in the array - ·v
– an array of floats
对integer类型也有一组类似的函数,不过要用i替换函数中的f。对bool类型没有专门的函数,但可以使用整数的0和1来表示真假。一旦你使用了一致变量数组,那么就必须使用向量版本的函数。
对sampler变量,使用函数glUniform1i和glUniform1iv。
矩阵也是一种GLSL的数据类型,所以也有一组针对矩阵的函数:
- GLint
glUniformMatrix{2,3,4}fv(GLint location, GLsizei count, GLboolean transpose, GLfloat *v); - 参数:
- location
– the previously queried location. - count
– the number of matrices. 1 if a single matrix is being set, or n for an array of n matrices. - transpose
– wheter to transpose the matrix values. A value of 1 indicates that the matrix values are specified in row major order, zero is column major order - v
– an array of floats.
还有一点要注意的是:使用这些函数之后,变量的值将保持到程序再次连接之时。一旦进行重新连接,所有变量的值将被重置为0。
最后是一些示例代码。假设一个shader中使用了如下变量:
- uniform
float specIntensity; - uniform
vec4 specColor; - uniform
float t[2]; - uniform
vec4 colors[3];
在OpenGL程序中可以使用下面的代码设置这些变量:
- GLint
loc1,loc2,loc3,loc4; - float
specIntensity = 0.98; - float
sc[4] = {0.8,0.8,0.8,1.0}; - float
threshold[2] = {0.5,0.25}; - float
colors[12] = {0.4,0.4,0.8,1.0, -
0.2,0.2,0.4,1.0, -
0.1,0.1,0.1,1.0}; -
- loc1
= glGetUniformLocation(p,"specIntensity"); - glUniform1f(loc1,specIntensity);
-
- loc2
= glGetUniformLocation(p,"specColor"); - glUniform4fv(loc2,1,sc);
-
- loc3
= glGetUniformLocation(p,"t"); - glUniform1fv(loc3,2,threshold);
-
- loc4
= glGetUniformLocation(p,"colors"); - glUniform4fv(loc4,3,colors);
注 意设置一个数组(例子中的t)与设置四元向量(例子中的colors和specColor)的区别。中间的count参数指在shader中声明的数组元 素数量,而不是在OpenGL程序中声明的。所以虽然specColor包含4个值,但glUniform4fv函数中的参数是1,因为它只是一个向量。 另一种设置specColor的方法:
- loc2
= glGetUniformLocation(p,"specColor"); - glUniform4f(loc2,sc[0],sc[1],sc[2],sc[3]);
- loct0
= glGetUniformLocation(p,"t[0]"); - glUniform1f(loct0,threshold[0]);
-
- loct1
= glGetUniformLocation(p,"t[1]"); - glUniform1f(loct1,threshold[1]);
注意在glGetUniformLocation中使用方括号指示的变量。
属性变量(Attribute Variables)
在前一节提到,一致变量只能针对一个图元全体,就是说不能在glBegin和glEnd之间改变。
如果要针对每个顶点设置变量,那就需要属性变量了。事实上属性变量可以在任何时刻更新。在顶点shader中属性变量是只读的。因为它包含的是顶点数据,所以在片断shader中不能直接应用。
与一致变量相似,首先你需要获得变量在内存中的位置,这个信息只有在连接程序之后才可获得。注意,对某些驱动程序,在获得存储位置前还必须使用程序。
- GLint
glGetAttribLocation(GLuint program,char *name); - 参数:
- program
– the handle to the program. - name
– the name of the variable
- void
glVertexAttrib1f(GLint location, GLfloat v0); - void
glVertexAttrib2f(GLint location, GLfloat v0, GLfloat v1); - void
glVertexAttrib3f(GLint location, GLfloat v0, GLfloat v1,GLfloat v2); - void
glVertexAttrib4f(GLint location, GLfloat v0, GLfloat v1,,GLfloat v2, GLfloat v3); - 或者
- GLint
glVertexAttrib{1,2,3,4}fv(GLint location, GLfloat *v); - 参数:
- location
– the previously queried location. - v0,v1,v2,v3
– float values. - v
– an array of floats.
- loc
= glGetAttribLocation(p,"height");
- glBegin(GL_TRIANGLE_STRIP);
-
glVertexAttrib1f(loc,2.0); -
glVertex2f(-1,1); -
glVertexAttrib1f(loc,2.0); -
glVertex2f(1,1); -
glVertexAttrib1f(loc,-2.0); -
glVertex2f(-1,-1); -
glVertexAttrib1f(loc,-2.0); -
glVertex2f(1,-1); - glEnd();
http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/glutglsl3_2.0.zip、
顶点数组和属性变量也可以一起使用。首先需要使能数组,使用如下函数:
- void
glEnableVertexAttribArra y(GLint loc); - 参数:
- loc
– the location of the variable.
- void
glVertexAttribPointer(GLint loc, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer); - 参数:
- loc
– the location of the variable. - size
– the number of components per element, for instance: 1 for float; 2 for vec2; 3 for vec3, and so on. - type
– The data type associated: GL_FLOAT is an example. - normalized
– if set to 1 then the array values will be normalized, converted to a range from -1 to 1 for signed data, or 0 to 1 for unsigned data. - stride
– the spacing between elements. Exactly the same as in OpenGL. - pointer
– pointer to the array containing the data.
- float
vertices[8] = {-1,1, 1,1, -1,-1, 1,-1}; - float
heights[4] = {2,2,-2,-2}; - ...
- loc
= glGetAttribLocation(p,"height"); -
- glEnableClientState(GL_VERTEX_ARRAY);
- glEnableVertexAttribArra
y(loc); - glVertexPointer(2,GL_FLOAT,0,vertices);
- glVertexAttribPointer(loc,1,GL_FLOAT,0,0,heights);
易变变量(Varying Variables)
前 面说过,shader包括两种类型:顶点shader和片断shader。为了计算片断的值,往往需要访问顶点的插值数据。例如,当使用逐片断光照时,我 们需要知道当前片断的法线,但是在OpenGL中只为每个顶点指定了法线。顶点shader可以访问这些法线,而片断shader不能,因为法线是 OpenGL程序作为属性变量指定的。
顶 点变换后的数据移动到流水线的下一个阶段,在这个阶段通过使用连接信息,生成了所有图元并完成片断化。对每个片断,有一组变量会被自动进行插值并提供给片 断shader,这些都是固定功能。片断的颜色就是这么处理的,到达片断shader的颜色就是组成图元的顶点颜色插值的结果。
像片断shader接收到的这种插值产生的变量,就是“易变变量”类型。GLSL包含一些预先定义的易变变量,例如前面提到的颜色。用户也可以自己定义易变变量,它们必须同时在顶点shader和片断shader中声明:
- varying
float intensity;
语句和函数
控制流语句
与C语言类似,GLSL中有类似if-else的条件语句,for,while,do-while等循环语句。
- if
(bool expression) -
... - else
-
... -
- for
(initialization; bool expression; loop expression) -
... -
- while
(bool expression) -
... -
- do
-
... - while
(bool expression)
·continue – available in loops, causes a jump to thenext iteration of the loop
·break – available in loops, causes an exit of theloop
·discard
最后的discard关键字只能在片断shader中使用,它将在不写入帧缓存或者深度缓存的情况下,终止当前片断的shader程序。
与C语言类似,shader也是由函数组成的结构化程序。至少每类shader都必须包含一个如下方式声明的主函数:
- void
main()
函数参数可以有如下修饰符:
·in – for input parameters
·out – for outputs of the function. The returnstatement is also an option for sending the result of a function.
·inout – for parameters that are both input andoutput of a function
如果没有指定修饰符,默认情况下为in类型。
最后还有两点要注意:
·允许函数重载,只要参数不同。
·在标准中没有定义递归行为。
结束本节之前来看一个函数的例子:
- vec4
toonify(in float intensity) - {
-
vec4 color; -
if (intensity > 0.98) -
color = vec4(0.8,0.8,0.8,1.0); -
else if (intensity > 0.5) -
color = vec4(0.4,0.4,0.8,1.0); -
else if (intensity > 0.25) -
color = vec4(0.2,0.2,0.4,1.0); -
else -
color = vec4(0.1,0.1,0.1,1.0); -
-
return(color); - }
【GLSL教程】(四)shder的简单示例
GLSL的Hello World
这一节中包含一个最基本的shader,它提供如下功能:顶点变换然后使用单一的颜色渲染图元。
顶点shader
前面已经说过,顶点shader负责完成顶点变换。这里将按照固定功能的方程完成顶点变换。
固定功能流水线中一个顶点通过模型视图矩阵以及投影矩阵进行变换,使用如下公式:
- vTrans
= projection * modelview *incomingVertex
- uniform
mat4 gl_ModelViewMatrix; - uniform
mat4 gl_ProjectionMatrix;
- attribute
vec4 gl_Vertex;
现在我们可以写一个仅仅进行顶点变换的顶点shader了。注意所有其他功能都将丧失,比如没有光照计算。顶点shader必须有一个main函数,如下面的代码所示:
- void
main() - {
-
gl_Position =gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex; - }
GLSL提供一些派生的矩阵,也就是说gl_ModelViewProjectionMatri
- void
main() - {
-
gl_Position =gl_ModelViewProjectionMatri x * gl_Vertex; - }
- vec4
ftransform(void);
这个函数按照与固定功能相同的步骤对输入顶点进行变换,然后返回变换后的顶点。所以shader可以重新写成如下形式:
- void
main() - {
-
gl_Position =ftransform(); - }
片断shader也有预先定义的变量gl_FragColor,可以向其中写入片断的颜色值。下面的代码就是一个片断shader,将所有片断绘制成淡蓝色:
- void
main() - {
-
gl_FragColor =vec4(0.4,0.4,0.8,1.0); - }
http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/glutglsl5_2.0.zip
颜色shader
GLSL可以读取一些OpenGL状态,在本节我们将学习如何访问在OpenGL中设置的glColor变量。
GLSL有一个属性变量记录当前颜色,也提供易变变量从顶点shader向片断shader传递颜色值。
- attribute
vec4 gl_Color; - varying
vec4 gl_FrontColor; // writable onthe vertex shader - varying
vec4 gl_BackColor; // writable onthe vertex shader - varying
vec4 gl_Color; // readable on thefragment shader
1、OpenGL程序通过glColor传送颜色信息。
2、顶点shader通过属性gl_Color接收颜色值。
3、顶点shader计算正面和反面的颜色,然后分别保存在gl_FrontColor和gl_BackColor中。
4、片断shader接收易变变量gl_Color中存储的插值产生的颜色,由当前图元的方向决定颜色是gl_FrontColor还是gl_BackColor插值产生的。
5、片断shader根据易变变量gl_Color设置gl_FragColor。
前 面说过顶点shader和片断shader中传递的易变变量要有相同的名字,但这里是个例外,顶点shader中的gl_FrontColor和 gl_BackColor会根据图元的方向,自动转变为片断shader中的gl_Color。还要注意属性变量gl_Color和易变变量 gl_Color没有冲突,因为前者只存在于顶点shader,后者只存在于片断shader。
下面是顶点shader的例子,只计算了正面颜色:
- void
main() - {
-
gl_FrontColor =gl_Color; -
gl_Position =ftransform(); - }
片断shader更加简单:
- void
main() - {
-
gl_FragColor = gl_Color; - }
http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/colorglut_2.0.zip
扁平shader(Flatten Shader)
着色器编程让我们可以探索一些新效果,本节的例子展示了用奇怪的方法操作顶点得到的效果。
首先我们要得到一个扁平的3D模型,只需要在应用模型视图变换时将模型顶点的z坐标设为0就行了。下面是顶点shader的代码:
- void
main(void) - {
-
vec4 v = vec4(gl_Vertex); -
v.z = 0.0; -
-
gl_Position =gl_ModelViewProjectionMatri x * v; - }
片断shader与“GLSL中的Hello World”一节相同,就只用设置一种颜色。
一个扁平的茶壶效果如下:
更进一步,我们需要在z坐标上使用一个正弦函数。将z坐标作为x坐标的函数,这样茶杯将呈现波浪的效果:
- void
main(void) - {
-
vec4 v =vec4(gl_Vertex); -
v.z = sin(5.0*v.x)*0.25; -
gl_Position =gl_ModelViewProjectionMatri x * v; - }
顶点shader的代码如下:
- uniform
float time; -
- void
main(void) - {
-
vec4 v =vec4(gl_Vertex); -
v.z = sin(5.0*v.x +time*0.01)*0.25; -
gl_Position =gl_ModelViewProjectionMatri x * v; - }
·setup: 获取一致变量的存储位置
·render: 更新一致变量
设置(setup)步骤只有一条语句:
- loc
=glGetUniformLocation(p,"time");
- void
renderScene(void) - {
-
glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT); -
glLoadIdentity(); -
gluLookAt(0.0,0.0,5.0, -
0.0,0.0,0.0, -
0.0f,1.0f,0.0f); -
glUniform1f(loc, time); -
-
glutSolidTeapot(1); -
time+=0.01; -
glutSwapBuffers(); - }
本节的GLEW源代码:
http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/flatten_2.0.zip
【GLSL教程】(五)卡通着色
卡通着色可能是最简单的非真实模式shader。它使用很少的颜色,通常是几种色调(tone),因此不同色调之间是突变的效果。下图显示的就是我们试图达到的效果:
茶壶上的色调是通过角度的余弦值选择的,这个角度是指光线和面的法线之间的夹角角度。如果法线和光的夹角比较小,我们使用较亮的色调,随着夹角变大,逐步使用更暗的色调。换句话说,角度余弦值将决定色调的强度。
在本教程中,我们先介绍逐顶点计算色调强度(intensity)的方法,之后把这个计算移到片断shader中,此外还将介绍如何访问OpenGL中光源的方向。
卡通着色——版本1
这个版本使用逐顶点计算色调强度的方法,之后片断shader使用顶点色调强度的插值来决定片断选择那个色调。因此顶点shader必须声明一个易变变量保存强度值,片断shader中也需要声明一个同名的易变变量,用来接收经过插值的强度值。
在顶点shader中,光线方向可以定义为一个局部变量或者常量,不过定义为一个一致变量将可以获得更大的灵活性,因为这样就可以在OpenGL程序中任意设置了。所以我们在shader中这样定义光的方向:
- uniform
vec3 lightDir;
顶点shader通过属性变量gl_Normal来访问在OpenGL程序中指定的法线,这些法线在OpenGL程序中通过glNormal函数定义,因此位于模型空间。
如果在OpenGL程序中没有对模型进行旋转或缩放等操作,那么传给顶点shader的位于世界空间的gl_Normal正好等于模型空间中定义的法线。另外法线只包含方向,所以不受移动变换的影响。
由于法线和光线方向定义在相同的空间中,顶点shader可以直接进行余弦计算,两个方向分别为lightDir和gl_Normal。计算余弦的公式如下:
cos(lightDir,normal) = lightDir . normal / (|lightDir| * |normal|)
公式中的“.”表示内积,亦称为点积。如果lightDir和gl_Normal已经经过了归一化:
| normal | = 1
| lightDir | = 1
那么计算余弦的公式可以简化为:
cos(lightDir,normal) = lightDir . normal
因为lightDir是由OpenGL程序提供的,所以我们可以假定它传到shader之前已经归一化了。只有光线方向改变时,才需要重新计算归一化。此外OpenGL程序传过来的法线也应该是经过归一化的。
我们将定义一个名为intensity的变量保存余弦值,这个值可以直接使用GLSL提供的dot函数计算。
- intensity
= dot(lightDir, gl_Normal);
- uniform
vec3 lightDir; - varying
float intensity; -
- void
main() - {
-
intensity = dot(lightDir,gl_Normal); -
gl_Position = ftransform(); - }
- varying
float intensity; -
- void
main() - {
-
vec3 lightDir = normalize(vec3(gl_LightSource[0].position)); -
intensity = dot(lightDir,gl_Normal); -
-
gl_Position = ftransform(); - }
- vec4
color; -
- if
(intensity > 0.95) -
color = vec4(1.0,0.5,0.5,1.0); - else
if (intensity > 0.5) -
color = vec4(0.6,0.3,0.3,1.0); - else
if (intensity > 0.25) -
color = vec4(0.4,0.2,0.2,1.0); - else
-
color = vec4(0.2,0.1,0.1,1.0);
- varying
float intensity; -
- void
main() - {
-
vec4 color; -
if (intensity > 0.95) -
-
color = vec4(1.0,0.5,0.5,1.0); -
else if (intensity > 0.5) -
color = vec4(0.6,0.3,0.3,1.0); -
else if (intensity > 0.25) -
color = vec4(0.4,0.2,0.2,1.0); -
else -
color = vec4(0.2,0.1,0.1,1.0); -
gl_FragColor = color; - }
卡通着色——版本2
本节中我们要实现逐片断的卡通着色效果。为了达到这个目的,我们需要访问每个片断的法线。顶点shader中需要将顶点的法线写入一个易变变量,这样在片断shader中就可以得到经过插值的法线。
顶点shader比上一版变得更简单了,因为颜色强度的计算移到片断shader中进行了。一致变量lightDir也要移到片断shader中,下面就是新的顶点shader代码:
- varying
vec3 normal; -
- void
main() - {
-
normal = gl_Normal; -
gl_Position = ftransform(); - }
- uniform
vec3 lightDir; - varying
vec3 normal; -
- void
main() - {
-
float intensity; -
vec4 color; -
intensity = dot(lightDir,normal); -
-
if (intensity > 0.95) -
color = vec4(1.0,0.5,0.5,1.0); -
else if (intensity > 0.5) -
color = vec4(0.6,0.3,0.3,1.0); -
else if (intensity > 0.25) -
color = vec4(0.4,0.2,0.2,1.0); -
else -
color = vec4(0.2,0.1,0.1,1.0); -
gl_FragColor = color; - }
令人吃惊的是新的渲染结果居然和前一节的一模一样,这是为什么呢?
让我们仔细看看这两个版本的区别。在第一版中,我们在顶点shader中计算出一个intensity值,然后在片断shader中使用这个值的插值结 果。在第二个版中,我们先对法线插值,然后在片断shader中计算点积。插值和点积都是线性运算,所以两者运算的顺序并不影响结果。
真正的问题在于片断shader中对插值后的法线进行点积运算的时候,尽管这时法线的方向是对的,但是它并没有归一化。
我们说法线方向是对的,因为我们假定传入顶点shader的法线是经过归一化的,对法线插值可以得到一个方向正确的向量。但是,这个向量的长度在大部分情 况下都是错的,因为对归一化法线进行插值时,只有在所有法线的方向一致时才会得到一个单位长度的向量。(关于法线插值的问题,后面的教程会专门解释)
综上所述,在片断shader中,我们接收到的是一个方向正确长度错误的法线,为了修正这个问题,我们必须将这个法线归一化。下面是正确实现的代码:
- uniform
vec3 lightDir; - varying
vec3 normal; -
- void
main() - {
-
float intensity; -
vec4 color; -
intensity = dot(lightDir,normalize(normal)); -
-
if (intensity > 0.95) -
color = vec4(1.0,0.5,0.5,1.0); -
else if (intensity > 0.5) -
color = vec4(0.6,0.3,0.3,1.0); -
else if (intensity > 0.25) -
color = vec4(0.4,0.2,0.2,1.0); -
else -
color = vec4(0.2,0.1,0.1,1.0); -
gl_FragColor = color; - }
下一节我们将在OpenGL程序中设置shader中的光线方向。
卡通着色——版本3
结束关于卡通着色的内容之前,还有一件事需要解决:使用OpenGL中的光来代替变量lightDir。我们需要在OpenGL程序中定义一个光源,然后 在我们的shader中使用这个光源的方向数据。注意:不需要用glEnable打开这个光源,因为我们使用了shader。
我们假设OpenGL程序中定义的1号光源(GL_LIGHT0)是方向光。GLSL已经声明了一个C语言形式的结构体,描述光源属性。这些结构体组成一个数组,保存所有光源的信息。
- struct
gl_LightSourceParameters - {
-
vec4 ambient; -
vec4 diffuse; -
vec4 specular; -
vec4 position; -
... - };
-
- uniform
gl_LightSourceParameters gl_LightSource[gl_MaxLights];
OpenGL标准中规定,当一个光源的位置确定后,将自动转换到视点空间(eye space)的坐标系中,例如摄像机坐标系。如果模型视图矩阵的左上3×3子阵是正交的(如果使用gluLookAt并且不使用缩放变换就可以满足这 点),便能保证光线方向向量在自动变换到视点空间之后保持归一化。
我们必须将法线也变换到视点空间,然后计算其与光线的点积。只有在相同空间,计算两个向量的点积得到余弦值才有意义。
为了将法线变换到视点空间,我们必须使用预先定义的mat3型的一致变量gl_NormalMatrix。这个矩阵是模型视图矩阵的左上3×3子阵的逆矩 阵的转置矩阵(关于这个问题,后面的教程会专门解释)。需要对每个法线进行这个变换,现在顶点shader变为如下形式:
- varying
vec3 normal; -
- void
main() - {
-
normal = gl_NormalMatrix * gl_Normal; -
gl_Position = ftransform(); - }
- varying
vec3 normal; -
- void
main() - {
-
float intensity; -
vec4 color; -
vec3 n = normalize(normal); -
intensity = dot(vec3(gl_LightSource[0].position),n); -
-
if (intensity > 0.95) -
color = vec4(1.0,0.5,0.5,1.0); -
else if (intensity > 0.5) -
color = vec4(0.6,0.3,0.3,1.0); -
else if (intensity > 0.25) -
color = vec4(0.4,0.2,0.2,1.0); -
else -
color = vec4(0.2,0.1,0.1,1.0); -
gl_FragColor = color; - }
http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/toonf2.zip
基于GLEW的源代码下载地址:
http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/toonglut_2.0.zip
【GLSL教程】(六)逐顶点的光照
引言在OpenGL中有三种类型的光:方向光(directional)、点光(point)、聚光(spotlight)。本教程将从方向光讲起,首先我们将使用GLSL来模仿OpenGL中的光。
我们将向shader中逐渐添加环境光、散射光和高光效果。
- struct
gl_LightSourceParameters - {
-
vec4 ambient; -
vec4 diffuse; -
vec4 specular; -
vec4 position; -
vec4 halfVector; -
vec3 spotDirection; -
float spotExponent; -
float spotCutoff; // (range: [0.0,90.0], 180.0) -
float spotCosCutoff; // (range: [1.0,0.0],-1.0) -
float constantAttenuation; -
float linearAttenuation; -
float quadraticAttenuation; - };
-
- uniform
gl_LightSourceParameters gl_LightSource[gl_MaxLights]; - struct
gl_LightModelParameters - {
-
vec4 ambient; - };
- uniform
gl_LightModelParameters gl_LightModel;
- struct
gl_MaterialParameters - {
-
vec4 emission; -
vec4 ambient; -
vec4 diffuse; -
vec4 specular; -
float shininess; - };
-
- uniform
gl_MaterialParameters gl_FrontMaterial; - uniform
gl_MaterialParameters gl_BackMaterial;
方向光I
本节的公式来自《OpenGL编程指南》中“和光照有关的数学知识”这一章。
我们从散射光开始讨论。在OpenGL中假定,不管观察者的角度如何,得到的散射光强度总是相同的。散射光的强度与光源中散射光成分以及材质中散射光反射系数相关,此外也和入射光角度与物体表面法线的夹角相关。
这个公式就是Lambert漫反射模型。Lambert余弦定律描述了平面散射光的亮度,正比于平面法线与入射光线夹角的余弦,这一理论提出已经超过200年了。
在顶点shader中要实现这个公式,需要用到光源参数中的方向、散射成分强度,还要用到材质中的散射成分值。因此使用此shader时,在OpenGL中需要像在平时一样设置好光源。注意:由于没有使用固定功能流水线,所以不需要对光源调用glEnable。
要计算余弦值,首先要确保光线方向向量(gl_LightSource[0].position)与法线向量都是归一化的,然后就可以使用点积得到余弦值。注意:对方向光,OpenGL中保存的方向是从顶点指向光源,与上面图中画的相反。
OpenGL将光源的方向保存在视点空间坐标系内,因此我们需要把法线也变换到视点空间。完成这个变换可以用预先定义的一致变量gl_NormalMatrix。这个矩阵是模型视图变换矩阵的左上3×3子矩阵的逆矩阵的转置。
以下就是上述内容的顶点shader代码:
- void
main() - {
-
vec3 normal, lightDir; -
vec4 diffuse; -
float NdotL; -
-
-
normal = normalize(gl_NormalMatrix * gl_Normal); -
-
lightDir = normalize(vec3(gl_LightSource[0].position)); -
-
NdotL = max(dot(normal, lightDir), 0.0); -
-
diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse; -
gl_FrontColor = NdotL * diffuse; -
-
gl_Position = ftransform(); - }
- void
main() - {
-
gl_FragColor = gl_Color; - }
- void
main() - {
-
vec3 normal, lightDir; -
vec4 diffuse, ambient, globalAmbient; -
float NdotL; -
-
normal = normalize(gl_NormalMatrix * gl_Normal); -
lightDir = normalize(vec3(gl_LightSource[0].position)); -
NdotL = max(dot(normal, lightDir), 0.0); -
diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse; -
-
ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient; -
globalAmbient = gl_FrontMaterial.ambient * gl_LightModel.ambient; -
gl_FrontColor = NdotL * diffuse + globalAmbient + ambient; -
-
gl_Position = ftransform(); - }
方向光II
下面介绍OpenGL方向光中的镜面反射部分。我们使用称为Blin-Phong模型的光照模型,这是Phong模型的简化版。在这之前,我们有必要先看看Phong模型,以便于更好地理解Blin-Phong模型。
在Phong模型中,镜面反射成分和反射光线与视线夹角的余弦相关,如下图:
如果视线正好和反射光重合,我们将接收到最大的反射强度。当视线与反射光相分离时,反射强度将随之下降,下降速率可以由一个称为shininess的因子 控制,shininess的值越大,下降速率越快。也就是说,shininess越大的话,镜面反射产生的亮点就越小。在OpenGL中这个值的范围是0 到128。
Blinn提出了一种简化的模型,也就是Blinn-Phong模型。它基于半向量(half-vector),也就是方向处在观察向量以及光线向量之间的一个向量:
-
- if
(NdotL > 0.0) - {
-
// normalize the half-vector, and then compute the -
// cosine (dot product) with the normal -
NdotHV = max(dot(normal, gl_LightSource[0].halfVector.xyz),0.0); -
specular = gl_FrontMaterial.specular * gl_LightSource[0].specular * -
pow(NdotHV,gl_FrontMaterial.shininess); - }
http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/ogldirsd.zip
【GLSL教程】(七)逐像素的光照
逐像素的方向光(Directional Light per Pixel)这一节将把前面的shader代码改为逐像素计算的方向光。我们需要将工作按照两个shader拆分,以确定哪些是需要逐像素操作的。
首先看看每个顶点接收到的信息:
•法线
•半向量
•光源方向
我们需要将法线变换到视点空间然后归一化。我们还需要将半向量和光源方向也归一化,不过它们已经位于视点空间中了。这些归一化之后的向量会进行插值,然后送入片断shader,所以需要声明易变变量保存这些向量。
我们也可以在顶点shader中完成一些与光和材质相关的计算,这样可以帮助平衡顶点shader和片断shader的负载。
顶点shader代码可以写成如下形式:
- varying
vec4 diffuse,ambient; - varying
vec3 normal,lightDir,halfVector; -
- void
main() - {
-
-
normal = normalize(gl_NormalMatrix * gl_Normal); -
-
lightDir = normalize(vec3(gl_LightSource[0].position)); -
-
halfVector = normalize(gl_LightSource[0].halfVector.xyz); -
-
diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse; -
ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient; -
ambient += gl_FrontMaterial.ambient * gl_LightModel.ambient; -
-
gl_Position = ftransform(); - }
- varying
vec4 diffuse,ambient; - varying
vec3 normal,lightDir,halfVector; -
- void
main() - {
-
vec3 n,halfV; -
float NdotL,NdotHV; -
-
vec4 color = ambient; -
-
n = normalize(normal); -
-
NdotL = max(dot(n,lightDir),0.0); -
...
- ...
- if
(NdotL > 0.0) - {
-
color += diffuse * NdotL; -
halfV = normalize(halfVector); -
NdotHV = max(dot(n,halfV),0.0); -
color += gl_FrontMaterial.specular * -
gl_LightSource[0].specular * -
pow(NdotHV, gl_FrontMaterial.shininess); - }
-
- gl_FragColor
= color;
http://www.lighthouse3d.com/wp-content/uploads/2011/03/dirpixsd.zip
逐像素的点光(Point Light Per Pixel)
本节基于前面有关方向光的内容,大部分代码都相同。本节内容主要涉及方向光和点光的不同之处。方向光一般假设光源在无限远的地方,所以到达物体时是平行光。相反,点光源有一个空间中的位置,并向四面八方辐射光线。此外,点光的强度会随到达顶点的距离而衰弱。
对于OpenGL程序来说,这两种光的区别主要有:
•光源的position域的w分量:对方向光来说它是0,表面这个position实际是一个方向(direction);对点光来说,这个分量是1。
•点光源的衰减由三个系数决定:一个常数项,一个线性项和一个二次项。
对方向光来说,光线的方向对所有顶点相同,但是对点光来说,方向是从顶点指向光源位置的向量。因此对我们来说需要修改的就是在顶点shader中加入计算光线方向的内容。
在OpenGL中衰减是按照如下公式计算的:
注意衰减与距离是非线性关系,所以我们不能逐顶点计算衰减再在片断shader中使用插值结果,不过我们可以在顶点shader中计算距离,然后在片断shader中使用距离的插值计算衰减。
使用点光计算颜色值的公式为:
- varying
vec4 diffuse,ambientGlobal,ambient; - varying
vec3 normal,lightDir,halfVector; - varying
float dist; -
- void
main() - {
-
vec4 ecPos; -
vec3 aux; -
normal = normalize(gl_NormalMatrix * gl_Normal); -
-
-
ecPos = gl_ModelViewMatrix * gl_Vertex; -
aux = vec3(gl_LightSource[0].position-ecPos); -
lightDir = normalize(aux); -
dist = length(aux); -
halfVector = normalize(gl_LightSource[0].halfVector.xyz); -
-
-
diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse; -
-
-
ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient; -
ambientGlobal = gl_FrontMaterial.ambient * gl_LightModel.ambient; -
gl_Position = ftransform(); - }
- varying
vec4 diffuse,ambientGlobal, ambient; - varying
vec3 normal,lightDir,halfVector; - varying
float dist; -
- void
main() - {
-
vec3 n,halfV,viewV,ldir; -
float NdotL,NdotHV; -
vec4 color = ambientGlobal; -
float att; -
-
n = normalize(normal); -
-
NdotL = max(dot(n,normalize(lightDir)),0.0); -
if (NdotL > 0.0) -
{ -
att = 1.0 / (gl_LightSource[0].constantAttenuation + -
gl_LightSource[0].linearAttenuation * dist + -
gl_LightSource[0].quadraticAttenuation * dist * dist); -
color += att * (diffuse * NdotL + ambient); -
halfV = normalize(halfVector); -
NdotHV = max(dot(n,halfV),0.0); -
color += att * gl_FrontMaterial.specular * gl_LightSource[0].specular * -
pow(NdotHV,gl_FrontMaterial.shininess); -
} -
gl_FragColor = color; - }
http://www.lighthouse3d.com/wp-content/uploads/2011/03/pointlightsd.zip
逐像素的聚光(Spot Light Per Pixel)
本节内容与上一节基本一致,唯一不同的就是聚光不同于点光,其发出的光线被限制在一个圆锥体中。
对于OpenGL程序来说,这两种光的区别主要有:
•聚光包含一个方向向量spotDirection,表示圆锥体的轴。
•圆锥体包含一个角度,在GLSL中可以使用应用程序设置的角度值以及对应的余弦值spotCosCutoff。
•最后还有一个衰减速率spotExponent,它表示从圆锥的中心轴向外表面变化时光强度的衰减。
聚光的顶点shader与点光完全相同,我们只需要对片断shader进行一些修改。只有当当前片断位于聚光的光锥内时,才需要对散射光、镜面反射光和环境光成分进行着色。所以我们首先要检查这个条件。
光源与某点连线向量以及聚光方向向量(spotDirection)之间夹角的余弦值必须大于spotCosCutoff,否则此点位于聚光之外,只能接收到全局环境光。
- ...
-
- n
= normalize(normal); -
-
- NdotL
= max(dot(n,normalize(lightDir)),0.0); -
- if
(NdotL > 0.0) - {
-
spotEffect = dot(normalize(gl_LightSource[0].spotDirection), -
normalize(-lightDir)); -
if (spotEffect > gl_LightSource[0].spotCosCutoff) -
{ -
-
} - }
-
- gl_FragColor
= ...
- spotEffect
= pow(spotEffect, gl_LightSource[0].spotExponent); - att
= spotEffect / (gl_LightSource[0].constantAttenuation + -
gl_LightSource[0].linearAttenuation * dist + -
gl_LightSource[0].quadraticAttenuation * dist * dist); -
- color
+= att * (diffuse * NdotL + ambient); -
- halfV
= normalize(halfVector); - NdotHV
= max(dot(n,halfV),0.0); - color
+= att * gl_FrontMaterial.specular * -
gl_LightSource[0].specular * -
pow(NdotHV,gl_FrontMaterial.shininess);
http://www.lighthouse3d.com/wp-content/uploads/2011/03/spotlightsd.zip
【GLSL教程】(八)纹理贴图
简单的纹理贴图(Simple Texture)为了在GLSL中应用纹理,我们需要访问每个顶点的纹理坐标。GLSL中提供了一些属性变量,每个纹理单元一个:
- attribute
vec4 gl_MultiTexCoord0; - attribute
vec4 gl_MultiTexCoord1; - attribute
vec4 gl_MultiTexCoord2; - attribute
vec4 gl_MultiTexCoord3; - attribute
vec4 gl_MultiTexCoord4; - attribute
vec4 gl_MultiTexCoord5; - attribute
vec4 gl_MultiTexCoord6; - attribute
vec4 gl_MultiTexCoord7;
- uniform
mat4 gl_TextureMatrix[gl_MaxTextureCoords];
下面这条语句直接复制OpenGL程序中指定的纹理坐标,作为纹理单元0的顶点纹理坐标。
- gl_TexCoord[0]
= gl_MultiTexCoord0;
- void
main() - {
-
gl_TexCoord[0] = gl_MultiTexCoord0; -
gl_Position = ftransform(); - }
- void
main() - {
-
gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0; -
gl_Position = ftransform(); - }
为了访问纹理的数值,在片断shader中有必要声明一个特殊的变量,对一个2D纹理可以可以这样写:
- uniform
sampler2D tex;
这个用户定义的变量tex包含我们将会使用的纹理单元,通过texture2D函数我们可以得到一个纹素(texel),这是一个纹理图片中的像素。函数参数分别为simpler2D以及纹理坐标:
- vec4
texture2D(sampler2D, vec2);
我们的片断shader可以写成如下形式:
- uniform
sampler2D tex; -
- void
main() - {
-
vec4 color = texture2D(tex,gl_TexCoord[0].st); -
gl_FragColor = color; - }
http://www.lighthouse3d.com/wp-content/uploads/2011/03/textureSimple.zip
组合纹理与片断
OpenGL允许我们通过多种方式将纹理颜色和片断颜色联合到一起。下表显示了RGBA模式时可用的联合方式:
GL_REPLACE | C = Ct | A = At |
GL_MODULATE | C = Ct*Cf | A = At*Af |
GL_DECAL | C = Cf * (1 – At) + Ct * At | A = Af |
上一节的例子就相当于使用了GL_REPLACE模式。下面我们我们准备在一个立方体上实现与GL_MODULATE等同的效果。两个shader只计算使用一个白色方向光的散射以及环境光成分,关于材质的完整定义请参照光照有关的章节。
因为使用了光照,所以顶点shader中必须处理法线信息。必须将法线变换到视图空间然后归一化,光线方向向量也必须归一化(光线方向向量已经由OpenGL变换到了视图空间)。现在新的顶点shader如下:
- varying
vec3 lightDir,normal; -
- void
main() - {
-
normal = normalize(gl_NormalMatrix * gl_Normal); -
-
lightDir = normalize(vec3(gl_LightSource[0].position)); -
gl_TexCoord[0] = gl_MultiTexCoord0; -
-
gl_Position = ftransform(); - }
- varying
vec3 lightDir,normal; - uniform
sampler2D tex; -
- void
main() - {
-
vec3 ct,cf; -
vec4 texel; -
float intensity,at,af; -
-
intensity = max(dot(lightDir,normalize(normal)),0.0); -
cf = intensity * (gl_FrontMaterial.diffuse).rgb + -
gl_FrontMaterial.ambient.rgb; -
af = gl_FrontMaterial.diffuse.a; -
-
texel = texture2D(tex,gl_TexCoord[0].st); -
ct = texel.rgb; -
at = texel.a; -
-
gl_FragColor = vec4(ct * cf, at * af); - }
http://www.lighthouse3d.com/wp-content/uploads/2011/03/textureComb.zip
多重纹理
在GLSL中实现多重纹理十分容易,我们只需要访问所有纹理即可。因为我们打算给每个纹理使用相同的纹理坐标,所以顶点shader不需要改动。片断shader中只需要进行些许改动,加上多个纹理的颜色值。
- varying
vec3 lightDir,normal; - uniform
sampler2D tex; -
- void
main() - {
-
vec3 ct,cf; -
vec4 texel; -
float intensity,at,af; -
-
intensity = max(dot(lightDir,normalize(normal)),0.0); -
cf = intensity * (gl_FrontMaterial.diffuse).rgb + -
gl_FrontMaterial.ambient.rgb; -
af = gl_FrontMaterial.diffuse.a; -
-
texel = texture2D(tex,gl_TexCoord[0].st) + -
texture2D(l3d,gl_TexCoord[0].st); -
ct = texel.rgb; -
at = texel.a; -
-
gl_FragColor = vec4(ct * cf, at * af); - }
如果indensity是0,第二个纹理单元取最大值,如果indensity为1,只取第二个纹理单元颜色的10%,当indensity在0和1之间时按这两个大小进行插值。可以使用smoothstep函数实现这个要求:
- genType
smoothStep(genType edge0, genType edge1, genType x);
coef = smoothStep(1.0, 0.2, intensity);
下面的片断shader实现了需要的效果:
- varying
vec3 lightDir,normal; - uniform
sampler2D tex,l3d; -
- void
main() - {
-
vec3 ct,cf,c; -
vec4 texel; -
float intensity,at,af,a; -
-
intensity = max(dot(lightDir,normalize(normal)),0.0); -
-
cf = intensity * (gl_FrontMaterial.diffuse).rgb + -
gl_FrontMaterial.ambient.rgb; -
af = gl_FrontMaterial.diffuse.a; -
-
texel = texture2D(tex,gl_TexCoord[0].st); -
-
ct = texel.rgb; -
at = texel.a; -
-
c = cf * ct; -
a = af * at; -
-
float coef = smoothstep(1.0,0.2,intensity); -
c += coef * vec3(texture2D(l3d,gl_TexCoord[0].st)); -
-
gl_FragColor = vec4(c, a); - }
http://www.lighthouse3d.com/wp-content/uploads/2011/03/textureGlow.zip
【GLSL教程】(九)其他说明
法线矩阵在很多顶点shader中都用到了gl_NormalMatrix。这里将介绍这个矩阵是什么,以及它的作用。
大部分计算是在视图空间内完成的,主要原因是光照的运算要放在这个空间内,否则一些依赖观察点坐标的效果,比如镜面反射光就很难实现。
所以我们需要将法线变换到视图空间。变换一个顶点到视图空间的方法如下:
- vertexEyeSpace
= gl_ModelViewMatrix * gl_Vertex;
下面的代码很简单地实现了这个要求:
- normalEyeSpace
= vec3(gl_ModelViewMatrix * vec4(gl_Normal,0.0));
让我们看看潜在的问题:
在上图中,模型视图矩阵应用到所有顶点以及法线上,最后的结果明显错误:法线不再与三角面垂直了。
现在我们知道模型视图矩阵在某些情况下,不能用来变换法线向量。下面的问题就是:那么该使用哪个矩阵?
考虑一个3×3矩阵G,让我们看看要正确变换法线,这个矩阵该是什么样子。
我们知道,变换前切线和法线是垂直的,即T•N = 0,在变换后切线和法线同样应该保持垂直,即T’•N’ = 0。现在假设G是正确变换法线的矩阵,同时模型视图矩阵的左上3×3子矩阵M可以正确变换切线T(T是一个向量,所以w成分为0)。因为T可以通过两个顶 点的差来计算,所以变换顶点的矩阵同样可以用来变换T。由此可以得到如下等式:
在本节开始讨论过,某些情况下使用模型视图矩阵也可以。当模型视图矩阵的左上3×3子矩阵M正交时,可以得到:
M在什么时候能确定为正交的呢?当我们把几何变换限制为旋转和平移时(在OpenGL应用程序中只使用glRotate和glTranslate,而不使用glScale),就可以保证M正交。注意:gluLookAt同样建立正交矩阵。
关于法线归一化
当一个法线到达顶点shader后,我们一般会将它归一化:
- normal
= normalize(gl_NormalMatrix * gl_Normal);
我们可以避免归一化计算吗?在某些情况下是可行的。如果gl_NormalMatrix是正交矩阵,那么经过变换后输入法线的长度不会变,依然等于gl_Normal。所以如果在OpenGL程序中法线已经是归一化的,那么在shader中就不需要在重复了。
也就是说,如果我们使用gluLookAt设置照相机,对模型值进行旋转和平移变换,就可以在shader中避免使用归一化操作。这对于归一化过的光线向量也是适用的。
片断shader的情况
在片断shader中,我们经常发现需要重新归一化在顶点shader中归一化的法线。这是必要的吗?答案是肯定的。
考虑一个包含三个不同顶点法线的三角面。片断shader接收经过插值的法线,插值基于距离三个顶点的远近。这样得到的法线方向是对的,但不再是单位长度了。
下图显示了原因。图中黑线表示三角面,顶点法线用蓝色表示,插值得到的片断法线用绿色表示。所有的插值法线排列在黑色的点划线上。从图上可以看出绿色的插值法线大小小于单位长度的顶点法线。
有一种情况,在片断shader中可以避免归一化操作,那就是每个顶点法线方向相同,而且顶点法线是经过归一化的。此时顶点法线插值得到的结果都相同。
以方向光为例,每个片断都需要考虑光线方向,如果光线向量已经在之前归一化了,在片断shader中就可以避免归一化这一步。