本文来自 lighthouse3d.com, 发布时间很久远, 内容也不如learnopengl丰富, 但作者写的很简洁易懂, 内容可以作为简单gl api的quick reference, 同时GLSL1.2 也是WebGL2.0使用的标准.
在本教程中,将介绍使用 GLSL 1.2 进行着色器编程。尽管许多部分现在被认为已弃用,但 GLSL 的本质仍然保持不变。此外,本教程将持续在线,因为学习已弃用的 OpenGL 的基础知识比学习新版本更容易。如果您正在寻找仅处理未弃用功能的 GLSL 教程,请访问 Lighthouse3D 中的[核心 GLSL 教程](Core GLSL tutorial (lighthouse3d.com))。 Shader(着色器)是一个热门话题,3D 游戏表明可以很好地利用着色器来获得非凡的效果。本教程旨在介绍着色器的世界。 本教程包含对规范的介绍,但如果您认真对待这一点,始终建议您阅读 OpenGL 2.0 和 GLSL 官方规范。假定读者熟悉 OpenGL 编程,因为这是理解本教程的某些部分所必需的。 GLSL 代表 GL Shading Language,通常称为 glslang,由 OpenGL 的管理机构 OpenGL 的架构审查委员会定义。 我不会与 Cg 争论或比较,Cg 是 Nvidia 提出的一种也与 OpenGL 兼容的着色语言。我在本教程中选择 GLSL 而不是 Cg 的唯一原因是 GLSL 与 OpenGL 的接近程度。 在使用任何语言编写着色器之前,最好了解图形管道的基础知识。这将提供介绍着色器、可用的着色器类型以及着色器应该做什么的上下文。它还将显示着色器不能做什么,这同样重要。 在此介绍之后,讨论了 GLSL 的 OpenGL 设置。详细讨论了在 OpenGL 应用程序中使用着色器的必要步骤。最后展示了 OpenGL 应用程序如何将数据提供给着色器,使其更加灵活和强大。 然后介绍了数据类型、变量、语句和函数定义等基本概念。
Table of Content
The Graphics Pipeline
Pipeline Overview
下图是管道阶段和在它们之间传输的数据的(非常)简化图。尽管极其简化,但足以介绍着色器编程的一些重要概念。在本小节中,介绍了管道的固定功能。请注意,此管道是一种抽象,并不一定在其所有步骤中都满足任何特定的实现
顶点变换
在这里,顶点(vertex)是一组属性,例如它在空间中的位置,以及它的颜色、法线、纹理坐标等。此阶段的输入是各个顶点属性。此阶段由固定功能执行的一些操作是:
- 顶点位置变换
- 每个顶点的光照计算
- 纹理坐标的生成和转换
图元组装和光栅化
这个阶段的输入是转换后的顶点,以及连接信息。后一条数据告诉管道顶点如何连接以形成图元。图元(primitives)就是在这里组装的。 这个阶段还负责对视锥体进行裁剪操作和背面剔除。 光栅化确定图元的片元和像素位置。此上下文中的片元是一段数据,将用于更新帧缓冲区中特定位置的像素。片元不仅包含颜色,还包含法线和纹理坐标,以及用于计算新像素颜色的其他可能属性。 这个阶段的输出是双重的:
- 片元在帧缓冲区中的位置
- 在顶点变换阶段计算的每个属性片元的插值 在顶点变换阶段计算的值与顶点连接信息相结合,允许该阶段计算片元的适当属性。例如,每个顶点都有一个变换后的位置。当考虑构成图元的顶点时,可以计算图元片元的位置。另一个例子是颜色的使用。如果一个三角形的顶点有不同的颜色,那么三角形内部片元的颜色是通过三角形顶点颜色的插值得到的,该顶点颜色由顶点到片元的相对距离加权。
片元纹理与着色
插值片元(fragment)信息是该阶段的输入。颜色已经在前一阶段通过插值计算出来,在这里它可以与一个纹素(纹理元素)结合起来。纹理坐标也已在前一阶段进行了插值。雾也在这个阶段应用。每个片元的这个阶段的共同最终结果是片元的颜色值和深度
光栅化操作
这个阶段的输入是:
- 像素坐标
- 片元的深度与颜色值 这个阶段是流水线的最后阶段, 它对片元做一系列的测试:
- 剪裁(Scissor)测试
- 透明测试
- 模板(Stencil)测试
- 深度测试 如果这些测试都通过,则使用片元信息根据当前混合模式更新像素值。请注意,混合只发生在这个阶段,因为片元纹理和着色阶段无法访问帧缓冲区。帧缓冲区只能在这个阶段访问。
可视化例子
下图对上述阶段进行了直观描述:
最近的图形卡使程序员能够定义上述两个阶段的功能:
- 可以为顶点变换阶段编写顶点着色器。
- 片元着色器取代了片元纹理和着色阶段的固定功能。 在接下来的小节中,将描述这些可编程阶段,以下是顶点(vertex)处理器和片元(fragment)处理器。
VertexProcessor
顶点处理器负责运行顶点着色器。顶点着色器的输入是顶点数据,即它的位置、颜色、法线等,具体取决于 OpenGL 应用程序发送的内容。 以下 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();
在顶点着色器中,您可以为任务编写如下代码:
- 使用模型视图和投影矩阵的顶点位置变换
- 正则转换,如果需要正则化
- 纹理坐标生成和转换
- 每个顶点的照明或计算每个像素的照明值
- 颜色计算
不需要执行上述所有操作,例如您的应用程序可能不使用照明。然而,一旦你编写了一个顶点着色器,你就替换了顶点处理器的全部功能,因此你不能执行正常的转换并期望固定的功能来执行纹理坐标生成。当使用顶点着色器时,它负责替换管道中这一阶段所需的所有功能。
从上一节可以看出,顶点处理器没有关于连通性的信息,因此需要拓扑知识的操作不能在这里执行。例如,顶点着色器不可能执行背面剔除,因为它对顶点而不是面进行操作。顶点处理器单独处理顶点并且不知道剩余的顶点。
顶点着色器至少负责写一个变量:
gl_Position
,通常用模型视图和投影矩阵来变换顶点。 顶点 处理器可以访问 OpenGL 状态,因此它可以执行涉及光照和使用材质等操作。它还可以访问纹理。但无法访问帧缓冲区。
Fragment Processor
片元处理器是片元着色器运行的地方。该单元负责以下操作:
- 计算每个像素的颜色和纹理坐标
- 纹理应用
- 雾计算
- 计算法线 获得每像素的光照, 该单元的输入是在管道的前一阶段计算的插值,例如顶点位置、颜色、法线等…… 在顶点着色器中,这些值是为每个顶点计算的。现在我们正在处理图元内部的片元,因此需要插值。 与在顶点处理器中一样,当您编写片元着色器时,它会替换所有固定功能。因此,不可能有一个片元着色器对片元进行纹理化并为固定功能留下雾。程序员必须编写应用程序所需的所有效果。 片元处理器对单个片元进行操作,即它对相邻片元一无所知。着色器可以访问 OpenGL 状态,类似于顶点着色器,因此它可以访问例如 OpenGL 应用程序中指定的雾颜色。 重要的一点是,片元着色器无法更改先前在管道中计算的像素坐标。回想一下,在顶点处理器中,模型视图和投影矩阵可用于变换顶点。视口在此之后但在片元处理器之前开始发挥作用。片元着色器可以访问屏幕上的像素位置,但不能更改它。 片元着色器有两个输出选项:
- 丢弃片元,因此不输出任何内容
- 计算
gl_FragColor
(片元的最终颜色)或gl_FragData
(在渲染到多个目标时) 深度也可以写,虽然它不是必需的,因为前一阶段已经计算过了。 请注意,片元着色器无法访问帧缓冲区。这意味着诸如混合之类的操作仅在片元着色器运行后才会发生。
OpenGL Setup for GLSL
本节“GLSL 的 OpenGL 设置”假设您有一对着色器,一个顶点着色器和一个片元着色器,并且您希望在 OpenGL 应用程序中使用它们。如果您还没有准备好编写自己的着色器,那么有很多地方可以从互联网上获取着色器。试试橙皮书中的网站。 shader开发的工具Shader Designer或者Render Monkey都有大量的shader example。 就 OpenGL 而言,设置您的应用程序类似于编写 C 程序的工作流程。每个着色器就像一个 C 模块,必须单独编译,就像在 C 中一样。编译的着色器集,然后链接到一个程序中,就像在 C 中一样。
ARB 扩展和 OpenGL2.0 都在这里使用。如果您不熟悉扩展或使用 1.1 版以上的 OpenGL(由 Microsoft 支持),我建议您看一下 GLEW。 GLEW 大大简化了 OpenGL 扩展和新版本的使用,因为新功能可以立即使用 如果依赖扩展,因为你还没有支持 OpenGL 2.0,那么需要两个扩展:
- GL_ARB_fragment_shader
- GL_ARB_vertex_shader 使用 GLEW 检查扩展的 GLUT 程序的一个小例子如下所示:
#include <GL/glew.h>
#include <GL/glut.h>
void main(int argc, char **argv) {
glutInit(&argc, argv);
...
glewInit();
if (GLEW_ARB_vertex_shader && GLEW_ARB_fragment_shader)
printf("Ready for GLSL\n");
else {
printf("Not totally ready :( \n");
exit(1);
}
setShaders();
glutMainLoop();
}
要检查 OpenGL 2.0 可用性,您可以尝试这样的操作
if (glewIsSupported("GL_VERSION_2_0"))
printf("Ready for OpenGL 2.0\n");
else {
printf("OpenGL 2.0 not supported\n");
exit(1);
}
下图显示了创建着色器的必要步骤(在 OpenGL 2.0 语法中),使用的函数将在后面的部分中详细介绍。
在接下来的小节中,将详细介绍创建程序的步骤。
Creating a Shader
下图显示了创建着色器的必要步骤。
第一步是创建一个将充当着色器容器的对象。可用于此目的的函数返回容器的句柄。 该函数的 OpenGL 2.0 语法如下
`GLuint glCreateShader(GLenum shaderType); 参数
shaderType – GL_VERTEX_SHADER or GL_FRAGMENT_SHADER.
您可以创建任意数量的着色器以添加到程序中,但请记住,在每个程序中只能有一个用于顶点着色器集的主函数和一个用于片元着色器集的主函数。 接下来的步骤是添加一些源代码。着色器的源代码是一个字符串数组,但您可以使用指向单个字符串的指针。 在 OpenGL 2.0 语法中,为着色器设置源代码的函数语法是:
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.
最后,必须编译着色器。使用 OpenGL 2.0 实现此功能的函数是:
void glCompileShader(GLuint shader);
参数:
shader – the handler to the shader.
Creating a Program
下图显示了准备和运行着色器程序的必要步骤。
第一步是创建一个将充当程序容器的对象。可用于此目的的函数返回容器的handler。 该函数的语法,在 OpenGL 2.0 语法中如下:
GLuint glCreateProgram(void);
您可以根据需要创建任意数量的程序。渲染后,您可以从一个程序切换到另一个程序,甚至可以在单帧期间返回到固定功能。例如,您可能想要使用折射和反射着色器绘制茶壶,同时使用 OpenGL 的固定功能为背景显示立方体贴图。 下一步将使用上一小节中创建的着色器附加到您刚刚创建的程序中。此时不需要编译着色器;他们甚至不必拥有源代码。将着色器附加到程序所需的全部是着色器容器。 要将着色器附加到程序,请使用函数:
`void glAttachShader(GLuint program, GLuint shader); 参数:
- `program – the handler to the program.
- `shader – the handler to the shader you want to attach.
如果你有一对顶点/着色器片元,你需要将它们附加到程序中。您可以将许多相同类型(顶点或片元)的着色器附加到同一个程序,就像 C 程序可以有许多模块一样。对于每种类型的着色器,只能有一个具有主要功能的着色器,这与 C 中的情况一样。 您可以将着色器附加到多个程序,例如,如果您计划在多个程序中使用相同的顶点着色器。 最后一步是链接程序。为了执行此步骤,必须按照上一小节中的描述编译着色器。 链接函数的语法如下:
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
如果一个程序正在使用中,再次链接,它会自动再次使用,所以在这种情况下你不需要再次调用这个函数。如果参数为0,则激活固定功能。
Setup for GLSL - Example
以下源代码包含前面描述的所有步骤。变量 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);
}
提供完整的GLUT示例:OpenGL 2.0 语法,包含两个简单的着色器,以及文本文件读取函数。感谢 Wojciech Milkowski,可以在[这里](here (lighthouse3d.com))获得 Unix 版本(仅限 ARB 扩展语法)
Troubleshooting: The InfoLog
调试着色器很困难。目前还没有 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.
关于第二个参数 type
有更多选项,但是这里不会探讨这些。
报告错误时,可以通过 InfoLog 获取更多信息。此日志存储有关上次执行的操作的信息,例如编译中的警告和错误、链接步骤中的问题。日志甚至可以告诉您着色器是否会在软件中运行,这意味着您的硬件不支持您正在使用的某些功能或硬件,这是理想情况。不幸的是,InfoLog 消息没有规范,因此不同的驱动程序/硬件可能会产生不同的日志。
取特定着色器或程序的信息日志,请使用以下函数:
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.
GLSL 规范在这里可能会更好。您必须知道 InfoLog 的长度才能检索它。要找到这一点宝贵的信息,请使用以下函数(以 OpenGL 表示法)
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.
在 OpenGL 2.0 中可以使用以下函数打印 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);
}
}
Cleaning Up
在前面的小节中,介绍了将着色器附加到程序的函数。还提供了从程序中分离着色器的功能。 OpenGL 2.0 语法如下:
`void glDetachShader(GLuint program, GLuint shader); 参数:
- `program – The program to detach from.
- `shader – The shader to detach.
只能删除未附加的着色器,因此此操作并非无关紧要。要在 OpenGL 2.0 中删除着色器或程序,请使用以下函数:
void glDeleteShader(GLuint id);
void glDeleteProgram(GLuint id);
参数:
- `id – The handler of the shader or program to delete.
在着色器仍然附加到某些(一个或多个)程序的情况下,着色器实际上并没有被删除,而只是被标记为删除。只有当着色器不再附加到任何程序时,删除操作才会结束,即它已经从它附加到的所有程序中分离出来。
Communication OpenGL => GLSL
OpenGL 中的应用程序有多种与着色器通信的方法。请注意,这是一种单向通信,因为着色器的唯一输出是渲染到某些目标,通常是颜色和深度缓冲区。 着色器可以访问部分 OpenGL 状态,因此当应用程序更改 OpenGL 状态的这个子集时,它实际上是在与着色器进行通信。因此,例如,如果一个应用程序想要将浅色传递给着色器,它可以简单地改变 OpenGL 状态,就像通常使用固定功能所做的那样。 但是,使用 OpenGL 状态并不总是为着色器设置值的最直观方式。例如,考虑一个着色器,它需要一个变量来告知执行某些动画所经过的时间。为此,OpenGL 状态中没有合适的命名变量。的确,您可以为此使用未使用的灯光镜面反射截止角,但这是非常违反直觉的。 幸运的是,GLSL 允许为 OpenGL 应用程序定义用户定义的变量,以便与着色器进行通信。多亏了这个简单的特性,您可以拥有一个用于计时的变量,该变量被恰当地称为 timeElapsed 或其他合适的名称。 在这种情况下,GLSL 有两种类型的变量限定符(更多限定符可用于着色器内部,详见数据类型和变量小节):
- Uniform
- Attribute 就着色器而言,使用这些限定符在着色器中定义的变量是只读的。 在以下小节中,详细说明了如何以及何时使用这些类型的变量。 还有另一种将值发送到着色器的方法:使用纹理。纹理不一定代表图像;它可以被解释为一组数据。事实上,使用着色器是您决定如何解释纹理数据的人,即使它是图像。本节不探讨纹理的使用,因为它超出了范围。
Uniform Variables
Uniform变量的值只能由图元改变,即它的值不能在 glBegin / glEnd 对之间改变。这意味着它不能用于顶点属性。如果您正在寻找有关Attribute变量的小节,请查找该小节。Uniform变量适用于沿图元、片元甚至整个场景保持不变的值。Uniform变量可以在顶点和片元着色器中读取(但不能写入)
您要做的第一件事是获取变量的内存位置。请注意,此信息仅在链接程序后可用。注意:对于某些驱动程序,您可能需要使用该程序,即在尝试获取位置之前,必须已经调用glUseProgram
根据着色器中定义的名称检索uniform变量位置的函数是:
`GLint glGetUniformLocation(GLuint program, const char *name); 参数:
- `program – the handler to the program
- `name – the name of the variable.
返回值是变量的位置,然后可用于为其赋值。提供了一组函数来设置uniform变量,其用法取决于变量的数据类型。定义了一组函数来设置浮点值
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);
or
`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.
一组类似的函数可用于数据类型整数,其中“f”被“i”替换。没有专门针对布尔值或布尔向量的函数。只需使用可用于浮点数或整数的函数,将零设置为假,将其他任何值设置为真。如果您有一组uniform变量,则应使用矢量版本。
对于采样器变量,使用函数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.
结束本小节前的重要注意事项:使用这些函数设置的值将保留它们的值,直到程序再次链接。一旦执行了新的链接过程,所有值都将重置为零。 现在来看一些源代码。假设正在使用具有以下变量的着色器
uniform float specIntensity;
uniform vec4 specColor;
uniform float t[2];
uniform vec4 colors[3];
在 OpenGL 2.0 应用程序中,设置变量的代码可以是:
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);
提供了一个带有源代码的工作示例:[OpenGL 2.0 语法](OpenGL 2.0 syntax (lighthouse3d.com))
请注意设置值数组(如 t 或 colors 的情况)与设置具有 4 个值的向量(如 specColor)之间的区别。计数参数(glGetUniform{1,2,3,4}fv
的中间参数)指定数组元素的数量,如在着色器中声明的那样,而不是在 OpenGL 应用程序中声明的那样。所以虽然 specColor 包含 4 个值,但是函数 glUniform4fv
参数的计数设置为 1,因为它只是一个向量。设置 specColor 变量的另一种方法是:
loc2 = glGetUniformLocation(p,"specColor");
glUniform4f(loc2,sc[0],sc[1],sc[2],sc[3]);
GLSL 提供的另一种可能性是获取变量在数组中的位置。例如,可以获取 t[1] 的位置。以下代码片元显示了这种设置 t 数组元素的方法。
loct0 = glGetUniformLocation(p,"t[0]");
glUniform1f(loct0,threshold[0]);
loct1 = glGetUniformLocation(p,"t[1]");
glUniform1f(loct1,threshold[1]);
请注意如何使用方括号在 glGetUniformLocation
中指定变量。
Attribute Variables
正如在 Uniform小节中提到的,uniform 变量只能由图元设置,即它们不能在 glBegin-glEnd 中设置。 如果需要为每个顶点设置变量,则必须使用attribute变量。实际上attribute变量可以随时更新。attribute变量只能在顶点着色器中读取(不能写入)。这是因为它们包含顶点数据,因此不能直接应用于片元着色器(请参阅varying变量部分)。 与Uniform变量类似,首先需要获取变量在内存中的位置。请注意,该程序必须先前已链接,并且某些驱动程序可能要求该程序正在使用中。 在 OpenGL 2.0 中使用以下函数:
`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.
为整数和其他一些数据类型提供了一组类似的函数。请注意,向量版本不像统一变量那样可用于数组。矢量版本只是提交单个属性变量值的选项。这类似于 OpenGL 中使用 glColor3f 和 glColor3fv 时发生的情况。 现在提供一个小例子。假定顶点着色器声明了一个名为高度的浮点属性。程序链接后要执行的设置阶段是:
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();
提供了一个小型工作示例的源代码:ARB 扩展语法或 OpenGL 2.0 语法。 顶点数组也可以与属性变量一起使用。首先要做的是启用数组。要对属性数组执行此操作,请使用以下函数(OpenGL 2.0 语法):
`void glEnableVertexAttribArray(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);
glEnableVertexAttribArray(loc);
glVertexPointer(2,GL_FLOAT,0,vertices);
glVertexAttribPointer(loc,1,GL_FLOAT,0,0,heights);
渲染与之前完全相同(没有着色器的 OpenGL),例如调用 glDrawArrays。提供了一个小型演示源代码: OpenGL 2.0 语法。
Shader Basics
Data Types and Variables
以下简单数据类型在 GLSL 中可用:
- float
- bool
- int float 和 int 的行为就像在 C 中一样,bool 类型可以采用 true 或 false 的值。 具有 2,3 或 4 个分量的向量也可用于上述每种简单数据类型。这些声明为:
- vec{2,3,4} 2,3 或 4 个浮点数的向量
- bvec{2,3,4} 布尔向量
- ivec{2,3,4} 整数向量 提供方阵 2×2、3×3 和 4×4,因为它们在图形中大量使用。各自的数据类型是:
- mat{2,3,4} 一组特殊类型可用于纹理访问。这些被称为采样器,需要访问纹理值,也称为texels(纹素)。 纹理采样的数据类型是:
- 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 中相同的语法来声明数组。但是,在声明时不能初始化数组。访问数组的元素就像在 C 中一样完成。 GLSL 中也允许使用结构体。语法与 C 相同。
struct dirlight {
vec3 direction;
vec3 color;
};
变量
声明一个简单变量与在 C 中几乎相同,也可以在声明变量时对其进行初始化。
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
声明其他类型的变量遵循相同的模式,但 GLSL 和 C 在初始化方面存在差异。 GLSL 在很大程度上依赖于构造函数来进行初始化和类型转换。
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
// 在使用其他变量初始化变量时,GLSL 非常灵活。它所需要的只是您提供必要数量的组件。
// 看看下面的例子
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));
// 在 GLSL 中,提供了一些额外的功能来简化我们的生活,并使代码更清晰一些。
// 可以使用字母和标准 C 选择器来访问向量。
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
如前面的代码片元所示,可以使用字母 x、y、z、w 来访问向量组件。如果你在谈论颜色,那么可以使用 r,g,b,a. 对于纹理坐标,可用的选择器是 s、t、p、q. 请注意,按照惯例,纹理坐标通常称为 s、t、r、q. 然而 r 已经被用作 RGBA 中“红色”的选择器。因此需要找到一个不同的字母,幸运的是 p. 矩阵选择器可以接受一个或两个参数,例如 m[0] 或 m[2][3]。在第一种情况下,选择了第一列,而在第二种情况下,选择了单个元素。 至于结构,结构元素的名称可以像在 C 中一样使用,因此假设上面描述的结构,可以编写以下代码行:
d1.direction = vec3(1.0,1.0,1.0);
变量修饰符
限定符赋予变量特殊的含义。有以下限定符可用:
- const – 声明是一个编译时常量
- attribute – 全局变量,每个顶点可能会发生变化,从 OpenGL 应用程序传递到顶点着色器。此限定符只能在顶点着色器中使用。对于着色器,这是一个只读变量。
- uniform – 全局变量, 可能会因图元而改变(可能不会在 glBegin、/glEnd 中设置),它们从 OpenGL 应用程序传递到着色器。此限定符可用于顶点着色器和片元着色器。对于着色器,这是一个只读变量。
- varying——用于顶点着色器和片元着色器之间的插值数据。在顶点着色器中可用于写入,在片元着色器中为只读
Statements and function
控制流语句
可用的选项与 C 中的几乎相同。有条件语句,如 if-else,迭代语句,如 for、while 和 do-while
if (bool expression)
...
else
...
for (initialization; bool expression; loop expression)
...
while (bool expression)
...
do
...
while (bool expression)
支持的跳转语句:
- continue——在循环中可用,导致跳转到循环的下一次迭代
- break——在循环中可用,导致循环退出
- discord discard 关键字只能在片元着色器中使用。它会导致当前片元的着色器终止,而不写入帧缓冲区或深度
函数
与在 C 中一样,着色器由函数构成。至少每种类型的着色器都必须有一个使用以下语法声明的主函数:
void main()
可以定义用户定义的函数。与在 C 中一样,函数可能有一个返回值,并且应该使用 return 语句来传递它的结果。一个函数当然可以是空的。返回类型可以是任何类型,但不能是数组。 函数的参数具有以下可用的限定符:
- in——用于输入参数
- out——函数的输出。 return 语句也是发送函数结果的选项。
- inout——用于既是函数的输入又是输出的参数 如果没有指定限定符, 默认情况下它被认为是 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);
}
Varying Variables
如前所述,我们有两种类型的着色器:顶点着色器和片元着色器。为了计算每个片元的值,通常需要访问顶点插值数据。例如,当对每个片元执行光照计算时,我们需要访问片元的法线。然而在 OpenGL 中,法线仅针对每个顶点指定。这些法线可供顶点着色器访问,但不能供片元着色器访问,因为在 OpenGL 应用程序中它们作为attribute变量。 在处理完顶点(包括所有顶点数据)后,它们将进入流水线的下一阶段(仍保持固定功能),其中连接信息可用。正是在这个阶段,原语被组装起来,片元被计算出来。对于每个片元,都有一组自动插值并提供给片元着色器的变量。一个例子是片元的颜色。到达片元着色器的颜色是构成图元的顶点颜色插值的结果。 这种类型的变量,在片元接收插值的地方,数据是“varying变量”。 GLSL 有一些预定义的可变变量,例如上面提到的颜色。 GLSL 还允许用户定义可变变量。这些必须在顶点和片元着色器中声明,例如:
varying float intensity;
必须在顶点着色器上写入一个可变变量,我们在其中计算每个顶点的变量值。在片元着色器中,变量的值来自先前计算的顶点值的插值,只能读取。