从GLSL着色器到VAO/VBO/EBO知识梳理

1. 着色器的含义

着色器是GPU上的小程序,为图形渲染管线的某个特定部分运行。换句话说,着色器就是将输入转化为输出的程序。
着色器是由一种叫做GLSL的类C语言编写成的。

2. GLSL

着色器的开头要声明版本、输入输出、uniform和main函数。比如顶点着色器,它的输入变量叫做顶点属性,我们能声明的顶点属性是有上限的,一般由硬件决定。OpenGL至少确保有16个包含4分量的顶点属性可用。
一个简单着色器如下:

#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;
}
2.1 数据类型
  • 默认数据类型 :int float double unit bool
  • 容器类型:向量(Vector)和矩阵(Matrix)
从GLSL着色器到VAO/VBO/EBO知识梳理_第1张图片
向量类型
2.2 输入输出

每个着色器使用关键字in和out决定输入输出,只要输出变量与下一个着色器的输入相匹配,它就会不断传递下去。顶点着色器与片段着色器会有点不一样。

  • 顶点着色器:我们必须使用location这个一元数据去指定输入变量,这样我们才可以在GPU上设置顶点属性。顶点着色器需要为它的输入变量提供额外的layout标识,这样我们才能将它链接到顶点数据
  • 片段着色器:它需要一个vec4颜色输出变量,因为片段着色器最终会生成颜色,如果你没有设置它的输出变量,那么OpenGL就会自动将你的物体渲染为黑色或白色。

所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,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;
}
  • 最终效果


    从GLSL着色器到VAO/VBO/EBO知识梳理_第2张图片
    最终渲染效果
2.3 Uniform

Uniform是一种从CPU应用向GPU着色器发送数据的一种方式。Uniform是全局的,在某一着色器里声明了它,其他着色器就可以使用它。
比如下面我们将在片段着色器里声明一个Uniform

#version 330 core
out vec4 FragColor;

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

void main()
{
    FragColor = ourColor;
}

下面我们来看一个特殊的函数glUniform4f,4f代表什么?4f其实就是函数需要4个float作为它的值。

glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

如下是我们函数名可能有的后缀


从GLSL着色器到VAO/VBO/EBO知识梳理_第3张图片
函数名后缀

下面我们打算当每一次渲染迭代都更新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.4 从文件流中读取着色器内容
Shader(const char* vertexPath, const char* fragmentPath)
{
    // 1. 从文件路径中获取顶点/片段着色器
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;
    // 保证ifstream对象可以抛出异常:
    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();
        // 转换数据流到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();
    [...]

3. 顶点缓冲对象(VBO)

3.1 定义

顶点缓冲对象是在显卡里开辟一块内存,用于存储顶点的各类属性信息。在渲染时,可以直接从VBO中取出顶点数据进行渲染,由于顶点数据存储在显卡中而不是内存中,不需要从CPU传输数据,渲染效率更高。

每个VBO在OpenGL里都有对应的ID,这个ID对应VBO的显存地址,通过这个ID可以对VBO的数据进行存取

3.2 VBO的创建和配置
  • 开辟显存空间并分配VBO的ID
//创建vertex buffer object对象  
GLuint vboId;//vertex buffer object句柄  
glGenBuffers(1, &vboId);  
  • 创建后需要通过分配的ID来绑定VBO,对于同一种类型的顶点数据一次只能绑定一个VBO。
    第一个参数指的是绑定的数据类型:GL_ARRAY_BUFFER(顶点数组传值), GL_ELEMENT_ARRAY_BUFFER(索引数组传值), GL_PIXEL_PACK_BUFFER,GL_PIXEL_UNPACK_BUFFER
glBindBuffer(GL_ARRAY_BUFFER, vboId);  
  • 将用户数据传输到当前绑定的显存缓冲区中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);  
  • 顶点数据传入GPU后,还需要告诉OpenGL如何解析这些顶点数据
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);  

第一个参数指定顶点属性位置(与顶点着色器的layout(location=0)有关)
第二个参数指定顶点数据的大小
第三个参数指定顶点数据类型
第四个参数指定顶点数据是否被标准化
第五个参数是步长,指定顶点之间的间隔
第六个参数是位置数据在缓冲区起始的偏移量

顶点属性glVertexAttribPointer默认是关闭的,使用时要以顶点属性位置值为参数调用glEnableVertexAttribArray开启。如glEnableVertexAttribArray(0);

