OpenGL学习笔记-2

你好,三角形(一)

前置知识

顶点数组对象:Vertex Array Object,VAO

顶点缓冲对象:Vertex Buffer Object,VBO

元素缓冲对象:Element BUffer Object,EBO 也叫 索引缓冲对象 Index Buffer Object,IBO

一.图形渲染管线(Graphics Pipeline)

在OpenGL中,空间是以三维的形式存在的,但是我们日常使用的屏幕是一块平面,这导致了OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素.

图形渲染管线实质上是把一堆原始图像数据途径一个输送管道,期间经过各种变化和处理最终出现在屏幕上.它可以被笼统的分为两个部分,第一部分是把3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素.

图形渲染管线的每个阶段:顶点着色器->图元装配->几何着色器->光栅化->片段着色器->测试与混合

其中每个阶段的输出都是写一个阶段的输入.

图形渲染管线的具体介绍如下:

零.顶点数据

我们用一个数组来传递3个3d坐标作为管线的初始输入,这表示了一个三角形,这个数组就叫做顶点数据(Vertex Data),顶点数据是一系列顶点的集合.

一个顶点(Vertex)是一个3D坐标的数据的集合,而顶点数据是用顶点属性(Vertex Attribute)来表示的,它可以包含任何我们想用的数据.

一.顶点着色器(Vertex Shader)

图形渲染管线的第一个部分叫做顶点着色器,他的输入是一个单独的顶点,他的任务是把3D坐标转换为另一种3D坐标,同时会对顶点属性做一些基本处理

二.图元装配(Primitive Assembly)

会把顶点着色器输出的所有顶点作为输入,装配为指定图元的形状,例如一个三角形.

三.几何着色器

几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。

四.光栅化阶段

这里把图元映射为最终屏幕上相应的像素,生成为片段着色器使用的片段(Fragment),他会在片段着色器运行之前执行裁切,裁切会丢弃你的视图以外的所有像素.

OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据。

五.片段着色器

片段着色器的目的是计算一个像素点最终颜色,通常,片段着色器包含3D场景的数据,比如光照,阴影,光的颜色等等.

六.测试与混合阶段

这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

二.顶点着色器(输入顶点)

我们在OpenGL中定义的3D坐标(x,y,z),只有当他们在区间(-1.0,1.0)这个范围内才会被处理,这个范围叫做标准化设备坐标(Normalized Device Coordinates).

z轴上的坐标叫做深度,它代表一个像素在空间中与你的距离,深度高的像素会被深度低的像素遮挡.

这里我们渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置,定义一个float数组,来表示它.

1
2
3
4
5
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};

之后我们把这个数组发送给顶点着色器,它会在GPU中创建内存来存储我们的顶点数据.

同时配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。

为了管理这个内存,我们使用一个对象,叫做顶点缓冲对象(Vertext Buffer Objects),他会在显存中存储大量的顶点,这样我们就可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次.

OpenGL中有许多对象,他们都有一个独一无二的id,所以我们可以使用glGenBuffers函数和一个缓冲ID来生成一个VBO.

1
2
unsigned int VBO;
glGenBuffers(1,&VBO);//把VBO的引用绑定到ID:1上

OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER,OpenGL允许我们同时绑定多个缓冲,只要他们是不同的缓冲类型,我们可以使用glBindBuffer函数,把新创建的缓冲绑定到缓冲类型GL_ARRAY_BUFFER目标上:

1
glBindBuffer(GL_ARRAY_BUFFER,VBO);

之后,我们使用的任何缓冲调用,在GL_ARRAY_BUFFER目标上,都会用来配置当前绑定缓冲,即VBO,然后我们可以调用glBufferdata函数,他会把之前定义的顶点数据复制到缓冲中.

1
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

这个函数是一个专门把用户定义的数据复制到当前绑定缓冲的函数

它的第一个参数是目标缓冲的类型(GL_ARRAY_BUFFER)

第二个参数是指定传输数据的大小(以字节为单位),用sizeof计算即可.

第三个参数是我们实际发送的数据

第四个参数是指定了我们希望显卡管理给定数据的方式:

  • GL_STATIC_DRAW :数据不会或几乎不会改变。
  • GL_DYNAMIC_DRAW:数据会被改变很多。
  • GL_STREAM_DRAW :数据每次绘制时都会改变。

这样是为了确保显卡把数据放入显存中可以高速写入的部分.

创建顶点着色器

顶点着色器是一个可编程着色器,在使用现代OpenGL进行着色的时候,我们至少需要设置一个顶点和一个片段着色器.

使用着色器语言GLSL(OpenGL Shading Language)来编写顶点着色器

1
2
3
4
5
6
7
#verson 330 core
layout(location = 0) in vec3 aPos;

void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

着色器起始于一个版本的声明,glsl的版本号和opengl的版本号是一一对应的.

下一步是使用in关键词,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute),由于我们现在只需要一个顶点属性,即位置.所以我们只需要声明一个向量vec3.3代表这个向量有三个分量(float),这对应了我们位置的三个坐标. 我们这个vec3变量去名字叫做aPos.

我们也用了layout(location = 0)设定输入变量的位置值.

在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。我们会在后面的教程中更详细地讨论向量。

对于这个着色器的内容,我们将位置数据(aPos)赋给预定义的gl_Position,当然,gl_Position是一个vec4,所以赋值的时候第四个参数我们使用1.0f.

所以这个非常简单的着色器做的事情包括,定义aPos,类型为vec,将aPos的x,y,z和1.0赋值给预定义的gl_Position.

编译着色器

