1-4你好三角形

148 阅读6分钟

顶点数组对象:Vertex Array ObjectVAO

顶点缓冲对象:Vertex Buffer ObjectVBO

元素缓冲对象:Element Buffer ObjectEBO

索引缓冲对象: Index Buffer ObjectIBO

OpenGL的大部分工作是把3D坐标转为2D坐标,以及将2D坐标转为像素点(近似值),这两部分称为图形渲染管线

每一个渲染管线阶段运行的小程序称为着色器(顶点着色器,几何着色器,片段(像素)着色器)

首先传入3个3D坐标作为输入,代表一个三角形,称为顶点数据。顶点是一个3D坐标数据的集合,而顶点数据是用顶点属性表示的。这里假设每个顶点由一个3D位置和颜色值组成

需要给OpenGL指定的数据,称为图元,例如GL_POINTSGL_TRIANGLESGL_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);