说明:本系列视频使用 mac平台的Xcode来实现,windows平台和Linux平台与之类似。
1 程序 功能简介
目的:显示2个三角形,一个橘色,一个黄色,分别在中点的左右。如下所示:
功能很简单重,主要是通过实战 理解整个openGL绘制图形的基本流程。
2 OpenGL程序中基本概念解读
在编写程序前,我们需要了解一些基本概念:顶点输入、顶点缓冲对象VBO、
2.1 向量(Vector)
在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x
、vec.y
、vec.z
和vec.w
来获取。注意vec.w
分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视划分(Perspective Division)上。
2.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
};
2.3 顶点缓冲对象(Vertex Buffer Objects, VBO)
定义顶点数据后,我们会把它作为输入发送给顶点着色器。它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。这时我们就通过VBO 来管理这个内存,它会在GPU内存(显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要有可能 我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显存中后,顶点着色器几乎能立即访问顶点。
我们一般使用glGenBuffers函数和一个缓冲ID生成一个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);
2.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就是可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态。
3 OpenGL 图形显示流程
3.1 着色器
3.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);
}
3.1.2 片段着色器:
片段着色器全计算像素最后的颜色输出。在计算机图形中颜色被表示为有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)中。
3.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);
3.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);
3.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 绘制物体
//...绘制相关函数
3.5 VAO绑定与最终渲染
当绘制一个物体时,使用VAO的流程如下所示:
//@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
调用进行的顶点缓冲对象与顶点属性链接。
至此,三角形就可以绘制出来了,下一篇文章会整理一份完整的可执行的代码。