LearnOpenGL从入门到入魔(2):绘制2D图形

总目录

LearnOpenGL从入门到入魔(1):OpenGL简介
LearnOpenGL从入门到入魔(2):绘制2D图形
LearnOpenGL从入门到入魔(3):绘制纹理
LearnOpenGL从入门到入魔(4):绘制3D图形
LearnOpenGL从入门到入魔(5):简单滤镜效果
LearnOpenGL从入门到入魔(6):光照(光照基础,材质)
LearnOpenGL从入门到入魔(7):光照(光照贴图,投光物)

1. OpenGL基本概念

1.1 图像渲染管线

 在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(GraphicsPipeline,或称管线)管理的,该过程可表述为将一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程。图形渲染管线可以被划分为两个主要部分:第一部分将输入的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。下图表示一个图形渲染管线的每个阶段的抽线表示:

管线

 蓝色部分着色器允许我们自定义,即可编程着色器,包括顶点着色器几何着色器片段着色器,而几何着色器通常使用默认的就可以了,其它部分也是用默认的即可。在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器,因为GPU中没有默认的顶点/片段着色器。

图形渲染管线各阶段功能如下:

  • 顶点着色器(Vertex Shader)

 顶点着色器接收一组顶点数组作为输入,这些顶点数据使用顶点属性表示,它会将每个顶点的3D坐标转换为另一种3D坐标,同时对该顶点的顶点属性进行一些基本处理,然后再将经过处理后的顶点数组。

  • 顶点属性

 顶点属性指定了每个顶点的各种属性,包括坐标(postion)、颜色(color)、法线(normal)以及纹理(Texture)等。下面演示了如何定义一组顶点数据,其中每个顶点包含了坐标、颜色和纹理属性:

  GLfloat vertex[ 4*(3+4+2)] =
    {   //x,y,z,              r,g,b,a                  s,t(纹理)
        -0.5f,  0.5f, 0.0f,   0.0f, 0.0f, 0.5f, 1.0f,  0.0f, 1.0f, // 第1个顶点
         0.5f,  0.5f, 0.0f,   0.0f, 0.5f, 0.0f, 1.0f,  1.0f, 0.0f, // 第2个顶点
        -0.5f, -0.5f, 0.0f,   0.5f, 0.0f, 1.0f, 1.0f,  0.0f, 1.0f, // 第3个顶点
         0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 0.5f, 1.0f,  1.0f, 1.0f, // 第4个顶点
    };

 在OpenGL中,顶点属性数据存储在一段连续的内存空间。下图展示了3个顶点的存储情况,其中每个顶点的坐标和颜色属性分别占3个float,即12字节(BYTE):
image

  • 形状(图元)装配(SHAPE/Primitive Assembly)

 图元(Primitive),或称形状,是指OpenGL的渲染类型,当我们将输入的坐标和颜色渲染成具体的某种表示时,需要在调用OpenGL的指令时指定图元,比如GL_POINTS表示点,GL_TRIANGLES表示三角形以及GL_LINE_STRIP表示一系列的连续直线等。因此,图元装配的作用时将顶点着色器输出的所有顶点作为输入,输出制定的基本图元。

  • 几何着色器(Gemometry Shader)

 几何着色器把基本图元形式的顶点的集合作为输入,可以通过产生新顶点构造出新的(或是其他的)基本图元来生成其他形状。

  • 光栅化(Rasterization Stage)

 光栅化阶段将几何着色器输出图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段(Fragment)。在片段着色器运行之前会执行裁切,而裁切的目的即为丢弃超出我们视图以外的所有像素,以便提升执行效率。

片段(Fragment):指OpenGL渲染一个像素所需的所有数据。

  • 片段着色器(Fragment Shader)

 片段着色器以光栅化阶段生成的片段作为输入,它的作用是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色值。

  • 测试与混合(Test & Blend)

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

1.2 着色器

 图形渲染管线接受一组3D坐标,然后把它们转变为屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。OpenGL的着色器使用着色器语言GLSL(OpenGL Shading Language)编写,通过编译顶点着色器的源码,就可以在程序中使用它。

