🍲



神奇的正方形 ☁️

在上一节中,我们绘制了一个墨绿色的三角形,这一节,我们来实现 “代码封装”,并绘制彩色正角形变色三角形
上一篇文章地址链接: 【OpenGL学习笔记②】——OpenGL基础【渲染管线 顶点着色器 片元着色器 VAO VBO 万字总结】.
下一篇文章地址链接: 【OpenGL学习笔记④】——纹理贴图【SOIL2的配置+绘制木板 + 纹理环绕】.
OpenGL总学习目录: https://blog.csdn.net/Wang_Dou_Dou_/article/details/121240714.


零、成果预览图:

           

  ● 左图是彩色三角形,右图是变色正方形。



一、着色器的初步了解:


  ● 从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。

  ● 着色器是使用一种叫 GLSL(全称 OpenGL Shading Language) 的类C语言写成的。GLSL 是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。

  ● 着色器的开头总是要声明版本,接着是 输入和输出变量uniformmain 函数。每个着色器的入口点都是 main 函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。对于 uniform ,后面会对其进行讲解。

  ● 一个典型的着色器的模板为:

#version version_number

in vector_type in_variable_name;
out vector_type out_variable_name;

uniform type uniform_name;

void main()
{
  // 处理输入并进行一些图形操作
  ...
  // 将处理过的结果送到输出变量
  out_variable_name = 处理过的结果;
}



  ● GLSL 中的向量是一个可以包含有1、2、3 或者 4 个分量的容器,分量的类型可以是前面默认基础类型的任意一个。它们可以是下面的形式( n 代表分量的数量):

类型含义
vec+n包含n个 float 分量的向量
bvec+n包含n个 bool 分量的向量
ivec+n包含n个 int 分量的向量
uvec+n包含n个 unsigned int 分量的向量
dvec+n包含n个 double 分量的向量

  ◆ 多数时候我们使用 “vec+n”,因为 float 足够满足大多数要求了。

  ◆ 一个向量的分量可以通过 vec.x 这种方式获取,这里 x 是指这个向量的第一个分量。我们可以分别使用 “ .x、.y、.z 和 .w ” 来获取它们的第 1、2、3、4 个分量。GLSL 也允许你对颜色使用 rgba ,或是对纹理坐标使用 stpq 访问相同的分量。



  ◆ 举个一个片元着色器的栗子:(环境 VS2010)

在这里插入图片描述



二、顶点/片元着色器 (文本文件 txt)


  ● 顶点着色器与片元着色器之间的联系
   我们一般先通过顶点着色器,在输入端获得颜色,作为顶点属性,再传到片元着色器中进行处理,最后输出出来。

  ● 虽然着色器是各自独立的小程序,但它们都是一个整体的一部分。GLSL 定义了 inout 关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。

  ● 顶点着色器应该接收的是一种特殊形式的输入。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用 location 这一元数据指定输入变量。书写格式:layout (location = 0)。顶点着色器需要为它的输入提供一个额外的 layout 标识,这样我们才能把它链接到顶点数据。

  ◆ 比如说,在渲染彩色三角形时,我们的顶点着色器 (文本文件 txt) 是这样写的:【注:这很关键,请把该 txt 和下面的 txt 一起放到同 .cpp 同一个目录下】

// 文件名为 “shader_v.txt”
#version 330 core							// 3.30版本
layout(location = 0) in vec3 position;		// 位置变量的属性位置值为0
layout(location = 1) in vec3 color;			// 颜色变量的属性位置值为1
out vec3 ourColor;							// 颜色输出
void main()
{
	gl_Position = vec4(position, 1.0f);		// 核心函数(位置信息赋值)
	ourColor = color;
} 


  ● 另一个是片段着色器,它需要一个 vec4 颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。

  ● 如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方中声明一个输出,并在接收方中声明一个类似的输入。当类型和名字都一样的时候,OpenGL 就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。为了展示这是如何工作的,我们会稍微改动一下之前教程里的那个着色器,让顶点着色器为片段着色器决定颜色。

  ◆ 比如说,在渲染彩色三角形时,我们的片元着色器 (文本文件 txt ) 是这样写的:

// 文件名为 “shader_f.txt”
#version 330 core		// 3.30版本
in vec3 ourColor;		// 输入(3维)颜色向量
out vec4 FragColor;     // 输出到四个浮点数构成的一个(4维)向量 FragColor
void main()
{
	FragColor = vec4(ourColor, 1.0f);	// 核心函数(颜色信息赋值)
}


