刚开始学习OpenGL的时候可能会对VAO、VBO、IBO的概念有些模糊,只是记得这些术语和API。
理清概念才能更好的使用这些API,下面我们花20分钟的时间谈谈这些术语。
这里的Object和面向对象编程没有太大关系,他俩之间最沾边的就是合理的使用了抽象这一概念。
我们都知道所有的顶点都需要输送到渲染管线当中,交付给GPU进行渲染。
然后我们在程序里写下了下列代码:
struct Vertex
{
vec3 position;
vec3 color;
vec2 texcoord;
...
}
Vertex[] vertices;
显然,vertices是存储在内存当中的,我们想把内存中的东西交付给GPU,中间还要经过CPU的各种指令操作,是比较麻烦的。
这时候,VBO应运而生。VBO的作用很简单,OpenGL借助VBO可以一次性将大量顶点交付给渲染管线,大大缩短了从内存到CPU再到GPU这一过程的时间。
如果仅仅靠VBO把一堆顶点交付给渲染管线,可是却不告诉渲染管线哪部分是位置坐标哪部分是纹理坐标那么肯定也无法渲染出任何东西,此时VAO就出现了。
我们可以这样理解,VAO是对顶点的结构的抽象描述,它告诉渲染管线哪部分是位置哪部分是颜色,好在渲染管线中的着色器去做相应的着色。
那么VAO和VBO是通过什么样的机制联系在一起的呢?
float planeVertices[] = {
// positions // texture Coords
5.0f, -0.5f, 5.0f, 2.0f, 0.0f,
-5.0f, -0.5f, 5.0f, 0.0f, 0.0f,
-5.0f, -0.5f, -5.0f, 0.0f, 2.0f,
5.0f, -0.5f, 5.0f, 2.0f, 0.0f,
-5.0f, -0.5f, -5.0f, 0.0f, 2.0f,
5.0f, -0.5f, -5.0f, 2.0f, 2.0f
}
uint32 planeVAO, planeVBO;
glGenVertexArrays(1, &planeVAO);
glBindVertexArray(planeVAO);
glGenBuffers(1, &planeVBO);
glBindBuffer(GL_ARRAY_BUFFER, planeVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(planeVertices), planeVertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(sizeof(float) * 3));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
OpenGL中的各种除VAO之外的Object都是使用glGenBuffers(count, &id)来创建的。
不关心代码的细节,我们使用glVertexAttribPointer()之前glBindVertexArray(planeVAO)。
绑定了VAO之后调用glVertexAttribPointer()建立了VAO和VBO之间的联系,使用该函数架起了VAO和VBO两者的桥梁。
有些文章也称其为EBO,Element Buffer Object。这样命名不太直观,这里我使用IBO作为术语。
顾名思义IBO是创建了一个索引缓存,使用索引缓存可以实现顶点复用。以立方体为例,一般来讲我们使用三角形作为基本图元,一个立方体6个面12个三角形,需要36个顶点来描述。可是我们都知道立方体有8个顶点,36个顶点定义了大量的重复的顶点。通知渲染管线每个顶点的编号实现顶点复用就是IBO的作用。
实际上,对应VAO、VBO、IBO,绑定和解绑的顺序是重中之重。如果能搞明白他们的次序,会对OpenGL的这个XXO系统有更深的理解。
很多文章上都会有下述几个经验告诉你:
(1)在绑定完数据后解绑VAO是个好习惯,glBindVertexArray(0)
(2)在绑定完数据后解绑VBO也是个不错的习惯,glBindBuffer(GL_ARRAY_BUFFER,VBO)
(3)不要随意解绑IBO
这里是我曾经的一个很大的疑问,IBO和VBO之间到底有何不同?为什么他们的解绑机制不完全一样?
让我们从VAO看起。
在绑定了VAO之后,接下来的绑定IBO的操作也会默认绑定到同一个VAO之下,可是VBO需要使用glVertexAttribPointer()函数来进行特殊的绑定。
在绑定VBO到VAO时,我们使用的是glVertexAttribPointer(),可以理解为在调用该函数之后VAO会有VBO的一份copy,所以解绑VBO也不会有什么影响。在绑定IBO到VAO时,我们必须提前绑定VAO到当前OpenGL的context,接下来在绑定IBO就会默认绑定到当前VAO,实际上VAO中只保存了一个到IBO的指针,所以IBO才不能再渲染结束前解绑。
(至于为什么要设计成这个样子,我就不知道了)。
到此为止,VAO中既包含了VBO的信息也能找到IBO的信息,所以在渲染循环时我们仅需要绑定VAO后就可以使用glDrawElements或者glDrawArray函数来渲染相应的结点信息。
我们可以使用下述伪代码来描述这个过程。
struct VertexAttributeState {
bool bIsEnabled = false;
int iSize = 4; //This is the number of elements in each attrib, 1-4.
unsigned int iStride = 0;
VertexAttribType eType = GL_FLOAT;
bool bIsNormalized = false;
bool bIsIntegral = false;
void * pPtrOrBufferObjectOffset = 0;
BufferObject * pBufferObj = 0;
};
struct VertexArrayObjectState {
BufferObject *pElementArrayBufferObject = NULL;
VertexAttributeState attributes[MAX_VERTEX_ATTRIB];
}
static VertexArrayObjectState *pContextVAOState = new VertexArrayObjectState();
static BufferObject *pCurrentArrayBuffer = NULL;
void glBindBuffer(enum target, uint buffer)
{
BufferObject *pBuffer = ConvNameToBufferObj(buffer);
switch(target) {
case GL_ARRAY_BUFFER: pCurrentArrayBuffer = pBuffer; break;
case GL_ELEMENT_ARRAY_BUFFER: pContextVAOState->pElementArrayBufferObject = pBuffer;
break; ...
}
}
void glEnableVertexAttribArray(uint index)
{
pContextVAOState->attributes[index].bIsEnabled = true;
}
void glDisableVertexAttribArray(uint index)
{
pContextVAOState->attributes[index].bIsEnabled = false;
}
void glVertexAttribPointer(uint index, int size, enum type, boolean normalized, sizei stride, const void *pointer)
{
VertexAttributeState &currAttrib = pContextVAOState->attributes[index];
currAttrib.iSize = size; currAttrib.eType = type;
currAttrib.iStride = stride;
currAttrib.bIsNormalized = normalized;
currAttrib.bIsIntegral = true;
currAttrib.pPtrOrBufferObjectOffset = pointer;
currAttrib.pBufferObj = pCurrentArrayBuffer;
}