1.2.1 顶点着色器

 顶点着色器接收一组顶点数据(VertextData)作为输入,这些顶点数据使用顶点属性表示,它会将每个顶点的3D坐标转换为另一种3D坐标(标准化设备坐标),同时对该顶点的顶点属性进行一些基本处理,需要注意的是,顶点着色器依次处理顶点集合中的顶点数据。比如,我们要绘制一个三角形,那么就需要向顶点着色器输入3个顶点数据,且每个顶点的坐标(postion)是3D的。下列代码描述一个非常简单的顶点着色器源码:

// 注释(1)
#version 330 core
// 注释(2)
layout (location = 0) in vec3 aPos;

void main()
{
    // 注释(3)
    // GLSL中一个向量最多有四个分量,使用vec4表示
    // 每个分量值表示空间中的坐标,即
    // vec.x 表示x轴坐标;
    // vec.y 表示y轴坐标;
    // vec.z 表示z轴坐标;
    // vec.w 不用做表达空间的位置,主要应用再透视除法(后续解释)
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

注释(1) 用于指明OpenGL的使用版本为3.3,采用核心模式;注释(2) 有两个作用,一是使用in关键字在顶点着色器中声明所有的输入顶点属性(注:本例中只关心顶点的位置position数据),二是通过layout (location = 0)设定输入变量的位置值Location;注释(3) 用于设置顶点着色器的输出,即将位置数据赋值给预定义变量gl_Position,该变量的类型为vec4,这是一个包含4个分量的向量。

  • 标准化设备坐标

 当顶点坐标经过顶点着色器处理后,将会被转换为标准化设备坐标,所谓标准化设备坐标,是一个x、y和z值在-1.0~1.0的一小段空间。任何落在范围外的坐标都会被丢弃或者裁剪,不会显示在设备屏幕上。下图展示了在标准化坐标中定义一个2D三角形,即忽略z轴。

image

1.2.2 片段着色器

 片段着色器用于计算一个像素的最终颜色值,该阶段是OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据,比如光照、阴影、光的颜色等等,这些数据将被用来计算最终像素的颜色值。在计算机图形中,一个像素的颜色值由4个分量组成,即R(红色)、G(绿色)、B(蓝色)和A(透明度),就是我们熟知的RGBA。在OpenGL中,颜色的每个分量的取值范围为0.0f~1.0f,颜色程度由浅色到深色。下列代码演示了如何创建一个简单的片段着色器:

// 指明OpenGL的使用版本为3.3,并采用核心模式
#version 330 core
// 定义片段着色器的输出
// 输出一个vec4类型的变量,该变量表示像素的颜色值
out vec4 FragColor;

void main()
{
    // 对变量进行赋值
    // 即每个像素的颜色为红色
    FragColor = vec4(1.0f, 0.0f, 0.0f, 1.0f);
} 

1.3 VAO、VBO和EBO

1.3.1 VBO

 VBO,Vertex Buffer Object,即顶点缓冲对象,该对象用于管理GPU的一段内存(在GPU中又称显存),这段内存存储了大量的顶点数据以供顶点着色器和片段着色器使用。使用这些VBO的好处就是我们可以一次性由CPU向GPU发送一大批数据,以提高着色器访问顶点数据的效率,而不是每个顶点发送一次,毕竟CPU把数据发送到显卡相对较慢。VBO创建过程:

  • 首先,调用glGenBuffers函数创建一个VBO对象,并生成一个对应的唯一ID;
unsigned int mVBOId;
// 函数原型:glGenBuffers(GLsizei n, GLuint *buffers);
//
// 函数作用:创建一组缓冲对象,并指定各自对应的唯一ID。这里创建的是VAO对象
// 参数说明:  
// n 表示VBO对象的数目
// buffers 指定这些VBO对象对应的唯一ID;
glGenBuffers(1, &mVBOId);
  • 其次,调用glBindBuffer函数将创建的VBO对象(缓存)绑定到GL_ARRAY_BUFFER目标上。OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。
// 函数原型:glBindBuffer(GLenum target, GLuint buffer)
//
// 函数作用:绑定VBO对象到指定目标缓存类型
// 参数说明:
// target 指定该VBO对象要绑定到的目标缓冲类型,比如GL_ARRAY_BUFFER;
// buffer 指定该VBO对象对应的ID;
glBindBuffer(GL_ARRAY_BUFFER, mVBOId);  
  • 第三,调用glBufferData函数将定义的顶点数据复制到当前绑定的缓冲的内存中。
// 函数原型:
//  glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage)  
//
// 函数作用:拷贝顶点数据到GPU显存
// 参数说明: 
// target 指定当前缓存内存(VBO)绑定的目标,这里为GL_ARRAY_BUFFER
// size 指定发送顶点数据的大小(字节)
// data 指定发送的顶点数据
// usage 指定GPU如何管理这些数据,有三种形式:  
//       GL_STATIC_DRAW :数据不会或几乎不会改变;
//       GL_DYNAMIC_DRAW:数据会被改变很多;
//       GL_STREAM_DRAW :数据每次绘制时都会改变;
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

注释:三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,这样就能确保显卡把数据放在能够高速写入的内存部分。

  • 最后,使用完VBO后需要释放相关资源。
// 函数原型:glDeleteBuffers(GLsizei n, GLuint *buffers);
//
// 函数作用:释放一组VBO对所占资源
// 参数说明:  
// n 表示VBO对象的数目
// buffers 指定这些VBO对象对应的唯一ID;
glDeleteBuffers(1, &mVBOId);

 下图演示了VBO在内存中的存储形式:

在这里插入图片描述

 其中,VBO1的每个顶点只包含位置(pos)属性;VBO2的每个顶点包含了位置(pos)和颜色(color)属性。然而,虽然我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它,但是,OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。因此,在渲染之前,我们必须指定OpenGL该如何解释顶点数据,而这个过程被称之为链接顶点属性。下图展示了经过链接顶点属性操作后,显存中的顶点数据被解析后的样子:

在这里插入图片描述

 从上图可知,这段GPU显存(缓存)中连续存储了三个顶点数据,即VEXTEX1、VEXTEX2、VEXTEX3,每个顶点由位置(pos)颜色(color)纹理三个属性组成,其中,位置属性包含X、Y、Z分量,每个分量占1float(=4字节),因此位置属性共占12个字节;颜色属性包含R、G、B分量,每个分量占1float(=4字节),因此颜色属性共占12个字节;纹理属性包含S、T分量,每个分量占1float(=4字节),因此颜色属性共占8个字节。此外,两个相邻顶点之间相同的属性之间的距离称为为步长(STRIDE),即第一个顶点某个属性起始分量到第二个顶点相同属性的起始分量所占字节数。在OpenGL中,我们使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据,即将这些数据标识具体的顶点属性,代码如下:

// 函数原型:glVertexAttribPointer(GLuint index, GLint size, GLenum type,
//              GLboolean normalized, GLsizei stride, const void *pointer);
//
// 函数作用:定义一个顶点属性数据
// 参数说明:
// index 指定要配置的某个顶点属性索引index,如着色器源码中layout(location=0)
//       表示将位置属性索引设置为0,当我们希望将数据传递到这个位置属性中,
//       直接传入这个位置属性的索引0即可;
// size 指定某个顶点属性的大小,比如位置属性,是一个vec3,因此大小是3;
// type 指定顶点属性的类型,比如位置属性,每个分量数据类型为浮点型;
// normalized 设定数据是否被标准化(归一化),即数据被映射到-1~1之间;
// stride 指定步长(Stride)大小;
// pointer 指定顶点属性数据在缓存中起始位置的偏移量(offset);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

// 函数原型:glEnableVertexAttribArray(GLuint index);
//
// 函数作用:启用顶点属性
// 参数说明:
// index 顶点属性索引
glEnableVertexAttribArray(0);

1.3.2 VAO

 VAO,Vertex Array Object,即顶点数组对象,该对象存储的是一系列指向顶点属性存储地址的指针,如上上图所示,以VAO2为例,它的atttribute pointer 0指向VBO2中第一个顶点的位置属性(pos[0])的存储地址,atttribute pointer 1指向VBO2中第一个顶点的颜色属性(col[0])的存储地址,atttribute pointer 1指向VBO2中第二个顶点的位置属性(pos[0])的存储地址等等。使用顶点数组对象的好处时,当我们配置链接好顶点属性后,只需要初始化的时候执行一次,再后续的绘制物体的时候就无需再执行一遍拷贝数据到显存,链接顶点属性,而是直接绑定相应的VAO即可。VAO创建过程如下:

  • 首先,创建一组VAO对象,并为每个VAO对象生成一个对应的唯一ID;
unsigned int mVAOId;
// 函数原型:glGenVertexArrays(GLsizei n, GLuint *arrays);
//
// 函数作用:创建一组VAO对象
// 参数说明:
// n 表示VAO对象的数目
// arrays 指定这些VAO对象对应的唯一ID,是一个数组;
glGenVertexArrays(1, &mVAOId);
  • 然后,调用glBindVertexArray绑定VAO。
// 函数原型: glBindVertexArray(GLuint array);
//
// 函数作用:绑定一个VAO对象
// 参数说明:
// array 表示一个VAO对象对应的ID
glBindVertexArray(mVAOId);
  • 最后,释放VAO对象所占资源。
// 函数原型:glDeleteVertexArrays(GLsizei n, GLuint *arrays);
//
// 函数作用:创建一组VAO对象
// 参数说明:
// n 表示VAO对象的数目
// arrays 指定这些VAO对象对应的唯一ID,是一个数组
glDeleteVertexArrays(1, &mVAOId);

1.3.3 EBO

 EBO,Element Buffer Object,即索引缓存对象,顾名思义,索引缓冲对象是用来存储索引数据的,所谓索引数据,是指一组索引,而这个索引就是顶点数据在数组中的下标。索引缓冲对象的作用,就是用于解决绘制一个物体时同一个顶点被重复指定的问题,这对于拥有上千个三角形来说,工作量是巨大的。比如,我们要绘制一个矩形,在OpenGL中通常通过绘制两个三角形实现,因为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,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

 "聪明绝顶"的你应该知道,两个三角形共用斜边,这就意味着矩形的左下角和右上角的顶点被指定两次,如上述数组可知。那么有没有一种方法,顶点数组中只包含不同的顶点,当需要绘制有重复的顶点时,我们只需要通过数组的下标将顶点数据取出来就好。有!这个方法就是索引缓冲对象。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, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

EBO创建、使用过程:

  • 首先,创建一组EBO对象,并分别指定对应的唯一ID;
unsigned int mEBOId;
// 函数原型:glGenBuffers(GLsizei n, GLuint *buffers);
//
// 函数作用:创建一组缓冲对象,并指定各自对应的唯一ID。
// 参数说明:这里创建的是EBO对象
// n 表示EBO对象的数目
// buffers 指定这些EBO对象对应的唯一ID;
glGenBuffers(1, &mEBOId);
  • 其次,绑定EBO,再调用glBufferData将索引数据拷贝到索引缓冲区中,其中缓冲目标设定为GL_ELEMENT_ARRAY_BUFFER
// 函数原型:glBindBuffer(GLenum target, GLuint buffer);
// 
// 函数作用:绑定一个命名的缓冲区对象
// 参数说明:
// target 指定缓冲对象被绑定到的目标类型,EBO的目标类型为GL_ELEMENT_ARRAY_BUFFER
// buffer 指定要绑定缓冲对象的ID
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mEBOId);

