OpenGL基础(02)渲染流程解析

1 渲染流程中基本概念解读

在编写程序前,我们需要了解一些基本概念:顶点输入、顶点缓冲对象VBO、

1.1 向量(Vector)

在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.xvec.yvec.zvec.w来获取。注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视划分(Perspective Division)上。

1.2 顶点输入

开始绘制图形之前,必须先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是3D坐标(x、y、z)。本节 我们希望渲染2个三角形,我们一共要指定6个顶点,每个三角形3个,每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式定义为一个GLfloat数组。如下所示:

    float first_vertices[] = {
        // first triangle
        -0.9f, -0.5f, 0.0f,  // left
        -0.0f, -0.5f, 0.0f,  // right
        -0.45f, 0.5f, 0.0f  // top
    };
    
    float second_vertices[] = {
        // second triangle
        0.0f, -0.5f, 0.0f,  // left
        0.9f, -0.5f, 0.0f,  // right
        0.45f, 0.5f, 0.0f   // top
    };

1.3 顶点缓冲对象(Vertex Buffer Objects, VBO)

定义顶点数据后,我们会把它作为输入发送给顶点着色器。它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。这时我们就通过VBO 来管理这个内存,它会在GPU内存(显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要有可能 我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显存中后,顶点着色器几乎能立即访问顶点。VBO对象的创建和绑定流程如下所示:

//定义一个VBO对象
GLuint VBO;

//@1 使用glGenBuffers函数和一个缓冲ID生成一个VBO对象:
glGenBuffers(1, &VBO);

//@2 把新创建的缓冲VBO绑定到GL_ARRAY_BUFFER目标上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);  

/*@3
目的:把之前定义的顶点数据复制到缓冲的内存中,即CPU向GPU传递数据
它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。
第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。
第三个参数是我们希望发送的实际数据。
第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。
*/
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

1.4 顶点数组对象VAO(Vertex Array Object)

顶点数组对象 可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,即绑定不同的VAO就行了。在OpenGL中绘制一个物体,如果没有VAO,代码会是这样:

// 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// @1 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// @2 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// @3 绘制物体
//...绘制相关函数

每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,成百上千个不同物体呢?这时候绑定正确的缓冲对象,为每个物体配置所有顶点属性就变成一件麻烦事。VAO就是可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态。

1.5 索引缓冲对象(Element Buffer Object,EBO)

假设我们不再绘制一个三角形而是绘制一个矩形。我们可以绘制两个三角形来组成一个矩形(因为OpenGL主要处理三角形)。这会生成下面的顶点的集合:

GLfloat 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   // 左上角
};

可以看到,有几个顶点叠加了。我们指定了右下角左上角两次!一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。当有上千个三角形的模型之后这个问题会更糟糕,这会对存储会产生一大堆浪费。而更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。这里就引入一个新的概念:索引缓冲对象。

和顶点缓冲对象VAO一样,EBO也是一个缓冲,它专门储存索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点。所谓的索引绘制(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义独一无二的顶点,和绘制出矩形所需的索引:

GLfloat 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   // 左上角
};

GLuint indices[] = { // 注意索引从0开始! 
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

可以看到,当时用索引的时候,我们只定义了4个顶点,而不是6个。EBO对象的创建和绑定流程如下所示:

//@1 创建索引缓冲对象
GLuint EBO;

//@2 绑定EBO
glGenBuffers(1, &EBO);

//@3 把创建的缓冲EBO绑定到GL_ELEMENT_ARRAY_BUFFER目标上
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

//@4 把索引数组内容复制到缓冲里
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); 

//@5 绘制时的流程,同@3
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

/*@6 目的:指明我们从索引缓冲渲染,我们会使用当前绑定的索引缓冲对象中的索引进行绘制:
  第一个参数:指定了我们绘制的模式,这个和glDrawArrays的一样。
  第二个参数:我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。
  第三个参数:索引的类型,这里是GL_UNSIGNED_INT。
  第四个参数:我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。
*/
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

说明:glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。这意味着我们必须在每次要用索引渲染一个物体时绑定相应的EBO,不过顶点数组对象同样可以保存索引缓冲对象的绑定状态。VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象。绑定VAO的同时也会自动绑定EBO。

2 OpenGL 图形显示流程

2.1 着色器

2.1.1 顶点着色器(vertex shader)

在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡

OpenGL需要我们至少设置一个顶点和一个片段着色器。用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后编译这个着色器,这样我们就可以在程序中使用它了。一个基础的GLSL顶点着色器的源代码如下所示:

#version 330 core //版本声明 OpenGL 3.3
/*
 layout (location = 0)设定了输入变量的位置值
 in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)
 vec3表示空间坐标(x,y,z)
*/
layout (location = 0) in vec3 position;
void main()
{
	//这里将vec3转换成vec4
    gl_Position = vec4(position.x, position.y, position.z, 1.0);
}