三、创建我们自己的着色器类(Shader.h)


  ● 前面提到的着色器都是单个的、独立的,现在我们要创建一个着色器类,把它们有机地联合起来。

  ◆ 一个典型的着色器类模板如下

#pragma once	// 为了避免同一个头文件被包含(include)多次
#include<string>
#include<fstream>
#include<sstream>
#include<iostream>
using namespace std;

// 我们自己的着色器
class Shader
{
private:
	GLuint vertex, fragment;	// 顶点着色器 和 片元着色器 
public:
	GLuint Program;				// 着色器程序的ID

	// Constructor(着色器构造函数)
	Shader( const GLchar *vertexPath, const GLchar *fragmentPath )		// 分别是主函数传进来的关于顶点/片段着色器GLSL的路径
	{
		// 文件读取系列的变量定义
		......

		// 异常机制处理:保证ifstream对象可以抛出异常:
		......

		try
		{
			// 打开文件
			......

			// 读取文件的缓冲内容到数据流中
			......

			// 关闭文件处理器
			......

			// 转换数据流到string
			......

		} catch( ifstream::failure e  ){	// 发生异常时输出
			cout<<"ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ"<<endl;
		}

		/* 将 string 类型的字符串转化为 char数组 类型 */
		......

		/* 顶点着色器 */
		......

		/* 片元着色器 */
		......

		/* 着色器程序 */
		......
	}

	// Deconstructor(着色器的析构函数)
	~Shader()
	{
		glDetachShader(this->Program, vertex);
		glDetachShader(this->Program, fragment);
		glDeleteShader(vertex);
		glDeleteShader(fragment);
		glDeleteProgram(this->Program);
	}
	
	// 渲染的时候调用
	void Use()
	{
		glUseProgram(this->Program);
	}
};

  ● 代码的细节补充(后面要渲染的彩色三角形和变色正方形,都是用这个通用的着色器)

#pragma once	// 为了避免同一个头文件被包含(include)多次
#include<string>
#include<fstream>
#include<sstream>
#include<iostream>
using namespace std;
#include"glew-2.2.0\include\GL\glew.h"					// 注:这一部分要根据个人情况进行设定
#include"glfw-3.3.4.bin.WIN32\include\GLFW\glfw3.h"

// 我们自己的着色器
class Shader
{
private:
	GLuint vertex, fragment;	// 顶点着色器 和 片元着色器 
public:
	GLuint Program;				// 着色器程序的ID

	// Constructor(着色器构造函数)
	Shader( const GLchar *vertexPath, const GLchar *fragmentPath )
	{
		// 文件读取系列的变量定义
		string vertexCode;
		string fragmentCode;
		ifstream vShaderFile;
		ifstream fShaderFile;

		// 异常机制处理:保证ifstream对象可以抛出异常:
		vShaderFile.exceptions(ifstream::badbit);
		fShaderFile.exceptions(ifstream::badbit);

		try
		{
			// 打开文件
			vShaderFile.open(vertexPath);
			fShaderFile.open(fragmentPath);
			stringstream vShaderStream, fShaderStream;

			// 读取文件的缓冲内容到数据流中
			vShaderStream << vShaderFile.rdbuf();
			fShaderStream << fShaderFile.rdbuf();

			// 关闭文件处理器
			vShaderFile.close();
			fShaderFile.close();

			// 转换数据流到string
			vertexCode = vShaderStream.str();
			fragmentCode = fShaderStream.str();

		} catch( ifstream::failure e  ){	// 发生异常时输出
			cout<<"ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ"<<endl;
		}

		/* 将 string 类型的字符串转化为 char数组 类型 */
		const GLchar *vShaderCode = vertexCode.c_str();
		const GLchar *fShaderCode = fragmentCode.c_str();

		/* 顶点着色器 */
		vertex = glCreateShader(GL_VERTEX_SHADER);				// 创建顶点着色器对象
		glShaderSource(vertex, 1, &vShaderCode, NULL);			// 将顶点着色器的内容传进来
		glCompileShader(vertex);								// 编译顶点着色器
		GLint flag;												// 用于判断编译是否成功
		GLchar infoLog[512];				
		glGetShaderiv(vertex, GL_COMPILE_STATUS, &flag); // 获取编译状态
		if( !flag )
		{
			glGetShaderInfoLog(vertex, 512, NULL, infoLog);    
			cout<<"ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"<<infoLog<<endl;
		}

		/* 片元着色器 */
		fragment = glCreateShader(GL_FRAGMENT_SHADER);			// 创建片元着色器对象
		glShaderSource(fragment, 1, &fShaderCode, NULL);		// 将片元着色器的内容传进来
		glCompileShader(fragment);								// 编译顶点着色器
		glGetShaderiv(fragment, GL_COMPILE_STATUS, &flag);		// 获取编译状态
		if( !flag )
		{
			glGetShaderInfoLog(fragment, 512, NULL, infoLog);	 
			cout<<"ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"<<infoLog<<endl;
		}

		/* 着色器程序 */
		this->Program = glCreateProgram();
		glAttachShader(this->Program, vertex);
		glAttachShader(this->Program, fragment);
		glLinkProgram(this->Program);
		if( !flag )
		{
			glGetProgramInfoLog(this->Program, 512, NULL, infoLog);    
			cout<<"ERROR::SHADER::PROGRAM::LINKING_FAILED\n"<<infoLog<<endl;
		}
		// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
		glDeleteShader(vertex);		
		glDeleteShader(fragment);
	}

