OpenGL:着色器

一.概述

在上一篇《绘制一个三角形》中涉及到了顶点着色器、片段着色器,也初步了解了如何编写和使用一个着色器

着色器是OpenGL的灵魂

跟着官方学习文档的脚步,专门写一篇来细致全面地了解着色器

从基本意义上来说,着色器只是一种把输入转化为输出的程序,也是一种非常独立的程序,为图形渲染管线的某个特定部分而运行在GPU上,它们之间不能相互通信

它们之间唯一的沟通只有通过各自的输入和输出

二.GLSL

着色器是使用一种叫GLSL的类C语言写成的,包含一些针对向量和矩阵操作的有用特性

官方参考文档:

    https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.1.20.pdf

    docs.gl

一个着色器程序的会包含如下几点:

  • 声明版本
  • 输入变量、输出变量、Uniform全局变量
  • main()函数

main()函数处理所有的输入变量,并将结果输出到输出变量

代码段: 

#version version_number           //声明版本

in type in_variable_name;         //输入变量1
in type in_variable_name;         //输入变量2

out type out_variable_name;       //输出变量

uniform type uniform_name;        //全局变量

int main()                        //main()函数
{
    // 处理输入并进行一些图形操作
    ...
    // 输出处理过的结果到输出变量
    out_variable_name = weird_stuff_we_processed; 
}

对顶点着色器而言,每个输入变量也叫顶点属性

OpenGL中能声明的顶点属性是有上限的,一般由硬件来决定

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个,大部分情况是够用了

三.数据类型 

3.1 GLSL数据类型

基础数据类型:int、float、double、uint、bool

两种容器类型:向量(Vector)、矩阵(Matrix)

3.2 向量

vecn: 包含n个float分量的默认向量

bvecn:包含n个bool分量的向量

ivecn: 包含n个int分量的向量

uvecn:包含n个unsigned int分量的向量

dvecn:包含n个double分量的向量

大多数情况下使用vecn就足够了

GLSL中向量的运算:

获取分量:vec.x、vec.y、vec.z、vec.w

重组运算:

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

四.输入与输出

GLSL定义了 in 和 out 关键字实现着色器的输入与输出

每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去

顶点着色器和片段着色器的输入输出稍有不同:

顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据

片段着色器,需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色

如果打算从一个着色器向另一个着色器发送数据

必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入

当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了

示例:让顶点着色器为片段着色器决定颜色

顶点着色器代码:

#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;
}

顶点着色器和片段着色器通过变量vertexColor衔接在一起

编译运行:

成功地从顶点着色器向片段着色器发送数据。

让我们更上一层楼,看看能否从应用程序中直接给片段着色器发送一个颜色!

五.Uniform

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式

Uniform是全局变量,全局变量就意味着:

        (1).我们可以在任何着色器中定义它们

        (2).在每个着色器程序对象中都是独一无二的,可以被着色器程序的任意着色器在任意阶段访问

        (3).无论把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新

示例:通过uniform设置三角形的颜色

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

这个Uniform现在还没有被赋值

首先需要使用glGetUniformLocation()函数获取到着色器中Uniform属性的索引/位置值,然后才能更新它的值了

这次不再赋一个固定的颜色值了,而是让它随着时间改变颜色:

//通过glfwGetTime()获取运行的秒数
float timeValue = glfwGetTime();
//使用sin()函数让颜色在0.0到1.0
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
/*查询uniform ourColor的位置值,返回-1表示没能找到
  参数:着色器程序、uniform名字*/
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
//设置uniform变量的值
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

查询Uniform地址不要求之前使用过着色器程序

更新一个Uniform之前必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的

OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数;

glUniform是一个典型例子,这个函数用特定的后缀标识设定的uniform的类型:

        f 函数需要一个float作为它的值

        i 函数需要一个int作为它的值

        ui 函数需要一个unsigned int作为它的值

        3f 函数需要3个float作为它的值

        fv 函数需要一个float向量/数组作为它的值

知道如何设置uniform变量的值以后

现在开始动态渲染三角形,在渲染循环的每一次迭代中(所以他会逐帧改变)更新这个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();
}

编译运行,效果如下:

可以看到,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    // 顶部
};

顶点数据中以前只有一个"位置属性",现在新增了一个"颜色属性"

顶点着色器的代码也相应进行修改:

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

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

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

片段着色器的代码也要进行修改:

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

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

新增了一个顶点属性,VBO也变化了:

 顶点属性指针也需要重新分配:

// 位置属性
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);

参数变化:

  • 颜色属性是顶点的第2个属性,所以第一个参数配1
  • 一个顶点有两个属性,6个float,所以第五个参数步长配 6*sizeof(float)
  • 颜色属性在每个顶点内存的偏移是3个float,所以最后一个参数填 3*sizeof(float)