// 函数原型:glBufferData(GLenum target,GLsizeiptr size,const void * data, GLenum usage);
// 
// 函数作用:把数据拷贝到缓冲区。这里是把索引数据拷贝到EBO
// 参数说明:
// target 指定缓冲对象被绑定到的目标类型;
// size 指定索引数据的大小,以字节为单位;
// data 指定索引数据
// usage 指定GPU如何管理这些数据,有三种形式:  
//       GL_STATIC_DRAW :数据不会或几乎不会改变;
//       GL_DYNAMIC_DRAW:数据会被改变很多;
//       GL_STREAM_DRAW :数据每次绘制时都会改变;
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
  • 第三,使用索引缓冲对象的索引数据,绘制图形,使用前需要绑定EBO。glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。由于VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象,绑定VAO的同时也会自动绑定EBO,因此,我们再使用索引渲染一个物体时只需要再初始化时绑定相应的EBO即可。
// 绑定一个EBO对象
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mEBOId);

// 函数原型:void glDrawElements(GLenum mode,GLsizei count,GLenum type,const void * indices);
//
// 函数作用:从数组获取数据绘制图元
// 参数说明:
// mode 指定渲染的图元类型,比如三角形为GL_TRIANGLES;
// count 指定渲染元素的数量,本例正方形为6个顶点;
// type 指定索引数据元素类型,本例为无符号整型(unsigned int);
// indices 指定一个指向索引存储位置的指针,本例起始位置为0;
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
  • 最后,释放缓冲对象所占资源。
