GLSL基础

转自: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程序发送数据到顶点处理器,每个顶点中包含一个颜色信息和一个位置信息。

[cpp]  view plain copy
  1. glBegin(...);  
  2.     glColor3f(0.2,0.4,0.6);  
  3.     glVertex3f(-1.0,1.0,2.0);  
  4.     glColor3f(0.2,0.4,0.8);  
  5.     glVertex3f(1.0,-1.0,2.0);  
  6. glEnd();  
一个顶点shader可以编写代码实现如下功能:

·使用模型视图矩阵以及投影矩阵进行顶点变换

·法线变换及归一化

·纹理坐标生成和变换

·逐顶点或逐像素光照计算

·颜色计算

不一定要完成上面的所有操作,例如你的程序可能不使用光照。但是,一旦你使用了顶点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库:

http://glew.sourceforge.net/

下面的代码检查OpenGL 2.0是否可用:

[cpp]  view plain copy
  1. #include <GL/glew.h>  
  2. #include <GL/glut.h>  
  3.   
  4. void main(int argc, char **argv)  
  5.  
  6.     glutInit(&argc, argv);  
  7.     ...  
  8.     glewInit();  
  9.   
  10.     if (glewIsSupported("GL_VERSION_2_0"))  
  11.         printf("Ready for OpenGL 2.0\n");  
  12.     else  
  13.      
  14.         printf("OpenGL 2.0 not supported\n");  
  15.         exit(1);  
  16.      
  17.     setShaders();  
  18.   
  19.     glutMainLoop();  
  20.  

下图显示了创建shader的必要步骤,函数的具体使用方法将在下面各小结描述:


创建shader

下图显示了创建shader的步骤:


首先创建一个对象作为shader的容器,这个创建函数将返回容器的句柄。

[cpp]  view plain copy
  1. GLuint glCreateShader(GLenum shaderType);  
  2. 参数:  
  3. ·shaderType – GL_VERTEX_SHADER or GL_FRAGMENT_SHADER.  
你可以创建许多shader,但记住所有的顶点shader只能有一个main函数,所有像素shader也一样。

下一步将添加源代码。shader的源代码是一个字符串数组,添加的语法如下:

[cpp]  view plain copy
  1. void glShaderSource(GLuint shader, int numOfStrings, const char **strings, int *lenOfStrings);  
  2. 参数:  
  3. ·shader – the handler to the shader.  
  4. ·numOfStrings – the number of strings in the array.  
  5. ·strings – the array of strings.  
  6. ·lenOfStrings – an array with the length of each string, or NULL, meaning that the strings are NULL terminated.  
最后编译shader:
[cpp]  view plain copy
  1. void glCompileShader(GLuint shader);  
  2. 参数:  
  3. •shader – the handler to the shader.  


创建程序

下图显示了获得一个可以运行的shader程序的步骤:


首先创建一个对象,作为程序的容器。此函数返回容器的句柄。

[cpp]  view plain copy
  1. GLuint glCreateProgram(void);  

你可以创建任意多个程序,在渲染时,可以在不同程序中切换,甚至在某帧返回固定功能流水线。比如你想用折射和反射shader绘制一个茶杯,然后回到固定功能生成立方体环境贴图(cube map)显示背景。

下面将把上一节编译的shader附加到刚刚创建的程序中。方法如下:

[cpp]  view plain copy
  1. void glAttachShader(GLuint program, GLuint shader);  
  2. 参数:  
  3. ·program – the handler to the program.  
  4. ·shader – the handler to the shader you want to attach.  
如果同时有顶点shader和片断shader,你需要把它们都附加到程序中。你可以把多个相同类型(顶点或像素)的shader附加到一个程序中,如同一个C程序可以有多个模块一样,但它们只能有一个main函数。

你也可以把一个shader附加到多个程序,比如你想在不同程序中使用某个相同的shader。

最后一步是连接程序。方法如下:

[cpp]  view plain copy
  1. void glLinkProgram(GLuint program);  
  2. 参数:  
  3. ·program – the handler to the program.  
在连接操作之后,shader的源代码可以被修改并重编译,并不会影响到整个程序。

程序连接后,可以调用glUseProgram来使用程序。每个程序都分配了一个句柄,你可以事先连接多个程序以备使用。

[cpp]  view plain copy
  1. void glUseProgram(GLuint prog);  
  2. 参数:  
  3. ·prog – the handler to the program you want to use, or zero to return to fixed functionality.  
当一个程序被使用后,如果被再次连接,它将被自动替换并投入使用,所以没有必要再次调用上面这个函数。如果使用的参数为0,表示将使用固定功能流水线。

 例子

下面的代码包含了上面描述的所有步骤,参数p,f,v是全局的GLuint型变量。

[cpp]  view plain copy
  1. void setShaders()  
  2.  
  3.     char *vs,*fs;  
  4.   
  5.     glCreateShader(GL_VERTEX_SHADER);  
  6.     glCreateShader(GL_FRAGMENT_SHADER);    
  7.   
  8.     vs textFileRead("toon.vert");  
  9.     fs textFileRead("toon.frag");  
  10.   
  11.     const char *vv vs;  
  12.     const char *ff fs;  
  13.   
  14.     glShaderSource(v, 1, &vv, NULL);  
  15.     glShaderSource(f, 1, &ff, NULL);  
  16.   
  17.     free(vs);free(fs);  
  18.   
  19.     glCompileShader(v);  
  20.     glCompileShader(f);  
  21.   
  22.     glCreateProgram();  
  23.   
  24.     glAttachShader(p, v);  
  25.     glAttachShader(p, f);  
  26.   
  27.     glLinkProgram(p);  
  28.     glUseProgram(p);  
  29.  
GLUT版的完整例子如下:

http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/glutglsl_2.0.zip

完整例子中包含了shader代码及文本文件读入程序。

 

错误处理

调试shader是很困难的。目前还没有像printf这样的东西,虽然未来可能出现有调试功能的开发工具。

编译阶段的状态可以用如下函数获得:

[cpp]  view plain copy
  1. void glGetShaderiv(GLuint object, GLenum type, int *param);  
  2. 参数:  
  3. ·object – the handler to the object. Either shader or program  
  4. ·type – GL_COMPILE_STATUS.  
  5. ·param – the return value, GL_TRUE if OK, GL_FALSE otherwise.  
连接阶段的状态可以用如下函数获得:
[cpp]  view plain copy
  1. void glGetProgramiv(GLuint object, GLenum type, int *param);  
  2. 参数:  
  3. ·object – the handler to the object. Either shader or program  
  4. ·type – GL_LINK_STATUS.  
  5. ·param – the return value, GL_TRUE if OK, GL_FALSE otherwise.  
如 果发生错误,就需要从InfoLog中找到更多的信息。这个日志保存了最后一次操作的信息,比如编译时的警告、错误,连接时发生的各种问题。这个日志甚至 可以告诉你硬件是否支持你的shader。不幸的是InfoLog没有一个规范,所以不同的驱动/硬件可能产生不同的日志信息。

为了获得特定shader或程序的日志,可以使用如下程序:

[cpp]  view plain copy
  1. void glGetShaderInfoLog(GLuint object, int maxLen, int *len, char *log);  
  2. void glGetProgramInfoLog(GLuint object, int maxLen, int *len, char *log);  
  3. 参数:  
  4. ·object – the handler to the object. Either shader or program  
  5. ·maxLen – The maximum number of chars to retrieve from the InfoLog.  
  6. ·len – returns the actual length of the retrieved InfoLog.  
  7. ·log – The log itself.  
GLSL规范有必要在这里进行一些改进:你必须知道接收InfoLog的长度。为了找到这个准确的值,使用下面的函数:
[cpp]  view plain copy
  1. void glGetShaderiv(GLuint object, GLenum type, int *param);  
  2. void glGetProgramiv(GLuint object, GLenum type, int *param);  
  3. 参数:  
  4. ·object – the handler to the object. Either shader or program  
  5. ·type – GL_INFO_LOG_LENGTH.  
  6. ·param – the return value, the length of the InfoLog.  

下面的函数可以用来打印InfoLog的内容:

[cpp]  view plain copy
  1. void printShaderInfoLog(GLuint obj)  
  2.  
  3.     int infologLength 0;  
  4.     int charsWritten  0;  
  5.     char *infoLog;  
  6.    
  7.     glGetShaderiv(obj, GL_INFO_LOG_LENGTH,&infologLength);  
  8.    
  9.     if (infologLength 0)  
  10.      
  11.         infoLog (char *)malloc(infologLength);  
  12.         glGetShaderInfoLog(obj, infologLength, &charsWritten, infoLog);  
  13.         printf("%s\n",infoLog);  
  14.         free(infoLog);  
  15.      
  16.   
  17.   
  18. void printProgramInfoLog(GLuint obj)  
  19.  
  20.     int infologLength 0;  
  21.     int charsWritten  0;  
  22.     char *infoLog;  
  23.    
  24.     glGetProgramiv(obj, GL_INFO_LOG_LENGTH,&infologLength);  
  25.    
  26.     if (infologLength 0)  
  27.      
  28.         infoLog (char *)malloc(infologLength);  
  29.         glGetProgramInfoLog(obj, infologLength, &charsWritten, infoLog);  
  30.         printf("%s\n",infoLog);  
  31.         free(infoLog);  
  32.      
  33.  

