Learn OpenGL系列【3】着色器与GLSL

上回书说到,OpenGL的渲染管线被划分为数个环节,每个环节的工作由一些专门的、相对简短的程序,即着色器来完成。编写着色器的语言是GLSL,它具有类似C语言的语法,专为图形渲染设计,因而具有许多对于向量和矩阵的有效操作。为了之后对OpenGL的深入了解,学习GLSL是必不可少的一环。

着色器的基本结构

从上一篇文章可以看出,不同的着色器可能有着不同的格式,顶点着色器通过layout=x的方式接收顶点属性输入(通常最多为16个),并将结果输出到gl_Position等变量中,而片段着色器从之前的着色器接收输入,并需要声明out变量用于输出。尽管如此,大部分着色器都有着类似的结构:

#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

void main()
{
  // 对输入变量进行处理
  ...
  // 输出结果
  out_variable_name = weird_stuff_we_processed;
}

GLSL的数据类型

GLSL是一门类C语言,数据类型方面也不例外。GLSL中有intuintfloatdoublebool等数据类型,此外还有两种为图形渲染量身定制的数据类型:vectormatrixvector是可以包含2、3、4个分量的向量,每个分量的数据类型是相同的。前面提到的5种基本数据类型都可以用于创建vector。vector的声明形式一般是TvecN,其中T是类型,N是分量数。默认的vecn表示n个分量的float向量,而dvecnivecnuvecnbvecn则表示其他几种类型的向量。

OpenGL的向量操作非常灵活。向量的分量可以使用类似vec.x这样的方式获取。vec4的分量有xyzw,OpenGL也允许对颜色使用rgba,对纹理使用stpq。更加方便的是,开发者可以任意排列xyzw的顺序来得到一个新的向量,也可以在构造向量时直接使用另一个向量作为新向量的一部分

vec2 someVec;                           // someVec = (1, 2)
vec4 differentVec = someVec.xyxx;       // differentVec = (1, 2, 1, 1)
vec3 anotherVec = differentVec.zyw;     // anotherVec = (1, 2, 1)
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;  
// otherVec = (1, 1, 1, 1) + (2, 1, 1, 2) = (3, 2, 2, 3)

vec4 newVec = vec4(someVec, 3.0, 4.0)   // newVec = (1, 2, 3, 4)

这种灵活的向量操作使得着色器代码可以更加简短,而省去了许多声明中间临时变量的工作。

三种重要的变量修饰符

输入、输出修饰符

除了顶点着色器直接从顶点数据中获取输入以外,其他着色器都是从上一环节的着色器获取输入,以及把处理后的结果输出到下一环节的着色器。为此,OpenGL设计了inout修饰符用来标明变量属于输入或输出变量。只要输出变量与下一个着色器的输入变量相匹配(在不使用layout指定输出的位置时,需要前后着色器相应的类型与变量名都相同才被视为匹配),就可以进行传输。

在上一篇文章中,三角形的颜色是通过片段着色器来指定的。而我们可以通过变量的传输,让顶点着色器来指定颜色:

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

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

void main()
{
    gl_Position = vec4(aPos, 1.0);          
    // 像之前说的,这里不必使用aPos.x等分量,而可以直接用aPos作为构造参数
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}

// 片段着色器
#version 330 core
out vec4 FragColor;

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

void main()
{
    FragColor = vertexColor;
}

顶点着色器指定了颜色向量vertexColor,并传给了片段着色器。事实上vertexColor也可以来自顶点数据,只要在顶点数据中加入颜色数据,就可以给每个顶点分别指定颜色。编译运行后,上篇中的矩形被改为了暗红色:

Uniform修饰符

目前为止我们的颜色数据要么在片段着色器中指定,要么在顶点着色器中指定并传递给片段着色器。OpenGL中还存在一种方式可以让任何着色器在任何阶段都可以获取变量。当在变量前添加uniform关键字时,这个变量就是一个为所有着色器共享的全局变量了。例如,我们可以在片段着色器中定义一个有关颜色的uniform变量:

#version 330 core
out vec4 FragColor;

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

void main()
{
    FragColor = ourColor;
}

在此处定义后,我们可以在任何着色器中使用这个变量。但是如果一个uniform变量被定义后却从未被使用,OpenGL将在编译时移除它,且不会产生任何通知,这可能导致一些严重的问题。

要给一个uniform变量赋值,我们可以使用一些函数找到着色器中uniform变量的位置,从而更新它的值。为了体现出颜色值的更新,我们可以将颜色设为时间的函数。

float timeValue = glfwGetTime();                    // 用于获得当前运行的时间
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;  // 将时间转化为G通道的颜色值

