三角形绘制

前提概念

  • 顶点数组对象(VAO,Vertex Array Object)
  • 顶点缓冲对象(VBO,Vertex Buffer Object)
  • 元素缓冲对象(EBO或IBO,Element Buffer Object Index Buffer Object)

渲染管线

物体是如何从3D空间一步一步转换为2D并通过屏幕展示在我们眼前的?这个过程边可以称作是图形渲染管线也可以叫做管线。数据就好像是通过一个流水线一样,从最开始的由顶点构成的集合,最后在屏幕上呈现多彩的形状。渲染管线大致可以分成几个阶段:

pipeline.png

其中每个阶段的输出都讲作为下一阶段的输入继续加工。可以看到其中有一些阶段是彩色的,这些阶段可以由我们自行的用代码去控制,而这些阶段也有专门的名字叫着色器。着色器有专门的语法进行编写,OpenGL的着色器是由OpenGL着色器语言 OpenGL Shading Language,GLSL编写的。

  • 顶点数据

首先数据以顶点数组的形式进行输入,要注意的是这里的顶点并不单纯的表示空间上的位置,有可能还会带有其他的属性,例如颜色等等,也就是说每一个顶点它都是多维的。

  • 顶点着色器

之后顶点数据会来到第一个着色器,顶点着色器。这里可能会将顶点做一些变换,也即是MVP变换(Model-View-Project),将模型的本地坐标系变换到标准坐标系,同时还可以对顶点做一些其他的处理。

  • 图元装配

这个阶段是将顶点着色器输出的顶点作为输入,并装配成指定的图元的形状,一般会装配成三角形。

  • 几何着色器

顾名思义,几何着色器会在图元装配的基础上进一步加工,将图元进行改造,形成新的几何图形。

  • 光栅化

图形最终是由像素的形式展现在屏幕上的,所以几何图形到这里要开始进行切分,将几何图形分成一个个的像素点,同时还会对几何图形进行裁剪,将超出你视图范围的像素裁剪掉,以提高渲染效率。在这个阶段每个像素会带有很多数据,这些数据也被称作片段,这些片段将会被送入下一个阶段继续进行加工。

  • 片段着色器

片段着色器便会决定像素最终的颜色,这里可以实现很多复杂的效果,例如光照、阴影等,可以通过不同的光照模型计算得出像素的颜色。

  • 测试与混合

在像素确定了最终颜色后,还不能直接显示出来,因为还要考虑像素之间的遮挡问题,也即是深度测试,同时还要考虑像素的Alpha值以及像素之间的混合,例如将两张图片混合在一起。所以在渲染多个物体后,物体的像素有可能是会一直变化的。

在OpenGL里面,顶点着色器和片段着色器是必须提供的,而几何着色器可以使用OpenGL的默认着色器,也可以自己编写。

渲染基本流程

根据上面所述的流程,当然要先准备好顶点数据,这里我们准备绘制一个三角形,所以便建立三个顶点坐标。

1
2
3
4
5
float vertices[]={
-0.5f,-0.5f,0.f,
0.5f,-0.5f,0.f,
0.f,0.5f,0.f
};

顶点数据不能立马发送到顶点着色器,因为数据是在CPU中,要将CPU中的数据发送到GPU需要耗费大量的时间,所以应该先用一片GPU中的内存(也就是显存)将数据缓存起来。顶点缓冲对象(VBO) 便是用来管理这一内存的对象。

1
2
3
4
5
6
7
8
unsigned int VBO;//顶点缓冲对象

glGenBuffers(1,&VBO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);//GL_STATIC_DRAW数据不会变,GL_DYNAMIC_DRAW数据会变改变很多,GL_STREAM_DRAW数据每次绘制都会改变

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

opengl中的对象都是有一个唯一的标识符的,就像这里的vbo一样,通过glGenBuffers()拿到标识符,然后再将其绑定到GL_ARRAY_BUFFER类型上,绑定好后,现在GL_ARRAY_BUFFER上就有我们生成的缓存了,然后再用glBufferData()将数据复制到缓存上

前面说到每个顶点可能不止包含一种属性,例如会有位置(x,y,z)、颜色(r,g,b,a)等等,所以为了让顶点着色器了解每一个顶点中各个属性对应的是着色器中的哪个部分,需要我们各个去指明,上面代码中glVertexAttribPointer(index,size,type,normalized,stride,pointer)便是做了这样一件事。其中各个参数的意义是:

  • index:指向的是着色器中的哪个属性,layout (location = 0)中的location=0就是这里指向的属性。
  • size:这里表明的是属性的维度,例如位置这个属性有三个维度分别是x,y,z。
  • type:数据的类型。
  • normalized:数据是否需要归一化,也即是要不要映射到[0,1]区间。
  • stride:属性的步长,也就是从当前属性跨到下一个同属性需要经过多长的距离,以位置属性为例,位置属性由三个Float参数构成,所以stride就是3*sizeof(float)
  • pointer:这个参数表示属性最开始的偏移量,例如如果一个顶点有两个属性构成,位置和颜色,位置在第一个,颜色在第二个,那么位置的偏移量就是0,颜色就是1,不过这里最后还需要转换成(void*)类型,也即是pointer分别是(void*)0(void*)1

