网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
在OpenGL程序中指定或者加载的数据是存储在CPU中的,要加快图形渲染,必定要充分利用GPU的优势,因此需要将数据发送到GPU中。在GPU中,VBO即vertex buffer object,顶点缓存对象负责实际数据的存储;而VAO即 vertex array object, 记录数据的存储和如何使用的细节信息。
OpenGL是一个状态机(state machine),我们绘制图形时需要在不同的状态之间切换。例如上一节中通过glClearColor设置清除颜色缓冲区时设定的颜色,OpenGL则记住了这一状态,当调用glClear时则使用这个颜色重置颜色缓冲区。直到再次使用glClearColor设置不同颜色为止,OpenGL会一直使用这个状态值。
使用VAO的优势就在于,如果有多个物体需要绘制,那么我们设置一次绘制物体需要的顶点数据、数据解析方式等信息,然后通过VAO保存起来后,后续的绘制操作不再需要重复这一过程,只需要将VAO设定为当前VAO,那么OpenGL则会使用这些状态信息。当场景中物体较多时,优势十分明显。VAO和VBO的关系如下图所示(图片来自Best Practices for Working with Vertex Data):
上图中表示,顶点属性包括位置、纹理坐标、法向量、颜色等多个属性,每个属性的数据可以存放在不同的buffer中。我们可以根据需求,在程序中创建多个VBO和VAO。
使用VAO和VBO的伪代码如下(来自SO):
initialization:
for each batch
generate, store, and bind a VAO
bind all the buffers needed for a draw call
unbind the VAO
main loop/whenever you render:
for each batch
bind VAO
glDrawArrays(...); or glDrawElements(...); etc.
unbind VAO
那么如何创建VBO和VAO呢? OpenGL中的对象创建和使用与C++中对象创建不一样,下面代码描述了在C++中创建和使用对象的方式(来自[Learning Modern 3D Graphics Programming]):
struct Object
{
int count;
float opacity;
char *name;
};
//创建对象.
Object newObject;
// 设置对象的状态
newObject.count = 5;
newObject.opacity = 0.4f;
newObject.name = "Some String";
在OpenGL中创建和使用对象却类似这样:
//创建对象 不允许使用自定义名字
GLuint objectName;
glGenObject(1, &objectName);
// 设置对象状态
glBindObject(GL\_MODIFY, objectName);
glObjectParameteri(GL\_MODIFY, GL\_OBJECT\_COUNT, 5);
glObjectParameterf(GL\_MODIFY, GL\_OBJECT\_OPACITY, 0.4f);
glObjectParameters(GL\_MODIFY, GL\_OBJECT\_NAME, "Some String");
注意OpenGL中创建一个对象,由GLuint 类型来作为对象标识符,而不允许使用自定义名字,这样就不会导致对象重名了。在OpenGL中每个对象在使用前,要绑定到上下文对象,即所谓的target,例如上例中就是GL_MODIFY这个target。
Step1: 创建VBO 我们这样来创建:
GLuint VBOId;
glGenBuffers(1, &VBOId);
API void glGenBuffers( GLsizei n, GLuint * buffers);
这里n指定产生buffer的数目,而buffers则是标识符的地址。一次可以产生一个或者多个buffer.
Step2: 将顶点数据传送到VBO或者为VBO预分配空间。
本节我们绘制一个三角形,对于三角形要在3D空间中指定顶点,必定使用三维坐标。这个顶点坐标需要经过顶点着色器处理后最终才能用于生产三角形,这里面涉及到坐标转换等内容,本节不做深入探讨。经过坐标转换后,顶点坐标最终落在规范化设备坐标系(normalized device coordinate , NDC)中, NDC中坐标范围均为[-1,1],因此这里我们简化处理,将顶点坐标全部定在这个范围内,指定为:
GLfloat vertices[] = {
-0.5f, 0.0f, 0.0f,
0.5f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f
};
将数据传送到GPU中需要通过函数glBufferData实现。
API void glBufferData( GLenum target,
GLsizeiptr size,
const GLvoid * data,
GLenum usage);
1.函数中target参数表示绑定的目标,包括像GL_ARRAY_BUFFER用于Vertex attributes(顶点属性),GL_ELEMENT_ARRAY_BUFFER用于索引绘制等目标。
2.size参数表示需要分配的空间大小,以字节为单位。
3.data参数用于指定数据源,如果data不为空将会拷贝其数据来初始化这个缓冲区,否则只是分配预定大小的空间。预分配空间后,后续可以通过glBufferSubData来更新缓冲区内容。
4.usage参数指定数据使用模式,例如GL_STATIC_DRAW指定为静态绘制,数据保持不变, GL_DYNAMIC_DRAW指定为动态绘制,数据会经常更新。
我们这里绘制一个静态的三角形,vertex attribute顶点属性这个概念包括顶点的位置、纹理坐标、法向量、颜色等属性数据,因此我们的顶点位置数据适合绑定到GL_ARRAY_BUFFER目标,同时数据在传送时初始化缓冲区,因此可以这样实现:
glBindBuffer(GL\_ARRAY\_BUFFER, VBOId);
glBufferData(GL\_ARRAY\_BUFFER, sizeof(vertices), vertices, GL\_STATIC\_DRAW);
Step3: 通知OpenGL如何解释这个顶点属性数组
将数据传送到GPU后,我们还需要告知OpenGL如何解释这个数据,也就是告知其数据格式,因为从底层来看数据一个字节块而已。要通知OpenGL如何解释数据,要使用函数glVertexAttribPointer.
API void glVertexAttribPointer( GLuint index,
GLint size,
GLenum type,
GLboolean normalized,
GLsizei stride,
const GLvoid * pointer);
- 参数index 表示顶点属性的索引 这个索引即是在顶点着色器中的属性索引,索引从0开始记起。
- 参数size 每个属性数据由几个分量组成。例如上面顶点每个属性为3个float组成的,size即为3。分量的个数必须为1,2,3,4这四个值之一。
- 参数type表示属性分量的数据类型,例如上面的顶点数据为float则填写GL_FLOAT.
- 参数normalized 表示是否规格化,当存储整型时,如果设置为GL_TRUE,那么当被以浮点数形式访问时,有符号整型转换到[-1,1],无符号转换到[0,1]。否则直接转换为float型,而不进行规格化。
- 参数stride表示连续的两个顶点属性之间的间隔,以字节大小计算。当顶点属性紧密排列(tightly packed)时,可以填0,由OpenGL代替我们计算出该值。
- 参数pointer表示当前绑定到 GL_ARRAY_BUFFER缓冲对象的缓冲区中,顶点属性的第一个分量距离数据的起点的偏移量,以字节为单位计算。
上面这个函数是很重要的,刚接触时可能对多个参数感到厌烦,慢慢就会习惯。这里以上述包含顶点位置的属性数组为例,做一个图解(来自:learn opengl):
这里我们可以看出,调用上述函数时,属性索引为0(稍后着色器中会与之对应), 属性的分量个数为3,分量的数据类型为GL_FLOAT, normalized设为GL_FALSE, 参数stride为3*sizeof(GL_FLOAT)=12,
pointer的偏移量为0,但是要写为(GLvoid*)0(强制转换),具体如下所示:
glVertexAttribPointer(0, 3, GL\_FLOAT,
3 \* sizeof(GL\_FLOAT), (GLvoid\*)0);
glEnableVertexAttribArray(0);
关于glVertexAttribPointer函数中stride和offset函数的详细解释,你还可以参考我的另一篇关于buffer object的文章。
这样我们创建了VBO,并将数据传送到GPU,并告知了OpenGL如何解析这些数据。在整个过程中,我们调用了很多函数,如果在以后绘制时好需要继续调用这些函数,那将会多么麻烦,因此这时候VAO就起到了关键作用。VAO能记录VBO的相关信息,在以后绘图时,只需要绑定对应的VAO就能找到这些状态,方便OPenGL使用。因此,在创建VBO这一过程中,我们要使用VAO来记录。方法便是,在所有VBO操作之前,先创建和绑定VAO。
绘制三角形时创建VAO和VBO的最终的代码如下:
// 指定顶点属性数据 顶点位置
GLfloat vertices[] = {
-0.5f, 0.0f, 0.0f,
0.5f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f
};
// 创建缓存对象
GLuint VAOId, VBOId;
// Step1: 创建并绑定VAO对象
glGenVertexArrays(1, &VAOId);
glBindVertexArray(VAOId);
// Step2: 创建并绑定VBO对象
glGenBuffers(1, &VBOId);
glBindBuffer(GL_ARRAY_BUFFER, VBOId);
// Step3: 分配空间 传送数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Step4: 指定解析方式 并启用顶点属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GL_FLOAT), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 解除绑定
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
在代码的最后,我们暂时解除绑定,能够防止后续操作干扰到了当前VAO和VBO。
现在在程序中使用VAO绘制三角形则只需要调用:
glBindVertexArray(VAOId); // 使用VAO信息
glUseProgram(shaderProgramId); // 使用着色器
glDrawArrays(GL\_TRIANGLES, 0, 3);
这里使用着色器,稍后介绍。glDrawArrays函数使用VBO数据绘制物体。其使用方法为:
API void glDrawArrays( GLenum mode,
GLint first,
GLsizei count);
1.mode 参数表示绘制的基本类型,OpenGL预制了 GL_POINTS, GL_LINE_STRIP等基本类型。一个复杂的图形,都是有这些基本类型构成的。
2.first表示启用的顶点属性数组中第一个数据的索引。
3.count表示绘制需要的顶点数目。
上述调用时我们选择GL_TRIANGLES表示绘制三角形,使用3个顶点。
着色器程序
目前我们主要使用顶点着色器和片元着色器。对于着色器,采用的是GLSL语言(OpenGL Shading Language)编写的程序,类似于C语言程序。
要使用着色器需要经历3个步骤:
- 创建和编译shader object
- 创建shader program,链接多个shader object到program
- 在绘制场景时启用shader program
具体流程如下图所示:
Created with Raphaël 2.1.0
着色器源文件(shader file)
读取源码(read source)
编译源码(compile source)
链接着色器对象(link shader objects)
着色器程序对象(program object)
在上面的流程中,一个着色器程序对象可以包含多个着色器对象,例如顶点着色器(vertex shader)、几何着色器(geometry shader,后续介绍)、片元着色器(fragment shader)。我们也可以将着色器源码放在程序代码中,当然这一做法仅作为示例,不值得提倡。
我们这里写一个简单的直通着色器,在顶点着色器中输出传入的顶点位置,在片元着色器中输出指定颜色。实际应用中这两个程序将决定图形最终效果,这里只是做一个简单示例。
顶点着色器代码为:
#version 330 // 指定GLSL版本3.3
layout(location = 0) in vec3 position; // 顶点属性索引
void main()
{
gl\_Position = vec4(position, 1.0); // 输出顶点
}
其中gl_Position为内置变量,表示顶点输出位置,以gl_前缀开头的一般都表示内置变量。position声明为vec3类型, vec3表示3个float类型的向量。gl_Positon为vec4类型,其中第四个分量为1.0,关于这个分量后面会做介绍。
片元着色器代码为:
#version 330
out vec4 color; // 输出片元颜色
void main()
{
color = vec4(0.8, 0.8, 0.0, 1.0);
}
通过color指定最终颜色为黄色,vec4类型表示颜色为RGB再加上alpha值构成最终的输出颜色。关于alpha值后面会介绍。
首先创建顶点和片元着色器对象,要注意其中的错误处理。其中顶点着色器代码如下:
const GLchar* vertexShaderSource = "#version 330\n"
"layout(location = 0) in vec3 position;\n"
"void main()\n"
"{\n gl\_Position = vec4(position, 1.0);\n}";
// Step2 创建Shader object
// 顶点着色器
GLuint vertexShaderId = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShaderId, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShaderId);
GLint compileStatus = 0;
glGetShaderiv(vertexShaderId, GL_COMPILE_STATUS, &compileStatus); // 检查编译状态
if (compileStatus == GL_FALSE) // 获取错误报告
{
GLint maxLength = 0;
glGetShaderiv(vertexShaderId, GL_INFO_LOG_LENGTH, &maxLength);
std::vector<GLchar> errLog(maxLength);
![img](https://img-blog.csdnimg.cn/img_convert/32c85c116685eaa22c42117caff43be1.png)
![img](https://img-blog.csdnimg.cn/img_convert/895bba342fb2427e87abe7aa1dde3268.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
std::vector<GLchar> errLog(maxLength);
[外链图片转存中...(img-DC98s7xq-1715839456106)]
[外链图片转存中...(img-HmQwhVhz-1715839456106)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618668825)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**