	// Deconstructor(析构函数)
	~Shader()
	{
		glDetachShader(this->Program, vertex);
		glDetachShader(this->Program, fragment);
		glDeleteShader(vertex);
		glDeleteShader(fragment);
		glDeleteProgram(this->Program);
	}

	void Use()
	{
		glUseProgram(this->Program);
	}
};


四、绘制彩色三角形(主函数)


  ● 常规的绘制流程(输出一个三角形)如下:
    ◆ 第一步:引入相应的库
    ◆ 第二步:编写顶点位置和颜色
    ◆ 第三步:编写顶点着色器
    ◆ 第四步:编写片元着色器(也称片段着色器)
    ◆ 第五步:编写着色器程序
    ◆ 第六步:设置链接顶点属性
    ◆ 第七步:设置顶点缓冲对象(VBO)【一般都是和后面的 VAO 一起出现】
    ◆ 第八步:设置顶点数组对象(VAO) (也称顶点阵列对象)
    ◆ 第九步:绘制三角形

在这里插入图片描述

  ● 但因为我们把 “第二步、第三步、第四步、第五步、第六步” 这几步都写入了相应的 “Shader.h”、“shader_v.txt”、“shader_f.txt” 里面【如上图所示】,已经封装好了,所以在主函数中,我们只用写剩下的几步即可:

/* 引入相应的库 */
#include <iostream>
using namespace std;
#define GLEW_STATIC	
#include <glew.h>	
#include <glfw3.h> 
#include "Shader.h"

/* 编写各顶点位置与颜色 */
GLfloat vertices_1[] = 
{	// position				// color
	0.0f, 0.5f, 0.0f,		1.0f, 0.0f, 0.0f,		// 上顶点(红色)
	-0.5f, -0.5f, 0.0f,		0.0f, 1.0f, 0.0f,		// 左顶点(绿色)
	0.5f, -0.5f, 0.0f,		0.0f, 0.0f, 1.0f		// 右顶点(蓝色)
};

const GLint WIDTH = 800, HEIGHT = 600;		// 窗口的长和宽

