OpenGL教程(四)

756 阅读10分钟

前言

片段着色器(Fragment shader)

片段着色器是我们需要编写的第二个也是最后一个着色器程序,其主要用于计算每一个像素的颜色,在例子中为了简单我们会将每个颜色设置为橘黄色。

计算机中的颜色是由一个四维数组组成的,分别是红、绿,蓝以及透明度,通常被称为RGBA,在OpenGL或者GLSL中定义一个颜色需要吧每个部分定义到0.0和1.0之间,例如我们将红色和绿色都设置为1.0,那么就会得到二者混合之后的颜色黄色。

片段着色器只需要一个输出变量,其是一个四维向量,需要我们自己计算得出最后的颜色,对于输出的结果,我们可以使用out关键字,在三角形这个例子中,我们命名为FragColor,并将其颜色设置为橘黄色,将透明度设置为1,也就是完全不透明。

#version 330 core
    out vec4 FragColor;

    void main()
    {
        FragColor = vec4(1.0, 0.5f, 0.2f, 1.0f);
    }
}

片段着色器的编译过程与顶点着色器类似,其中需要注意的是将着色器的类型设置为GL_FRAGMENT_SHADER

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader)

现在已经将需要的着色器都已经编译了,现在需要做的就是将编译好的着色器对象链接到我们之后需要使用的着色器程序,需要注意的是要检查编译着色器是否出错。

着色器程序

一个着色器程序对象是将多个着色器组合的结果,为了使用最近编译的着色器程序我们需要将这些着色器程序链接到着色器程序,然后在渲染对象的时候激活着色器对象,当我们调用渲染函数的时候就可以使用激活的着色器程序的着色器。

当我们将一个着色器链接进一个着色器程序的时候,每一个着色器的输出都是下一个着色器的输入,所以当输入与输出不匹配会引起错误。可以通过以下代码创建一个着色器程序:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

函数glCreateProgram会创建一个着色器程序并返回器创建对象的ID,在创建好着色器程序后,需要将编译好的着色器对象链接进着色器程序:

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

与检查着色器编译相同,我们也需要检查链接的过程是否出错,

int success;
    char infoLog[512];
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    }

现在已经创建了着色器程序,然后可以通过glUseProgram函数来激活。

glUseProgram(shaderProgram);

需要注意的是一旦我们将着色器编译进着色器程序后我们就不再需要着色器对象了,需要使用glDeleteShader将着色器对象删除:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

现在我们已经将数据传输到GPU,且通过编写顶点着色器和片段着色器规定了GPU该如何处理顶点数据,但是现在OpenGL还不知道如何将顶点数据转化为顶点着色器的属性。顶点着色器允许我们任何我们想要的输入,只要这些输入符合顶点属性的格式,这带来了很大的灵活性,与此同时也以为着我们需要手动指定我们输入数据的哪一部分对应顶点属性的哪一部分,这也意味着我们需要指定在渲染前OpenGL该如何解释顶点数据。

我们的顶点缓冲数据是以下面这种形式组织的:

image.png

  • 位置数据被存储为32-bit(4个byte)的浮点值
  • 每个位置由三个这样的值组成
  • 三个值之间没有空隙,紧密排列在一个数组中
  • 数据的第一个值就是缓冲区域的开头

知道了顶点数据的排列形式,我们就可以通过glVertexAttribPointer告诉OpenGL该如何获取顶点数据(每一个顶点属性):

glVertexAttribPointer(0, 3, GL_POINT, GL_FALSE, 3 * sizeof(float), (void*) 0);
glEnableVertexAttribArray(0);

glVertexAttribPointer的参数较为复杂,其含义如下:

  • 其中的第一个参数指定我们想要配置哪一个顶点属性,在我们编写顶点着色器时,通过layout(location = 0)将定位的位置属性的loaction设置为0,所以这里我们将第一个属性设置为0。
  • 第二个参数指定顶点属性的大小,位置是一个vec3的三维向量,所以这里设置为3。
  • 第三个参数指定数据的类型,这里是GL_FLOAT。
  • 第四个参数是指定是否我们想要将我们的数据归一化,暂时与我们无关设置为GL_FALSE。
  • 第五个参数是步长,这个参数规定连续的顶点属性的偏移量,由于每个顶点都是由三个浮点数组成,然后每个顶点数据之间没有间隔,所以偏移量就是3个浮点数。
  • 最后一个参数是缓冲区域开始的偏移量,这是设置为0。

每一个顶点属性都是从VBO管理的内存中获取数据,内存中有很多VBO,那么该如何确定是哪一个VBO呢,调用glVertexAttribPointer时会指定获取GL_ARRAY_BUFFER类型的VBO,现在顶点属性0已经与顶点数据绑定。

