在开始绘图以前,简单的了解一下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提供的接口,绘制的简单图形。
对应的代码在这里。