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

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

  这六个步骤以下所示:github

 

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

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

一、简单的着色器程序并发

  首先是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赋值,即咱们传过来的顶点位置属性值。oop

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

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类型的变量,这个表示是着色器的输出数据,也就是片断着色器处理好的像素颜色值的数据,没有这个返回,咱们的绘图出来的将会是空白或者黑的。spa

  而后咱们须要根据程序代码编译连接成着色器程序翻译

   // 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。

二、提供顶点数据

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

  咱们能够一次提供一个顶点数据,可是这个方式效率过低。之前的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个顶点位置的顶点数据,建立好了着色器程序,同时也引入了VAO概念,简化了咱们的渲染流程。opengl提供了简单的绘图方法供咱们使用,在这里咱们须要绘制的是三角形,咱们能够用以下的代码来绘图:

  glDrawArrays(GL_TRIANGLES, 0, 3);

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

 

 四、绘制一个矩形

  在上面咱们绘制了一个三角形,如今咱们要绘制一个矩形,该如何操做?

  咱们知道一个矩形能够经过画两个三角形实现,因此咱们能够改一下咱们的顶点数据数组,提供两个三角形的顶点数据

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提供的接口,绘制的简单图形。

  对应的代码在这里

相关文章
相关标签/搜索