2.1.2 片段着色器(fragment shader)

片段着色器全计算像素最后的颜色输出。在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。一个基础的GLSL片段着色器的源代码如下所示:

#version 330 core //版本声明 OpenGL 3.3
/*
 out关键字声明输出变量color
 vec4表示RGBA
*/
out vec4 color;
void main()
{
	//将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。
    color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}

两个着色器现在写好了,剩下的事情是把两个着色器对象编译后 链接到一个用来渲染的着色器程序(Shader Program)中。

2.2 编译着色器(compile shader)

我们已经写了一个顶点着色器源码(储存在一个C的字符串中),但是为了能够让OpenGL使用它,我们必须在运行时动态编译它的源码。流程如下:

//@1 创建一个着色器对象
GLuint vertexShader;
//@2 创建一个顶点着色器,传递的参数为GL_VERTEX_SHADER。
vertexShader = glCreateShader(GL_VERTEX_SHADER);
/*@3 把这个着色器源码附加到着色器对象上
  第一个参数:着色器对象。
  第二参数指定了传递的源码字符串数量,这里表示只有1个。
  第三个参数是顶点着色器真正的源码
  第四个参数暂时不用。
*/
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//@4 编译着色器源码:
glCompileShader(vertexShader);

2.3 链接着色器(link shader)

链接着色器的流程相关代码如下所示:

//@1 创建一个程序对象很简单
GLuint shaderProgram;
//@2 创建一个程序,并返回新创建程序对象的ID引用
shaderProgram = glCreateProgram();
//@3 把之前编译的着色器附加到程序对象上
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
//@4 用glLinkProgram链接它们并检测错误
glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
  ...
}
//@5 用刚创建的程序对象作为它的参数,以激活这个程序对象
glUseProgram(shaderProgram);

//@6 在glUseProgram之后 就可以删除对应的shader了。
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

2.4 链接顶点属性

顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。即:我们必须在渲染前指定OpenGL该如何解释顶点数据。顶点缓冲数据一般会被解析为如下形式:

  • 位置数据被储存为32-bit(4字节)浮点值,即每一个 X,Y,Z均为4字节。
  • 每个位置包含3个这样的值,Vertex1、Vertex2、Vertex3。
  • 在这3个值之间没有空隙且 在数组中紧密排列。
  • 数据中第一个值在缓冲开始的位置。

有了这些信息我们使用glVertexAttribPointer告诉OpenGL该如何解析顶点数据,该函数的解读如下所示:

/*目的:告诉OpenGL该如何解析顶点数据
 第一个参数:指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
 第二个参数:指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
 第三个参数:指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。 
 第四个参数:定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
 第五个参数:步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个GLfloat之后,我们把步长设置为3 * sizeof(GLfloat)。 
 第六个参数:类型是GLvoid*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。
*/
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);

已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,代码会是这样:

// 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// @1 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// @2 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// @3 绘制物体
//...绘制相关函数

2.5 VAO绑定与最终渲染

2.5.1 VAO与VBO绑定

当绘制一个物体时,使用VAO绑定VBO的流程如下所示:

//@1 创建一个VAO对象
GLuint VAO;
//@2 绑定VAO
glGenVertexArrays(1, &VAO);  
//...
//@3 绑定VAO
glBindVertexArray(VAO);
//@4 把顶点数组复制到缓冲中供OpenGL使用,绑定和配置对应的VBO和属性指针
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//@5 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
//@6 解绑VAO
glBindVertexArray(0);

//@7 loop循环,绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
glBindVertexArray(0);

这里我们也要了解:一个顶点数组VAO对象会储存以下这些内容:

  • glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
  • 通过glVertexAttribPointer设置的顶点属性配置。
  • 通过glVertexAttribPointer调用进行的顶点缓冲对象与顶点属性链接。

至此,三角形就可以绘制出来了,下一篇文章会整理一份完整的可执行的代码。

2.5.2 VAO与EBO绑定

当绘制一个物体时,使用VAO绑定EBO的流程如下所示:

//@1 绑定顶点数组对象
glBindVertexArray(VAO);
//@2 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用,绑定和配置对应的VBO和属性指针
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//@3 复制我们的索引数组到一个索引缓冲中,供OpenGL使用,绑定和配置对应的EBO和属性指针
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(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
//@5 解绑VAO
glBindVertexArray(0);

//@6 loop循环,绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);

至此,2个三角形拼出来的四边形就可以绘制出来了,后面文章会整理一份完整的可执行的代码,来绘制四边形。


该系列文章主要参考openGL官网 和学习 learnopenGL官网 进行知识体系的梳理和重构,重在形成自己对openGL知识的理解和知识体系。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

图王大胜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值