int main()
{
	/* 初始化 */
	glfwInit();
	GLFWwindow* window_1 = glfwCreateWindow(WIDTH, HEIGHT, "A Beautiful Triangle", nullptr, nullptr);
	int screenWidth_1, screenHeight_1;
	glfwGetFramebufferSize(window_1, &screenWidth_1, &screenHeight_1);
	glfwMakeContextCurrent(window_1);
	glewInit();

	/* 将我们自己设置的着色器文本传进来 */
	Shader ourShader = Shader("shader_v.txt", "shader_f.txt");	// 相对路径

	/* 设置顶点缓冲对象(VBO) + 设置顶点数组对象(VAO)  */
	GLuint VAO, VBO;					
	glGenVertexArrays(1, &VAO);		
	glGenBuffers(1, &VBO);			
	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);	
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);	

	/* 设置链接顶点属性 */
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (GLvoid*)0);
	glEnableVertexAttribArray(0);	// 通道 0 打开
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (GLvoid*)(3*sizeof(GLfloat)));
	glEnableVertexAttribArray(1);	// 通道 1 打开

	// draw loop 画图循环
	while (!glfwWindowShouldClose(window_1))
	{
		glViewport(0, 0, screenWidth_1, screenHeight_1);
		glfwPollEvents();
		glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		/*  第九步:绘制三角形 */
		ourShader.Use();					// 图形渲染
		glBindVertexArray(VAO);				// 绑定 VAO
		glDrawArrays(GL_TRIANGLES, 0, 3);	// 画三角形  从第0个顶点开始 一共画3次
		glBindVertexArray(0);				// 解绑定

		glfwSwapBuffers(window_1);
	}

	glDeleteVertexArrays(1, &VAO);			// 释放资源	
	glDeleteBuffers(1, &VBO);
	glfwTerminate();						// 结束
	return 0;
}

  ● 运行结果

在这里插入图片描述



五、索引缓冲对象(EBO)


  ● 如果我们要绘制一个正方形的话,能想到的是用两个三角形来拼凑。这个时候就要用到 EBO 了。


1、VBO、VAO 与 EBO 之间的联系与区别

  ● VBO、VAO 与 EBO 之间的联系与区别 ⭐️ ⭐️
   顶点缓冲对象 VBO 是在显卡存储空间中开辟出的一块内存缓存区,用于存储顶点的各类属性信息,如顶点坐标、顶点法向量、顶点颜色数据等。在渲染时,可以直接从 VBO 中取出顶点的各类属性数据,由于 VBO 在显存而不是在内存中,不需要从CPU传输数据,所以处理效率更高。
   所以可以理解为 VBO 就是显存中的一个存储区域,可以保持大量的顶点属性信息。并且可以开辟很多个 VBO ,每个 VBO 在 OpenGL 中有它的唯一标识 ID ,这个 ID 对应着具体的 VBO 的显存地址,通过这个 ID 可以对特定的 VBO 内的数据进行存取操作。


   VAO 是一个保存了所有顶点数据属性的状态结合,它存储了顶点数据的格式以及顶点数据所需的 VBO 对象的引用。
   因为 VBO 保存了一个模型的顶点属性信息,每次绘制模型之前需要绑定顶点的所有信息。当数据量很大时,重复这样的动作变得非常麻烦。VAO 可以把这些所有的配置都存储在一个对象中,每次绘制模型时,只需要绑定这个 VAO 对象就可以了。
   另外,VAO 本身并没有存储顶点的相关属性数据,这些信息是存储在 VBO 中的,VAO 相当于是对很多个 VBO 的引用,把一些 VBO 组合在一起作为一个对象统一管理。

   索引缓冲对象 EBO 相当于 OpenGL 中的顶点数组的概念,是为了解决同一个顶点多次重复调用的问题,可以减少内存空间浪费,提高执行效率。当需要使用重复的顶点时,通过顶点的位置索引来调用顶点,而不是对重复的顶点信息重复记录,重复调用。
   EBO 中存储的内容就是顶点位置的索引 indices,EBO 跟 VBO 类似,也是在显存中的一块内存缓冲器,只不过 EBO 保存的是顶点的索引。


2、EBO

  ● 关于 VAO 和 VBO,在上一节已经细致地讲解了,下面就讲 EBO。

  ● 首先,我们现在要绘制正方形,则需要 4 个顶点。在主函数中我们将 4 个顶点的位置信息给出。并用顶点位置的索引 indices 数组将它们 “缝合” 起来。示意图如下:

在这里插入图片描述

  ● 代码如下:

/* 编写各顶点位置 */
GLfloat vertices_1[] =
{
	//position					
	0.5f, 0.5f, 0.0f,			// top right		0
	0.5f, -0.5f, 0.0f,			// bottom right		1
	-0.5f, -0.5f, 0.0f,			// bottom left		2
	-0.5f, 0.5f, 0.0f,			// top left			3
};

/* 四个顶点的连接信息给出来 */
GLuint indices_1[] =
{
	0, 1, 3,		// 序号为 0、1、3 的顶点组合成一个三角形
	1, 2, 3			// 序号为 1、2、3 的顶点组合成一个三角形
};

  ● 然后,在创建完 VAO 和VBO 后,再创建 EBO 并绑定,用 glBufferData(以GL_ELEMENT_ARRAY_BUFFER为参数)把索引存储到 EBO 中:

