OpenGL学习笔记《二》绘制简单的图形

  在开始绘图之前,简单的了解一下opengl的绘图流程。在opengl里面,所有的事物都是处于3D空间中,而我们的屏幕及像素是以2D表现的,所以就需要将3D转换为2D。opengl内部管理这个流程的叫做渲染管线,主要分为两个部分:3D坐标转换为2D坐标,2D坐标转换为带颜色的像素数据。细分来看主要分为6个流程/步骤,每个流程相互独立,以流程数据的输入、输出作为数据传递方式。每个流程/步骤,由一小段程序/代码组成,所以又可以称之为shader/着色器程序。而gpu对于这些片段程序的执行效率非常高,并且由于gpu的核心数多,可以并发执行的量也非常大。

  这六个步骤如下所示:

   上图中蓝色标识的步骤,就是一般我们做shader编程的步骤。不过目前我们主要是做第一步(Vertex Shader顶点着色器)和第五步(Fragment Shader片段着色器)这两个步骤的编程。顶点着色器处理输入的顶点数据,转换好坐标,片段着色器处理进过光栅化后的像素数据,生成最终显示的颜色信息。

  我们要在屏幕上绘制出简单的图形,那么就必须要提供顶点着色器和片段着色器,才能最终显示出样式、颜色正常的图形。

1、简单的着色器程序

  首先是Vertex Shader,顶点着色器程序代码

const char* vertexShaderSource = "#version 330 core\n"
        "layout (location = 0) in vec3 aPos;\n"
        "void main()\n{"
        "    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
        "}\0";

  代码中指定了opengl的版本,然后指定了 in vec3类型的输入变量,这个是我们输入的顶点数据,注意到这里我们额外用到了layout(location = 0),这个在后面会提到。然后就是着色器程序的核心main函数了,在这里我们给opengl的內建变量gl_Position赋值,即我们传过来的顶点位置属性值。

  其次是Fragment Shader,片段着色器程序代码

const char* fragmentShaderSource = "#version 330 core\n"
        "out vec4 FragColor;\n"
        "void main()\n{"
        "    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
        "}\0";

  同样的需要指定opengl的版本,然后声明一个out vec4类型的变量,这个表示是着色器的输出数据,也就是片段着色器处理好的像素颜色值的数据,没有这个返回,我们的绘图出来的将会是空白或者黑的。

扫描二维码关注公众号,回复: 7376536 查看本文章

  然后我们需要根据程序代码编译链接成着色器程序

   // build and compile shader
    // --------------------------------
    // create vertex shader
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    // attach shader source
    glShaderSource(vertexShader, 1, &vSource, nullptr);
    // compile shader
    glCompileShader(vertexShader);
    // check error
    int success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    // create fragment shader
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fSource, nullptr);
    glCompileShader(fragmentShader);
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    // build and link shaderprogram
    // --------------------------------
    // create shader program
    unsigned int shaderProgram = glCreateProgram();
    // attach shader and link program
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    // check error
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);
        std::cout << "ERROR::PROGRAM::ATTACH_LINK_FAILED\n" << infoLog << std::endl;
    }
    // delete shader 
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

  创建和编译着色器的流程都差不多:

  glCreateShader根据参数类型,返回我们需要创建的着色器类型;

  glShaderSource将我们提供的着色器代码绑定到着色器上;

  glCompileShader编译我们的着色器;

  glGetShaderiv可以检查我们的编译结果;

   编译好着色器之后,我们需要创建着色器程序:

  glCreateProgram创建着色器程序,返回一个unsigned int类型的索引ID,在后面我们需要用到;

  glAttachShader将我们编译好的着色器附加到着色器程序上;

  glLinkProgram链接好着色器程序;

  glGetProgramiv可以检查我们的链接结果。

  最后,我们需要删除编译的着色器,通过glDeleteShader方法。

  我们最终需要用到的是glCreateProgram方法返回的着色器程序的索引ID。

2、提供顶点数据

  上面的流程图中可以看到,第一步就是对输入的顶点数据进行处理,所以我们需要提供顶点数据。顶点数据包含顶点的位置、颜色、纹理坐标等多种类型的数据,在这里我们先简单的提供位置数据,颜色的话直接在片段着色器中固定一种颜色。

  我们可以一次提供一个顶点数据,但是这个方式效率太低。以前的opengl也有一个Display list 的概念,一次打包提供一组数据,这样效率高,但是由于数据是直接存储在了GPU端,数据一旦提供了就无法再调整了。后来又提供了VBO(vertex buffer object)的概念,也是一次收集/打包好一组数据,但相较于Display List,数据是收集在CPU端,每次渲染会再传递一次。所以这种方式相较于一次提供一个顶点数据,效率更高,但是相较于Display List方式,效率稍微低一点,不过灵活性提高了。

  所以我们首先可以声明一个顶点数据数组,然后绑定到VBO对象上:

float vertices[] = {
        -1.0f, -0.5f, 0.0f, // left
        0.0f, -0.5f, 0.0f, // right
        -0.5f, 0.5f, 0.0f // top
    };