清理

前面的小节讲到了附加一个shader到一个程序中,这里的调用是将shader从程序中分离:

[cpp]  view plain copy
  1. void glDetachShader(GLuint program, GLuint shader);  
  2. 参数:  
  3. ·program – The program to detach from.  
  4. ·shader – The shader to detach.  
注意,只有没有附加到任何程序的shader可以被删除,删除shader和程序的调用如下:
[cpp]  view plain copy
  1. void glDeleteShader(GLuint id);  
  2. void glDeleteProgram(GLuint id);  
  3. 参数:  
  4. ·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还可以定义结构体:

[cpp]  view plain copy
  1. struct dirlight  
  2.  
  3.     vec3 direction;  
  4.     vec3 color;  
  5. };  
变量 声明一个基本类型变量的方法与C类似,你还可以在声明它的同时进行初始化。
[cpp]  view plain copy
  1. float a,b;       // two vector (yes, the comments are like in C)  
  2. int 2;       // is initialized with 2  
  3. bool true // is true  
声明其它类型变量也是按照这种方法,但是初始化与C语言有区别。GLSL非常依赖构造函数实现初始化和类型转换。
[cpp]  view plain copy
  1. float 2;          // incorrect, there is no automatic type casting  
  2. float (float)2; // incorrect, requires constructors for type casting  
  3. int 2;  
  4. float float(a); // correct. is 2.0  
  5. vec3 f;                // declaring as vec3  
  6. vec3 vec3(1.0,2.0,3.0); // declaring and initializing g  
在GLSL中使用一些变量初始化其它变量是非常灵活的。你只需要给出需要的数据成员即可。请看下面的例子:
[cpp]  view plain copy
  1. vec2 vec2(1.0,2.0);  
  2. vec2 vec2(3.0,4.0);  
  3. vec4 vec4(a,b)   // vec4(1.0,2.0,3.0,4.0);  
  4. vec2 vec2(1.0,2.0);  
  5. float 3.0;  
  6. vec3 vec3(g,h);  
矩阵的初始化也是类似方法,矩阵包含很多种构造函数,下面的例子给出了一些初始化矩阵的构造函数:
[cpp]  view plain copy
  1. mat4 mat4(1.0)   // initializing the diagonal of the matrix with 1.0  
  2. vec2 vec2(1.0,2.0);  
  3. vec2 vec2(3.0,4.0);  
  4. mat2 mat2(a,b); // matrices are assigned in column major order  
  5. mat2 mat2(1.0,0.0,1.0,0.0); // all elements are specified  

下面的例子给出了初始化结构体的方法:

[cpp]  view plain copy
  1. struct dirlight     // type definition  
  2.  
  3.     vec3 direction;  
  4.     vec3 color;  
  5. };  
  6. dirlight d1;  
  7. dirlight d2 dirlight(vec3(1.0,1.0,0.0),vec3(0.8,0.8,0.4));  
在GLSL中还有一些实用的选择子(selector),可以简化我们的操作并让代码更简洁。访问一个向量可以使用如下的方法:
[cpp]  view plain copy
  1. vec4 vec4(1.0,2.0,3.0,4.0);  
  2. float posX a.x;  
  3. float posY a[1];  
  4. vec2 posXY a.xy;  
  5. float depth a.w  
在上面的代码片段中,可以使用x、y、z、w来访问向量成员。如果是颜色的话可以使用r、g、b、a,如果是纹理坐标的话可以使用s、t、p、q。注意表示纹理坐标通常是使用s、t、r、q,但r已经表示颜色中的红色(red)了,所以纹理坐标中需要使用p来代替。 矩阵的选择子可以使用一个或两个参数,比如m[0]或者m[2][3]。第一种情况选择了第一列,第二种情况选择了一个数据成员。

对于结构体来说,可以像在C语言中一样访问其成员。所以访问前面定义的结构体,可以使用如下的代码:

[cpp]  view plain copy
  1. 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中定义的变量名即可:

[cpp]  view plain copy
  1. GLint glGetUniformLocation(GLuint program, const char *name);  
  2. 参数:  
  3. ·program – the hanuler to the program  
  4. ·name – the name of the variable  
返回值就是变量位置,可以用此信息设置变量的值。根据变量的数据类型不同,有一系列函数可以用来设置一致变量。用来设置浮点值的一组函数如下:
[cpp]  view plain copy
  1. void glUniform1f(GLint location, GLfloat v0);  
  2. void glUniform2f(GLint location, GLfloat v0, GLfloat v1);  
  3. void glUniform3f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2);  
  4. void glUniform4f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);  
  5. 或者  
  6. GLint glUniform{1,2,3,4}fv(GLint location, GLsizei count, GLfloat *v);  
  7. 参数:  
  8. ·location – the previously queried location  
  9. ·v0,v1,v2,v3 – float values  
  10. ·count – the number of elements in the array  
  11. ·v – an array of floats  

对integer类型也有一组类似的函数,不过要用i替换函数中的f。对bool类型没有专门的函数,但可以使用整数的0和1来表示真假。一旦你使用了一致变量数组,那么就必须使用向量版本的函数。

对sampler变量,使用函数glUniform1i和glUniform1iv。

矩阵也是一种GLSL的数据类型,所以也有一组针对矩阵的函数:

[cpp]  view plain copy
  1. GLint glUniformMatrix{2,3,4}fv(GLint location, GLsizei count, GLboolean transpose, GLfloat *v);  
  2. 参数:  
  3. location – the previously queried location.  
  4. count – the number of matrices. if single matrix is being set, or for an array of matrices.  
  5. transpose – wheter to transpose the matrix values. value of indicates that the matrix values are specified in row major order, zero is column major order  
  6. – an array of floats.  

还有一点要注意的是:使用这些函数之后,变量的值将保持到程序再次连接之时。一旦进行重新连接,所有变量的值将被重置为0。

最后是一些示例代码。假设一个shader中使用了如下变量:

[cpp]  view plain copy
  1. uniform float specIntensity;  
  2. uniform vec4 specColor;  
  3. uniform float t[2];  
  4. uniform vec4 colors[3];  

在OpenGL程序中可以使用下面的代码设置这些变量:

[cpp]  view plain copy
  1. GLint loc1,loc2,loc3,loc4;  
  2. float specIntensity 0.98;  
  3. float sc[4] {0.8,0.8,0.8,1.0};  
  4. float threshold[2] {0.5,0.25};  
  5. float colors[12] {0.4,0.4,0.8,1.0,  
  6.                 0.2,0.2,0.4,1.0,  
  7.                 0.1,0.1,0.1,1.0};  
  8.   
  9. loc1 glGetUniformLocation(p,"specIntensity");  
  10. glUniform1f(loc1,specIntensity);  
  11.   
  12. loc2 glGetUniformLocation(p,"specColor");  
  13. glUniform4fv(loc2,1,sc);  
  14.   
  15. loc3 glGetUniformLocation(p,"t");  
  16. glUniform1fv(loc3,2,threshold);  
  17.   
  18. loc4 glGetUniformLocation(p,"colors");  
  19. glUniform4fv(loc4,3,colors);  
例子代码的下载地址: http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/glutglsl2_2.0.zip

注 意设置一个数组(例子中的t)与设置四元向量(例子中的colors和specColor)的区别。中间的count参数指在shader中声明的数组元 素数量,而不是在OpenGL程序中声明的。所以虽然specColor包含4个值,但glUniform4fv函数中的参数是1,因为它只是一个向量。 另一种设置specColor的方法:

[cpp]  view plain copy
  1. loc2 glGetUniformLocation(p,"specColor");  
  2. glUniform4f(loc2,sc[0],sc[1],sc[2],sc[3]);  
GLSL中还可以获取数组中某个变量的地址。比如,可以获得t[1]的地址。下面的代码片段展示了设置t数组元素的另一种方法:
[cpp]  view plain copy
  1. loct0 glGetUniformLocation(p,"t[0]");  
  2. glUniform1f(loct0,threshold[0]);  
  3.   
  4. loct1 glGetUniformLocation(p,"t[1]");  
  5. glUniform1f(loct1,threshold[1]);  

注意在glGetUniformLocation中使用方括号指示的变量。


属性变量(Attribute Variables)

在前一节提到,一致变量只能针对一个图元全体,就是说不能在glBegin和glEnd之间改变。

如果要针对每个顶点设置变量,那就需要属性变量了。事实上属性变量可以在任何时刻更新。在顶点shader中属性变量是只读的。因为它包含的是顶点数据,所以在片断shader中不能直接应用。

与一致变量相似,首先你需要获得变量在内存中的位置,这个信息只有在连接程序之后才可获得。注意,对某些驱动程序,在获得存储位置前还必须使用程序。

[cpp]  view plain copy
  1. GLint glGetAttribLocation(GLuint program,char *name);  
  2. 参数:  
  3. program – the handle to the program.  
  4. name – the name of the variable  