总的来讲,VBO的配置如下:

  1. 开辟显存空间并分配ID
  2. 通过分配的ID来绑定VBO
  3. 传入用户数据到绑定的显存缓冲区BO
  4. 设置OpenGL如何解析顶点数据

4. 顶点数组对象(VAO)

4.1 优点

当我们使用VBO时,每次绘制模型的时候都需要绑定顶点的所有信息,数据量大的时候就显得十分麻烦。VAO可以将所有的配置信息都保存在对象中,下一次绘制模型的时候直接绑定VAO对象就可以了。

4.2 含义

VAO保存了所有顶点属性的状态集合,它存储了顶点数据的格式以及顶点数据所需的VBO对象的引用.它不能单独使用,都是结合VBO来一起使用的

4.3 VAO的配置
  • 生成一个VAO对象并绑定VAOID,之后我们所有关于顶点数据的设置都会被存储在VAO中,在设置完成之后一般会解绑VAO,然后在需要绘制的时候启用相应的VAO对象。
//创建VAO
GLuint VAO;
glGenVertexArrays(1, &VAO);
//设置当前VAO,之后所有操作(注意:这些操作必须是上文VAO中包含的内容所注明的调用,其他非VAO中存储的内容即使调用了也不会影响VAO)存储在该VAO中
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO); //设置了VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//设置VBO中的数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); //设置顶点属性(索引为0的属性,与shader中的内容有交互)
glEnableVertexAttribArray(0); //设置开启顶点属性(索引为0的属性,与shader中的内容有交互)
glBindVertexArray(0); //解绑VAO(解绑主要是为了不影响后续VAO的设置,有点类似于C++中指针delete后置空,是个好习惯)

当我们需要绘制的时候

glUseProgram(shaderProgram);
glBindVertexArray(VAO); //绑定我们需要的VAO,会导致上面所有VAO保存的设置自动设置完成
someOpenGLFunctionThatDrawsOurTriangle();   
glBindVertexArray(0);   //解绑VAO

VAO对象中主要包含以下几个信息

  1. VAO开启或关闭的状态(glEnableVertexAttribArray和glDisableVertexAttribArray)
  2. 使用glVertexAttribPointer设置顶点属性信息
  3. 存储顶点数据的VBO对象
从GLSL着色器到VAO/VBO/EBO知识梳理_第4张图片
顶点数组与顶点对象的联系

4. 索引缓冲对象(EBO)

4.1 含义

索引缓冲对象是为了解决同一顶点重复调用的问题,可以减少内存浪费提高执行效率。当需要使用重复顶点的时候,可以通过顶点索引来调用顶点,而不是重复记录。

是不是看起来有点糊里糊涂,下面我们用一个小例子来说明索引缓存对象的用处。
比如我们在绘制一个正方体的时候,它的结构如下:

从GLSL着色器到VAO/VBO/EBO知识梳理_第5张图片
正方体

正方体的顶点只有八个,但是我们在构造它的数组的时候可以发现有许多重复的值, 最好的方式应该是每个顶点只需要存储一次,当我们需要这些顶点时,只需要调用顶点的索引来引用的需要的顶点数据

GLfloat vertices[] = {
//前
-1,-1,1, //v4
1, -1, 1 //v5
1, 1, 1 //v6
-1, 1, 1 //v7
//左
-1,1,-1 //v0
-1,-1,1 //v4
-1,1,1 //v7
-1,1,-1 //v3
......
};
从GLSL着色器到VAO/VBO/EBO知识梳理_第6张图片
索引的好处
4.2 索引的使用
  • 创建EBO(与VBO类似)
GLuint eboID;
glGenBuffers(1, &eboID);
  • 绑定EBO,传入索引数据到EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //绑定EBO
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
 //传入索引数据到EBO中
  • 绘制几何体(glDrawElements,如果不使用EBO,就是使用glDrawArrays进行绘制)
void glDrawElements(    
    GLenum mode,    //绘制模式,可以是GL_TRIANGLES、GL_POINTS等     
    GLsizei count, //绘制顶点的次数
    GLenum type,   //索引数据的类型
    const GLvoid * indices //EBO中的偏移量(如果不使用EBO,那么indices指向的是索引数组的指针)
);

当我们使用了EBO后,VAO中也会存储EBO的信息

从GLSL着色器到VAO/VBO/EBO知识梳理_第7张图片
VAO

引用:
OpenGL缓冲区对象之VAO
OpenGL缓冲区对象之EBO
OpenGL图形渲染管线、VBO、VAO、EBO概念及用例
官方文档-着色器

你可能感兴趣的:(从GLSL着色器到VAO/VBO/EBO知识梳理)