现在我们已经明确了OpenGL该如何获取顶点数据,然后需要通过glEnbaleVertexAttribArray来使用顶点属性,其传入的参数就是顶点属性的位置,顶点属性默认是不可用的,从这点来看我们需要启动所有事:首先需要通过VBO来初始化顶点数据,然后创建一个顶点着色器和片段着色器,然后告诉OpenGL如何将顶点数据转化为顶点属性,所以OpenGL绘制一个对象的调用过程如下:

 //从内存中拷贝数据给OpenGL使用
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //然后设置顶点属性的指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

    //使用着色器程序
    glUseProgram(shaderProgram);

    //现在绘制对象
    someOpenGLFunctionThatDrawsOurTriangle();

顶点数组对象(Vertex Array Object) 每一次绘制我们都要重复上述过程,可以想象一个情况,如果我们有5个顶点属性,然后有上百个不同的对象,为每一个对象绑定缓冲数据然后配置顶点属性是非常繁琐的,那么有没有提供对象去存储状态配置呢?openGL提供了VAO,一个VAO就像VBO,该点所有随后的顶点属性都被存储在VAO,这样做的好处是只要在配置顶点属性的时候调用一次这些函数即可,无论什么时候想要绘制该对象之需要绑定相应的VAO即可,切换不同的顶点数据和配置顶点属性之需要简单地绑定不同的VAO,所有的状态都保存在VAO中。

如果绑定VAO对象失败,OpenGL将会拒绝绘制任何东西。 VAO存储的内容如下:

  • 调用glEnableVertexAttribArray或则glDisableVertexAttribArray
  • 通过glVertexAttribPointer配置顶点属性
  • 通过glVertexAttribPointer将VBO与顶点属性关联

VAO的创建过程与VBO类似:

unsigned int VAO;
glad_glGenVertexArrays(1, &VAO);

为了使用VAO还需要通过调用glBindVertexArray绑定VAO,从这一点上看,我们应该绑定和配置对应的VBO和属性指针,然后解绑VAO,以便之后使用。只要我们想绘制一个对象,之需要在绘制前绑定需要的设置即可,从代码层面就像这样:

 //绑定VAO
    glBindVertexArray(VAO);

     //从内存中拷贝数据给OpenGL使用
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //然后设置顶点属性的指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

    glEnableVertexAttribArray(0);

    //绘制代码(在一个渲染循环中)
     
    //使用着色器程序
    glUseProgram(shaderProgram);
    glBindVertexArray(VAO);
    someOpenGLFunctionThatDrawsOurTriangle();

现在一个VAO存储顶点属性配置以及使用的VBO,通常当我们有多个多个对象想去绘制时,首先需要创建所有的VAO(包含需要的VBO和属性指针),存储这些以便以后使用,当我们需要绘制一个对象时获取对应的VAO然后绑定,之后绘制其他对象,绑定其他VAO。

为了绘制对象,OpengL提供了glDrawArrays函数去使用当前激活的着色器、之前定义的顶带你属性和VBO的顶点数据(通过VAO间接绑定)去绘制图元,

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3)

glDrawArrays有三个参数,第一个参数是使用的图元类型,我们使用的是三角形,第二个参数是从开始绘制的顶点的索引,我们设置为0,第三个参数是多少个顶点,只花一个三角形,所以设置为3。然后编译项目,如果没有出错的话,最后的结果如下

image.png

代码如下

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

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"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";
const char *fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(1.0f, 0.5f, 0.2f, 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"<<std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cout<<"failed to init glad"<<std::endl;
        glfwTerminate();
        return -1;
    }

    //定义顶点着色器
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);

    int success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout<<"ERROR::SHADER::VERTEX::COMPLIATION_FAILED\n"<<infoLog<<std::endl;
    }
    //定义片段着色器
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout<<"ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"<<infoLog<<std::endl;
    }

    //链接着色器
    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    //检查错误
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout<<"ERROR""SHADER::PROGRAM::LINKING_FAILED\n" <<infoLog<<std::endl;
    }

    //删除着色器
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    //顶点数据
    float vertices[] = {
        -0.5f, -0.5f, 0.0f,
        0.5f, -0.5f, 0.0f,
        0.0f, 0.5f, 0.0f
    };

    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    //先绑定VAO
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    //解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    //解绑VAO
    glBindVertexArray(0);

    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    while (!glfwWindowShouldClose(window))
    {
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProgram);
        //因为我们只有一个三角形,不需要每次绑定VAO,为了看起更合理,我依然每次都绑定
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    //清除数据
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    glfwTerminate();
    return -1;

}

void framebuffer_size_callback(GLFWwindow *window, int width, int height) {
        glViewport(0, 0, width, height);
    }


元素缓冲对象(Element Buffer Objects)