unsigned int VBO;
glGenBuffers(1, &VBO);
... loop
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); 
...draw
...loop

  glBindBuffer 和 glBufferData 两个方法,及将顶点数据绑定VBO及赋值VBO对象的方法。然后GPU需要知道如何使用这些数据,glVertexAttribPointer方法负责让GPU知道如何使用这些数据。

  glVertexAttribPointer的参数1对应我们在顶点着色器中提到的(layout = 0)的属性,参数2表示这个属性值用到的数据数量(我们这里用到的是位置属性,每个属性由三个浮点数据构成),参数3表示数据类型(我们这里用到的是浮点型),参数4表示是否需要对数据类型进行转换,参数5表示两两数据属性间的间隔(因为我们这里每个属性值之间是紧挨着的,所以间隔就是一个属性的数据量),参数6表示属性数据的起始索引(在这里我们填0,在后面涉及到配置多个顶点属性的时候,这个地方的值就会有变化);

  glEnableVertexAttribArray 方法的参数,指定激活我们配置的哪个属性,在上面我提到了配置的是(layout = 0) 的属性,所以这里填0;

  在上面的代码中我们可以看到,在渲染的循环中,我们一直要调用绑定VBO、赋值VBO、设置数据使用方法等接口,有点复杂。这个时候就需要引入另外一个概念vertex array object(VAO),来简化我们的操作。VAO概念是用来记录我们在绑定、赋值VBO对象,设置数据使用方法的,在引入VAO之后,我们的渲染流程可以调整为:    unsigned int VBO, VAO;    glGenVertexArrays(1, &VAO);

    glGenBuffers(1, &VBO);
    // bind the vertex array object
    glBindVertexArray(VAO);
    // bind the vertex buffer object
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // fill data uage:GL_STATIC_DRAW, GL_DYNAMIC_DRAW, GL_STREAM_DRAW
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    // tell opengl how it should interpret the vertex data
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
   ...loop
   glUseProgram(shaderProgram);
   glBindVertexArray(VAO);
   ...draw
   ...loop
   glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);

  可以看到,我们的渲染流程变得简单了,数据的初始化和配置,放到了渲染流程之外,在渲染的时候,只需要绑定VAO对象,就可以直接执行绘图操作了。当然在循环退出之后,我们也要释放掉VAO和VBO对象。

3、绘制第一个三角形

  在上面我们提供了一个包含3个顶点位置的顶点数据,创建好了着色器程序,同时也引入了VAO概念,简化了我们的渲染流程。opengl提供了简单的绘图方法供我们使用,在这里我们需要绘制的是三角形,我们可以用如下的代码来绘图:

  glDrawArrays(GL_TRIANGLES, 0, 3);

  我们画的是三角形,所以我们第一个参数类型填的就是三角形;第二个参数指定顶点数据在数组中的起始位置,在这里我们填0;第三个参数表示要绘制的点数量,我们这里有三个顶点,我们传3就可以了。编译执行我们的项目,可以得到一个简单的三角形:

 4、绘制一个矩形

  在上面我们绘制了一个三角形,现在我们要绘制一个矩形,该如何操作?

  我们知道一个矩形可以通过画两个三角形实现,所以我们可以改一下我们的顶点数据数组,提供两个三角形的顶点数据

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

  然后修改我们的绘图函数,将顶点数量改为6,我们就可以得到一个矩形了。

  但是再看一下数组的数据,发现其中其实是有重复的,比如第一个三角形的右下角和第二个三角形的右下角。如果要绘制的矩形数量较少影响不大,如果矩形数量多了,那么我们就会有好多重复的数据,将会占用额外的内存,同时也造成项目数据复杂化了。所以此时我们需要引入另外一个概念element buffer object(EBO)对象,来简化我们的操作,在中文中这个对象也翻译为顶点索引对象。顾名思义,这个是对顶点进行索引编号,告诉GPU该如何使用我们提供的顶点数据。

  

   // indice data
    unsigned int indices[] = {  // note that we start from 0!
        0, 1, 3,   // first triangle
        1, 2, 3    // second triangle
    };
   unsigned int VAO, VBO, EBO;
    // gen
    glGenBuffers(1, &EBO);
    // bind VAO
    // bind VBO
    // bind EBO
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    // set pointer
    ... render loop
    ... render loop
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);

  简化一下流程。我们需要提供一份索引列表,对应的就是顶点数据中的顶点索引。然后绑定并赋值EBO对象,其余的操作跟之前类似,编译执行,我们就得到了一个简单的矩形。

  在这里我们需要注意的是,在最后删除VAO、VBO、EBO的时候,一定不能在删除VAO对象之前先删除EBO,因为在VAO对象内部其实维护了一份EBO对象的数据,如果先删除了EBO,会导致删除出现异常。

  以上就是利用opoengl提供的接口,绘制的简单图形。

  这里是项目的代码。

猜你喜欢

转载自www.cnblogs.com/zhong-dev/p/11598177.html