大家可以看到,前面几个章节的例子都是平面的,顶点着色器顶点数据只用了x,y分量,z分量完全没用,之所以没用立体的例子,绘制立体图形,需配合矩阵旋转来用,否则画出来了也看不到效果,立体图形相对复杂一些,最好能配合缓冲对象来使用,效率更高,代码也会简单一些。这一章通过2个例子和一段用c语言解释vao与vbo的方式向大家讲解缓冲对象的概念。
1. 红宝书第一个例子
基本概念:
1. vao(vertex-array object)顶点数组对象,用来管理vbo。
2. vbo(vertex buffer object)顶点缓冲对象,用来缓存用户传入的顶点数据。
3. ebo(element buffer object)索引缓冲对象,用来存放顶点索引数据。
这里的object指的都是GPU中的一块内存,每个内存对象都有不同的作用,但创建、绑定、数据传送等方式都比较类似,通过不同的类型加以区分,掌握了一种,其他的就很好理解。
vbo是GPU中的一块buffer,用户可以向这个buffer中写入数据,前面几章的程序没用vbo,都是在调用glDrawArrays时才把顶点数据或颜色数据传送到GPU,使用了vbo,可以把数据传送和绘制操作分开,可以在glDraw 之前把数据传送给GPU,如游戏场景,使用了大量的顶点数据,但在游戏过程中,顶点数据只需要做坐标变换,而不用重新传送,如果不使用vbo,效率将非常低,每帧数据都需重新传送数据给GPU。
vao并不是必须的,vbo可以独立使用,vbo值缓存了数据,而数据的使用 方式(glVertexAttribPointer 指定的数据宽度等信息)并没有缓存,当切换vbo时(有多个vbo时,通过glBindBuffer切换 ),数据使用方式信息就丢失了,vao则可以弥补这一点,可以额外记录数据的使用方式。
ebo也不是必须的,如果使用ebo,绘制过程将更清晰简单,ebo需配合vbo使用,索引必须指定索引的对象。
理解缓冲对象,可以用红宝书第一个例子,虽然第一个例子中的vao完全可以去掉,下面的例子是根据红宝书第一个例子稍作修改,主要改动:
1. 把LoadShaders copy到代码中,方便大家编译。
2. vao进行了分组,原代码中vao完全可以删除,反而会影响程序的理解(可有可无的东西放代码里有什么特殊用途)。
3. display的时候进行了vao切换,更能体现出vao的作用。
代码如下:
#include <iostream>
using namespace std;
#include <GL/glew.h>
#include <GL/glut.h>
typedef struct {
GLenum type;
const char* filename;
GLuint shader;
} ShaderInfo;
static const GLchar* ReadShader( const char* filename )
{
FILE* infile;
fopen_s( &infile, filename, "rb" );
if ( !infile )
{
std::cerr << "Unable to open file '" << filename << "'" << std::endl;
return NULL;
}
fseek( infile, 0, SEEK_END );
int len = ftell( infile );
fseek( infile, 0, SEEK_SET );
GLchar* source = new GLchar[len+1];
fread( source, 1, len, infile );
fclose( infile );
source[len] = 0;
return const_cast<const GLchar*>(source);
}
GLuint LoadShaders( ShaderInfo* shaders )
{
if ( shaders == NULL ) { return 0; }
GLuint program = glCreateProgram();
ShaderInfo* entry = shaders;
while ( entry->type != GL_NONE )
{
GLuint shader = glCreateShader( entry->type );
entry->shader = shader;
const GLchar* source = ReadShader( entry->filename );
if ( source == NULL ) {
for ( entry = shaders; entry->type != GL_NONE; ++entry ) {
glDeleteShader( entry->shader );
entry->shader = 0;
}
return 0;
}
glShaderSource( shader, 1, &source, NULL );
delete [] source;
glCompileShader( shader );
GLint compiled;
glGetShaderiv( shader, GL_COMPILE_STATUS, &compiled );
if ( !compiled )
{
GLsizei len;
glGetShaderiv( shader, GL_INFO_LOG_LENGTH, &len );
GLchar* log = new GLchar[len+1];
glGetShaderInfoLog( shader, len, &len, log );
std::cerr << "Shader compilation failed: " << log << std::endl;
delete [] log;
return 0;
}
glAttachShader( program, shader );
++entry;
}
glLinkProgram( program );
GLint linked;
glGetProgramiv( program, GL_LINK_STATUS, &linked );
if ( !linked )
{
GLsizei len;
glGetProgramiv( program, GL_INFO_LOG_LENGTH, &len );
GLchar* log = new GLchar[len+1];
glGetProgramInfoLog( program, len, &len, log );
std::cerr << "Shader linking failed: " << log << std::endl;
delete [] log;
for ( entry = shaders; entry->type != GL_NONE; ++entry )
{
glDeleteShader( entry->shader );
entry->shader = 0;
}
return 0;
}
return program;
}
enum VAO_IDs { Triangles,Triangles1, NumVAOs };
enum Buffer_IDs { ArrayBuffer,ArrayBuffer1, NumBuffers };
enum Attrib_IDs { vPosition = 1 };
GLuint VAOs[NumVAOs];
GLuint Buffers[NumBuffers];
const GLuint NumVertices = 6;
void init(void)
{
ShaderInfo shaders[] =
{
{ GL_VERTEX_SHADER, "triangles.vert" },
{ GL_FRAGMENT_SHADER, "triangles.frag" },
{ GL_NONE, NULL }
};
GLuint program = LoadShaders(shaders);
glUseProgram(program);
glGenVertexArrays(NumVAOs, VAOs);
glBindVertexArray(VAOs[Triangles]);
printf("Triangles:%d NumVAOs:%d id:%d\n",Triangles,NumVAOs,VAOs[0]);
GLfloat vertices[NumVertices][2] =
{
{ -0.90, -0.90 }, // Triangle 1
{ 0.85, -0.90 },
{ -0.90, 0.85 },
{ 0.90, -0.85 }, // Triangle 2
{ 0.90, 0.90 },
{ -0.85, 0.90 }
};
glGenVertexArrays(NumVAOs, VAOs);
glGenBuffers(NumBuffers, Buffers);
glBindVertexArray(VAOs[Triangles]);
glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices)/2, vertices, GL_STATIC_DRAW);
glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(vPosition);
glBindVertexArray(VAOs[Triangles1]);
glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices)/2, &vertices[3], GL_STATIC_DRAW);
glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(vPosition);
}
void display(void)
{
static int i = 0;
glClear(GL_COLOR_BUFFER_BIT);
if(i++%2 == 0)
{
glBindVertexArray(VAOs[Triangles]);
glDrawArrays(GL_TRIANGLES, 0, NumVertices/2);
}
else
{
glBindVertexArray(VAOs[Triangles1]);
glDrawArrays(GL_TRIANGLES, 0, NumVertices/2);
}
printf("display i:%d\n",i);
glFlush();
}
int main(int argc, char** argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutInitWindowSize(512, 512);
glutCreateWindow(argv[0]);
if (glewInit())
{
cerr << "Unable to initialize GLEW ... exiting" << endl; exit(EXIT_FAILURE);
}
init();
glutDisplayFunc(display);
glutMainLoop();
}
shader程序源码,需把下面的shader代码保存为triangles.vert与triangles.frag文件
#version 330 core
layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec4 vPosition1;
void main()
{
gl_Position = vPosition1;
}
#version 330 core
out vec4 fColor;
void main()
{
fColor = vec4(0.0, 0.5, 0.5, 0.0);
}
程序的效果如下(改变窗口大小时触发重绘,可以看到2
个三角形切换):
API可以自己看红宝书,这里把程序流程讲解一下:
1. 先创建2个vao对象用来管理vbo对象,对应的vbo也创建了2个。先通过glBindVertexArray切换vao,让后续的操作都作用到对应的vao上,设置完vbo相关参数,此时vbo的设置都关联到vao上了。
2. 可以通过改变窗口大小来触发重绘,display函数切换着画三角形,只调用了2句话,即切换vao与glDrawArrays,如不使用vao,那么glDrawArrays 之前init函数里初始化vbo的几句话都需要带上才能达成同样的效果。
注意:
1. 使用vbo的时候,glVertexAttribPointer的参数含义有变化,参数Pointer 可以传NULL,第二章有说明。
2. glBufferData的时候应该把数据传送到了GPU,而不像未使用vbo时,调用glDrawArrays的时候才传送数据,程序用的是局部变量就可以说明这一点(display时数据指针已经失效)。
2. 用c代码解释vbo与vao
最开始接触这个概念的时候,也是半天没理解,对vao id、vbo id及vPosition位置的对应关系不是很理解,看了一些资料,都是强调“状态 ”这两个字,个人对状态的理解就是全局量保存的当前信息。为方便理解,下面是按个人理解的情况,整理的实现模型,模拟用户程序、openGL api、GPU程序交互过程, 大家可以参考一下。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/* -------------------以下部分是对api部分的模拟-----------------*/
typedef int GLuint;
typedef float GLfloat;
#define MAX_VAO 16
#define MAX_VBO 16
/* 为了编译,随便定义的*/
#define GL_ARRAY_BUFFER 1
#define GL_ARRAY_BUFFER 1
#define GL_STATIC_DRAW 1
#define GL_FLOAT 1
#define GL_FALSE 1
#define GL_ARRAY_BUFFER 1
#define GL_ARRAY_BUFFER 1
#define GL_STATIC_DRAW 1
#define GL_FLOAT 1
#define GL_FALSE 1
#define GL_TRIANGLES 1
#define GL_TRIANGLES 1
/* vbo对象结构,存储数据指针及大小*/
typedef struct
{
int enable;
char * vData;
GLuint vDataSize;
}VBO;
/* vao对象结构,存储了vbo id及相关的信息*/
typedef struct
{
int enable;
GLuint vType;
GLuint vboId;
GLuint vPosition;
GLuint vSize;
GLuint vNormalized;
GLuint vStride;
}VAO;
VAO gVaos[MAX_VAO];
VBO gVbos[MAX_VBO];
VAO * gCurVao = NULL;
VBO * gCurVbo = NULL;
/* 从vao数组中返回未使用的vao数组下标*/
void glGenVertexArrays(int numVao,int * array)
{
int cnt = 0;
for(int i = 0;i<MAX_VAO && cnt < numVao;i++)
{
if( !gVaos[i].enable)
{
array[cnt] = i;
cnt++;
gVaos[i].enable = 1;
}
}
}
/* 把指定的vao_id设置为当前全局vao */
void glBindVertexArray(int vao_id)
{
gCurVao = &gVaos[vao_id];
}
/* 从vbo数组中返回未使用的vbo数组下标*/
void glGenBuffers(int numVbo,int * array)
{
int cnt = 0;
for(int i = 0;i<MAX_VBO && cnt < numVbo;i++)
{
if( !gVbos[i].enable)
{
array[cnt] = i;
cnt++;
gVbos[i].enable = 1;
}
}
}
/* 把指定的vbo_id设置为当前全局bao */
void glBindBuffer(int vtype,int vbo_id)
{
if( gCurVao)
{
gCurVao->vType = vtype;
gCurVao->vboId = vbo_id; /* 使用了vao,则把vbo_id等信息存入当前的vao中*/
}
gCurVbo = &gVbos[vbo_id];
}
/* 数据存储到当前的vbo中*/
void glBufferData(int vtype, int size, const void * data, int usage)
{
gCurVbo->vDataSize = size;
if( gCurVbo->vData == NULL)
{
gCurVbo->vData = (char *)malloc(size);
memcpy(gCurVbo->vData,data,size);
}
}
/* 告知shader数据怎么使用,如有vao,则把信息存下来*/
void glVertexAttribPointer(int vpos,int size, int type, int normalized,int stride,const void * pointer)
{
if( gCurVao)
{
gCurVao->vPosition = vpos; /* 存储的信息也包括顶点数据的位置索引*/
gCurVao->vSize = size;
gCurVao->vNormalized = normalized;
gCurVao->vStride = stride;
}
}
void glEnableVertexAttribArray(int vpos)
{
}
void glDrawArrays(int type,int begin,int cnt)
{
}
/* -------------------以下部分是用户程序-----------------*/
enum VAO_IDs { Triangles,Triangles1, NumVAOs };
enum Buffer_IDs { ArrayBuffer,ArrayBuffer1, NumBuffers };
enum Attrib_IDs { vPosition = 1 };
GLuint VAOs[NumVAOs];
GLuint Buffers[NumBuffers];
const GLuint NumVertices = 6;
void init(void)
{
GLfloat vertices[NumVertices][2] =
{
{ -0.90f, -0.90f }, // Triangle 1
{ 0.85f, -0.90f },
{ -0.90f, 0.85f },
{ 0.90f, -0.85f }, // Triangle 2
{ 0.90f, 0.90f },
{ -0.85f, 0.90f }
};
/* 创建个vao和个vbo */
glGenVertexArrays(NumVAOs, VAOs);
glGenBuffers(NumBuffers, Buffers);
/* 先绑定第一个vao,此时对vbo的操作全部与vao关联*/
glBindVertexArray(VAOs[Triangles]);
glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices)/2, vertices, GL_STATIC_DRAW);
glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(vPosition);
/* 再绑定第二个vao,此时对vbo的操作全部与该vao关联*/
glBindVertexArray(VAOs[Triangles1]);
glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices)/2, &vertices[3], GL_STATIC_DRAW);
glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(vPosition);
}
void display(void)
{
static int i = 0;
/* 通过绑定来启动当前vao,就可以直接绘制相应的图形*/
if(i++%2 == 0)
{
glBindVertexArray(VAOs[Triangles]);
glDrawArrays(GL_TRIANGLES, 0, NumVertices/2);
}
else
{
glBindVertexArray(VAOs[Triangles1]);
glDrawArrays(GL_TRIANGLES, 0, NumVertices/2);
}
printf("display i:%d\n",i);
}
int main(int argc, char** argv)
{
init();
display();
return 0;
}
以上程序分为2部分,上面的不是是对缓冲对象操作逻辑的模拟,后面是应用程序,程序结果输出不重要。程序的重点:
1. 缓冲对象可以认为是用结构体数组管理的,缓冲对象操作的对象是当前(cur)对象。
2. 缓冲对象glGen(创建)的过程可以认为是分配数组下标,实际上分配出的缓冲对象的值都是从0或者1开始的(0有时候是保留对象),很像是数组的下标。
3. bind过程,可以理解为把当前的缓冲对象指针指向指定的缓冲对象。
3. 使用ebo简化绘制过程
直接通过一个例子来说明ebo的用途和用法,下面的例子是绘制2个田字形的格子,分别使用线和四边形的方式绘制,考虑一下,如果不使用ebo,设计坐标的时候就需要考虑绘制方式,用线的方式绘制时,坐标传送应该用线的方式,如例子中的田字格,使用GL_LINES方式绘制,需设计48个float型的坐标(共12条短线,每条线2个坐标,每个坐标2个float 数据),而使用GL_QUADS方式绘制,需要设计32个float型坐标,很麻烦,可以看出来,中间的很多坐标都是相同的。
这个时候使用ebo就非常简单了,先设计田字格的9个顶点,然后再设计绘制方式的索引,顶点设计和绘制方法设计完全分开,使程序更明了。
ebo与vbo使用方式类似,先使用glGenBuffers创建ebo,再使用glBindBuffer绑定ebo,然后使用glBindBuffer传送数据,注意类型为GL_ELEMENT_ARRAY_BUFFER,绘图的时候使用glDrawElements函数。
例子源码如下:
#include <stdlib.h>
#include <stdio.h>
#include <GL/glew.h>
#include <GL/glut.h>
static const GLchar * vertex_source =
"#version 330 core\n"
"uniform float translate;\n"
"layout (location = 0) in vec2 position;\n"
"layout (location = 1) in vec3 color;\n"
"flat out vec3 vertex_color;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x+translate,position.y,0.0,1.0);\n"
"vertex_color = color;\n"
"}\0";
static const GLchar * frag_source =
"#version 330 core\n"
"flat in vec3 vertex_color;\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(vertex_color,1.0);\n"
"}\n\0";
void loadShader(GLuint program, GLuint type, const GLchar * source)
{
const GLchar * shaderSource[] = {source};
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, shaderSource, 0);
glCompileShader(shader);
glAttachShader(program, shader);
}
GLint translate_loc;
GLuint vao, vbo, ebo[2];
void init()
{
GLuint program = glCreateProgram();
/* 同时加载了顶点着色器和片元着色器*/
loadShader(program, GL_VERTEX_SHADER, vertex_source);
loadShader(program, GL_FRAGMENT_SHADER, frag_source);
glLinkProgram(program);
glUseProgram(program);
translate_loc = glGetUniformLocation(program, "translate");
/* 田字格的顶点坐标*/
GLfloat vertices[][2] =
{
{ -0.9f, 0.5f}, { -0.5f, 0.5f}, { -0.1f, 0.5f}, /* 第一行*/
{ -0.9f, 0.0f}, { -0.5f, 0.0f}, { -0.1f, 0.0f}, /* 第二行*/
{ -0.9f, -0.5f}, { -0.5f, -0.5f}, { -0.1f, -0.5f}, /* 第三行*/
};
/* 每个点给一种颜色*/
GLfloat colors[][3] =
{
{1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f},
{0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f},
};
/* 线的方式画田字格,每条线个点*/
GLint index_line[][2] =
{
{0, 1}, {1, 2}, /* 第一行线索引*/
{3, 4}, {4, 5},
{6, 7}, {7, 8},
{0, 3}, {3, 6}, /* 第一列线索引*/
{1, 4}, {4, 7},
{2, 5}, {5, 8},
};
/* 四边形的方式画田字格,每条四边形个点*/
GLint index_quad[][4] =
{
{0, 1, 4, 3}, /* 第一个格子*/
{1, 2, 5, 4},
{3, 4, 7, 6},
{4, 5, 8, 7},
};
glGenBuffers(1, &vao);
glBindBuffer(GL_ARRAY_BUFFER, vao);
/* 顶点数组和颜色数据放同一个vbo中,设置时候注意数据宽度和偏移*/
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices) + sizeof(colors), NULL, GL_STATIC_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(vertices), sizeof(colors), colors);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid *)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, (GLvoid *)(sizeof(vertices)));
glEnableVertexAttribArray(1);
/* 创建画格子方式的索引,也可以用一个ebo,通过偏移量来存放索引*/
glGenBuffers(2, ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0]); /* 存放用线画田字格的索引*/
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(index_line), index_line, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[1]); /* 存放用线画田字格的索引*/
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(index_quad), index_quad, GL_STATIC_DRAW);
glLineWidth(2.0);
glClearColor(0.5f, 0.5f, 1.0f, 1.0f);
glProvokingVertex(GL_FIRST_VERTEX_CONVENTION);
}
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
glBindBuffer(GL_ARRAY_BUFFER, vao);
/* 12根线,每根个索引*/
glUniform1f(translate_loc,0.0f);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[0]);
glDrawElements(GL_LINES,2*12, GL_UNSIGNED_INT, (GLvoid *)(0));
/* 用偏移的方式画第二个田字格*/
glUniform1f(translate_loc,1.0f);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo[1]);
glDrawElements(GL_QUADS,4*4, GL_UNSIGNED_INT, (GLvoid *)(0));
glFlush();
}
int main(int argc, char * argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutInitWindowPosition(200, 200);
glutInitWindowSize(600, 400);
glutCreateWindow("ebo");
glewInit();
init();
glutDisplayFunc(display);
glutMainLoop();
return 0;
}
效果如下:
注意:
1. 为方便看效果,例子画图的颜色没有使用默认的渐变色,使用了glProvokingVertex禁用渐变(参考第3章介绍)。
2. 传送顶点坐标和颜色时,使用了glBufferSubData,该函数作用是分段传数据,与glBufferData 配合使用,注意偏移值。