OpenGL(二)-核心模式下的三角形

在上篇blog中已经在立即渲染模式下画了一个三角形了,这篇讲解一下核心模式下一个三角形的渲染过程。
上篇blog中的glbegin搭配glend的流程,在OpenGL3.2中已经被弃用了,3.3以后推荐使用VBO+EBO+VAO的流程。

图形渲染管线

作用:将三维坐标经过一系列变换,生成一个二维坐标,这个二维坐标的值就是渲染的结果。
在这里插入图片描述
在这里插入图片描述
渲染管线的过程就如上图所示,不同的图形API可能每个阶段的任务和名称有所不同,但是整体流程都大同小异。
顶点数据中包含了很多数据,包括:顶点坐标,顶点颜色,顶点法线等。

  • 顶点着色器:处理顶点
  • 曲面细分着色器:4.0以后才有的功能,自动将一个曲面细分为很多三角形,在复杂地形构建中很实用
  • 几何着色器:处理一个图元,可以同时访问图元中所有的三角形的所有顶点
  • 光栅化:将3D连续的三角形进行格栅化,对应输出像素。连续的三角形变成离散的像素点数据
  • 片段着色器:片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。

数据的流动

我们使用显卡进行渲染,需要将数据由内存放到显存里面,OpenGL中使用 顶点缓冲对象(Vertex Buffer Objects, VBO) 管理这部分用于存放顶点数据的显存。但是VBO只是存储数据,OpenGL读取的数据的时候,需要知道数据的格式,数据存储的位置,每个顶点数据的大小等等。这步我们可以使用 顶点数组对象(Vertex Array Object, VAO) 来记录,后续当我们想使用这个VBO的数据时,先绑定一下对应的VAO,就可以知道数据的具体信息了。

相邻三角形的两个顶点是共用的,如果按照上面的存储方式三角形的共同顶点会被存储三次,这样会浪费很多空间,所以常见的存储方式是,将顶点单独存储起来,三角形中存储顶点索引,渲染的时候根据索引在顶点数据序列中找顶点数据。在OpenGL中,使用 元素缓冲对象(Element Buffer Object,EBO) 来实现这个功能,

其中蓝色部分,是要使用着色器语言编程的,OpenGL使用GLSL着色器语言。

渲染管线是软硬相耦合的,具体发展历史可以看这两篇blog:
GPU硬件发展
GPU软件发展

VBO & VAO & EBO

在这里插入图片描述

  • VBO:用于管理数据,数据存储在VBO中,按照顶点顺序存储,上图是每个顶点6个属性的情况。
  • VAO:因为VBO只负责存储,本质就是一个float数组,着色器从VBO读取数据,肯定是要知道数据排列结构的,例如一个顶点着色器需要两个输入pos和color,如何将VBO的数据输入着色器?这就需要VAO的帮助。VAO是在VBO上的一层封装,OpenGL绑定VAO,VAO绑定VBO,VAO还记录下VBO中的每个顶点的数据组成。还是上面那个例子,定义好VAO后就可以定义两个vertexAttribArray,定义好每个vertexAttribArray的输入layou,大小,顶点间的步长,开始位置在VBO中的偏移量。这样在着色器中根据layout就能够接收到对应数据。
  • EBO:用于索引数据,因为每个三角形存储三个顶点会浪费很多数据,所以多个三角形时,使用VBO存储顶点数据,EBO存储每个三角形对应顶点的索引。

总结就是,VBO负责存储数据,类似仓库。VAO负责解释数据给着色器,类似于销售。EBO负责通过索引来降低冗余存储,类似于额外的库管系统。可以没有EBO,但是不能没有VBO和VAO。

代码

使用VAO和VBO画三角形的代码
OpenGL代码:

#include <iostream>
#include "glad/glad.h" //管理OpenGL的函数指针的
#include "GLFW/glfw3.h"
#include "gl/GL.h"



void framebufferSizeCallback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
void rendering(unsigned int VAO, unsigned int shaderProgram);
unsigned int shaderProgramInit();

