【OpenGL基础】|| 着色器介绍

文章目录

    • 1. GLSL
    • 2. 数据类型
    • 3. 输入与输出
    • 4. uniform变量
    • 5. 更多属性

着色器(shader)是运行在GPU上的小程序,将输入转化为输出。着色器之间不能相互通信,唯一的沟通方式是输入和输出。

1. GLSL

着色器使用GLSL编写(类似C语言),GLSL专为图形计算设计,包含针对向量和矩阵操作的特性。

着色器的开头是声明版本,其次是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数。一个着色器示例如下:

#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来获取具体的上限:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

2. 数据类型

GLSL包含C语言的大部分默认基础类型:intfloatdoubleuint(unsigned int)bool。GLSL有两种容器类型:向量(vertor)和矩阵(Matrix)(之后再介绍)。

GLSL中的向量可以是包含2、3或者4分量的容器,分量的类型可以是默认基础类型的任意一个,可以是以下形式(n表示分量的数量):

类型 含义
vecn n个float分量
bvecn n个bool分量
ivecn n个int分量
uvecn n个unsigned int 分量
dvecn n个double分量

一个向量的分量可以通过.x,.y,.z,.w来分别获取1、2、3、4个分量。GLSL也允许对颜色使用rgba,对纹理坐标使用stpq进行访问获取。

向量允许使用灵活的分量选择方式,称之为重组(swizzling)。重组允许这样的语法:

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

也可以把向量作为一个参数传给不同的向量构造函数:

vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

3. 输入与输出

着色器的输入和输出是数据交流和传递的途径。GLSL定义了inout关键字,每个着色器使用这两个关键字设定输入和输出。

顶点着色器从顶点数据中直接接收输入。使用location这一元数据指定输入变量,这样才能在CPU上配置顶点属性。顶点着色器需要为其输入设置layout标识,这样才能链接到顶点数据。(也可以省略layout (location = 0),在OpenGL中使用glGetAttribLocation查询属性位置值,但在着色器中设置更便于理解和减少工作量)。

片元着色器需要一个vec4颜色输出变量,若没有定义输出,则OpenGL会把物体渲染成黑色(或白色)。

因此,当一个着色器向另一个着色器发送数据时,必须在发送方着色器中声明一个输出,并在接收方着色器声明一个输入,两者的类型和名称都完全一致时,OpenGL会把两个变量链接到一起,他们之间就能发送数据了。

示例如下,在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片元着色器中声明了一个类似的vertexColor。两者的类型和名称完全一致,两者就会被链接

顶点着色器:

#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0

out vec4 vertexColor; // 为片段着色器指定一个颜色输出

void main()
{
    gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}

片元着色器:

#version 330 core
out vec4 FragColor;

in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)

void main()
{
    FragColor = vertexColor;
}

4. uniform变量

Uniform是一种从CPU应用程序向GPU中的着色器发送数据的方式,uniform与顶点属性不同,一是uniform变量是全局(global)变量,即uniform变量在每个着色器程序对象中都是唯一的,且可以被任意着色器在任意阶段访问;二是uniform会一直保持数据,直到被重置或更新。

可以在着色器中使用uniform关键字在类型和变量名前声明一个GLSL的uniform变量。

#version 330 core
out vec4 FragColor;

uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量

void main()
{
    FragColor = ourColor;
}

在片元着色器中声明了一个uniform vec4类型的ourColor变量,并设置为片元着色器的输出。

若声明了一个uniform变量,但在GLSL代码中没有使用,编译器会移除这个变量,但可能会导致错误!

为了给uniform变量添加数据,首先需要找到着色器中uniform属性的索引/位置值,然后更新其值。

float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

上述代码首先通过glfwGetTime()获取运行秒数,然后使用sin函数,让其在0.0-1.0之间改变,最后将结果存储到greenValue中。接着使用glGetUniformLocation查询uniform ourcolor的位置值,若返回-1表示没有找到这个位置值,最后通过glUniform4f函数设置uniform的值,在更新uniform变量之前,必须要激活着色器程序。

OpenGL核心是一个C语言库,不支持类型重载,因此在函数参数不同时,需要定义新的函数,以glUniform为例,使用不同的后缀表示不同的uniform类型:

后缀 含义
f float
i int
ui unsigned int
3f 3个float值
fv float向量

当将颜色变化时,需要在每一帧中更新uniform,如下:

while(!glfwWindowShouldClose(window))
{
    // 输入
    processInput(window);

    // 渲染
    // 清除颜色缓冲
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // 记得激活着色器
    glUseProgram(shaderProgram);

    // 更新uniform颜色
    float timeValue = glfwGetTime();
    float greenValue = sin(timeValue) / 2.0f + 0.5f;
    int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
    glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

    // 绘制三角形
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    // 交换缓冲并查询IO事件
    glfwSwapBuffers(window);
    glfwPollEvents();
}

5. 更多属性

可以将顶点位置信息和颜色信息都存储进顶点数据中,如下:

float vertices[] = {
    // 位置              // 颜色
     0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 顶部
};

现在将更多的数据发送到顶点着色器,需要调整定点着色器,使其能够接收颜色值作为一个顶点属性输入,使用layoutaColor属性的位置值设置为1:

#version 330 core
layout (location = 0) in vec3 aPos;   // 位置变量的属性位置值为 0 
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1

out vec3 ourColor; // 向片元着色器输出一个颜色

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}

在片元着色器中接收ourColor颜色输入:

#version 330 core
out vec4 FragColor;  
in vec3 ourColor;

void main()
{
    FragColor = vec4(ourColor, 1.0);
}

更新后的VBO内存如下所示:

【OpenGL基础】|| 着色器介绍_第1张图片然后使用glVertexAttribPointer函数更新顶点格式:

// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);

由于包含了位置和颜色信息,因此这里的步长设置为6个float,即3个是位置值,3个是颜色值。对于每个顶点来说,位置顶点属性在前,所以偏移量为0,颜色属性在后,偏移量为3*sizeof(float)
【OpenGL基础】|| 着色器介绍_第2张图片运行结果如上图所示,因为只提供了三个颜色,在片元着色器中会进行片元插值(Fragment Interpolation)。当渲染三角形时,光栅化(Rasterization)会产生比顶点更多的片元,光栅会根据每个片元在三角形形状上所处的相对位置决定这些片元的位置。基于这些位置,会插值所有片元着色器的输入变量,如一个线段上端为绿色,下端为蓝色,那么在距上端70%位置的颜色线性插值为70%蓝色+30%绿色。

你可能感兴趣的:(c++,opengl,着色器)