// 找到uniform变量 ourColor 的位置,变量不存在时会返回-1
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram); // 更新着色器的uniform变量前必须启用着色器
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // 设置uniform变量的值

关于glUniform4f

OpenGL的核心是C语言,这意味着它不支持多态,也就没有函数重载。而uniform变量可以是各种类型的,因此更新uniform变量的函数glUniform也需要有各种各样的衍生,而不是重载。OpenGL的设计是在函数后面加各种后缀。例如glUniformf表示这个uniform变量是单个float,glUniformi表示这个uniform变量是单个int变量,glUniformfv表示这个uniform变量是一个float vector。

每个着色器中都有一个uniform存储区,专门用于存储uniform变量。调用glUniform时,第一个参数就是这个uniform变量在存储区中的位置,后面的则是与这个uniform变量类型匹配的要更新的值。


编译运行后我们就可以看到颜色随时间变化的矩形了:

让顶点来决定颜色

前面我们提到可以让顶点着色器来决定颜色,并把颜色传给片段着色器。一个自然的想法是将颜色作为一种顶点属性输入到顶点着色器中,这样就可以对每个顶点指定不同的颜色。

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    // 顶部(蓝色)
};

现在我们有更多的属性输入到顶点着色器中了。因此我们需要调整着色器的输入变量,否则OpenGL并不知道那些float值是颜色。具体而言,我们需要添加一个输入位置:

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

out vec3 vertexColor;
void main()
{
    gl_Position = vec4(aPos, 1.0);
    vertexColor = aColor;
}

// 在片段着色器中使用这个变量
in vec3 vertexColor;
...
FragColor = vec4(vertexColor, 1.0);
...

回忆上一篇文章,除了在着色器代码中设置输入位置以外,还需要在外部设置好顶点属性的大小,输入步长、偏移量等(顶点属性指针)。这些信息被存储在VAO中,要输入的顶点数据被储存在VBO中,而我们通过EBO来指挥OpenGL将哪些点绘制成三角形。

// 配置EBO
unsigned int indices[] = {
    0, 1, 2
};
...
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// 设置并启用两种顶点属性,注意步长是6 * sizeof(float),
// 因为现在一个顶点属性的大小是6个float
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);

//渲染循环中
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);

这样我们就可以画出三个顶点颜色各不相同的三角形了。


为什么上图的三角形是渐变色的

可以注意到,我们在着色器中仅仅给三个顶点指定了三种颜色,并要求OpenGL渲染一个由这三个顶点决定的三角形。我们并没有规定三角形内部的点的颜色应当如何决定,而看起来OpenGL基于内部点的位置对三个顶点的颜色进行了重心坐标插值,来决定这个点的颜色,这使得三角形看起来是渐变色的。

出现这种状况的原因是OpenGL默认情况下会使用平滑插值(smooth interpolation),也就是用重心坐标插值的方式决定三角形内部点的颜色。开发者也可以使用另一种插值限定符,将插值模式改为平面插值(flat interpolation),使得三角形的颜色由其中一个顶点(被称为激发顶点(Provoking Vertex)决定。默认情况下激发顶点是该三角形中最后一个输入的顶点。例如,我们可以这样修改着色器代码来修改插值模式:

// 顶点着色器
flat out vec3 vertexColor;
...

// 片段着色器
flat in vec3 vertexColor;
out vec4 FragColor
...

添加了这样的限定符后,编译运行的结果就会变为纯色三角形(颜色同最后一个输入的顶点相同,即蓝色):

激发顶点的选择规则可以更改,例如使用glProvokingVertex(GL_FIRST_VERTEX_CONVENTION);,就会将选择规则改为第一个输入的顶点。


管理着色器

到目前为止,我们的着色器代码还只是在cpp源码中声明两个静态字符串,并将其注入着色器对象中。当着色器数量增加时这明显不利于管理。因此我们可以创建一个着色器类,它将能够从文件中读取着色器代码并注入着色器中,还能负责着色器的错误检测。

#ifndef SHADER_H // 预处理指令可以保证只有在这个头文件没被包含时
#define SHADER_H // 才定义以下内容,可以防止链接冲突

#include <glad/glad.h>; // 包含glad来获取所有的必须OpenGL头文件

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>


class Shader
{
public:
    // 程序ID
    unsigned int ID;

    // 构造函数读取并构建着色器,需要一个顶点着色器源文件和一个片段着色器源文件
    Shader(const char* vertexPath, const char* fragmentPath);
    // 使用/激活程序
    void use();
    // uniform工具函数
    void setBool(const std::string &name, bool value) const;  
    void setInt(const std::string &name, int value) const;   
    void setFloat(const std::string &name, float value) const;
};

#endif

接下来我们实现Shader类的构造函数和其他成员函数

Shader(const char* vertexPath, const char* fragmentPath)
{
    // 1. 用C++从着色器源文件中获取源码
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;
    // 注意错误检查
    vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
    fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
    try
    {
        // 打开源文件
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        std::stringstream vShaderStream, fShaderStream;
        // 将文件读入字符串流中
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();
        // 现在可以关闭文件了
        vShaderFile.close();
        fShaderFile.close();
        // 从字符串流中得到字符串
        vertexCode = vShaderStream.str();
        fragmentCode = fShaderStream.str();
    }
    catch (std::ifstream::failure& e)
    {
        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;
    }
    // 转换为C风格字符串
    const char* vShaderCode = vertexCode.c_str();
    const char* fShaderCode = fragmentCode.c_str();
    // 2. 编译着色器
    unsigned int vertex, fragment;
    // 顶点着色器
    vertex = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertex, 1, &vShaderCode, NULL);
    glCompileShader(vertex);
    checkCompileErrors(vertex, "VERTEX");
    // 片段着色器
    fragment = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragment, 1, &fShaderCode, NULL);
    glCompileShader(fragment);
    checkCompileErrors(fragment, "FRAGMENT");
    // 将着色器附加到着色器程序
    ID = glCreateProgram();
    glAttachShader(ID, vertex);
    glAttachShader(ID, fragment);
    glLinkProgram(ID);
    checkCompileErrors(ID, "PROGRAM");
    // 完成后着色器本身就不需要了,保留着色器程序即可
    glDeleteShader(vertex);
    glDeleteShader(fragment);
}
// 另外的成员函数
void use() 
{
    glUseProgram(ID);
}
// 设置uniform变量的值
void setBool(const std::string &name, bool value) const
{
    glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); 
}
void setInt(const std::string &name, int value) const
{ 
    glUniform1i(glGetUniformLocation(ID, name.c_str()), value); 
}
void setFloat(const std::string &name, float value) const
{ 
    glUniform1f(glGetUniformLocation(ID, name.c_str()), value); 
}