int main()
{
	glfwInit(); // 初始化GLFW
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); 
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);// 配置GLFW,OpenGL版本,Core核心模式
	
	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL); //创建窗口
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}

	glfwMakeContextCurrent(window); //将上下文设置为该窗口

	// 给GLAD传入用来加载系统相关的OpenGL函数指针地址的函数
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed initialize GLAD" << std::endl;
		return -1;
	}
	glViewport(0, 0, 800, 600); 
	//设置视口,即窗口中需要渲染的部分,前两个参数是左下角坐标,后两个是宽高
	//当用户改变窗口的大小的时候,视口也应该被调整,可以设置一个窗口回调函数,自动调整
	//将回调函数注册到GLFW
	glfwSetFramebufferSizeCallback(window, framebufferSizeCallback);

	//OpenGL的坐标系是-1到1之间,中心点在屏幕中心,与OpenCV和DX的不同。
	float vertices[] = {
		-0.5f, -0.5f, 0.0f,
		 0.5f, -0.5f, 0.0f,
		 0.0f,  0.5f, 0.0f
	};
	unsigned int shaderProgram = shader_program_init();
	

	unsigned int VBO;
	glGenBuffers(1, &VBO); //创建顶点缓冲buffer,在显存上分配一段空间,存储顶点数据

	unsigned int VAO;
	glGenVertexArrays(1, &VAO); //创建VAO,存储应该使用的VBO位置,以及该VBO中顶点数据的组成和分布
	glBindVertexArray(VAO);


	glBindBuffer(GL_ARRAY_BUFFER, VBO); //将这段内存绑定到顶点缓存上,GL_ARRAY_BUFFER表示缓存buffer的类型
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 
	// 传输数据,类似于cudaMemcpy作用,
	// 最后的GL_STATIC_DRAW是向显卡表示如何管理该段数据,
	// GL_STATIC_DRAW表示该段数据几乎不变化,GL_DYNAMIC_DRAW表示该段数据经常变换,GL_STREAM_DRAW表示该段数据每次渲染都会变换
	// 显卡会根据数据的特性选择如何管理数据,以达到最快速度

	//渲染程序已经搞定了,接下来要告诉程序如何从缓冲区获取并解析顶点数据
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
	// arg1表示顶点坐标信息在顶点数据中的哪个位置,arg2表示数据格式,arg3决定是数据否需要归一化,arg4表示每个顶点数据的大小,作为step来判断取下一个顶点数据
	// arg5表示顶点数据在缓冲区的那个位置,因为在缓冲区开头位置,所以直接为0
	
	//一般不需要主动解绑VAO和VBO,当我们再次绑定其他VAO,VBO时,会自动把旧的解绑换新的。

	//当我们要渲染一个物体的时候
	glEnableVertexAttribArray(0);// 开启顶点渲染功能

	while (!glfwWindowShouldClose(window)) //渲染主循环
	{
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //使用颜色清除buffer
		glClear(GL_COLOR_BUFFER_BIT);

		processInput(window); //处理输入事件
		rendering(VAO, shaderProgram);
		glfwSwapBuffers(window); //交换渲染buffer,将glfw窗口显示到屏幕。双缓冲buffer,之前龚大大视频讲过
		glfwPollEvents(); //检查有没有触发什么事件,调用相应的回调函数
	}

	glfwTerminate(); //结束glfw
	return 0;

}

void framebufferSizeCallback(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window)
{
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);
}

void rendering(unsigned int VAO,unsigned int shaderProgram)
{
	
	glBindVertexArray(VAO); //告诉显卡,用这个VAO
	glUseProgram(shaderProgram); //告诉显卡,使用这个着色器
	glDrawArrays(GL_TRIANGLES, 0, 3);
}

unsigned int shaderProgramInit()
{
	const char* vertexShaderSource = "#version 330 core\n"
		"layout (location = 0) in vec3 aPos;\n" //告诉顶点坐标信息在哪里,position=0,也就是一开始就是坐标信息
		"void main()\n"
		"{\n"
		"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
		"}\0"; // GLSL代码,先存储到一个字符串中,后续多了可以存到文件里

	unsigned int vertexShader; //着色器ID
	vertexShader = glCreateShader(GL_VERTEX_SHADER); //创建VERTEX_SHADER类型的shader,返回shader的ID
	glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); //将着色器源代码绑定到shader
	glCompileShader(vertexShader); //编译shader
	int  success;
	char infoLog[512];
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); //收集着色器语言编译的结果状态
	if (!success)
	{
		glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
	}

	const char* fragmentShaderSource = "#version 330 core \n"
		"out vec4 FragColor;\n" //声明输出变量FragColor,类型为vec4
		"void main()\n"
		"{FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);}\0";
	unsigned int fragmentShader;
	fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); //创建片段着色器
	glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
	glCompileShader(fragmentShader);
	glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
	}

	unsigned int shaderProgram;
	shaderProgram = glCreateProgram(); //创建着色程序,可以理解为前面都是在编译目标文件,这一步是将所有目标文件链接成一个可执行文件
	glAttachShader(shaderProgram, vertexShader);
	glAttachShader(shaderProgram, fragmentShader); //添加shader,着色器程序会自动把上一个shader的输出作为下一个shader的输入,如果输出输入格式不匹配,就会报错
	glLinkProgram(shaderProgram);
	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
	if (!success) {
		glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n" << infoLog << std::endl;
	}

	glDeleteShader(vertexShader);
	glDeleteShader(fragmentShader); //清理掉之前的shader,已经不需要了
	
	return shaderProgram;
}

使用VAO+VBO+EBO画矩形的代码:


int main()
{
	// 将数据准备阶段换掉
	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,2,3)就是顶点数组vertices的下标,
		// 这样可以由下标代表顶点组合成矩形

		0, 1, 3, // 第一个三角形
		1, 2, 3  // 第二个三角形
	};

	unsigned int shaderProgram = shaderProgramInit();

	unsigned int VBO;
	glGenBuffers(1, &VBO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	unsigned int VAO;
	glGenVertexArrays(1, &VAO); //创建VAO,存储应该使用的VBO位置,以及该VBO中顶点数据的组成和分布
	glBindVertexArray(VAO);

	unsigned int EBO;
	glGenBuffers(1, &EBO);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

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

	glEnableVertexAttribArray(0);
}

void rectangleRendering(unsigned int VAO, unsigned int shaderProgram)
{

	glUseProgram(shaderProgram);
	glBindVertexArray(VAO);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
	// 跟glDrawArrays基本一样,arg1表示模式,arg2表示绘画点数,arg3表示数据类型,arg4表示索引偏移量
}

参考

LearnOpenGL
LearnOpenGL CN
《计算机图形学编程(使用OpenGL和C++)第二版》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值