因为项目需要所以要进行三维纹理图像的绘制,用固定管线进行绘图的时候,每次绘制都要给GPU传递大量的坐标点,这个点的数量达到亿级,绘制图像的时候都不不敢使用四通道的图像(否则程序卡成JPG),这里我只是用了两通道的图像进行显示,此时图像绘制和由旋转之类的操作引发的重绘卡成了PPT。(实际上因为显卡太low,用了顶点数组也没用,这里就当是学习记录)
当使用庞大数量的点进行绘制的时候,固定管线的传输效率是非常不友好的,使用固定管线的函数开销非常大。之后查找了一些资料发现了显示列表这个东西,显示列表会比直接使用固定管线的函数进行绘制要稍好一点。
显示列表的优点在于不用每次将顶点信息从CPU发送到GPU,也就提高了效率。但是显示列表是固定的,只适合于顶点不需要进行修改的绘制。而且显示列表与固定管线函数的绘制有相同的缺点,开销大同时扩展和修改困难。(显示列表也就是减少从CPU到GPU的过程,并且不能被改变,CPU到GPU的发送过程相对而言是非常慢的)。
OpenGL顶点数组是一种更加高效的绘制方式,它会将数据传入一次性传入,减少传入造成的额外开销。结合顶点缓冲对象,将这些数据存储在GPU端,这样就可以减少每次发送数据造成的时间消耗。
因为之前写代码的时候为了省事直接用的固定管线,所以此处想要提速只替换部分代码,整体上还是固定管线,只是数据的传入方式改变了。
VAO与VBO
VAO与VBO的关系有点像容器与内容,VAO是容器,调用的时候只要把容器拿来就可以使用,VBO是容器里的内容,容器内部已经设置好了内容,不需要我们每次设置好再使用,只要在容器内设置了内容,直接使用内容,防止每次调用的时候都要设置。
这里先介绍内容的设置VBO,之后再叙述VAO,然后将两部分结合。
VBO
VBO与使用纹理比较像,因为都是OpenGL下的接口,所以采用的都是同一种规范。首先,先创建VBO对象的唯一标识,这是VBO对象的名字,采用GLuint类型的变量来代替,与纹理的使用相同由专门的接口创建(也可以自己创建,但是极度不推荐使用,因为OpenGL内部对这些标识范围有限制,为了防止自己设置的名字超出OpenGL的识别范围同时保证唯一性,使用OpenGL的函数接口是标准用法,函数接口可以保证不出现以上问题)。其次,绑定这个名称,然后为这个对象开辟一定的空间,将数据出入GPU中的缓存里。最后在绘制的时候调用。
生成VBO对象调用以下代码:参数:需要生成的数量,VBO对象指针。
glGenBuffers(GLsizei n,GLuint *buffer);//生成对象ID
glGenBuffers(1,&vbo);//生成一个纹理对象
这个函数可以指定需要生成的对象的数量,生成多个对象的时候,需要传入一个用来装对象名称的数组(也就是一个GLuint[]类型的数组)。这里生成的只是一个名称(ID),并没有开辟空间。
生成ID之后要进行绑定,绑定后所有的设置才会对这个对象起作用。
参数:目标类型(缓冲对象的类型,OpenGL存在许多类似枚举值,比如这里要用到GL_ARRAY_BUFFER,这些值都是OpenGL提前设置好的),需要绑定的纹理对象的ID。
glBindBuffer(GLenum target,GLuint buffer);//绑定纹理
glBindBuffer(GL_ARRAY_BUFFER,vbo);//绑定纹理对象VBO
与使用纹理类似,绑定一个VBO的ID也就代表这个对象是当前目标类型激活的对象(没有激活的对象现在设置的内容不起作用),如果绑定的值是0,那么则不使用缓冲对象。
注意:OpenGL可以同时绑定多个不同目标类型,但是不能同时绑定两个或以上的相同的目标类型的对象,因为绑定目标类型后,所有的函数会用来设置当前被激活的对象,不能同时操作两个对象,也就是说每个目标类型只存在一个激活的对象,所有操作只针对这个对象。
绑定ID之后要为这个对象在GPU中申请一个空间以及传入数据。
参数:目标类型(GL_ARRAY_BUFFER(顶点数组),GL_ELEMENT_ARRAY_BUFFER(索引数组),索引数组用来减少重复数据),数组需要的字节数(sizeof(data)获得数组的字符数),用于初始化对象(这里出入顶点数组的指针),数据的读写方式。
glBufferData(GLenum target,GLsizeiptr size,const GLvoid *data, GLenum usage);//进行数据分配以及初始化对象
glBufferData(GL_ARRAY_BUFFER,sizeof(vertex),vertex,GL_STATIC_DRAW);//开辟空间,并将数据传入
目标类型使用OpenGL预定义的值就可以,利用sizeof函数获得数组所需要的字符数,也可以使用类似num*sizeof(float)这种方式,不过需要确定顶点数组中坐标的数量。然后传入数组的指针,让函数可以读取数据,之后定义读写方式。
读写方式常用的有三种:
GL_STATIC_DRAW
GL_DYNAMIC_DRAW
GL_STREAM_DRAW
GL_STATIC_DRAW表示数据不改变或者几乎不改变,GL_DYNAMIC_DRAW表示数据需要经常被改变,GL_STREAM_DRAW表示每次都改变。这些标识会帮助OpenGL更高效的处理,比如经常要改变的数据可以存入高速内存中,提升数据传输的速度。
然后告诉OpenGL如何解释数组中的数据,使用数组有两种,一种是普通的顶点数组,顶点、纹理、颜色分别使用,也就是有三个数组。另一种是交错数组,把顶点、纹理、颜色放入一个数组中,在使用函数对数组进行解释的时候使用偏移量来告诉OpenGL分别从什么位置开始时顶点、纹理、颜色。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);//告诉OpenGL如何使用
glVertexPointer(3, GL_FLOAT, 0, (void*)0);
这个函数也有两种形式,因为固定管线不使用shader也就不用指定在shader中的位置,所以可以省一个参数。
这两个函数参数具体意思为:
第一个参数,代表顶点着色器中location的值,也就是变量的位置。(第二种方法省掉了这个值)
第二个参数,代表顶点的维数,如何一个坐标有x,y,z三个顶点则为3,如果只要有x,y则为2.
第三个参数,代表数组的类型,一般坐标我们会采用float类型,设置的时候可以使用OpenGL设定的值,方便跨平台。
第四个参数,是否希望归一化,如果选择是无符号的值会被归一化到0-1,有符号则是从-1到1.
第五个参数,步长,主要是交错数组的时候使用,因为纹理与颜色需要间隔顶点,所以要计算他们之间间隔了多少个字符。比如如果是纹理坐标,步长设为3*sizeof(float),则代表从第一个纹理坐标开始每隔这个步长值会出现一次纹理坐标,通过这样的计算可以在交错数组中对坐标进行定位。如果设为0,则表示让OpenGL自己去识别步长,如果顶点数组不是交错数组那么直接使用0就可以。
第六个参数,表示某一类坐标数据在数组中的起始位置的偏移量,比如vertex+3可以表示纹理坐标的起始位置。
设置完成后,需要在绘图的时候进行调用。在查找资料的过程中看到过一篇介绍用法的博客写的比较好,其中包含了固定管线相关的内容。
有两种写法,固定管线可以采用第二种,因为固定管线不用自己写shader,所以直接传入数据就可以,当然第一种方法也可以使用。
glEnableVertexAttribArray(0);//开启顶点数组
glDrawArrays(GL_TRIANGLES, 0, 3);//绘制三角形
glDisableVertexAttribArray(0);//关闭顶点数组
glEnableClientState(GL_VERTEX_ARRAY);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableClientState(GL_VERTEX_ARRAY);
这里有两种开启顶点数组的方式
glEnableVertexAttribArray(0);//开启顶点数组
glEnableClientState(GL_VERTEX_ARRAY);
第一种的0表示的是着色器中input的location,第二种直接比较简单只要开启对应目标类型就可以使用。
接着就是绘图。
glDrawArrays(GL_TRANGLES,0,3);
需要指定数据类型,从什么位置开始(一般是0),绘制顶点的数量。
最后关掉顶点数组。
glDisableVertexAttribArray(0);//关闭顶点数组
glDisableClientState(GL_VERTEX_ARRAY);
因为OpenGL是个状态机,所以用完就关养成个好习惯。
最后别忘了释放,防止溢出。
glDeleteBuffers(1,&vbo);
VAO
生成VAO用来放之前申请好的VBO,这样调用的时候只要绑定VAO就可以,不需要每次调用VBO进行绘制,尤其是使用纹理贴图等操作的时候,可以将多个坐标当做一个物体进行操作。
VAO也是一个GLuint类型的值,表示的是这个VAO的名称或者ID,同样需要通过使用OpenGL的函数进行生成。
glGenVertexArrays(1,&vao);//生成ID
这样可以获得一个唯一的标识ID,所有操作都可以针对这个ID进行。
之后与VBO一样,要绑定这个VAO,相当于将当前环境变成VAO的环境,所有设置都是针对这个VAO,可以对这个VAO绑定VBO。
glBindVertexArray(vao);
环境切换到VAO后,对这个VAO做一些列的设置,然后将VAO绑定为零,防止误操作。
glBindVertexArray(0);
一个比较完成的过程:
static const GLfloat vertexs[] = {
-1.0f,-1.0f,0.0f,
1.0f,-1.0f,0.0f,
1.0f,1.0f,0.0f,
-1.0f,1.0f,0.0f
};
static const GLfloat tex[] = { 0.0,0.0,1.0,0.0,1.0,1.0,0.0,1.0 };
glGenVertexArrays(1, &vao);//获取vao的id
glBindVertexArray(vao);//绑定vao
//为了方便使用固定管线,所以选择了第二种缓冲区处理
glEnableClientState(GL_VERTEX_ARRAY);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexs), vertexs, GL_STATIC_DRAW);
glVertexPointer(3, GL_FLOAT, 0, 0);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glGenBuffers(1, &tex_vbo);
glBindBuffer(GL_ARRAY_BUFFER, tex_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(tex), tex, GL_STATIC_DRAW);
glTexCoordPointer(2, GL_FLOAT, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
上面的代码可以用来进行纹理贴图,将顶点坐标和纹理坐标绑定为一个VAO,这样在绘图的时候只需要绑定VAO和纹理就可以绘制图像,不需要在使用glVertex函数进行数据传输,可以提升速度。
调用过程就可以比较简单:
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, tex_id);
glBindVertexArray(vao);
glDrawArrays(GL_QUADS, 0, 4);
glBindVertexArray(0);
最后在不使用的时候要释放。
glDeleteVertexArrays(1, &vao);
举个完整的栗子:(这里使用QT结合OpenGL写的)
OpenglControl::~OpenglControl()
{
glDeleteBuffers(1,&vbo);
glDeleteBuffers(1, &tex_vbo);
glDeleteVertexArrays(1, &vao);
}
void OpenglControl::initializeGL()
{
GLenum err = glewInit();
if (GLEW_OK != err)
{
qDebug() << glewGetErrorString(err);
}
std::cout<< "out" << std::endl;
glClearColor(0,0,0,0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
LoadData();
}
void OpenglControl::resizeGL(int w, int h)
{
if (h == 0)
{
h = 1;
}
glViewport(0, 0, (GLint)w, (GLint)h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0, (GLfloat)w / (GLfloat)h, 0.1, 100.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
ArcBall->setBounds((GLfloat)w, (GLfloat)h);//轨迹球边界
}
void OpenglControl::paintGL()
{
glClearColor(0,0,0, 0.0);//设置背景颜色
glClear(GL_COLOR_BUFFER_BIT);//清空颜色和深度缓存
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glDepthFunc(GL_LEQUAL);
glLoadIdentity();
glTranslatef(0, 0, dist);
glMultMatrixf(ArcBall->Transform.M);//利用轨迹球,在这个栗子里不重要
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, tex_id);
glBindVertexArray(vao);
glDrawArrays(GL_QUADS, 0, 4);
glBindVertexArray(0);
}
bool OpenglControl::LoadTexture()
{
QImage image("1.jpg");
if (image.isNull())//图像为空
{
return false;
}
QImage tex = QGLWidget::convertToGLFormat(image);
glGenTextures(1, &tex_id);
glBindTexture(GL_TEXTURE_2D, tex_id);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);//x方向
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);//y方向
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tex.width(), tex.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, tex.bits());
glBindTexture(GL_TEXTURE_2D, 0);
}
void OpenglControl::LoadData()
{
LoadTexture();
static const GLfloat vertexs[] = {
-1.0f,-1.0f,0.0f,
1.0f,-1.0f,0.0f,
1.0f,1.0f,0.0f,
-1.0f,1.0f,0.0f
};
static const GLfloat tex[] = { 0.0,0.0,1.0,0.0,1.0,1.0,0.0,1.0 };
//
glGenVertexArrays(1, &vao);//获取vao的id
glBindVertexArray(vao);//绑定vao
glEnableClientState(GL_VERTEX_ARRAY);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexs), vertexs, GL_STATIC_DRAW);
glVertexPointer(3, GL_FLOAT, 0, 0);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glGenBuffers(1, &tex_vbo);
glBindBuffer(GL_ARRAY_BUFFER, tex_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(tex), tex, GL_STATIC_DRAW);
glTexCoordPointer(2, GL_FLOAT, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
这里没有贴鼠标操作以及轨迹球代码,在这个栗子里它们都不重要,这里只贴了一些主要的过程,当作参考。
参考
有一篇博客对这些函数的调用总结写的很好,更详细的内容可以参考。
https://www.cnblogs.com/iRidescent-ZONE/p/5475337.html