顶点数组对象:Vertex Array Object,VAO
顶点缓冲对象:Vertex Buffer Object,VBO
元素缓冲对象:Element Buffer Object,EBO
索引缓冲对象: Index Buffer Object,IBO
OpenGL的大部分工作是把3D坐标转为2D坐标,以及将2D坐标转为像素点(近似值),这两部分称为图形渲染管线
每一个渲染管线阶段运行的小程序称为着色器(顶点着色器,几何着色器,片段(像素)着色器)
首先传入3个3D坐标作为输入,代表一个三角形,称为顶点数据。顶点是一个3D坐标数据的集合,而顶点数据是用顶点属性表示的。这里假设每个顶点由一个3D位置和颜色值组成
需要给OpenGL指定的数据,称为图元,例如GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP
第一部分是顶点着色器,主要目的是将3D坐标转为另一种3D坐标并对顶点属性进行一些处理
第二部分是集合着色器,顶点着色器的输出可以选择给集合着色器,来形成新的图元来生成其他形状
图元装配指的是将顶点着色器的输出顶点作为输入,并装配为指定图元
图元装配的输出被传入光栅化阶段,映射为最终像素,生成供片段着色器使用的片段。在片段着色器运行前执行裁剪,丢弃视图外的像素
片段着色器的目的是计算最终颜色
最后传入Alpha测试和混合阶段,来检测深度和模板,检查是否应该丢弃,并用Alpha进行混合
大部分时候只需要装备顶点和片段着色器,使用默认的几何着色器。必须至少定义一个顶点和片段着色器
绘制图形之前应先传入顶点数据,仅在NDF(标准化设备坐标[-1,1])内时显示在屏幕上,定义一个float数组
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
渲染2D三角形时只需要将z坐标设为0.0f
在顶点着色器中处理过后变为标准化设备坐标,在范围外的坐标会被裁剪,其中(0,0)点在屏幕正中间
通过glViewPort函数进行视口变化,标准化设备坐标变为屏幕空间坐标
将坐标传入片段着色器,在GPU中创建内存储存顶点数据,通过VBO管理内存,这样可以一次性发送一大批数据到显卡
使用glGenBuffers函数生成带有ID的VBO对象
unsigned int VBO;
glGenBuffers(1, &VBO);
使用glBindBuffer函数将缓冲绑定到GL_ARRAY_BUFFER上
glBindBuffer(GL_ARRAY_BUFFER, VBO);
绑定后,使用的任何缓冲调用都会配置当前VBO
调用glBufferData函数,将顶点数据复制到缓冲内存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices),vertices, GL_STATIC_DRAW);
其中第四个参数有三种模式
-
GL_STATIC_DRAW:数据不会或几乎不会改变。 -
GL_DYNAMIC_DRAW:数据会被改变很多。 -
GL_STREAM_DRAW:数据每次绘制时都会改变。 若每次渲染调用时都使用原数据,使用GL_STATIC_DRAW效率最高。反之,如果缓冲数据频繁改变,那么使用GL_DYNAMIC_DRAW和GL_STREAM_DRAW
接下来使用顶点着色器,使用GLSL语言来编写
#version 330 core
layout (location = 0) in vec3 aPos; //从外部传入aPos数据
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);//设置gl顶点数据
}
接下来创建一个着色器对象
unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER);
然后将着色器传给glCreateShader
glShaderSource(vertexShader, 1,&vertexShaderSource,NULL);
glCompileShader(vertexShader);
接下来判断调用glCompileShader后是否编译成功
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success
if(!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
接下来写片段着色器,也就是为了计算像素最后的颜色输出
#version 330 core out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
接下来创建着色器对象并且绑定
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
接下来把两个着色器对象链接到一个着色器程序上
首先创建一个对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
接着用glCreateProgram函数将之前的着色器附加到程序对象中,然后glLinkProgram链接
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
检查链接程序是否失败
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success)
{
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
}
调用glUseProgram函数来激活程序
glUseProgram(shaderProgram);
将着色器对象链接到程序对象后,删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
使用glVertexAttribPointer函数告诉OpenGL解析顶点数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float),(void*)0);
glEnableVertexAttribArray(0);
第一个参数代表属性的位置值,第二个参数代表顶点属性的大小,第三个参数代表数据的类型,第四个参数代表是否被标准化,第五个参数代表步长,也就是连续顶点属性之间的间隔,最后一个参数代表位置数据在缓冲中起始位置的偏移值
接下来在OpenGL中绘制一个物体
glBindBuffer(GL_ARRAY_BUFFER, VBO); //绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //设置数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);//设置顶点属性
glEnableVertexAttribArray(0); //解绑
glUseProgram(shaderProgram); //使用着色器
someOpenGLFunctionThatDrawsOurTriangle();//绘制图形
接下来创建一个VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
接下来绑定VAO,步骤和VBO相似
glBindVertexArray(VAO);//绑定VAO
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);//绑定VAO
someOpenGLFunctionThatDrawsOurTriangle();
接下来使用glDrawArrays函数,使用激活的着色器,顶点属性和VBO顶点数据来绘制图元
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
最后来讨论EBO(元素缓冲对象)或者IBO(索引缓冲对象)
设置顶点
float vertices[] =
{
// 第一个三角形
0.5f, 0.5f, 0.0f,// 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二个三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
这种方式顶点叠加,产生了额外的开销,所以我们需要使用EBO来制定绘制的顺序,提高效率
EBO是一个缓冲区,存储OpenGL绘制顶点的索引
float vertices[] =
{
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] =
{
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
下面我们创建一个EBO
unsigned int EBO;
glGenBuffers(1, &EBO);
绑定EBO然后用glBufferData把索引复制到缓冲里
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
最后用glDrawElements来替换glDrawArrays,使用绑定的索引进行绘制
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
最终代码
// ..:: 初始化代码 :: ..
// 1. 绑定顶点数组对象 glBindVertexArray(VAO);
// 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);