GLuint EBO;
glGenBuffers(1, &EBO);						// 绑定 EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);	// 使用 glBindBuffer 函数把新创建的索引缓冲对象绑定到 GL_ELEMENT_ARRAY_BUFFER目标上
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices_1), indices_1, GL_STATIC_DRAW); // GL_STATIC_DRAW:静态的画图(因为要频繁地读)

  ● 当用 EBO 绑定顶点索引的方式绘制模型时,需要使用 glDrawElements 而不是 glDrawArrays :

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);				// 绑定 EBO
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);	// 画两个三角形 从第0个顶点开始 一共画 6 次(顺序为0,1,3,1,2,3)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);				// 解绑定 EBO

  ◆ glDrawElements() 函数说明
    ① 第一个参数:绘制的模式
    ② 第二个参数:绘制的顶点个数
    ③ 第三个参数:索引的数据类型
    ④ 第四个参数:可选的 EBO 中偏移量设定



六、Uniform


  ● 若要画一个颜色跟着时间变化的正方形,着色器的传入就是随时间变化的。以往的知识满足不了这一点,这时就需要用到 Uniform。

  ● Uniform 是一种从 CPU中的应用 向 GPU中的着色器 发送数据的方式,但 uniform 和顶点属性有些不同。首先,uniform 是全局的(Global)。全局意味着 uniform 变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。其次,无论你把 uniform 值设置成什么,uniform 会一直保存它们的数据,直到它们被重置或更新。

  ● 修改过后的顶点着色器

#version 330 core							// 3.30版本
layout(location = 0) in vec3 position;		// 位置变量的属性位置值为0
void main()
{
	gl_Position = vec4(position, 1.0f);		// 核心函数(位置信息赋值)
} 

  ● 修改过后的片元着色器

#version 330 core 		// 3.30版本
out vec4 FragColor;     // 输出是四个浮点数构成的一个向量 RGB+aerfa
uniform vec4 time;		// 在OpenGL程序代码中设定这个变量(uniform:实时地变量表示)
void main()
{
	FragColor = time;	// 颜色随时间变化
}

  ● “ Shader.h ”头文件,不用修改(和彩色三角形的一样)。


  ● 这个 uniform 现在还是空的。我们还没有给它添加任何数据。首先需要用 glGetUniformLocation() 函数找到着色器中 uniform 属性的索引 (即位置值) 。当我们得到 uniform 的索引后,就可以用 glUniform…() 相关函数来更新它的值了。

float time = glfwGetTime();						// 获取时间(运行的秒数)
float redValue = sin(time) / 2.0f + 0.5f;		// 红色数值计算,范围[0,1]
float greenValue = 1 - redValue;				// 绿色数值计算,范围[0.1]。且满足 “redValue + greenValue = 1”
int vertexColorLocation = glGetUniformLocation(ourShader.Program, "time");	// 找到 “time” 的索引
glUniform4f(vertexColorLocation, redValue, greenValue, 0.0f, 1.0f );		// 更新颜色

  ◆ 补充说明:因为OpenGL在其核心是一个 C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数。glUniform 函数是一个典型例子,它有一个特定的后缀。标识设定为 uniform 的类型时,可能的后缀有:

后缀含义
n+f函数需要一个n个 float 作为它的值
n+i函数需要一个n个 int 作为它的值
n+ui函数需要一个n个 unsigned int 作为它的值
fv函数需要一个 float 向量/数组作为它的值


七、绘制变色正方形(主函数)


  ● 万事俱备,只欠东风

/* 引入相应的库 */
#include <iostream>
using namespace std;
#define GLEW_STATIC	
#include<glew.h>	
#include<glfw3.h> 
#include"Shader.h"

/* 编写各顶点位置 */
GLfloat vertices_1[] =
{
	//position					
	0.5f, 0.5f, 0.0f,			// top right		0
	0.5f, -0.5f, 0.0f,			// bottom right		1
	-0.5f, -0.5f, 0.0f,			// bottom left		2
	-0.5f, 0.5f, 0.0f,			// top left			3
};