以上就是我们自己的着色器类。有了它,我们就不需要在程序中手动声明源码字符串和创建着色器对象了。我们可以修改源码并使用着色器类来创建着色器对象。

// 从文件中读入、编译、链接着色器到着色器程序
// 此处的两个文件中保存的就是我们此前使用的两个着色器的源码
Shader ourShader("3_Vshader.vs", "3_Fshader.fs");
...
// 渲染循环中启用着色器程序
// 代替glUseProgram()
ourShader.use();
...

目前为止的源码

// 此处的两个头文件不能交换顺序
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <learnopengl/shader_s.h>

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    // 顶部
};

unsigned int indices[] = {
    // 0, 1, 3, // 第一个三角形由索引为0、1、3的三个顶点组成
    // 1, 2, 3  // 第二个三角形由索引为1、2、3的三个顶点组成
    0, 1, 2
};

void framebuffer_size_callback(GLFWwindow* window, int width, int height);

// 当用户改变窗口大小时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        // 设置窗口应当关闭,将在渲染循环中关闭窗口
        glfwSetWindowShouldClose(window, true);
}

int main()
{
    glfwInit();
    // 指定opengl版本
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    // 将窗口上下文设置为当前线程的主上下文
    glfwMakeContextCurrent(window);

    // GLAD用于管理opengl的函数指针,在调用opengl的函数之前要先初始化GLAD,
    // 此处将一个glfw的函数传入了GLAD中
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // 设置视口尺寸
    glViewport(0, 0, 800, 600);

    // 设置窗口尺寸的回调函数
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    unsigned int VBO;
    // 创建缓冲
    glGenBuffers(1, &VBO);
    // 绑定缓冲
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // 传入数据 
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    unsigned int EBO;
    glGenBuffers(1, &EBO);
    // 将缓冲区对象绑定到目标并传入数据
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    Shader ourShader("3_Vshader.vs", "3_Fshader.fs");

    // 创建顶点数组对象
    unsigned int VAO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    // 设置顶点属性指针
    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);

    // 渲染循环
    while (!glfwWindowShouldClose(window))
    {
        // 检测输入
        processInput(window);

        // 渲染指令(状态设置函数、状态使用函数等)
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
        
        ourShader.use();

        // 处理可变颜色
        // 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);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

        // 使用glDrawElements代替glDrawArrays
        glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);

        // 绘制后解绑VAO
        glBindVertexArray(0);

        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

        // 交换双缓冲
        glfwSwapBuffers(window);
        // 检查事件触发
        glfwPollEvents();
    }

    // 结束
    glfwTerminate();

    return 0;
}
$\displaystyle{=|\overline{\underline{To\ Be\ Continued}}|\Rightarrow}$
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