• OpenGL教程(五)


    前言

    正如之前章节所提到的,着色器就是运行在GPU上的小程序,简单来说,着色器就是仅仅是一个将输入数据经过一定转换然后输出的过程,着色器之间是非常独立的,彼此之间除了输入输出之外没有其他交流,这篇文章将会详细介绍着色器以及编写着色器的语言GLSL。

    GLSL

    着色器是使用一种类似C语言的语言GLSL编写的,GLSL是为显卡使用而设计的,包含了很多针对向量和矩阵操作的特性。着色器一般以版本声明为开头,紧接着就是一些输入和输出的变量,uniform以及main函数,每一个着色器的入口都是main函数,在main函数中处理输入变量然后将处理的结果输出到输出变量,至于uinform,之后我们会详细介绍,一个典型的着色器的结构如下

    #version version_number
    in type in_variable_name;
    in type in_variable_name;
    out type out_variable_name;
    uniform type uniform_name;
    void main()
        {
        // process input(s) and do some weird graphics stuff
            ...
        // output processed stuff to output variable
            out_variable_name = weird_stuff_we_processed;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    当提及顶点着色器时我们都知道它的输入变量是顶点属性,硬件支持我们声明的顶点属性的数量有一个最大值,OpenGL规定至少有16个4分量的顶点属性可用,一些硬件可能会提供更多,这个值可以通过GL_MAX_VERTEX_ATTRIBS查询:

    int nrAttributes;
    glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
    std::cout<< "Maxium nr of vertex attributes supported: " <
    • 1
    • 2
    • 3
    • 4

    类型

    与其他编程语言相同,GLSL也拥有数据类型去指定我们使用的数据的类型,GLSL提供了C语言提供的默认基本类型如int, float, double, uint以及bool。GLSL同样提供了两种容器类型vectors和matrices,matrices之后我们再讨论。

    Vectors

    GLSL的vector拥有1,2,3或4个之前提到的基本类型的分量,它们的形式有以下几种:

    • vecn:拥有n个浮点数的vector
    • bvecn:包含n个布尔值的vector
    • ivecn:包含n个整型的vector
    • uvecn:包含n个无符号整型的vector
    • dvecn:包含n个double的vector
      一般情况下vecn已经能够满足使用需求。

    vector分量的获取有很多方式,也非常的灵活,例如vec.x就可以获取vector的第一个分量,使用.x, .y, .z 和.w可以分别获取vector的第一、第二、第三和第四个分量,同样的GLSL也允许通过rgba去获取颜色分量,通过stpq获取纹理的坐标,vector的赋值与计算也非常的灵活,就像这样:

    vec2 someVec;
    vec4 differentVec;
    vec3 anotherVec = differentVec.zyw;
    vec4 othorVec = someVec.xxxx + anotherVec.yxzy;
    
    • 1
    • 2
    • 3
    • 4
    输入与输出

    着色器本身是非常小的程序,但是是整个图形管线的一部分,所以每一个着色器都必须有输入和输出,GLSL提供了in和out关键字来指定输入和输出,一个着色器的输出将会是另一个着色器的输入。

    顶点着色器需要接受特定形式的输入否则效率会特别低下,而且顶点着色器是直接从顶点数据获取输入,为了解决这个问题,可以通过指定变量的位置就像layout(location = 0)来说明顶点数据是如何组织的。

    其实也可以通过glGetAttribLocation省略指定布局(layout(location=0)),但是还是建议写在顶点着色器中,这样易于理解,也可以减少我们和OpenGL的工作。

    除了指定输入有时候我们还需要指定输出,例如片段着色器需要输出每一个像素的颜色,如果片段着色器没有输出颜色,OpenGL将会渲染成白色或黑色。所以如果想要从一个着色器向另一个着色器传递数据,我们需要在一个着色器声明输出,在一个着色器声明输入,当变量的类型和名字都相同时,OpenGL将会链接这些数据从而实现数据的传递(这个过程一般是在链接项目对象时实现的),为了说明这个过程,我们通过一下例子说明,在顶点着色器就定义颜色:
    顶点着色器

    #version 330 core
    layout(location = 0) in vec3 aPos;
    
    out vec4 vertexColor;
    
    void main()
    {
        gl_Postion = vec4(aPos, 1.0);
        vertexColor = vec4(0.5, 0.0, 0.0, 1.0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    片段着色器

    out vec4 FragColor;
    
    in vec4 vertexColor;
    
    void main() {
        FragColor = vertexColor;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    Uniforms

    uniform是另一种方式将CPU上的数据传递到GPU的着色器上,uniform与顶点属性差异很大,首先,uniform是全局的。全局则意味着在每一个着色器程序(shader program)中都是唯一的,其次无论给uniform设置了什么值,uniform都会保持这个值除非重置或更新这个值。

    #version 330 core
    out vec4 FragColor;
    
    uniform vec4 ourColor;
    
    void main() {
        FragColor = ourColor;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们需要使用uniform关键字去声明,从上面代码可以看出我们使用在着色器中使用uniform,并且使用uniform定义三角形的颜色,由于uniform是一个全局变量,我们可以在任意的着色器中去定义。

    需要注意的是如果声明了一个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);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    首先我们需要通过glfwGetTime()获取到当时的运行时间(以秒为单位),然后通过sin函数将greenValue的值设置在0.0到1.0之间,然后通过glGetUniformLocation函数查询uniform变量的位置,其第一个参数是着色器程序,第二个参数是uinform变量的名称,如如果其返回值是-1则说明没有找到位置。然后通过glUniform4f函数来设置uniform变量的值。需要注意的是在查询uniform变量的位置时不需要使用该着色器程序,但是在更新uniform变量的之前需要使用着色器程序(通过调用glUseProgram),这是因为设置uniform变量是在当前活跃的着色器程序。

    因为OpenGL是一个C库,所以不支持函数的重载,所以一个函数支持不同的数据类型,就需要为每一个数据类型定义一个新的函数,glUniform函数就是一个很好的例子,这个函数通过不同的后缀来区分不同类型的参数。

    • f:入参是一个float
    • i:入参是一个int
    • ui:入参是一个unsigned int
    • 3分:入参是3个float
    • fv:入参是一个float类型的vector或array
      我们使用的glUniform4f就是需要四个float类型入参。

    现在我们已经知道了如何设置uniform变量的值,我们可以使用它们去渲染,如果我们想改变颜色,还需要在每一帧渲染的时候更新uniform,所以我们需要在渲染循环中计算和跟新greenValue。

    while(!glfwWindowShouldClose(window)) {
        processInput(window);
    
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
    
        glUseProgram(shaderProgram);
    
        float timeValue = glfwGetTime();
        float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
    i   nt vertexColorLocation = glGetUniformLocation(shaderProgram,     "ourColor");
        glUseProgram(shaderProgram);
        glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
    
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
    
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    以上可以看到uinform是为每一帧动态设置属性很好的方法,但是如果我们想要为每一个顶点设置颜色呢,通过以上的方法可以需要为每一个顶点声明一个uniform变量,显然这种方法比较繁琐,更好的方式是在顶点属性中定义更多的数据。

    更多的属性

    之前的章节我们介绍了如何填充VBO、配置顶点属性指针以及将它们全部存储到VAO中,这一次我们也可以将颜色数据添加到顶点数据中,所以我们需要将3个浮点数颜色数据添加到顶点数组中。

    float vertices[] = {
    // positions         // colors
    0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right
    -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left
    0.0f,  0.5f, 0.0f, 0.0f, 0.0f, 1.0f  // top
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为我们需要传递更多的数据给顶点着色器,所以我们需要调整顶点着色器去接受颜色值作为顶点属性的输入,以下代码则是修改后的顶点着色器,可以注意到,我们将颜色属性的layout设置为1。

    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aColor;
    
    out vec3 ourColor;
    
    void main() {
        gl_Position = vec4(aPos, 1.0);
        ourColor = aColor;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    由于我们不在片段着色器中使用uniform而是使用ourColor变量设置颜色,所以我们也需要修改片段着色器的内容:

    #version 330 core
    out vec4 FragColor;
    in vec3 ourColor;
    
    void main() {
        FragColor = vec4(ourColor, 1.0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    因为我们新增了其他的顶点属性,我们需要配置顶点属性指针,在VBO中更新后的数据的组织情况如下图所示:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tq9nMPUP-1668824944502)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54a8eb096643428585a03c683f11387b~tplv-k3u1fbpfcp-watermark.image?)]

    已经知道了现在数据的布局,我们可以通过glVertexAttribPointer来格式化。

    glVertexAttribPointer(0, 3, GL_FLOAT, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 3, GL_POINTER, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    
    • 1
    • 2
    • 3
    • 4

    需要注意的是我们将位置属性的layout设置为0,颜色属性的layout设置为1,颜色属性在位置属性的厚片,位置属性是三个float,所以颜色属性的偏移量就是3 * sizeof(float)。

    最后的结果如下

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6vwocAEY-1668824944502)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5a6b7470da8c4e4ead8cb2f6749522a2~tplv-k3u1fbpfcp-watermark.image?)]

    代码如下

    # include
    # include
    # include
    
    void framebuffer_size_callback(GLFWwindow *window, int width, int height);
    
    const unsigned int WIDTH = 800;
    const unsigned int HEIGHT = 600;
    
    const char *vertexShaderSource ="#version 330 core\n"
        "layout (location = 0) in vec3 aPos;\n"
        "layout (location = 1) in vec3 aColor;\n"
        "out vec3 ourColor;\n"
        "void main()\n"
        "{\n"
        "   gl_Position = vec4(aPos, 1.0);\n"
        "   ourColor = aColor;\n"
        "}\0";
    
    const char *fragmentShaderSource = "#version 330 core\n"
        "out vec4 FragColor;\n"
        "in vec3 ourColor;\n"
        "void main()\n"
        "{\n"
        "   FragColor = vec4(ourColor, 1.0f);\n"
        "}\n\0";
    
    int main() {
        glfwInit();
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    
        GLFWwindow *window = glfwCreateWindow(WIDTH, HEIGHT, "QStackOpenGL", NULL, NULL);
        if (window == NULL) {
            std::cout<<"creat window failed"<
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144

    定义自己的着色器类

    编写、编译和管理着色器十分的繁琐,为了方便管理我们通过建立一个着色器的类从硬盘读取着色器,然后编译并且链接它们,检查是否有错误,十分方便使用。

    出于学习的目的,我们将着色器的类定义在一个头文件内,其结构如下

    #ifndef SHADER_H
    #define SHADER_H
    #include  // include glad to get the required OpenGL headers
    #include 
    #include 
    #include 
    #include 
    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    着色器的类需要获取着色器项目的ID,它的构造函数需要顶点着色器和片段着色器源码文件的地址。

    最后

    这篇文章主要介绍了编写着色器的语言GLSL,更多内容可以关注公众号QStack。

  • 相关阅读:
    飞书Webhook触发操作指南,实现事件驱动型工作流自动化
    SpringBoot-EasyExcel(大数据处理)
    面试题:有了 for 循环 为什么还要 forEach ?
    大语言模型 (LLM) 红队测试:提前解决模型漏洞
    vue3项目实战中的接口调用方法(一)async/await用法 对axios二次封装 实现异步请求
    3.ICMP
    AI 正在取代程序员
    深度学习项目部署遇到的错误【记录】
    SqlServer 系统表
    【LeetCode】Python | 1760. 袋子里最少数目的球(二分)
  • 原文地址:https://blog.csdn.net/QStack/article/details/127933577