代码修改好之后,编译运行:

看到这个三角形就会有个疑问:

为什么我们只设置了三个顶点的颜色,但是显示出来的却是调色板三角形?

        这是在片段着色器中进行的片段插值(Fragment Interpolation)的结果

        当渲染一个三角形时,光栅化(Rasterization)阶段会将三角形分割成无数个像素片段进行上色

        上色的原则可以理解为"近朱则赤,近墨者黑",就是采取周围的像素的像素值进行插值填充

        这样就造成了调色板的效果

七.封装着色器类

到现在为止,我们所有的代码都写在了一个main.cpp中

着色器的编写、编译、检测、链接、调用等相关代码完全可以单独抽离封装成文件,保证代码的可读性和可移植性

写一个着色器类的头文件 Shader.h:

#include <glad/glad.h>

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

using namespace std;

class Shader
{
    public:
    //着色器程序ID
    unsigned int ID;
    
    // 构造器读取并构建着色器
    Shader(const char* vertexPath, const char* fragmentPath) 
    {
        //1.从filePath中检索顶点 / 片段源代码
        // 顶点和片段着色器代码文件
        ifstream vShaderFile;
        ifstream fShaderFile;
        
        // 顶点和片段着色器源码字符串
        string vertexCode;
        string fragmentCode;
        
        // 确保ifstream对象能够抛出异常
        vShaderFile.exceptions(ifstream::failbit | ifstream::badbit);
        fShaderFile.exceptions(ifstream::failbit | ifstream::badbit);
        
        try 
        {
        // open file
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        
        // 读取文件缓冲内容到streams
        std::stringstream vShaderStream, fShaderStream;
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();
        
        // close file hander
        vShaderFile.close();
        fShaderFile.close();
        
        // stream转换赋值给string字符串,最终string还需要转换成char*指针
        vertexCode = vShaderStream.str();
        fragmentCode = fShaderStream.str();
        }
        catch (std::ifstream::failure& e) 
        {
        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;
        }
        
          //string转换成char*
        const char* vShaderCode = vertexCode.c_str();
        const char* fShaderCode = fragmentCode.c_str();
        
        //2.编译shader
        //创建顶点着色器
        unsigned int vertex;
        vertex = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertex, 1, &vShaderCode, NULL);
        glCompileShader(vertex);
        checkCompileErrors(vertex, "VERTEX");
                 
        //创建片段着色器
        unsigned int fragment;
        fragment = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragment, 1, &fShaderCode, NULL);
        glCompileShader(fragment);
        checkCompileErrors(fragment, "FRAGMENT");
        
        //创建着色器程序
        ID = glCreateProgram();
        //附件着色器到着色器程序,然后链接着色器程序到OpenGL
        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);
    }

    private:
    // 检测着色器编译错误和着色器程序链接错误
    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;
            }
        }
    }
};

main.cpp文件中关于着色器的代码基本可以删掉了

只需要保留着色器加载和着色器程序使用:

#include "Shader.h"

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

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

// 窗口大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

int main()
{
    // glfw: 初始化窗口配置
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // glfw:创建窗口
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load OpenGL所有函数指针
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    //加载着色器文件
    Shader ourShader("assets/shader.vs", "assets/shader.fs");

    // 配置顶点数据buffer和顶点属性
    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 VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // 绑定VAO先,然后绑定和设置顶点buffer,再配置顶点属性
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

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

    //激活着色器程序
    ourShader.use();

    // 渲染循环
    while (!glfwWindowShouldClose(window))
    {
        // 处理外设输入
        processInput(window);

        // render
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 绘制三角形
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        // glBindVertexArray(0); // no need to unbind it every time 

        // glfw: 交换缓冲区和轮询IO事件(按键按下/释放、鼠标移动等)
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 删除分配的所有资源:
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

    // glfw:终止、清除所有先前分配的GLFW资源。
    glfwTerminate();
    return 0;
}

// glfw:处理窗口事件
void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: 窗口尺寸变化回调
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // 确保视口与新窗口尺寸匹配
    glViewport(0, 0, width, height);
}

从代码中可以看到,main.cpp中对着色器的调用就只剩下两处了:

//加载着色器文件

Shader ourShader("assets/shader.vs", "assets/shader.fs");

.....

//激活着色器程序

ourShader.use();

.....

在工程目录下新建了一个assets文件夹

再新建两个txt文本,将顶点和片段着色器的内容保存后,分别修改后缀为vs和fs

名称、后缀可以随便修改,自己喜欢就好

编译运行,结果与之前的一样:

猜你喜欢

转载自blog.csdn.net/geyichongchujianghu/article/details/129779727