1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00
Files
LearnOpenGL-CN/01 Getting started/05 Shaders.md
2015-07-27 18:48:41 +08:00

22 KiB
Raw Blame History

着色器Shaders

原文 | Shaders ---|--- 作者 | JoeyDeVries 翻译 | Django 校对 | Geequlim

Hello Triangle教程中提到着色器是运行在GPU上的小程序。这些小程序为图形渲染管线的一个特定部分而运行。从基本意义上来说着色器不是别的只是一种把输入转化为输出的程序。着色器也是一种相当独立的程序它们不能相互通信只能通过输入和输出的方式来进行沟通。

前面的教程里我们简要地触及了一点着色器的皮毛。了解了如何恰当地使用它们。现在我们会用一种更加通用的方式详细解释着色器特别是OpenGL着色器语言。

GLSL

着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的它包含瞄准向量和矩阵操纵的有用特性。

着色器的开头总是要声明版本接着是输入和输出变量、uniform和main函数。每个着色器的入口都是main函数在这里我们处理所有输入变量用输出变量输出结果。如果你不知道什么是uniform也不用担心我们后面会进行讲解。

一个典型的着色器有下面的结构:

#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来获取这个数目。

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是这样的语法

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

你可以使用上面任何4个字母组合来创建一个新的和原来向量一样长的向量但4个元素需要是同一种类型不允许在一个vec2向量中去获取.z元素。我们可以把一个向量作为一个参数传给不同的向量构造函数以减少参数需求的数量

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对象时完成的。为了展示这是这么工作的我们会改变前面教程里的那个着色器让顶点着色器为片段着色器决定颜色。

顶点着色器
#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
}
片段着色器
#version 330 core
in vec4 vertexColor; // 和顶点着色器的vertexColor变量类型相同、名称相同

out vec4 color; //片段着色器输出的变量名可以任意命名类型必须是vec4

void main()
{
    color = vertexColor;
}

你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出在片段着色器声明了一个一样的vertexColor。由于它们类型相同并且名字也相同片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。因为我们在顶点着色器中设置的颜色是深红色的片段着色器输出的结果也是深红色的。下面的图片展示了输出结果

我们完成了从顶点着色器向片段着色器发送数据。让我们更上一层楼,看看能否从应用程序中直接给片段着色器发送一个颜色!

uniform

uniform是另一种从CPU应用向GPU着色器发送数据的方式但uniform和顶点属性有点不同。首先uniform是全局的。这里全局的意思是uniform变量必须在所有着色器程序对象中都是独一无二的它可以在着色器程序的任何着色器任何阶段使用。第二无论你把uniform值设置成什么uniform会一直保存它们的数据直到它们被重置或更新。

我们可以简单地通过在片段着色器中设置uniform关键字接类型和变量名来声明一个GLSL的uniform。之后我们可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色

#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的索引/地址后,我们就可以更新它的值了。这里我们不去给像素传递一个颜色,而是随着时间让它改变颜色:

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

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了你会看到你的三角形逐渐由绿变黑再变绿。