现在数据来到了顶点着色器,关于着色器的只是后面将会提及,这里先简单带过。

1
2
3
4
5
#version 330 core
layout (location=0) in vec3 aPos;
void main(){
gl_Position=vec4(aPos.x,aPos.y,aPos.z,1.0);
}

这便是着色器的源码,有了源码后,还不能直接用,所以还需要进行编译,这里和VBO一样,需要先创建一个vertexShader对象,然后再将源码赋给顶点着色器对象,然后再进行动态编译。

1
2
3
4
unsigned int vertexShader;
vertexShader=glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader,1,&vertexShaderSource,NULL);
glCompileShader(vertexShader);

随着渲染管线,数据来到了片段着色器,片段着色器是决定像素点的颜色。和顶点着色器一样,片段着色器也需要先申明着色器,再对着色器进行编译。

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

但是OpenGL最终需要的是一个将这几种着色器链接在一起的着色器程序,所以最后需要申明一个着色器程序,然后再将顶点着色器和片段着色器进行链接。

1
2
3
4
5
unsigned int shaderProgram;
shaderProgram=glCreateProgram();
glAttachShader(shaderProgram,vertexShader);
glAttachShader(shaderProgram,fragmentShader);
glLinkProgram(shaderProgram);

就这样我们先是定义了顶点坐标,然后绑定了VBO,然后又申明了attribPointer来指明顶点中各个属性对应shader中哪个部分,然后又写了顶点着色器和片段着色器,然后再将其链接成着色器程序。现在我们终于可以开始绘制三角形了。利用glDrawArrays;函数我们便可以绘制我们的三角形。

1
2
glUseProgram(shaderProgram);
glDrawArrays(GL_TRIANGLES,0,3);

首先我们需要先将着色器程序加载到openGL管线,然后再利用glDrawArrays绘制出最终的图形。

顶点数组对象和元素缓冲对象

对于上述过程其实还存在两个问题,首先对于绘制一个图形来说,需要经过glGenBuffersglBindBufferglBufferDataglVertexAttribPointer这一系列操作,如果这个图形需要在多个地方出现那么对于代码量来说将会是灾难的,那有什么办法可以只声明一遍这个物体,然后再其他地方直接可以复用呢?这时候就需要 顶点数组对象(VAO) 了。VAO创建和VBO很像,创建好后先绑定一次,要记住openGL本质是状态机,当绑定好VAO后,接下来的操作都会是建立在VAO上的,也就是说VBO的信息会记录到VAO上,最后再将VAO解绑,等后面要使用时再进行绑定即可。

1
2
3
4
5
unsigned int VAO;
glGenVertexArrays(1,&VAO);
glBindVertexArray(VAO);
//接下进行VBO的声明和绑定操作
glBindVertexArray(0);//最后解除绑定,也即是绑定到0上去。

对于三角形来说,我们只需要声明三个顶点,但是对于多边形来说,例如对于一个矩形,我们便需要用两个三角形来构成它,那么对于顶点坐标数组来说我们就要声明六个顶点,但实际上矩形只需要四个点就需要确定,这种性能上的浪费在实现多边形的时候将会是灾难,那么有什么办法只需要四个点就可以声明出这个矩形?实际上,对于矩形的四个顶点而言,我们只要定义好两个三角形顶点的顺序,那么就可以实现四个点来确定一个矩形。而这个顺序就是所谓的元素缓冲对象(EBO) 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
float rectangle[]={
0.5f,0.5f,0.f,
0.5f,-0.5f,0.f,
-0.5f,-0.5f,0.f,
-0.5f,0.5f,0.f
};

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

unsigned int VAO_rec;
unsigned int VBO_rec;
glGenBuffers(1,&VBO_rec);
glGenVertexArrays(1,&VAO_rec);
glBindVertexArray(VAO_rec);
unsigned int EBO;
glGenBuffers(1,&EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER,VBO_rec);
glBufferData(GL_ARRAY_BUFFER,sizeof(rectangle),rectangle,GL_STATIC_DRAW);
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);

这样解决了这些问题后,我们就可以随心所欲的绘制各种图形了,下面代码绘制出了一个三角形和矩形,并且可以通过A、D键进行图形的切换。源码可以在此处下载

f06d0bfd61d4b7cacda71f89ee04911.png

0c9f35cedc0aa36d339533b1e8aa1b4.png