/* 四个顶点的连接信息给出来 */
GLuint indices_1[] =
{
	0, 1, 3,		// 序号为 0、1、3 的顶点组合成一个三角形
	1, 2, 3			// 序号为 1、2、3 的顶点组合成一个三角形
};

const GLint WIDTH = 600, HEIGHT = 600;		// 正方形窗口

int main()
{
	glfwInit();
	GLFWwindow* window_1 = glfwCreateWindow(WIDTH, HEIGHT, "Learn OpenGL Triangle test", nullptr, nullptr);
	int screenWidth_1, screenHeight_1;
	glfwGetFramebufferSize(window_1, &screenWidth_1, &screenHeight_1);
	glfwMakeContextCurrent(window_1);
	glewInit();

	/* 将我们自己设置的着色器文本传进来 */
	Shader ourShader = Shader("shader_v.txt", "shader_f.txt");		// 相对路径

	/* 设置顶点缓冲对象(VBO) + 设置顶点数组对象(VAO)  */
	GLuint VAO, VBO;				
	glGenVertexArrays(1, &VAO);		
	glGenBuffers(1, &VBO);			
	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);	
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);	

	/* 设置链接顶点属性 */
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (GLvoid*)0);
	glEnableVertexAttribArray(0);	// 通道 0 打开

	/* 设置索引缓冲对象	*/
	GLuint EBO;
	glGenBuffers(1, &EBO);		
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices_1), indices_1, GL_STATIC_DRAW); 

	// draw loop 画图循环
	while (!glfwWindowShouldClose(window_1))
	{
		// 视口 + 时间 
		glViewport(0, 0, screenWidth_1, screenHeight_1);
		glfwPollEvents();

		// 渲染 + 清除颜色缓冲
		glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		/*  绘制图形 */
		ourShader.Use();
		float time = glfwGetTime();						// 获取时间
		float redValue = sin(time) / 2.0f + 0.5f;		// 红色数值计算,范围[0,1]
		float greenValue = 1 - redValue;				// 绿色数值计算,范围[0.1]。且满足 “redValue + greenValue = 1”
		int vertexColorLocation = glGetUniformLocation(ourShader.Program, "time");
		glUniform4f(vertexColorLocation, redValue, greenValue, 0.0f, 1.0f );

		glBindVertexArray(VAO);									// 绑定 VAO
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);				// 绑定 EBO
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);	// 画两个三角形 从第0个顶点开始 一共画6次
		glBindVertexArray(0);									// 解绑定 VAO
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);				// 解绑定 EBO

		// 交换缓冲
		glfwSwapBuffers(window_1);
	}

	glDeleteVertexArrays(1, &VAO);	// 释放资源
	glDeleteBuffers(1, &VBO);
	glDeleteBuffers(1, &EBO);
	glfwTerminate();				// 结束
	return 0;
}

  ● 运行结果:

在这里插入图片描述



八、小结(整体的思维导图)


  ● 绘制流程(输出一个三角形):
    ◆ 第一步:引入相应的库
    ◆ 第二步:编写顶点属性
    ◆ 第三步:编写顶点着色器
    ◆ 第四步:编写片元着色器(也称片段着色器)
    ◆ 第五步:编写着色器程序
    ◆ 第六步:设置链接顶点属性
    ◆ 第七步:设置顶点缓冲对象(VBO)
    ◆ 第八步:设置顶点数组对象(VAO)
    ◆ 第九步:设置索引缓冲对象(EBO)
    ◆ 第十步:绘制三角形

  ● 把“第二步、第三步、第四步、第五步、第六步” 这几步封装在相应的 “Shader.h”、“shader_v.txt”、“shader_f.txt” 里面即可。再在主函数中,写剩下的几步即可。


九、参考附录:

[1] 《LearnOpenGL CN —— 着色器》
链接: https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/.

[2] 《OpenGL图形渲染管线、VBO、VAO、EBO概念及用例》
链接: https://blog.csdn.net/weixin_30735745/article/details/95616490.

上一篇文章地址链接: 【OpenGL学习笔记②】——OpenGL基础【渲染管线 顶点着色器 片元着色器 VAO VBO 万字总结】.

下一篇文章地址链接: 【OpenGL学习笔记④】——纹理贴图【SOIL2的配置+绘制木板 + 纹理环绕】.

OpenGL总学习目录: https://blog.csdn.net/Wang_Dou_Dou_/article/details/121240714.


⭐️ ⭐️

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