// 函数原型:glDeleteBuffers(GLsizei n, GLuint *buffers);
//
// 函数作用:释放缓冲对象所占资源
// 参数说明:
// n 表示缓冲对象的数目,这里表示EBO对象的数目;
// buffers 指定这些缓冲对应唯一ID,这里表示一组EBO对象对应的ID;
glDeleteBuffers(1, &mEBOId);

 下图演示了EBO在内存中的存储:
在这里插入图片描述

 从上图可知,EBO对象会被保持为VAO的元素缓冲对象。

2. OpenGL实战

 CPU与GPU通信过程:

在这里插入图片描述

 根据上图可知,OpenGL的工作过程:

  • 首先,我们从CPU将要绘制的形状顶点数据和纹理数据传递到GPU显存中,在GPU显存中主要是通过VBO、VAO和EBO来管理这些数据;
  • 其次,分别创建顶点着色器和片段着色器,并将顶点数据输入给顶点着色器处理;
  • 第三,创建一个程序对象,并将编译好的着色器附加到这个程序对象,再将这些着色器链接起来;
  • 最后,绘制图形。

2.1 案例1:绘制三角形

1. 创建着色器

(1)创建着色器Shader对象,并返回该对象对应的唯一ID;

GLuint mShaderId = NULL;

// 函数原型:GLuint glCreateShader(	GLenum shaderType)
//
// 函数作用:创建一个Shader对象
// 参数说明:
// shaderType 指定要创建的Shader对象类型,
//            比如 GL_VERTEX_SHADER 表示顶点着色器
//                 GL_FRAGMENT_SHADER 表示片段着色器
// mShaderId = glCreateShader(GL_VERTEX_SHADER);
mShaderId = glCreateShader(GL_FRAGMENT_SHADER);