上述函数调用的返回变量在存储器中的地址。下面就可以为它指定一个值,类似一致变量,每种数据类型都有对应的函数。
[cpp]  view plain copy
  1. void glVertexAttrib1f(GLint location, GLfloat v0);  
  2. void glVertexAttrib2f(GLint location, GLfloat v0, GLfloat v1);  
  3. void glVertexAttrib3f(GLint location, GLfloat v0, GLfloat v1,GLfloat v2);  
  4. void glVertexAttrib4f(GLint location, GLfloat v0, GLfloat v1,,GLfloat v2, GLfloat v3);  
  5. 或者  
  6. GLint glVertexAttrib{1,2,3,4}fv(GLint location, GLfloat *v);  
  7. 参数:  
  8. location – the previously queried location.  
  9. v0,v1,v2,v3 – float values.  
  10. – an array of floats.  
对于integer类型,也有一组类似的函数。与一致变量不同,这里向量版的函数并不支持对向量数组的赋值,所以函数参数用向量或是分别指定的效果没有太大区别,就好像OpenGL中glColor3f和glColor3fv的关系。 下面是一个简单的例子,假定顶点shader中声明了一个名为height的浮点属性变量,在程序连接之后可以进行如下操作:
[cpp]  view plain copy
  1. loc glGetAttribLocation(p,"height");  
在执行渲染的代码中间可以为shader中的变量赋值:
[cpp]  view plain copy
  1. glBegin(GL_TRIANGLE_STRIP);  
  2.     glVertexAttrib1f(loc,2.0);  
  3.     glVertex2f(-1,1);  
  4.     glVertexAttrib1f(loc,2.0);  
  5.     glVertex2f(1,1);  
  6.     glVertexAttrib1f(loc,-2.0);  
  7.     glVertex2f(-1,-1);  
  8.     glVertexAttrib1f(loc,-2.0);  
  9.     glVertex2f(1,-1);  
  10. glEnd();  
例子代码的下载地址:

http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/glutglsl3_2.0.zip

顶点数组和属性变量也可以一起使用。首先需要使能数组,使用如下函数:

 

[cpp]  view plain copy
  1. void glEnableVertexAttribArray(GLint loc);  
  2. 参数:  
  3. loc – the location of the variable.  
接下来使用函数提交包含数据的数组指针:

 

[cpp]  view plain copy
  1. void glVertexAttribPointer(GLint loc, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer);  
  2. 参数:  
  3. loc – the location of the variable.  
  4. size – the number of components per element, for instance: for floatfor vec2; for vec3, and so on.  
  5. type – The data type associated: GL_FLOAT is an example.  
  6. normalized – if set to then the array values will be normalized, converted to range from -1 to for signed data, or to for unsigned data.  
  7. stride – the spacing between elements. Exactly the same as in OpenGL.  
  8. pointer – pointer to the array containing the data.  
下面是示例代码,首先执行初始化,定义了顶点数组和属性数组。

 

[cpp]  view plain copy
  1. float vertices[8] {-1,1, 1,1, -1,-1, 1,-1};  
  2. float heights[4] {2,2,-2,-2};  
  3. ...  
  4. loc glGetAttribLocation(p,"height");  
  5.   
  6. glEnableClientState(GL_VERTEX_ARRAY);  
  7. glEnableVertexAttribArray(loc);  
  8. glVertexPointer(2,GL_FLOAT,0,vertices);  
  9. glVertexAttribPointer(loc,1,GL_FLOAT,0,0,heights);  
接下来的渲染步骤与OpenGL中的通常做法一致,比如调用glDrawArrays。示例源代码下载地址: http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/glutglsl4_2.0.zip


易变变量(Varying Variables)

前 面说过,shader包括两种类型:顶点shader和片断shader。为了计算片断的值,往往需要访问顶点的插值数据。例如,当使用逐片断光照时,我 们需要知道当前片断的法线,但是在OpenGL中只为每个顶点指定了法线。顶点shader可以访问这些法线,而片断shader不能,因为法线是 OpenGL程序作为属性变量指定的。

顶 点变换后的数据移动到流水线的下一个阶段,在这个阶段通过使用连接信息,生成了所有图元并完成片断化。对每个片断,有一组变量会被自动进行插值并提供给片 断shader,这些都是固定功能。片断的颜色就是这么处理的,到达片断shader的颜色就是组成图元的顶点颜色插值的结果。

像片断shader接收到的这种插值产生的变量,就是“易变变量”类型。GLSL包含一些预先定义的易变变量,例如前面提到的颜色。用户也可以自己定义易变变量,它们必须同时在顶点shader和片断shader中声明:

[cpp]  view plain copy
  1. varying float intensity;  
一个易变变量必须先在顶点shader中声明,然后计算每个顶点的变量值。在片断shader中,接收这个变量通过插值得到的结果,注意此时这个变量是只读的。


语句和函数

控制流语句

与C语言类似,GLSL中有类似if-else的条件语句,for,while,do-while等循环语句。

 

[cpp]  view plain copy
  1. if (bool expression)  
  2.     ...  
  3. else  
  4.     ...  
  5.   
  6. for (initialization; bool expression; loop expression)  
  7.     ...  
  8.   
  9. while (bool expression)  
  10.     ...  
  11.   
  12. do  
  13.     ...  
  14. while (bool expression)  
GLSL也有跳转语句:

·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都必须包含一个如下方式声明的主函数:

 

[cpp]  view plain copy
  1. void main()  
此外用户还可以自定义函数。这些函数像C函数一样,一般都会有返回值,返回值的类型没有限制,但不能是数组。

函数参数可以有如下修饰符:

·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类型。

最后还有两点要注意:

·允许函数重载,只要参数不同。

·在标准中没有定义递归行为。

结束本节之前来看一个函数的例子:

[cpp]  view plain copy
  1. vec4 toonify(in float intensity)  
  2.  
  3.     vec4 color;  
  4.     if (intensity 0.98)  
  5.        color vec4(0.8,0.8,0.8,1.0);  
  6.     else if (intensity 0.5)  
  7.        color vec4(0.4,0.4,0.8,1.0);  
  8.     else if (intensity 0.25)  
  9.        color vec4(0.2,0.2,0.4,1.0);  
  10.     else  
  11.        color vec4(0.1,0.1,0.1,1.0);   
  12.   
  13.     return(color);  


【GLSL教程】(四)shder的简单示例

GLSL的Hello World

这一节中包含一个最基本的shader,它提供如下功能:顶点变换然后使用单一的颜色渲染图元。

顶点shader

前面已经说过,顶点shader负责完成顶点变换。这里将按照固定功能的方程完成顶点变换。

固定功能流水线中一个顶点通过模型视图矩阵以及投影矩阵进行变换,使用如下公式:

[cpp]  view plain copy
  1. vTrans projection modelview *incomingVertex  
首先GLSL需要访问OpenGL状态,获得公式中的前两个矩阵。前面讲过,GLSL可以获取某些OpenGL状态信息的,这两个矩阵当然包括在内。可以通过预先定义的一致变量来获取它们:
[cpp]  view plain copy
  1. uniform mat4 gl_ModelViewMatrix;  
  2. uniform mat4 gl_ProjectionMatrix;  
接下来需要得到输入的顶点。通过预先定义的属性变量,所有的顶点将可以一个个传入顶点shader中。
[cpp]  view plain copy
  1. attribute vec4 gl_Vertex;  
为了输出变换后的顶点,shader必须写入预先定义的vec4型变量gl_Position中,注意这个变量没有修饰符。

现在我们可以写一个仅仅进行顶点变换的顶点shader了。注意所有其他功能都将丧失,比如没有光照计算。顶点shader必须有一个main函数,如下面的代码所示:

[cpp]  view plain copy
  1. void main()  
  2.  
  3.     gl_Position =gl_ProjectionMatrix gl_ModelViewMatrix gl_Vertex;  
  4.  
上面代码中变换每个顶点时,投影矩阵都将乘上模型视图矩阵,这显然非常浪费时间,因为这些矩阵不是随每个顶点变化的。注意这些矩阵是一致变量

GLSL提供一些派生的矩阵,也就是说gl_ModelViewProjectionMatrix是上面两个矩阵的乘积,所以顶点shader也可以写成下面这样:

[cpp]  view plain copy
  1. void main()  
  2.  
  3.     gl_Position =gl_ModelViewProjectionMatrigl_Vertex;  
  4.  
上面的操作能够获得和固定功能流水线相同的结果吗?理论上是如此,但实际上对顶点变换操作的顺序可能会不同。顶点变换通常在显卡中是高度优化的任务,所以有一个利用了这种优化的特定函数用来处理这个任务。这个神奇的函数如下:
[cpp]  view plain copy
  1. vec4 ftransform(void);  
使用这个函数的另一个原因是float数据类型的精度限制。由于数据精度的限制,当使用不同的顺序计算时,可能得到不同的结果,因此GLSL提供这个函数保证获得最佳性能的同时,还能得到与固定功能流水线相同的结果。

这个函数按照与固定功能相同的步骤对输入顶点进行变换,然后返回变换后的顶点。所以shader可以重新写成如下形式:

[cpp]  view plain copy
  1. void main()  
  2.  
  3.     gl_Position =ftransform();  
  4.  
片断shader

片断shader也有预先定义的变量gl_FragColor,可以向其中写入片断的颜色值。下面的代码就是一个片断shader,将所有片断绘制成淡蓝色:

[cpp]  view plain copy
  1. void main()  
  2.  
  3.     gl_FragColor =vec4(0.4,0.4,0.8,1.0);  
  4.  
可以在此获得本节例子的源码:

http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/glutglsl5_2.0.zip


颜色shader

GLSL可以读取一些OpenGL状态,在本节我们将学习如何访问在OpenGL中设置的glColor变量。

GLSL有一个属性变量记录当前颜色,也提供易变变量从顶点shader向片断shader传递颜色值。

[cpp]  view plain copy
  1. attribute vec4 gl_Color;  
  2. varying vec4 gl_FrontColor; // writable onthe vertex shader  
  3. varying vec4 gl_BackColor; // writable onthe vertex shader  
  4. 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的例子,只计算了正面颜色:

[cpp]  view plain copy
  1. void main()  
  2.  
  3.     gl_FrontColor =gl_Color;  
  4.     gl_Position =ftransform();  
  5.  

片断shader更加简单:

[cpp]  view plain copy
  1. void main()  
  2.  
  3.     gl_FragColor gl_Color;  
  4.  
基于GLEW的源代码:

http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/colorglut_2.0.zip

 

扁平shader(Flatten Shader)

着色器编程让我们可以探索一些新效果,本节的例子展示了用奇怪的方法操作顶点得到的效果。

首先我们要得到一个扁平的3D模型,只需要在应用模型视图变换时将模型顶点的z坐标设为0就行了。下面是顶点shader的代码:

[cpp]  view plain copy
  1. void main(void 
  2.  
  3.     vec4 vec4(gl_Vertex);  
  4.     v.z 0.0;  
  5.   
  6.     gl_Position =gl_ModelViewProjectionMatriv;  
  7.  
我们先将gl_Vertex变量复制到一个局部变量v中。gl_Vertex是一个GLSL提供的属性变量,所以在顶点shader中它是只读的。

片断shader与“GLSL中的Hello World”一节相同,就只用设置一种颜色。

一个扁平的茶壶效果如下:


更进一步,我们需要在z坐标上使用一个正弦函数。将z坐标作为x坐标的函数,这样茶杯将呈现波浪的效果:

[cpp]  view plain copy
  1. void main(void 
  2.  
  3.     vec4 =vec4(gl_Vertex);  
  4.     v.z sin(5.0*v.x)*0.25;  
  5.     gl_Position =gl_ModelViewProjectionMatriv;  
  6.  
最 后我们需要加入一些顶点动画效果。为了达到这个目的我们需要增加一个变量记录变化的时间,或者帧数。一个顶点shader是无法记录不同顶点值的,更不用 说记录不同的帧了。所以我们需要在OpenGL程序中定义这个变量,然后作为一致变量传递给shader。假设在OpenGL程序中有一个名为time的 帧计数器,在shader中有个同名的一致变量。

顶点shader的代码如下:

[cpp]  view plain copy
  1. uniform float time;  
  2.   
  3. void main(void 
  4.  
  5.     vec4 =vec4(gl_Vertex);  
  6.     v.z sin(5.0*v.x +time*0.01)*0.25;  
  7.     gl_Position =gl_ModelViewProjectionMatriv;  
  8.  
在有关一致变量的小节讲过,在OpenGL程序中需要两个步骤:

·setup: 获取一致变量的存储位置

·render: 更新一致变量

设置(setup)步骤只有一条语句:

[cpp]  view plain copy
  1. loc =glGetUniformLocation(p,"time");  
这里p是程序的句柄,time与顶点shader中定义的一致变量名称相同。变量loc是Glint类型的,必须定义在下面的渲染(render)函数也可以访问到的地方。渲染函数如下所示:
[cpp]  view plain copy
  1. void renderScene(void 
  2.  
  3.     glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);  
  4.     glLoadIdentity();  
  5.     gluLookAt(0.0,0.0,5.0,  
  6.           0.0,0.0,0.0,  
  7.           0.0f,1.0f,0.0f);  
  8.     glUniform1f(loc, time);  
  9.   
  10.     glutSolidTeapot(1);  
  11.     time+=0.01;  
  12.     glutSwapBuffers();  
  13.  
函数中的变量time在程序一开始初始化,然后每帧都会进行自增运算。

本节的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中这样定义光的方向:
[cpp]  view plain copy
  1. 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函数计算。
[cpp]  view plain copy
  1. intensity dot(lightDir, gl_Normal);  
最后顶点要做的就是变换顶点坐标。顶点shader的完整代码如下:
[cpp]  view plain copy
  1. uniform vec3 lightDir;  
  2. varying float intensity;  
  3.   
  4. void main()  
  5.  
  6.     intensity dot(lightDir,gl_Normal);  
  7.     gl_Position ftransform();  
  8.  
如果想使用OpenGL中的变量作为光的方向,那么可以用gl_LightSource[0].position代替一致变量lightDir,代码如下:
[cpp]  view plain copy
  1. varying float intensity;  
  2.   
  3. void main()  
  4.  
  5.     vec3 lightDir normalize(vec3(gl_LightSource[0].position));  
  6.     intensity dot(lightDir,gl_Normal);  
  7.   
  8.     gl_Position ftransform();  
  9.  
现 在在片断shader中唯一要做的就是根据intensity定义片断的颜色。前面已经提到,变量intensity在两个shader中都定义为易变变 量,所以它将会在顶点shader中写入,然后再片断shader中读出。片断shader中的颜色可以用如下方式计算:
[cpp]  view plain copy
  1. vec4 color;  
  2.   
  3. if (intensity 0.95)  
  4.     color vec4(1.0,0.5,0.5,1.0);  
  5. else if (intensity 0.5)  
  6.     color vec4(0.6,0.3,0.3,1.0);  
  7. else if (intensity 0.25)  
  8.     color vec4(0.4,0.2,0.2,1.0);  
  9. else  
  10.     color vec4(0.2,0.1,0.1,1.0);  
可以看到,余弦大于0.95时使用最亮的颜色,小于0.25时使用最暗的颜色。得到这个颜色后只需要再将其写入gl_FragColor即可,片断shader的完整代码如下:
[cpp]  view plain copy
  1. varying float intensity;  
  2.   
  3. void main()  
  4.  
  5.     vec4 color;  
  6.     if (intensity 0.95)  
  7.   
  8.         color vec4(1.0,0.5,0.5,1.0);  
  9.     else if (intensity 0.5)  
  10.         color vec4(0.6,0.3,0.3,1.0);  
  11.     else if (intensity 0.25)  
  12.         color vec4(0.4,0.2,0.2,1.0);  
  13.     else  
  14.         color vec4(0.2,0.1,0.1,1.0);  
  15.     gl_FragColor color;  
  16.  
下图显示出本节的最终效果,看起来不是很好。主要原因是因为我们对intensity进行插值,插值的结果与用片断法线算出的intensity有区别,下一节我们将展示如何更好的实现卡通着色效果。


卡通着色——版本2
本节中我们要实现逐片断的卡通着色效果。为了达到这个目的,我们需要访问每个片断的法线。顶点shader中需要将顶点的法线写入一个易变变量,这样在片断shader中就可以得到经过插值的法线。
顶点shader比上一版变得更简单了,因为颜色强度的计算移到片断shader中进行了。一致变量lightDir也要移到片断shader中,下面就是新的顶点shader代码:
[cpp]  view plain copy
  1. varying vec3 normal;  
  2.   
  3. void main()  
  4.  
  5.     normal gl_Normal;  
  6.     gl_Position ftransform();  
  7.  
在片断shader中,我们需要声明一致变量lightDir,还需要一个易变变量接收插值后的法线。片断shader的代码如下:
[cpp]  view plain copy
  1. uniform vec3 lightDir;  
  2. varying vec3 normal;  
  3.   
  4. void main()  
  5.  
  6.     float intensity;  
  7.     vec4 color;  
  8.     intensity dot(lightDir,normal);  
  9.   
  10.     if (intensity 0.95)  
  11.         color vec4(1.0,0.5,0.5,1.0);  
  12.     else if (intensity 0.5)  
  13.         color vec4(0.6,0.3,0.3,1.0);  
  14.     else if (intensity 0.25)  
  15.         color vec4(0.4,0.2,0.2,1.0);  
  16.     else  
  17.         color vec4(0.2,0.1,0.1,1.0);  
  18.     gl_FragColor color;  
  19.  
下图就是渲染结果:

令人吃惊的是新的渲染结果居然和前一节的一模一样,这是为什么呢?
让我们仔细看看这两个版本的区别。在第一版中,我们在顶点shader中计算出一个intensity值,然后在片断shader中使用这个值的插值结 果。在第二个版中,我们先对法线插值,然后在片断shader中计算点积。插值和点积都是线性运算,所以两者运算的顺序并不影响结果。
真正的问题在于片断shader中对插值后的法线进行点积运算的时候,尽管这时法线的方向是对的,但是它并没有归一化。
我们说法线方向是对的,因为我们假定传入顶点shader的法线是经过归一化的,对法线插值可以得到一个方向正确的向量。但是,这个向量的长度在大部分情 况下都是错的,因为对归一化法线进行插值时,只有在所有法线的方向一致时才会得到一个单位长度的向量。(关于法线插值的问题,后面的教程会专门解释)
综上所述,在片断shader中,我们接收到的是一个方向正确长度错误的法线,为了修正这个问题,我们必须将这个法线归一化。下面是正确实现的代码:
[cpp]  view plain copy
  1. uniform vec3 lightDir;  
  2. varying vec3 normal;  
  3.   
  4. void main()  
  5.  
  6.     float intensity;  
  7.     vec4 color;  
  8.     intensity dot(lightDir,normalize(normal));  
  9.   
  10.     if (intensity 0.95)  
  11.         color vec4(1.0,0.5,0.5,1.0);  
  12.     else if (intensity 0.5)  
  13.         color vec4(0.6,0.3,0.3,1.0);  
  14.     else if (intensity 0.25)  
  15.         color vec4(0.4,0.2,0.2,1.0);  
  16.     else  
  17.         color vec4(0.2,0.1,0.1,1.0);  
  18.     gl_FragColor color;  
  19.  
下图就是新版本卡通着色的效果,看起来漂亮多了,虽然并不完美。图中物体还有些锯齿(aliasing)问题,不过这超出了本教程讨论的范围。

下一节我们将在OpenGL程序中设置shader中的光线方向。

卡通着色——版本3
结束关于卡通着色的内容之前,还有一件事需要解决:使用OpenGL中的光来代替变量lightDir。我们需要在OpenGL程序中定义一个光源,然后 在我们的shader中使用这个光源的方向数据。注意:不需要用glEnable打开这个光源,因为我们使用了shader。
我们假设OpenGL程序中定义的1号光源(GL_LIGHT0)是方向光。GLSL已经声明了一个C语言形式的结构体,描述光源属性。这些结构体组成一个数组,保存所有光源的信息。
[cpp]  view plain copy
  1. struct gl_LightSourceParameters  
  2.  
  3.     vec4 ambient;  
  4.     vec4 diffuse;  
  5.     vec4 specular;  
  6.     vec4 position;  
  7.     ...  
  8. };  
  9.   
  10. uniform gl_LightSourceParameters gl_LightSource[gl_MaxLights];  
这意味着我们可以在shader中访问光源的方向(使用结构体中的position域),这里依然假定OpenGL程序对光源方向进行了归一化。
OpenGL标准中规定,当一个光源的位置确定后,将自动转换到视点空间(eye space)的坐标系中,例如摄像机坐标系。如果模型视图矩阵的左上3×3子阵是正交的(如果使用gluLookAt并且不使用缩放变换就可以满足这 点),便能保证光线方向向量在自动变换到视点空间之后保持归一化。
我们必须将法线也变换到视点空间,然后计算其与光线的点积。只有在相同空间,计算两个向量的点积得到余弦值才有意义。
为了将法线变换到视点空间,我们必须使用预先定义的mat3型的一致变量gl_NormalMatrix。这个矩阵是模型视图矩阵的左上3×3子阵的逆矩 阵的转置矩阵(关于这个问题,后面的教程会专门解释)。需要对每个法线进行这个变换,现在顶点shader变为如下形式:
[cpp]  view plain copy
  1. varying vec3 normal;  
  2.   
  3. void main()  
  4.  
  5.     normal gl_NormalMatrix gl_Normal;  
  6.     gl_Position ftransform();  
  7.  
在片断shader中,我们必须访问光线方向来计算intensity值:
[cpp]  view plain copy
  1. varying vec3 normal;  
  2.   
  3. void main()  
  4.  
  5.     float intensity;  
  6.     vec4 color;  
  7.     vec3 normalize(normal);  
  8.     intensity dot(vec3(gl_LightSource[0].position),n);  
  9.   
  10.     if (intensity 0.95)  
  11.         color vec4(1.0,0.5,0.5,1.0);  
  12.     else if (intensity 0.5)  
  13.         color vec4(0.6,0.3,0.3,1.0);  
  14.     else if (intensity 0.25)  
  15.         color vec4(0.4,0.2,0.2,1.0);  
  16.     else  
  17.         color vec4(0.2,0.1,0.1,1.0);  
  18.     gl_FragColor color;  
  19.  
本小节内容的Shader Desinger工程下载地址:
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中逐渐添加环境光、散射光和高光效果。

后面的教程中我们将使用逐像素光照以获得更好的效果。

接下来我们将实现逐像素的点光和聚光。这些内容与方向光很相近,大部分代码都是通用的。

在卡通着色的教程中我们接触过在GLSL中如何访问OpenGL状态中关于光源的部分,这些数据描述了每个光源的参数。
[cpp]  view plain copy
  1. struct gl_LightSourceParameters  
  2.  
  3.     vec4 ambient;  
  4.     vec4 diffuse;  
  5.     vec4 specular;  
  6.     vec4 position;  
  7.     vec4 halfVector;  
  8.     vec3 spotDirection;  
  9.     float spotExponent;  
  10.     float spotCutoff; // (range: [0.0,90.0], 180.0)  
  11.     float spotCosCutoff; // (range: [1.0,0.0],-1.0)  
  12.     float constantAttenuation;  
  13.     float linearAttenuation;  
  14.     float quadraticAttenuation;  
  15. };  
  16.   
  17. uniform gl_LightSourceParameters gl_LightSource[gl_MaxLights];  
  18. struct gl_LightModelParameters  
  19.  
  20.     vec4 ambient;  
  21. };  
  22. uniform gl_LightModelParameters gl_LightModel;  