最后我们还是要提一下元素缓冲对象(Element Buffer Objects),可以简称为EBO,为了更好的解释元素缓冲对象,我们相信一下如果我们需要绘制一个矩形而不是一个三角形,可以通过两个三角形拼成一个矩形(OpenGL主要使用三角形),这时候会需要以下顶点

float vertices[] = {
       // first triangle
        0.5f,  0.5f, 0.0f, // top right
        0.5f, -0.5f, 0.0f, // bottom right
       -0.5f,  0.5f, 0.0f, // top left
       // second triangle
        0.5f, -0.5f, 0.0f, // bottom right
       -0.5f, -0.5f, 0.0f, // bottom left
       -0.5f,  0.5f, 0.0f  // top left
};

可以看到有两个顶点被重复定义,当有很多复杂的图形时会造成很大的性能损耗,更好的办法是每个顶点只存储一次,但是规定顶点绘制顺序。OpenGL提供了EBO来解决这个问题,EBO是一个缓冲对象,就像VBO一样,它存储了OpenGL绘制的顶点的顺序,所以可以通过顶点位置和顶点索引来绘制矩形:

float vertices[] = {
        0.5f,  0.5f, 0.0f, // top right
        0.5f, -0.5f, 0.0f, // bottom right
       -0.5f, -0.5f, 0.0f, // bottom left
       -0.5f,  0.5f, 0.0f  // top left
   };
   unsigned int indices[] = { // note that we start from 0!
       0, 1, 3, // first triangle
       1, 2, 3  // second triangle
};

从上面可以看出来运用四个顶点就可以绘制一个矩形,首先我们需要创建一个元素缓冲对象,

unsigned int EBO;
glGenBuffers(1, &EBO);

与VBO类似,我们需要绑定EBO,然后想数据传递给EBO,不同的是我们需要吧类型设置为GL_ELEMENT_ARRAY_BUFFER。

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

最后一件需要做的是将glDrawArrays函数替换为glDrawElements函数,在使用glDrawElements表明我们需要从索引缓冲渲染三角形。

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

glDrawElements第一个参数是图元类型,第二个参数是想要绘制元素数量,第三个参数是索引的数据类型, 最后一个参数是设置EBO的偏移量(当没有使用EBO时可以传递一个索引数组),我们这里设置为0。

glDrawElements是从EBO获取索引,这意味着我们每次想要渲染一个对象时都要绑定对应的EBO,这看起来有点麻烦,碰巧的是VAO对象也要追踪EBO的绑定,当VAO绑定时EBO也会被绑定,所以EBO会存储到VAO中。

需要注意的是当缓冲类型是GL_ELEMENT_ARRAY_BUFFER时,VAO会存储glBindBuffer函数,总这意味着它同样会存储解绑函数,所以要确保没有解绑VAO前不要解绑EBO,柔则会导致找不到EBO配置。

所以最后的初始化和绘制代码看起来如下:

// ..:: Initialization code :: ..
   // 绑定VAO
   glBindVertexArray(VAO);
   //拷贝VBO
   glBindBuffer(GL_ARRAY_BUFFER, VBO);
   glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
   // 拷贝EBO
   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
   glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

最后效果如下:

image.png 代码如下

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

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"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";
const char *fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(1.0f, 0.5f, 0.2f, 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"<<std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cout<<"failed to init glad"<<std::endl;
        glfwTerminate();
        return -1;
    }

    //定义顶点着色器
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);

    int success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout<<"ERROR::SHADER::VERTEX::COMPLIATION_FAILED\n"<<infoLog<<std::endl;
    }
    //定义片段着色器
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout<<"ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"<<infoLog<<std::endl;
    }

    //链接着色器
    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    //检查错误
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout<<"ERROR""SHADER::PROGRAM::LINKING_FAILED\n" <<infoLog<<std::endl;
    }

    //删除着色器
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    //顶点数据
    float vertices[] = {
        -0.5f, -0.5f, 0.0f,
        0.5f, -0.5f, 0.0f,
        -0.5f, 0.5f, 0.0f,
        0.5f, 0.5f, 0.0f
    };

    unsigned int indices[] = {
        0, 1, 3,
        0, 2, 3

    };

    unsigned int VAO, VBO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    //先绑定VAO
    glBindVertexArray(VAO);

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

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    //解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    //解绑VAO
    glBindVertexArray(0);

    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    while (!glfwWindowShouldClose(window))
    {
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProgram);
        //因为我们只有一个三角形,不需要每次绑定VAO,为了看起更合理,我依然每次都绑定
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    //清除数据
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    glfwTerminate();
    return -1;

}

void framebuffer_size_callback(GLFWwindow *window, int width, int height) {
        glViewport(0, 0, width, height);
    }

最后

这篇文章主要讲了片段着色器,VAO,VBO,EBO等。更多文章可以关注公众号QStack。