(2)将着色器源码附着到着色器。由于该案例是创建一个渐变色的三角形,因此,我们将顶点着色器的输出作为片段着色器的输入,以实现每个像素的颜色值尽可能不一致;

// 顶点着色器源码
const char* mVertexShaderStr = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n" // 顶点着色器输入
"out vec3 outPos;\n" // 顶点着色器输出
"void main()\n"
"{\n"
"	outPos = aPos;"
"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";

// 片段着色器源码
const char* mFragmentShaderStr = "#version 330 core\n"
"out vec4 rgbColor;\n" // 片段着色器输出
"in vec3 outPos;\n" // 以顶点着色器的输出变量作为输入
"void main()\n"
"{\n"
"   rgbColor = vec4(outPos, 1.0f);\n"
"}\n\0";

// 函数原型:glShaderSource(GLuint shader,GLsizei count,const GLchar **string,const GLint *length);
//
// 函数作用:替换着色器对象源码(将着色器源码附着到着色器对象中)
// 参数说明: 
// shader 着色器对象ID;
// count 指定字符串数量或者数组中字符串中的数量,本例字符串数为1;
// string 着色器源码
// length 着色器源码string长度(以字节为单位),通常传入NULL会自动计算;
glShaderSource(mShaderId, 1, &shaderStr, NULL);

(3)编译着色器对象,实质是编译着色器对象中存储的源码,以便于着色器程序能够链接它。

// 函数原型:void glCompileShader(GLuint shader);
//
// 函数作用:编译一个着色器对象
// 参数说明: 
// shader 指定要编译的着色器对象ID;
glCompileShader(mShaderId);

2. 创建程序Program

(1)创建一个程序对象。glCreateProgram()函数将创建一个空的程序对象,之前创建的着色器对象将都附着在这个程序对象上;

GLuint mShaderProgramId = NULL;
mShaderProgramId = glCreateProgram();

(2)将编译好的着色器对象附着到程序对象;

// 函数原型:void glAttachShader(GLuint program, GLuint shader);
//
// 函数作用:附着着色器对象到程序对象
// 参数说明:
// program 创建的程序对象ID;
// shader 要附着到程序对象的着色器对象ID;
glAttachShader(mShaderProgramId, vertexShaderId);
glAttachShader(mShaderProgramId, fragmentShaderId);

(3)链接程序对象。只有在链接程序对象成功后,才能在后续调用glUseProgram函数使用该程序对象,从而让着色器生效。当然,链接程序对象成功后,我们可以进行释放着色器对象操作,以释放相关资源。

// 函数原型:void glLinkProgram(GLuint program);
//
// 函数作用:链接程序对象
// 参数说明:
// program 创建的程序对象ID;
glLinkProgram(mShaderProgramId);