在GLSL中也同样可以访问材质参数:
[cpp]  view plain copy
  1. struct gl_MaterialParameters  
  2.  
  3.     vec4 emission;  
  4.     vec4 ambient;  
  5.     vec4 diffuse;  
  6.     vec4 specular;  
  7.     float shininess;  
  8. };  
  9.   
  10. uniform gl_MaterialParameters gl_FrontMaterial;  
  11. uniform gl_MaterialParameters gl_BackMaterial;  
在OpenGL程序中,这些参数中的大部分,不论属于光源还是材质,用起来都是相似的。我们将使用这些参数实现自己的方向光。

方向光I
本节的公式来自《OpenGL编程指南》中“和光照有关的数学知识”这一章。
我们从散射光开始讨论。在OpenGL中假定,不管观察者的角度如何,得到的散射光强度总是相同的。散射光的强度与光源中散射光成分以及材质中散射光反射系数相关,此外也和入射光角度与物体表面法线的夹角相关。

OpenGL用下面的公式计算散射光成分:

I是反射光的强度,Ld是光源的散射成分(gl_LightSource[0].diffuse),Md是材质的散射系数(gl_FrontMaterial.diffuse)。
这个公式就是Lambert漫反射模型。Lambert余弦定律描述了平面散射光的亮度,正比于平面法线与入射光线夹角的余弦,这一理论提出已经超过200年了。
在顶点shader中要实现这个公式,需要用到光源参数中的方向、散射成分强度,还要用到材质中的散射成分值。因此使用此shader时,在OpenGL中需要像在平时一样设置好光源。注意:由于没有使用固定功能流水线,所以不需要对光源调用glEnable。
要计算余弦值,首先要确保光线方向向量(gl_LightSource[0].position)与法线向量都是归一化的,然后就可以使用点积得到余弦值。注意:对方向光,OpenGL中保存的方向是从顶点指向光源,与上面图中画的相反。
OpenGL将光源的方向保存在视点空间坐标系内,因此我们需要把法线也变换到视点空间。完成这个变换可以用预先定义的一致变量gl_NormalMatrix。这个矩阵是模型视图变换矩阵的左上3×3子矩阵的逆矩阵的转置。
以下就是上述内容的顶点shader代码:
[cpp]  view plain copy
  1. void main()  
  2.  
  3.     vec3 normal, lightDir;  
  4.     vec4 diffuse;  
  5.     float NdotL;  
  6.   
  7.       
  8.     normal normalize(gl_NormalMatrix gl_Normal);  
  9.       
  10.     lightDir normalize(vec3(gl_LightSource[0].position));  
  11.       
  12.     NdotL max(dot(normal, lightDir), 0.0);  
  13.       
  14.     diffuse gl_FrontMaterial.diffuse gl_LightSource[0].diffuse;  
  15.     gl_FrontColor  NdotL diffuse;  
  16.   
  17.     gl_Position ftransform();  
  18.  
在片断shader中要做的就是使用易变变量gl_Color设置颜色。
[cpp]  view plain copy
  1. void main()  
  2.  
  3.     gl_FragColor gl_Color;  
  4.  
下图显示了应用此shader的茶壶效果。注意茶壶的底部非常黑,这是因为还没有使用环境光的缘故。

加入环境光非常容易,只需要使用一个全局的环境光参数以及光源的环境光参数即可,公式如下所示:

前面的顶点shader中需要加入几条语句完成环境光的计算:
[cpp]  view plain copy
  1. void main()  
  2.  
  3.     vec3 normal, lightDir;  
  4.     vec4 diffuse, ambient, globalAmbient;  
  5.     float NdotL;  
  6.   
  7.     normal normalize(gl_NormalMatrix gl_Normal);  
  8.     lightDir normalize(vec3(gl_LightSource[0].position));  
  9.     NdotL max(dot(normal, lightDir), 0.0);  
  10.     diffuse gl_FrontMaterial.diffuse gl_LightSource[0].diffuse;  
  11.       
  12.     ambient gl_FrontMaterial.ambient gl_LightSource[0].ambient;  
  13.     globalAmbient gl_FrontMaterial.ambient gl_LightModel.ambient;  
  14.     gl_FrontColor  NdotL diffuse globalAmbient ambient;  
  15.   
  16.     gl_Position ftransform();  
  17.  
下图显示了最终效果。加入环境光后整个画面都变亮了,不过相对于应用了反射光效果的全局光照模型(global illumination model),这种计算环境光的方式只能算廉价的解决方案。


方向光II
下面介绍OpenGL方向光中的镜面反射部分。我们使用称为Blin-Phong模型的光照模型,这是Phong模型的简化版。在这之前,我们有必要先看看Phong模型,以便于更好地理解Blin-Phong模型。
在Phong模型中,镜面反射成分和反射光线与视线夹角的余弦相关,如下图:

L表示入射光,N表示法线,Eye表示从顶点指向观察点的视线,R是L经镜面反射后的结果,镜面反射成分与α角的余弦相关。
如果视线正好和反射光重合,我们将接收到最大的反射强度。当视线与反射光相分离时,反射强度将随之下降,下降速率可以由一个称为shininess的因子 控制,shininess的值越大,下降速率越快。也就是说,shininess越大的话,镜面反射产生的亮点就越小。在OpenGL中这个值的范围是0 到128。

计算反射光向量的公式:

OpenGL中使用Phong模型计算镜面反射成分的公式:

式中指数s就是shininess因子,Ls是光源中镜面反射强度,Ms是材质中的镜面反射系数。
Blinn提出了一种简化的模型,也就是Blinn-Phong模型。它基于半向量(half-vector),也就是方向处在观察向量以及光线向量之间的一个向量:

现在可以利用半向量和法线之间夹角的余弦来计算镜面反射成分。OpenGL所使用的Blinn-Phong模型计算镜面反射的公式如下:

这个方法与显卡的固定流水线中使用的方法相同。因为我们要模拟OpenGL中的方向光,所以在shader中也使用此公式。幸运的是:OpenGL会帮我们算半向量,我们只需要使用下面的代码:
[cpp]  view plain copy
  1.   
  2. if (NdotL 0.0)  
  3.  
  4.     // normalize the half-vector, and then compute the  
  5.     // cosine (dot product) with the normal  
  6.     NdotHV max(dot(normal, gl_LightSource[0].halfVector.xyz),0.0);  
  7.     specular gl_FrontMaterial.specular gl_LightSource[0].specular  
  8.             pow(NdotHV,gl_FrontMaterial.shininess);  
  9.  
完整的Shader Designer工程下载:
http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/ogldirsd.zip

【GLSL教程】(七)逐像素的光照

逐像素的方向光(Directional Light per Pixel)
这一节将把前面的shader代码改为逐像素计算的方向光。我们需要将工作按照两个shader拆分,以确定哪些是需要逐像素操作的。
首先看看每个顶点接收到的信息:
•法线
•半向量
•光源方向
我们需要将法线变换到视点空间然后归一化。我们还需要将半向量和光源方向也归一化,不过它们已经位于视点空间中了。这些归一化之后的向量会进行插值,然后送入片断shader,所以需要声明易变变量保存这些向量。
我们也可以在顶点shader中完成一些与光和材质相关的计算,这样可以帮助平衡顶点shader和片断shader的负载。
顶点shader代码可以写成如下形式:
[cpp]  view plain copy
  1. varying vec4 diffuse,ambient;  
  2. varying vec3 normal,lightDir,halfVector;  
  3.   
  4. void main()  
  5.  
  6.       
  7.     normal normalize(gl_NormalMatrix gl_Normal);  
  8.       
  9.     lightDir normalize(vec3(gl_LightSource[0].position));  
  10.       
  11.     halfVector normalize(gl_LightSource[0].halfVector.xyz);  
  12.       
  13.     diffuse gl_FrontMaterial.diffuse gl_LightSource[0].diffuse;  
  14.     ambient gl_FrontMaterial.ambient gl_LightSource[0].ambient;  
  15.     ambient += gl_FrontMaterial.ambient gl_LightModel.ambient;  
  16.   
  17.     gl_Position ftransform();  
  18.  
接下来在片断shader中,首先要声明同样的易变变量。此外还要再次对法线进行归一化,光线向量不需要进行归一化了,因为方向光对所有顶点都是一致的,插值得到的结果自然也不会变。之后就是计算插值过的法线向量与光线向量的点积。
[cpp]  view plain copy
  1. varying vec4 diffuse,ambient;  
  2. varying vec3 normal,lightDir,halfVector;  
  3.   
  4. void main()  
  5.  
  6.     vec3 n,halfV;  
  7.     float NdotL,NdotHV;  
  8.       
  9.     vec4 color ambient;  
  10.       
  11.     normalize(normal);  
  12.       
  13.     NdotL max(dot(n,lightDir),0.0);  
  14.     ...  
如果点积结果NdotL大于0,我们就必须计算散射光,也就是用顶点shader传过来的散射项乘以这个点积。我们还需要计算镜面反射光,计算时首先对接收到的半向量归一化,然后计算半向量和法线之间的点积。
[cpp]  view plain copy
  1. ...  
  2. if (NdotL 0.0)  
  3.  
  4.     color += diffuse NdotL;  
  5.     halfV normalize(halfVector);  
  6.     NdotHV max(dot(n,halfV),0.0);  
  7.     color += gl_FrontMaterial.specular  
  8.             gl_LightSource[0].specular  
  9.             pow(NdotHV, gl_FrontMaterial.shininess);  
  10.  
  11.   
  12. gl_FragColor color;  
下图显示了逐像素光照和逐顶点光照效果的区别:

本节内容Shader Designer的工程下载地址:
http://www.lighthouse3d.com/wp-content/uploads/2011/03/dirpixsd.zip

逐像素的点光(Point Light Per Pixel)
本节基于前面有关方向光的内容,大部分代码都相同。本节内容主要涉及方向光和点光的不同之处。方向光一般假设光源在无限远的地方,所以到达物体时是平行光。相反,点光源有一个空间中的位置,并向四面八方辐射光线。此外,点光的强度会随到达顶点的距离而衰弱。
对于OpenGL程序来说,这两种光的区别主要有:
•光源的position域的w分量:对方向光来说它是0,表面这个position实际是一个方向(direction);对点光来说,这个分量是1。
•点光源的衰减由三个系数决定:一个常数项,一个线性项和一个二次项。
对方向光来说,光线的方向对所有顶点相同,但是对点光来说,方向是从顶点指向光源位置的向量。因此对我们来说需要修改的就是在顶点shader中加入计算光线方向的内容。
在OpenGL中衰减是按照如下公式计算的:

式中k0是常数衰减系数,k1是线性衰减系数,k2是二次衰减系数,d是光源位置到顶点的距离。
注意衰减与距离是非线性关系,所以我们不能逐顶点计算衰减再在片断shader中使用插值结果,不过我们可以在顶点shader中计算距离,然后在片断shader中使用距离的插值计算衰减。
使用点光计算颜色值的公式为:

在上面公式中,环境光部分必须分解为两项:使用光照模型的全局环境光设置和光源中的环境光设置。顶点shader也必须分别计算这两个环境光成分。新的顶点shader如下:
[cpp]  view plain copy
  1. varying vec4 diffuse,ambientGlobal,ambient;  
  2. varying vec3 normal,lightDir,halfVector;  
  3. varying float dist;  
  4.   
  5. void main()  
  6.  
  7.     vec4 ecPos;  
  8.     vec3 aux;  
  9.     normal normalize(gl_NormalMatrix gl_Normal);  
  10.   
  11.       
  12.     ecPos gl_ModelViewMatrix gl_Vertex;  
  13.     aux vec3(gl_LightSource[0].position-ecPos);  
  14.     lightDir normalize(aux);  
  15.     dist length(aux);  
  16.     halfVector normalize(gl_LightSource[0].halfVector.xyz);  
  17.   
  18.       
  19.     diffuse gl_FrontMaterial.diffuse gl_LightSource[0].diffuse;  
  20.       
  21.       
  22.     ambient gl_FrontMaterial.ambient gl_LightSource[0].ambient;  
  23.     ambientGlobal gl_FrontMaterial.ambient gl_LightModel.ambient;  
  24.     gl_Position ftransform();  
  25.  