我们先使用一个字符串来硬编码着色器.

1
2
3
4
5
6
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";

为了在OpenGL中使用它,我们需要在OpenGL中使用一个ID来引用着色器.这个ID我们用int类型,用glCreateShader的返回值来赋值.

1
2
unsigned int vrtexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

下一步,我们把着色源码,附加到着色器对象上,然后对他编译

1
2
3
4
5
6
7
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//这个函数把要编译的着色器对象作为第一个参数
//第二个参数,是传递的字符串的数量
//第三个是真的的源码,我们之前创建了一个char*这里使用它的地址作为参数。
//第四个先设置为NULL
glCompileShader(vertexShader);
//编译着色器,参数是一个着色器对象。

片段着色器(Fragment Shader)

这是我们第二个也是最后一个创建出来用来渲染三角形的着色器。

它的作用是计算像素的颜色。

在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。比如说我们设置红为1.0f,绿为1.0f,我们会得到两个颜色的混合色,即黄色。

1
2
3
4
5
6
7
#version 330 core
out vec4 FragColor;//输出变量是一个有四个分量的变量。这里使用out声明变量。

void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);//赋值FragColor,其中,第四个分量代表了透明度,1.0f为完全不透明.
}

之后编译片段着色器,是一样的方法.

1
2
3
4
5
6
7
8
9
10
11
12
const char *fragmentShaderSource = "#version 330 core
out vec4 FragColor;//输出变量是一个有四个分量的变量。这里使用out声明变量。

void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);//赋值FragColor,其中,第四个分量代表了透明度,1.0f为完全不透明.
}"

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader,1,&fragmentShaderSource,NULL);
glCompliesShader(fragmentShader);

之后的事情就是把两个着色器对象连接到着色器程序(ShaderProgram)上

着色器程序

Shader Profram Object 是多个着色器合并之后最终连接完成的版本. 如果要使用刚才我们编译的两个着色器,我们需要把他们link为一个Shader Program Object,然后在渲染对象的时候激活这个Shader Program,这样我们才可以在发送渲染调用的时候调用我们编译的着色器.

创建着色器程序对象

1
2
unsigned int shaderProgram;
shaderProgram = glCreateProgram();

然后把着色器附加到程序对象上,在连接他们.

1
2
3
glAttachShader(shaderProgram,vertexShader);
glAttachShader(shaderProgram,fragmentShader);
glLinkProgram(shaderProgram);

然后是激活对象

1
glUseProgram(shaderProgram);//在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。

记得删除着色器对象

1
2
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

总结

现在,我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它.

还差最后一步,OpenGL还不知道该如何解释内存中的顶点数据,以及他该如何将顶点数据链接到顶点着色器的属性上.

链接顶点属性(设置顶点属性指针)

顶点着色器允许我们以任意的形式,来输入顶点属性.

这意味着我们需要手动的指定我们输入的数据是以怎样的形式来存储的. 所以在渲染开始前,我们必须指定OpenGL来如何解释顶点数据.

我们用一个数组来传递3个3d坐标作为管线的初始输入,这表示了一个三角形,这个数组就叫做顶点数据(Vertex Data),顶点数据是一系列顶点的集合.

1
2
3
4
5
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};

而我们希望这个数组被这样解释:

  • 我们的顶点数据只存储位置(Position) 这一属性,储存的数据类型是占四个字节的浮点值.
  • 每个位置包含三个这样的值.
  • 在这三个值之间没空隙,他们在数组中是紧密排列(Tightly Packed)
  • 数据中第一个值在缓冲开始的位置.

为此,我们需要函数glVertexAtttribPointer

1
2
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3 * sizeof(float),(void*)0);
glEnableVertexAttribArray(0);

它的参数比较多,我们逐一介绍

  • 第一个是我们要指定的顶点属性,我们在顶点着色器中用layout(location = 0)定义了position顶点属性的Location,它把顶点属性的位置值设置为0.
  • 第二个参数指定顶点属性的大小,这里的顶点属性是一个vec3,它由3个值组成,所以大小是3.
  • 第三个参数是我们的数据类型,这里使用的是GLSL中的float类型,GL_FLOAT
  • 第四个参数定义我们是否希望参数被标准化(Normalize),GL_TRUE和GL_FALSE
  • 第五个参数为步长(Stride),它告诉OpenGL在连续的顶点属性组之间的间隔,即下一个顶点的属性在3个float之后,所以步长我们设置为3 * sizeof(float)
  • 第六个参数要求的类型是void*,所以我们使用强制类型转换,他表示的是数据在缓冲中起始位置的偏移量(Offset),由于我们的数据在开头,所以是0.

现在我们定义好了OpenGL是如何解释顶点数据的,现在我们应该使用glEnableVertexAttribArray,以顶点属性的位置值作为参数,启用顶点属性.

顶点数组对象

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

一个顶点数组对象会储存这些内容

  • glEnableVertexAttribArray和glDisableVertexAttribArray的调用
  • 通过glVertexAttribPointer设置的顶点属性配置
  • 通过glVertexAttribPointer调用与顶点属性关联的VBO

使用顶点数组对象

创建

1
2
unsigned int VAO;
glGenVertexArrays(1, &VAO);

使用glBindVertexArray绑定VAO,从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

[...]

// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

总结

自此,我们的渲染管线就设置好了:一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO,(和必须的VBO及属性指针) 然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。`

这篇是learningOpenGL第二篇——你好三角形的学习笔记,但是离画出三角形还差一步,但是在迈出着最后一步的前,我们还需要理清楚我们已经学习过的一些概念.