## 着色器(Shaders) 原文 | [Shaders](http://learnopengl.com/#!Getting-started/Shaders) ---|--- 作者 | JoeyDeVries 翻译 | [Django](http://bullteacher.com/) 校对 | Geequlim 在[Hello Triangle](http://learnopengl-cn.readthedocs.org/zh/latest/01%20Getting%20started/04%20Hello%20Triangle/)教程中提到,着色器是运行在GPU上的小程序。这些小程序为图形渲染管线的一个特定部分而运行。从基本意义上来说,着色器不是别的,只是一种把输入转化为输出的程序。着色器也是一种相当独立的程序,它们不能相互通信;只能通过输入和输出的方式来进行沟通。 前面的教程里我们简要地触及了一点着色器的皮毛。了解了如何恰当地使用它们。现在我们会用一种更加通用的方式详细解释着色器,特别是OpenGL着色器语言。 ### GLSL 着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含瞄准向量和矩阵操纵的有用特性。 着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口都是main函数,在这里我们处理所有输入变量,用输出变量输出结果。如果你不知道什么是uniform也不用担心,我们后面会进行讲解。 一个典型的着色器有下面的结构: ```c++ #version version_number in type in_variable_name; in type in_variable_name; out type out_variable_name; uniform type uniform_name; int main() { // 处理输入 ... // 输出 out_variable_name = weird_stuff_we_processed; } ``` 当我们谈论特别是谈到顶点着色器的时候,每个输入变量也叫顶点属性(vertex attribute)。能声明多少个顶点属性是由硬件决定的。OpenGL确保至少有16个包含4个元素的顶点属性可用,但是有些硬件或许可用更多,你可以查询GL_MAX_VERTEX_ATTRIBS来获取这个数目。 ```c++ GLint nrAttributes; glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes); std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl; ``` ### 数据类型 GLSL有像其他编程语言相似的数据类型。GLSL有C风格的默认基础数据类型:int、float、double、uint和bool。GLSL也有两种容器类型,教程中我们会使用很多,它们是向量(vector)和矩阵(matrix),我们会在后面的教程里讨论他们。 #### 向量 GLSL中的向量是前面提到的一个有1、2、3或4个元素的基本类型容器。它们可以是下面的形式(n代表元素数量): 类型|含义 ---|--- vecn | 包含n个默认为float元素的向量 bvecn| 包含n个布尔元素向量 ivecn| 包含n个int元素的向量 uvecn| 包含n个unsigned int元素的向量 dvecn| 包含n个double元素的向量 大多数时候我们使用vecn,因为float足够满足大多数要求。 一个向量的元素可以通过vec.x这种方式获取,这里x是指这个向量的第一个元素。你可以分别使用.x、.y、.z和.w来获取它们的第1、2、3、4号元素。GLSL也允许你使用rgba来获取颜色的元素,或是stpq获取纹理坐标元素。 向量的数据类型也允许一些有趣而灵活的元素选择方式,叫做swizzling(混合)。swizzling是这样的语法: ```c++ vec2 someVec; vec4 differentVec = someVec.xyxx; vec3 anotherVec = differentVec.zyw; vec4 otherVec = someVec.xxxx + anotherVec.yxzy; ``` 你可以使用上面任何4个字母组合来创建一个新的和原来向量一样长的向量(但4个元素需要是同一种类型);不允许在一个vec2向量中去获取.z元素。我们可以把一个向量作为一个参数传给不同的向量构造函数,以减少参数需求的数量: ```c++ vec2 vect = vec2(0.5f, 0.7f); vec4 result = vec4(vect, 0.0f, 0.0f); vec4 otherResult = vec4(result.xyz, 1.0f); ``` 向量是一种灵活的数据类型,我们可以把用在所有输入和输出上。学完教程你会看到足够多如何创造性地管理向量的例子。 #### 输入与输出(in vs out) 着色器是各自独立的小程序,但是它们都是一个整体的局部,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字来实现这个目的。每个着色器使用这些关键字定义输入和输出,无论在哪儿,一个输出变量就能与一个下一个阶段的输入变量相匹配。他们在顶点和片段着色器之间有点不同。 顶点着色器应该接收的输入是一种特有形式,否则就会效率低下。顶点着色器的输入是特殊的,它所接受的是从顶点数据直接输入的。为了定义顶点数据被如何组织,我们使用`location`元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。我们已经在前面的教程看过`layout (location = 0)`。顶点着色器需要为它的输入提供一个额外的layout定义,这样我们才能把它链接到顶点数据。 !!! Important 也可以移除`layout (location = 0)`,通过在OpenGL代码中使用`glGetAttribLocation`请求属性地址(location),但是我更喜欢在着色器中设置它们,理解容易而且节省时间。 另一个例外是片段着色器需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。 所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须**在发送方着色器中声明一个输出,在接收方着色器中声明一个同名输入**。当名字和类型都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序(program)对象时完成的)。为了展示这是这么工作的,我们会改变前面教程里的那个着色器,让顶点着色器为片段着色器决定颜色。 ##### 顶点着色器 ```c++ #version 330 core layout (location = 0) in vec3 position; // 位置变量的属性position为0 attribute position 0 out vec4 vertexColor; // 为片段着色器指定一个颜色输出 void main() { gl_Position = vec4(position, 1.0); // 把一个vec3作为vec4的构造器的参数 to vec4's constructor vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出颜色设置为暗红色 to a dark-red color } ``` ##### 片段着色器 ```c++ #version 330 core in vec4 vertexColor; // 和顶点着色器的vertexColor变量类型相同、名称相同 out vec4 color; //片段着色器输出的变量名可以任意命名,类型必须是vec4 void main() { color = vertexColor; } ``` 你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,在片段着色器声明了一个一样的vertexColor。由于它们**类型相同并且名字也相同**,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。因为我们在顶点着色器中设置的颜色是深红色的,片段着色器输出的结果也是深红色的。下面的图片展示了输出结果: ![](http://learnopengl.com/img/getting-started/shaders.png) 我们完成了从顶点着色器向片段着色器发送数据。让我们更上一层楼,看看能否从应用程序中直接给片段着色器发送一个颜色! #### uniform uniform是另一种从CPU应用向GPU着色器发送数据的方式,但uniform和顶点属性有点不同。首先,uniform是全局的。这里全局的意思是uniform变量必须在所有着色器程序对象中都是独一无二的,它可以在着色器程序的任何着色器任何阶段使用。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。 我们可以简单地通过在片段着色器中设置uniform关键字接类型和变量名来声明一个GLSL的uniform。之后,我们可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色: ```c++ #version 330 core out vec4 color; uniform vec4 ourColor; //在程序代码中设置 void main() { color = ourColor; } ``` 我们在片段着色器中声明了一个uniform vec4 ourColor,并把片段着色器的输出颜色设置为uniform值。因为uniform是全局变量,我们我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform所以不用在那里定义它。 !!! Attention 如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,从而最后编译出的版本中并不会包含它,如果有一个从没用过的uniform出现在已编译版本中会出现几个错误,记住这点! uniform现在还是空的;我们没有给它添加任何数据,所以下面就做这件事。我们首先需要找到着色器中uniform的索引/地址。当我们得到uniform的索引/地址后,我们就可以更新它的值了。这里我们不去给像素传递一个颜色,而是随着时间让它改变颜色: ```c++ GLfloat timeValue = glfwGetTime(); GLfloat greenValue = (sin(timeValue) / 2) + 0.5; GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUseProgram(shaderProgram); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); ``` 首先我们通过`glfwGetTime()`获取运行的秒数。然后我们使用余弦函数在0.0到-1.0之间改变颜色,最后储存到`greenValue`里。 接着,我们用`glGetUniformLocation`请求`uniform ourColor`的地址。我们为请求函数提供着色器程序和uniform的名字(这是我们希望获得的地址的来源)。如果`glGetUniformLocation`返回`-1`就代表没有找到这个地址。最后,我们可以通过`glUniform4f`函数设置uniform值。注意,查询uniform地址不需要在之前使用着色器程序(use shader program),但是更新一个unform之前必须不许使用程序(调用`glUseProgram`),因为它是在当前激活的着色器程序中设置unform的。 !!! Important 因为OpenGL是C库内核,所以它不支持函数重载,在函数参数不同的时候就要定义新的函数;glUniform是一个典型例子。这个函数有一个特定的作为类型的后缀。有几种可用的后缀: 后缀|含义 ---|-- f | 函数需要以一个float作为它的值 i | 函数需要一个int作为它的值 ui| 函数需要一个unsigned int作为它的值 3f| 函数需要3个float作为它的值 fv| 函数需要一个float向量/数组作为它的值 每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载的函数。在我们的例子里,我们使用uniform的4float版,所以我们通过`glUniform4f`传递我们的数据(注意,我们也可以使用fv版本)。 现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一帧更新这个uniform,否则三角形就不会改变颜色。下面我们就计算greenValue然后每个渲染迭代都更新这个uniform: ```c++ while(!glfwWindowShouldClose(window)) { // 检测事件 glfwPollEvents(); // 渲染 // 清空颜色缓冲 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 激活着色器 glUseProgram(shaderProgram); // 更新uniform颜色 GLfloat timeValue = glfwGetTime(); GLfloat greenValue = (sin(timeValue) / 2) + 0.5; GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // 绘制三角形 glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); } ``` 新代码和上一节的很相似。这次,我们在每个循环绘制三角形前先更新uniform值。如果你成功更新uniform了,你会看到你的三角形逐渐由绿变黑再变绿。