在片断shader中需要计算衰减,还需要将插值得到的光线方向向量归一化,因为一般来说照到每个顶点的光线方向都不同。
[cpp]  view plain copy
  1. varying vec4 diffuse,ambientGlobal, ambient;  
  2. varying vec3 normal,lightDir,halfVector;  
  3. varying float dist;  
  4.   
  5. void main()  
  6.  
  7.     vec3 n,halfV,viewV,ldir;  
  8.     float NdotL,NdotHV;  
  9.     vec4 color ambientGlobal;  
  10.     float att;  
  11.       
  12.     normalize(normal);  
  13.       
  14.     NdotL max(dot(n,normalize(lightDir)),0.0);  
  15.     if (NdotL 0.0)  
  16.      
  17.         att 1.0 (gl_LightSource[0].constantAttenuation  
  18.                 gl_LightSource[0].linearAttenuation dist  
  19.                 gl_LightSource[0].quadraticAttenuation dist dist);  
  20.         color += att (diffuse NdotL ambient);  
  21.         halfV normalize(halfVector);  
  22.         NdotHV max(dot(n,halfV),0.0);  
  23.         color += att gl_FrontMaterial.specular gl_LightSource[0].specular  
  24.                         pow(NdotHV,gl_FrontMaterial.shininess);  
  25.      
  26.     gl_FragColor color;  
  27.  
下图显示了固定功能的逐顶点与本节中逐像素计算得到的点光效果:

本节内容Shader Designer工程下载地址:
http://www.lighthouse3d.com/wp-content/uploads/2011/03/pointlightsd.zip

逐像素的聚光(Spot Light Per Pixel)
本节内容与上一节基本一致,唯一不同的就是聚光不同于点光,其发出的光线被限制在一个圆锥体中。
对于OpenGL程序来说,这两种光的区别主要有:
•聚光包含一个方向向量spotDirection,表示圆锥体的轴。
•圆锥体包含一个角度,在GLSL中可以使用应用程序设置的角度值以及对应的余弦值spotCosCutoff。
•最后还有一个衰减速率spotExponent,它表示从圆锥的中心轴向外表面变化时光强度的衰减。
聚光的顶点shader与点光完全相同,我们只需要对片断shader进行一些修改。只有当当前片断位于聚光的光锥内时,才需要对散射光、镜面反射光和环境光成分进行着色。所以我们首先要检查这个条件。
光源与某点连线向量以及聚光方向向量(spotDirection)之间夹角的余弦值必须大于spotCosCutoff,否则此点位于聚光之外,只能接收到全局环境光。
[cpp]  view plain copy
  1. ...  
  2.   
  3. normalize(normal);  
  4.   
  5.   
  6. NdotL max(dot(n,normalize(lightDir)),0.0);  
  7.   
  8. if (NdotL 0.0)  
  9.  
  10.     spotEffect dot(normalize(gl_LightSource[0].spotDirection),  
  11.             normalize(-lightDir));  
  12.     if (spotEffect gl_LightSource[0].spotCosCutoff)  
  13.      
  14.           
  15.      
  16.  
  17.   
  18. gl_FragColor ...  
下面的光照计算与点光非常相似,唯一区别是衰减必须乘以聚光效果(spotlight effect),这个值按如下公式计算:

上式中spotDirection来自 OpenGL中设置的状态,lightDir是光源到某点的向量,spotExp是聚光衰减率,这个值也是在OpenGL程序中设置的,它用来控制从聚光 光锥中心到边缘的衰减。spotExp越大衰减越快,如果为0表示在光锥内光强是常数。
[cpp]  view plain copy
  1. spotEffect pow(spotEffect, gl_LightSource[0].spotExponent);  
  2. att spotEffect (gl_LightSource[0].constantAttenuation  
  3.         gl_LightSource[0].linearAttenuation dist  
  4.         gl_LightSource[0].quadraticAttenuation dist dist);  
  5.   
  6. color += att (diffuse NdotL ambient);  
  7.   
  8. halfV normalize(halfVector);  
  9. NdotHV max(dot(n,halfV),0.0);  
  10. color += att gl_FrontMaterial.specular  
  11.             gl_LightSource[0].specular  
  12.             pow(NdotHV,gl_FrontMaterial.shininess);  
下图分别显示了使用固定功能流水线的逐顶点光照计算,以及使用本节shader的逐像素光照计算得到的聚光效果。

本节内容Shader Designer的工程下载地址:
http://www.lighthouse3d.com/wp-content/uploads/2011/03/spotlightsd.zip


【GLSL教程】(八)纹理贴图

简单的纹理贴图(Simple Texture)
为了在GLSL中应用纹理,我们需要访问每个顶点的纹理坐标。GLSL中提供了一些属性变量,每个纹理单元一个:
[cpp]  view plain copy
  1. attribute vec4 gl_MultiTexCoord0;  
  2. attribute vec4 gl_MultiTexCoord1;  
  3. attribute vec4 gl_MultiTexCoord2;  
  4. attribute vec4 gl_MultiTexCoord3;  
  5. attribute vec4 gl_MultiTexCoord4;  
  6. attribute vec4 gl_MultiTexCoord5;  
  7. attribute vec4 gl_MultiTexCoord6;  
  8. attribute vec4 gl_MultiTexCoord7;  
GLSL还为访问每个纹理的纹理矩阵提供了一个一致变量数组:
[cpp]  view plain copy
  1. uniform mat4 gl_TextureMatrix[gl_MaxTextureCoords];  
顶点shader可以通过上面所示的内容访问OpenGL程序中指定的纹理坐标。然后必须为每个顶点计算纹理坐标,并保存在预先定义的易变变量gl_TexCoord[i]中,i表示纹理单元号。
下面这条语句直接复制OpenGL程序中指定的纹理坐标,作为纹理单元0的顶点纹理坐标。
[cpp]  view plain copy
  1. gl_TexCoord[0]  gl_MultiTexCoord0;  
下面是个简单的例子,在顶点shader中设置纹理单元0的纹理坐标。
[cpp]  view plain copy
  1. void main()  
  2.  
  3.     gl_TexCoord[0] gl_MultiTexCoord0;  
  4.     gl_Position ftransform();  
  5.  
如果你想使用纹理矩阵,可以这样操作:
[cpp]  view plain copy
  1. void main()  
  2.  
  3.     gl_TexCoord[0] gl_TextureMatrix[0] gl_MultiTexCoord0;  
  4.     gl_Position ftransform();  
  5.  
前面说过,gl_TexCoord是一个易变变量,所以在片断shder中可以访问经过插值的纹理坐标。
为了访问纹理的数值,在片断shader中有必要声明一个特殊的变量,对一个2D纹理可以可以这样写:
[cpp]  view plain copy
  1. uniform sampler2D tex;  
如果是1D或者3D的纹理,可以改成sampler1D和sampler3D。
这个用户定义的变量tex包含我们将会使用的纹理单元,通过texture2D函数我们可以得到一个纹素(texel),这是一个纹理图片中的像素。函数参数分别为simpler2D以及纹理坐标:
[cpp]  view plain copy
  1. vec4 texture2D(sampler2D, vec2);  
函数的返回值已经考虑了所有在OpenGL程序中定义的纹理设置,比如过滤、mipmap、clamp等。
我们的片断shader可以写成如下形式:
[cpp]  view plain copy
  1. uniform sampler2D tex;  
  2.   
  3. void main()  
  4.  
  5.     vec4 color texture2D(tex,gl_TexCoord[0].st);  
  6.     gl_FragColor color;  
  7.  
注意访问gl_TexCoord时选择子st的使用。在本教程前面关于数据类型和变量的讨论中说过,访问纹理坐标时可以使用如下选择子:s、t、p、q。(r因为和rgb选择子冲突而没有使用)

本节内容Shader Designer的工程下载地址:
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
表中Ct和At表示纹理的颜色和alpha值,Cf和Af表示片断(fragment)的颜色和alpha值,C和A表示最终的颜色和alpha值。
上一节的例子就相当于使用了GL_REPLACE模式。下面我们我们准备在一个立方体上实现与GL_MODULATE等同的效果。两个shader只计算使用一个白色方向光的散射以及环境光成分,关于材质的完整定义请参照光照有关的章节。
因为使用了光照,所以顶点shader中必须处理法线信息。必须将法线变换到视图空间然后归一化,光线方向向量也必须归一化(光线方向向量已经由OpenGL变换到了视图空间)。现在新的顶点shader如下:

[cpp]  view plain copy
  1. varying vec3 lightDir,normal;  
  2.   
  3. void main()  
  4.  
  5.     normal normalize(gl_NormalMatrix gl_Normal);  
  6.   
  7.     lightDir normalize(vec3(gl_LightSource[0].position));  
  8.     gl_TexCoord[0] gl_MultiTexCoord0;  
  9.   
  10.     gl_Position ftransform();  
  11.  