// 删除着色器对象
glDeleteShader(vertexShaderId);
glDeleteShader(fragmentShaderId);

3. 拷贝数据到显存,并绘制三角形

(1)创建VBO、VAO对象,拷贝顶点数据到GPU显存并链接顶点属性;

// 1. 创建VAO对象
glGenVertexArrays(1, &mVAOId);

// 2. 创建VBO,拷贝数据到GPU显存,再配置顶点属性
// (1) 分别创建VAO对象
GLuint vboId = 0;
glGenBuffers(1, &vboId);
// (2) 绑定VAO对象
glBindVertexArray(mVAOId);
// (3) 将新创建的缓冲绑定到顶点缓冲类型GL_ARRAY_BUFFER上
glBindBuffer(GL_ARRAY_BUFFER, vboId);
// (4) 将顶点数据复制到缓冲的显存供OpenGL使用
// 并指定显卡管理数据模式为GL_STATIC_DRAW,即数据不会或几乎不会改变
glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW);
// (5) 链接顶点属性
glVertexAttribPointer(layout, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(layout);
// (6) 解绑VAO, VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

(2)使用程序对象,并绑定VAO对象,然后调用glDrawArray函数绘制三角形。

// 函数原型:void glUseProgram(GLuint program);
//
// 函数作用:使用程序对象
// 参数说明:
// program 创建的程序对象ID
glUseProgram(mShaderProgramId);

// 函数原型:void glBindVertexArray(GLuint id);
//
// 函数作用:绑定VAO对象
// 参数说明:
// id 创建的VAO对象对应的ID;
glBindVertexArray(mVAOId);

// 函数原型:void glDrawArrays(GLenum mode,GLint first,GLsizei count);
//
// 函数作用:绘制渲染形状
// 参数说明:
// mode 指定渲染的图元,比如三角形为GL_TRIANGLES;
// first 第一个元素的数组下标,本例第一个顶点从0开始;
// count 绘制元素的数量,本例元素为3个顶点;
glDrawArrays(GL_TRIANGLES, 0, 3);

4. 释放资源

// 释放着色器对象资源
if (mShaderId != NULL) {
	glDeleteShader(mShaderId);
	mShaderId = NULL;
}

// 释放VAO资源
if (mVAOId != NULL) {
	glDeleteVertexArrays(1, &mVAOId);
	mVAOId = NULL;
}

// 释放VBO资源
if (vboId != NULL) {
	glDeleteBuffers(1, &vboId);
	vboId = NULL;
}

// 释放程序对象资源
if (mShaderProgramId != NULL) {
	glDeleteProgram(mShaderProgramId);
	mShaderProgramId = NULL;
}

 程序结果如下:

.

2.2 案例2:绘制正方形

 绘制正方形案例演示了如何使用EBO对象,现在我们在案例1代码的基础上作如下处理。

(1)创建EBO对象,在链接顶点属性之前将索引数据拷贝到EBO缓存;

// 顶点数组
float data[] = {
	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, 3, // 第一个三角形
	1, 2, 3  // 第二个三角形
};

// 1. 创建VAO对象
glGenVertexArrays(1, &mVAOId);

// 2. 创建VBO,拷贝数据到GPU显存,再配置顶点属性
// (1) 分别创建VAO对象
GLuint vboId = 0;
glGenBuffers(1, &vboId);
// (2) 绑定VAO对象
glBindVertexArray(mVAOId);
// (3) 将新创建的缓冲绑定到顶点缓冲类型GL_ARRAY_BUFFER上
glBindBuffer(GL_ARRAY_BUFFER, vboId);
// (4) 将顶点数据复制到缓冲的显存供OpenGL使用
// 并指定显卡管理数据模式为GL_STATIC_DRAW,即数据不会或几乎不会改变
glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW);

// 创建EBO对象
glGenBuffers(1, &mEBOId);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mEBOId);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// (5) 链接顶点属性
glVertexAttribPointer(layout, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(layout);
// (6) 解绑VAO, VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

(2)调用glDrawElements函数使用索引数据渲染图元;

glUseProgram(mShaderProgramId);
glBindVertexArray(mVAOId);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

 程序执行结果:

3. 参考文献

1. LearnOpenGL中文文档
2. OpenGL3 Reference Pages


Github源码:LearnOpenGL(如果觉得有用,记得给个小star哈~)

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 黑客帝国 设计师:白松林 返回首页