美文网首页音视频
[OpenGL] 笔记(2)Shader

[OpenGL] 笔记(2)Shader

作者: 木小易Ying | 来源:发表于2021-02-10 19:31 被阅读0次

    上周是没有看这节就写了三角形的作业QAQ,所以这周来shader和texture叭~

    上节提到过每个 Shader 都是一个小程序,他们彼此之间非常独立,只能通过输入和输出交流。着色器有自己的编程语言叫做OpenGL着色器语言(GLSL)


    1. 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;
    

    通常情况下它至少会返回16个,大部分情况下是够用了。

    数据类型

    GLSL也有两种容器类型,分别是向量(Vector)和矩阵(Matrix),其中矩阵我们会在之后的教程里再讨论

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

    类型 含义
    vecn 包含n个float分量的默认向量
    bvecn 包含n个bool分量的向量
    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)。重组允许这样的语法:

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

    而,你不允许在一个vec2向量中去获取.z元素。我们也可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:

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

    输入输出

    虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配(名称 & 类型),它就会传递下去。

    当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。

    顶点属性会比较特殊,需要规定 location~ 另一个例外是片段着色器,它需要一个vec4(不能是vec3哦)颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。


    uniform 全局变量

    Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。

    因为uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform,所以我们不用在那里定义它。

    #version 330 core
    out vec4 FragColor;
    
    uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
    
    void main()
    {
        FragColor = ourColor;
    }
    

    传值让颜色随时间变化:

    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);
    

    如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!

    接着,我们用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)。如果glGetUniformLocation返回-1就代表没有找到这个位置值。最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。
    (我们希望分别设定uniform的4个float值,所以我们通过glUniform4f传递我们的数据)

    下面我们就计算greenValue然后每个渲染迭代都更新这个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();
    }
    

    2. Shader Class 不再用string啦

    shader 用 string 写真的太累了,所以改成 class 叭,但这就需要我们从文件读取转为 string 并且生成 shader 啦,这个过程可以用一个类来实现:

    #ifndef Shader_hpp
    #define Shader_hpp
    
    #include <glad/glad.h>
    
    #include <string>
    #include <fstream>
    #include <sstream>
    #include <iostream>
    
    class Shader
    {
    public:
        unsigned int ID;
        // constructor generates the shader on the fly
        // ------------------------------------------------------------------------
        Shader(const char* vertexPath, const char* fragmentPath)
        {
            // 1. retrieve the vertex/fragment source code from filePath
            std::string vertexCode;
            std::string fragmentCode;
            std::ifstream vShaderFile;
            std::ifstream fShaderFile;
            // ensure ifstream objects can throw exceptions:
            vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
            fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
            try
            {
                // open files
                vShaderFile.open(vertexPath);
                fShaderFile.open(fragmentPath);
                std::stringstream vShaderStream, fShaderStream;
                // read file's buffer contents into streams
                vShaderStream << vShaderFile.rdbuf();
                fShaderStream << fShaderFile.rdbuf();
                // close file handlers
                vShaderFile.close();
                fShaderFile.close();
                // convert stream into string
                vertexCode   = vShaderStream.str();
                fragmentCode = fShaderStream.str();
            }
            catch (std::ifstream::failure& e)
            {
                std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
            }
            const char* vShaderCode = vertexCode.c_str();
            const char * fShaderCode = fragmentCode.c_str();
            // 2. compile shaders
            unsigned int vertex, fragment;
            // vertex shader
            vertex = glCreateShader(GL_VERTEX_SHADER);
            glShaderSource(vertex, 1, &vShaderCode, NULL);
            glCompileShader(vertex);
            checkCompileErrors(vertex, "VERTEX");
            // fragment Shader
            fragment = glCreateShader(GL_FRAGMENT_SHADER);
            glShaderSource(fragment, 1, &fShaderCode, NULL);
            glCompileShader(fragment);
            checkCompileErrors(fragment, "FRAGMENT");
            // shader Program
            ID = glCreateProgram();
            glAttachShader(ID, vertex);
            glAttachShader(ID, fragment);
            glLinkProgram(ID);
            checkCompileErrors(ID, "PROGRAM");
            // delete the shaders as they're linked into our program now and no longer necessary
            glDeleteShader(vertex);
            glDeleteShader(fragment);
        }
        // activate the shader
        // ------------------------------------------------------------------------
        void use()
        {
            glUseProgram(ID);
        }
        // utility uniform functions
        // ------------------------------------------------------------------------
        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);
        }
    
    private:
        // utility function for checking shader compilation/linking errors.
        // ------------------------------------------------------------------------
        void checkCompileErrors(unsigned int shader, std::string type)
        {
            int success;
            char infoLog[1024];
            if (type != "PROGRAM")
            {
                glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
                if (!success)
                {
                    glGetShaderInfoLog(shader, 1024, NULL, infoLog);
                    std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
                }
            }
            else
            {
                glGetProgramiv(shader, GL_LINK_STATUS, &success);
                if (!success)
                {
                    glGetProgramInfoLog(shader, 1024, NULL, infoLog);
                    std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
                }
            }
        }
    };
    
    #endif /* Shader_hpp */
    

    我们就可以开始使用了:

    Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");
    ...
    while(...)
    {
        ourShader.use();
        ourShader.setFloat("someUniform", 1.0f);
        DrawStuff();
    }
    

    我们把顶点和片段着色器储存为两个叫做shader.vs和shader.fs的文件。你可以使用自己喜欢的名字命名着色器文件;我自己觉得用.vs和.fs作为扩展名很直观。

    tips:

    3. Homework

    (1)画个随时间渐变的三角形
    ezgif-7-82db84d2e46a.gif

    这个的实现我感觉我的方式不是很正确~ 就是每顶点传入一个颜色,然后再和全局的时间变量乘一下~

    顶点着色器:

    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aColor;
    
    out vec3 vColor;
    
    void main()
    {
        gl_Position = vec4(aPos, 1.0);
        vColor = aColor;
    }
    

    着色器:

    #version 330 core
    out vec4 FragColor;
    in vec3 vColor;
    
    uniform vec4 timeColor;
    
    void main()
    {
        FragColor = vec4(vColor.x * timeColor.x, vColor.y * timeColor.y, vColor.z * timeColor.z, timeColor.w);
    }
    

    主程序:

    // vertice
        float vertices[] = {
            -0.5f, -0.5f, 0.0f, 1.f, 0.f, 0.f,
            0.0f, -0.5f, 0.0f, 0.f, 1.f, 0.f,
            -0.25f,  0.5f, 0.0f, 0.f, 0.f, 1.f,
        };
        
        unsigned int VBO;
        glGenBuffers(1, &VBO);
        
        unsigned int VAO;
        glGenVertexArrays(1, &VAO);
        
        glBindVertexArray(VAO);
        
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        
        // read vertex
        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);
        
        // unbind VBO
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        
        // unbind VAO
        glBindVertexArray(0);
    
        // shader
        Shader ourShader("shader1.vs", "shader1.fs");
        
        // render loop
        // -----------
        while (!glfwWindowShouldClose(window))
        {
            // input
            // -----
            processInput2(window);
            
            // render
            // ------
            glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);
    
            float timeValue = glfwGetTime();
            float color1 = (sin(timeValue) / 2.0f) + 0.5f;
            float color2 = (cos(timeValue) / 2.0f) + 0.5f;
            int vertexColorLocation = glGetUniformLocation(ourShader.ID, "timeColor");
            // program
            ourShader.use();
            
            glUniform4f(vertexColorLocation, color1, color2, color1, color2);
            
            glBindVertexArray(VAO);
            // draw
            glDrawArrays(GL_TRIANGLES, 0, 3);
            
            // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
            // -------------------------------------------------------------------------------
            glfwSwapBuffers(window);
            glfwPollEvents();
        }
    
    (2)把三角形倒过来

    这个很简单改一下顶点着色器就好:

    #version 330 core
    layout (location = 0) in vec3 aPos;
    
    void main()
    {
        gl_Position = vec4(aPos.x, -aPos.y, aPos.z, 1.0);
    }
    

    这篇太水了哈哈哈哈哈Orz~

    相关文章

      网友评论

        本文标题:[OpenGL] 笔记(2)Shader

        本文链接:https://www.haomeiwen.com/subject/faqcxltx.html