在片断shader中,光照得到的片断的颜色和alpha值在cf和af中分别计算。shader中剩余代码按照GL_MODULATE的公式计算:
[cpp]  view plain copy
  1. varying vec3 lightDir,normal;  
  2. uniform sampler2D tex;  
  3.   
  4. void main()  
  5.  
  6.     vec3 ct,cf;  
  7.     vec4 texel;  
  8.     float intensity,at,af;  
  9.   
  10.     intensity max(dot(lightDir,normalize(normal)),0.0);  
  11.     cf intensity (gl_FrontMaterial.diffuse).rgb  
  12.                   gl_FrontMaterial.ambient.rgb;  
  13.     af gl_FrontMaterial.diffuse.a;  
  14.   
  15.     texel texture2D(tex,gl_TexCoord[0].st);  
  16.     ct texel.rgb;  
  17.     at texel.a;  
  18.   
  19.     gl_FragColor vec4(ct cf, at af);  
  20.  

Shader Designer的工程下载地址:
http://www.lighthouse3d.com/wp-content/uploads/2011/03/textureComb.zip

多重纹理
在GLSL中实现多重纹理十分容易,我们只需要访问所有纹理即可。因为我们打算给每个纹理使用相同的纹理坐标,所以顶点shader不需要改动。片断shader中只需要进行些许改动,加上多个纹理的颜色值。
[cpp]  view plain copy
  1. varying vec3 lightDir,normal;  
  2. uniform sampler2D tex;  
  3.   
  4. void main()  
  5.  
  6.     vec3 ct,cf;  
  7.     vec4 texel;  
  8.     float intensity,at,af;  
  9.   
  10.     intensity max(dot(lightDir,normalize(normal)),0.0);  
  11.     cf intensity (gl_FrontMaterial.diffuse).rgb  
  12.                   gl_FrontMaterial.ambient.rgb;  
  13.     af gl_FrontMaterial.diffuse.a;  
  14.   
  15.     texel texture2D(tex,gl_TexCoord[0].st)  
  16.           texture2D(l3d,gl_TexCoord[0].st);  
  17.     ct texel.rgb;  
  18.     at texel.a;  
  19.   
  20.     gl_FragColor vec4(ct cf, at af);  
  21.  
效果如下:

下面添加点不同的效果:在黑暗中发光。我们希望第二个纹理能在黑暗中发光,在没有光照时达到最亮,在有光照时变暗。

我们通过两步计算最终的颜色:首先将第一个纹理与片断颜色进行modulate计算,然后根据光照强度(indensity)加上第二个纹理单元。
如果indensity是0,第二个纹理单元取最大值,如果indensity为1,只取第二个纹理单元颜色的10%,当indensity在0和1之间时按这两个大小进行插值。可以使用smoothstep函数实现这个要求:
[cpp]  view plain copy
  1. genType smoothStep(genType edge0, genType edge1, genType x);  
如果x <= edge0结果是0,如果x >= edge1结果为1,如果edge0 < x < edge1结果在0和1之间进行Hermite插值。在本例中我们按如下方式调用:
coef = smoothStep(1.0, 0.2, intensity);
下面的片断shader实现了需要的效果:
[cpp]  view plain copy
  1. varying vec3 lightDir,normal;  
  2. uniform sampler2D tex,l3d;  
  3.   
  4. void main()  
  5.  
  6.     vec3 ct,cf,c;  
  7.     vec4 texel;  
  8.     float intensity,at,af,a;  
  9.   
  10.     intensity max(dot(lightDir,normalize(normal)),0.0);  
  11.   
  12.     cf intensity (gl_FrontMaterial.diffuse).rgb  
  13.                       gl_FrontMaterial.ambient.rgb;  
  14.     af gl_FrontMaterial.diffuse.a;  
  15.   
  16.     texel texture2D(tex,gl_TexCoord[0].st);  
  17.   
  18.     ct texel.rgb;  
  19.     at texel.a;  
  20.   
  21.     cf ct;  
  22.     af at;  
  23.   
  24.     float coef smoothstep(1.0,0.2,intensity);  
  25.     += coef  vec3(texture2D(l3d,gl_TexCoord[0].st));  
  26.   
  27.     gl_FragColor vec4(c, a);  
  28.  
Shader Designer的工程下载地址:
http://www.lighthouse3d.com/wp-content/uploads/2011/03/textureGlow.zip

【GLSL教程】(九)其他说明

法线矩阵
在很多顶点shader中都用到了gl_NormalMatrix。这里将介绍这个矩阵是什么,以及它的作用。
大部分计算是在视图空间内完成的,主要原因是光照的运算要放在这个空间内,否则一些依赖观察点坐标的效果,比如镜面反射光就很难实现。
所以我们需要将法线变换到视图空间。变换一个顶点到视图空间的方法如下:
[cpp]  view plain copy
  1. vertexEyeSpace gl_ModelViewMatrix gl_Vertex;  
对法线也能如此操作吗?一个法线是3个浮点数组成的向量,而模型视图矩阵是一个4×4的矩阵。另外,因为法线是一个向量,我们只需要变换它的方向,而模型视图矩阵中左上方的3×3子矩阵正好包含了旋转变换。所以可不可以用法线来乘这个子矩阵呢?
下面的代码很简单地实现了这个要求:
[cpp]  view plain copy
  1. normalEyeSpace vec3(gl_ModelViewMatrix vec4(gl_Normal,0.0));  
这样的话,gl_NormalMatrix还有什么用呢?只是为了简化代码书写吗?实际当然不是这么简单,上面的代码在某些情况下是有效的,但不能应对所有情况。
让我们看看潜在的问题:

在上图中我们可以看到一个三角面以及它的法线和切线。下图显示了如果模型视图矩阵包含非一致缩放(non-uniform scale)的话会发生什么。

注意,如果缩放是一致的(uniform),那么法线方向保持不变,变的只是长度,而且可以通过归一化修正这个影响。
在上图中,模型视图矩阵应用到所有顶点以及法线上,最后的结果明显错误:法线不再与三角面垂直了。
现在我们知道模型视图矩阵在某些情况下,不能用来变换法线向量。下面的问题就是:那么该使用哪个矩阵?
考虑一个3×3矩阵G,让我们看看要正确变换法线,这个矩阵该是什么样子。
我们知道,变换前切线和法线是垂直的,即T•N = 0,在变换后切线和法线同样应该保持垂直,即T’•N’ = 0。现在假设G是正确变换法线的矩阵,同时模型视图矩阵的左上3×3子矩阵M可以正确变换切线T(T是一个向量,所以w成分为0)。因为T可以通过两个顶 点的差来计算,所以变换顶点的矩阵同样可以用来变换T。由此可以得到如下等式:

向量的点乘相当于向量的内积,所以有:

我们知道相乘的转置等于分别转置再交换顺序相乘:

已知N和T点乘结果为0,所以如果下式成立就可以满足等式为0:

即有:

可见变换法线的正确矩阵是M的逆的转置。OpenGL计算出的这个矩阵就保存在gl_NormalMatrix里。
在本节开始讨论过,某些情况下使用模型视图矩阵也可以。当模型视图矩阵的左上3×3子矩阵M正交时,可以得到:

一个正交矩阵的所有行/列都为单位向量,并且互相正交。当两个向量乘上正交矩阵时,它们之间的夹角在变换前后不变。由于这种保角变换的关系,所以法线和切线依然保存垂直。此外,向量的长度也保持不变。
M在什么时候能确定为正交的呢?当我们把几何变换限制为旋转和平移时(在OpenGL应用程序中只使用glRotate和glTranslate,而不使用glScale),就可以保证M正交。注意:gluLookAt同样建立正交矩阵。

关于法线归一化

当一个法线到达顶点shader后,我们一般会将它归一化:
[cpp]  view plain copy
  1. normal normalize(gl_NormalMatrix gl_Normal);  
法线与gl_NormalMatrix矩阵相乘,将会被变换到视图空间。归一化向量可以保证使用点乘得到余弦值。
我们可以避免归一化计算吗?在某些情况下是可行的。如果gl_NormalMatrix是正交矩阵,那么经过变换后输入法线的长度不会变,依然等于gl_Normal。所以如果在OpenGL程序中法线已经是归一化的,那么在shader中就不需要在重复了。
也就是说,如果我们使用gluLookAt设置照相机,对模型值进行旋转和平移变换,就可以在shader中避免使用归一化操作。这对于归一化过的光线向量也是适用的。
片断shader的情况
在片断shader中,我们经常发现需要重新归一化在顶点shader中归一化的法线。这是必要的吗?答案是肯定的。
考虑一个包含三个不同顶点法线的三角面。片断shader接收经过插值的法线,插值基于距离三个顶点的远近。这样得到的法线方向是对的,但不再是单位长度了。
下图显示了原因。图中黑线表示三角面,顶点法线用蓝色表示,插值得到的片断法线用绿色表示。所有的插值法线排列在黑色的点划线上。从图上可以看出绿色的插值法线大小小于单位长度的顶点法线。

注意,如果顶点法线没有单位化,那么得到的插值法线的方向也将是错误的。所以,即使一个顶点没有在顶点shader用到,也可能要对它在顶点shader中进行归一化。
有一种情况,在片断shader中可以避免归一化操作,那就是每个顶点法线方向相同,而且顶点法线是经过归一化的。此时顶点法线插值得到的结果都相同。
以方向光为例,每个片断都需要考虑光线方向,如果光线向量已经在之前归一化了,在片断shader中就可以避免归一化这一步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值