LearnOpenGL学习笔记:第一个三角形

( 本文对应学习章节:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/ )

0.前言

本章节主要介绍了VAO/VBO以及顶点着色器/片段着色器,并绘制了一个三角形。

1.重要的东西

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline)管理的。图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。

LearnOpenGL学习笔记:第一个三角形_第1张图片

由于我这里只是应用型的学习,就不深入讲解(好吧,毕竟我也还没学会),具体的渲染流程请参考前言中的链接。对于应用,主要关注顶点着色器(Vertex Shader)片段着色器(Fragment Shader)就行了。而且,在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。顶点着色器可用于传统的基于顶点操作,例如通过矩阵变换位置、计算照明方程式以生成逐顶点的颜色以及生成或者变换纹理坐标。片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。着色器需要用着色器语言GLSL(OpenGL Shading Language)编写,语法类似C语言,这个是后面章节的内容。

另外一个重点是VAO(顶点数组对象:Vertex Array Object)VBO(顶点缓冲对象:Vertex Buffer Object)。VAO是一个对象,其中包含一个或者更多的Vertex Buffer Objects。而VBO是Graphics Card中的一个内存缓冲区,用来保存顶点信息,颜色信息,法线信息,纹理坐标信息和索引信息等等。一个VAO有多个VBO,它们之间通过上下文关联,只有唯一的激活VAO,在VAO后配置的VBO都属于该VAO(要在配置顶点属性glVertexAttribPointer之前,而glEnableVertexAttribArray在glVertexAttribPointer前后调用都可以)。不过有点没懂的是一开始我把VBO配置完了才去创建VAO,也是正常的。

还有就是顶点中的坐标,使用的标准化设备坐标(Normalized Device Coordinates, NDC)。与通常的屏幕坐标不同,y轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。

LearnOpenGL学习笔记:第一个三角形_第2张图片

还有就是顶点着色器中的 layout(location = 0) 这个输入偏移怎么来的,学到后半部分才看到是使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据。每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。

(对于VAO/VBO或者着色器的操作流程还有疑问可以参考教程的参考http://antongerdelan.net/opengl/hellotriangle.html)

2.第一个三角形

LearnOpenGL学习笔记:第一个三角形_第3张图片

代码中着色器应用流程:

  1. glCreateShader  创建着色器对象
  2. glShaderSource  把着色器代码附加到着色器对象上
  3. glCompileShader  编译着色器
  4. glGetShaderiv  检测是否编译成功
  5. glCreateProgram  创建程序对象
  6. glAttachShader  把顶点和片段着色器附加到程序对象
  7. glLinkProgram  链接程序对象
  8. glGetProgramiv  检测是否链接成功
  9. glDeleteShader  删除着色器对象(附加到了程序对象,后面不需要了)
  10. glUseProgram  每次渲染时使用,使程序对象作为当前渲染状态的一部分

代码中VAO/VBO应用流程: 

  1. glGenVertexArrays  创建VAO
  2. glGenBuffers  创建VBO
  3. glBindVertexArray  绑定VAO
  4. glBindBuffer  绑定VBO
  5. glBufferData  把定义的顶点数据复制到缓冲的内存中
  6. glVertexAttribPointer  定义通用顶点属性数据的数组
  7. glEnableVertexAttribArray  使能顶点属性数组
  8. glBindVertexArray  每次渲染时使用,绑定一个顶点数组对象
  9. glDrawArrays  每次渲染时使用,绘制图元
  10. glDeleteVertexArrays  退出时删除VAO
  11. glDeleteBuffers  退出时删除VBO

从这些接口有点理解了OpenGL是以状态机来实现的。

#include 

#include 
#include 

void error_callback(int error, const char* description); //声明
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods);
void bufferresize_callback(GLFWwindow* window, int width, int height);

//顶点着色器程序,着色器语言GLSL下一节才学
//为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量
//layout对应glVertexAttribPointer中的参数1
static const char* vertex_code = R"(
#version 330 core
layout(location=0) in vec3 aPos;

void main()
{
	gl_Position = vec4(aPos,1.0f);
}
)";

//片段着色器代码
//片段着色器所做的是计算像素最后的颜色输出
static const char* frag_code = R"(
#version 330 core
out vec4 FragColor;

void main()
{
	FragColor = vec4(0.1f,1.0f,0.1f,1.0f);
}
)";

int main()
{
	if (!glfwInit()) {
		return -1; //初始化GLFW库失败
	}
	//注册错误回调,大多数事件都是通过回调报告的
	glfwSetErrorCallback(error_callback);
	//设置最低主版本号
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	//设置最低次版本号
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	//设置为核心模式
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); //mac系统加这一句
	//创建窗口对象,如果要销毁窗口则glfwDestroyWindow(window)
	GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
	//创建失败则返回NULL,退出程序
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	//窗口位置,试了下不包含标题栏
	glfwSetWindowPos(window, 100, 100);
	//注册按键按下的回调
	glfwSetKeyCallback(window, key_callback);
	//注册帧缓冲区大小改变的回调
	glfwSetFramebufferSizeCallback(window, bufferresize_callback);
	//通知GLFW将我们窗口的上下文设置为当前线程的主上下文
	//必须先具有当前的OpenGL上下文,然后才能使用OpenGL API,加载程序需要当前上下文才能加载
	glfwMakeContextCurrent(window);

	//在调用任何OpenGL的函数之前我们需要初始化GLAD
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		glfwTerminate();
		return -1;
	}

	//创建顶点着色器对象
	//GLuint glCreateShader(GLenum shaderType​);
	//参数为着色器类型
	//GL_VERTEX_SHADER:顶点着色器
	//GL_FRAGMENT_SHADER:片段着色器	
	unsigned int vertexShader;
	vertexShader = glCreateShader(GL_VERTEX_SHADER);

	//把着色器代码附加到着色器对象上
	//void glShaderSource(GLuint shader​, GLsizei count​, const GLchar **string​, const GLint *length​);
	//参数1着色器对象,参数2指定字符串和长度数组中的元素数
	//参数3指定指向要加载到着色器的源码字符串的指针数组(二级指针)
	//参数4指定字符串长度的数组(暂时还没明白第四个参数)
	glShaderSource(vertexShader, 1, &vertex_code, NULL);
	//编译着色器
	glCompileShader(vertexShader);

	//检测是否编译成功
	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" << infoLog << std::endl;
	}

	//创建片段着色器对象 
	unsigned int fragmentShader;
	fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);

	//把着色器代码附加到着色器对象上,并编译
	glShaderSource(fragmentShader, 1, &frag_code, NULL);
	glCompileShader(fragmentShader);

	//检测是否编译成功
	glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED" << infoLog << std::endl;
	}

	//现在两个着色器都编译了
	//接下来把两个着色器对象连接到一个用来渲染的着色器程序中

	//着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本
	//创建程序对象
	//GLuint glCreateProgram(void​);
	//glCreateProgram创建一个空的程序对象,并返回一个非零值,该值可以被引用
	unsigned int shaderProgram;
	shaderProgram = glCreateProgram();

	//把之前编译的着色器附加到程序对象上
	//void glAttachShader(GLuint program​, GLuint shader​);
	//参数1程序对象,参数2待附加的着色器对象
	glAttachShader(shaderProgram, vertexShader);
	glAttachShader(shaderProgram, fragmentShader);

	//链接程序对象
	//void glLinkProgram(GLuint program​);
	//参数为程序对象
	//成功链接程序对象后,可以通过调用glUseProgram使该程序对象成为当前状态的一部分
	glLinkProgram(shaderProgram);

	//检测是否链接成功
	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
	if (!success) {
		glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED" << infoLog << std::endl;
	}

	//把着色器对象链接到程序对象以后,
	//记得删除着色器对象,我们不再需要它们了
	glDeleteShader(vertexShader);
	glDeleteShader(fragmentShader);


	//VAO是一个对象,其中包含一个或者更多的Vertex Buffer Objects。
	//而VBO是Graphics Card中的一个内存缓冲区,用来保存顶点信息,颜色信息,法线信息,纹理坐标信息和索引信息等等。
	//一个VAO有多个VBO,它们之间也是通过上下文,只有唯一的激活VAO,在VAO后创建的VBO都属于该VAO。
	//关联VBO数据用取得当前激活的缓存区对象偏移来指定。

	//开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据
	//我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置
	float vertices[] = {
	-0.5f, -0.5f, 0.0f,
	 0.5f, -0.5f, 0.0f,
	 0.0f,  0.5f, 0.0f
	};


	unsigned int VAO;
	//生成顶点数组对象名称
	//void glGenVertexArrays(GLsizei n​, GLuint *arrays​);
	//参数1为生成名称数量,参数2为顶点数组 数组
	glGenVertexArrays(1, &VAO);

	unsigned int VBO;
	//生成缓冲区对象名称
	//void glGenBuffers(GLsizei n​, GLuint * buffers​);
	//参数1为生成名称数量,参数2为缓冲区数组
	glGenBuffers(1, &VBO);

	//先绑定顶点数组对象,然后绑定并设置顶点缓冲区,然后配置顶点属性
	//绑定一个命名的顶点数组对象
	//void glBindVertexArray(GLuint array​);
	//参数为顶点数组的名称
	glBindVertexArray(VAO);

	//OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER
	//OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。
	//绑定一个命名的缓冲区对象:
	//void glBindBuffer(GLenum target​, GLuint buffer​);
	//参数1缓冲对象类型,参数2缓冲对象名称
	glBindBuffer(GL_ARRAY_BUFFER, VBO);

	//接下来把之前定义的顶点数据复制到缓冲的内存中
	//glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数
	//void glBufferData(GLenum target​, GLsizeiptr size​, const GLvoid * data​, GLenum usage​);
	//参数1缓冲对象类型,参数2数据字节大小,参数3数据指针,没数据则为NULL
	//参数4指定了我们希望显卡如何管理给定的数据,他有三种形式:
	//GL_STATIC_DRAW/READ/COPY :数据不会或几乎不会改变。
	//GL_DYNAMIC_DRAW/READ/COPY:数据会被改变很多。
	//GL_STREAM_DRAW/READ/COPY :数据每次绘制时都会改变。	
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	//定义通用顶点属性数据的数组
	//void glVertexAttribPointer(GLuint index​, GLint size​, GLenum type​, 
    //    GLboolean normalized​, GLsizei stride​, const GLvoid * pointer​);
	//参数1指定要配置的通用顶点属性的索引,对应顶点着色器中的(layout(location = 0))
	//参数2指定顶点属性的大小,1-4,这里顶点属性是vec3,所以填3
	//参数3指定数据类型
	//参数4定义我们是否希望数据被标准化,为true则数据被归一化0-1
	//参数5为字节步长,告诉我们在连续的顶点属性组之间的间隔
	//由于下个组位于3个float之后,所以置为3 * sizeof(float)
	//参数6表示位置数据在缓冲中起始位置的偏移量(Offset)
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
	//使能顶点属性数组
	//void glEnableVertexAttribArray(GLuint index​);
	//参数为顶点属性的索引
	glEnableVertexAttribArray(0);

	//对glVertexAttribPointer的调用将VBO注册为顶点属性的绑定顶点缓冲区对象,
	//这样之后我们可以安全地解除绑定
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	//可以解绑VAO,这样其他VAO调用就不会以外地修改这个VAO
	glBindVertexArray(0);
	
	//设置清空屏幕所用的颜色
	glClearColor(0.2f, 0.3f, 0.3f, 1.0f);

	//检查GLFW是否被要求退出
	while (!glfwWindowShouldClose(window))
	{		
		//清空颜色缓冲
		glClear(GL_COLOR_BUFFER_BIT);

		//安装所指定的程序对象程序作为当前再现状态的一部分
		//void glUseProgram(GLuint program​);
		glUseProgram(shaderProgram);

		//绑定一个顶点数组对象
		//void glBindVertexArray(GLuint array​);
		//参数为要绑定的顶点数组的名称
		glBindVertexArray(VAO); 

		//使用当前激活的着色器和顶点属性配置和VBO(通过VAO间接绑定)来绘制图元
		//void glDrawArrays(GLenum mode​, GLint first​, GLsizei count​);
		//参数1为图元类型
		//参数2指定顶点数组的起始索引
		//参数3指定顶点个数
		glDrawArrays(GL_TRIANGLES, 0, 3);

		//交换颜色缓冲
		//默认情况下,GLFW窗口使用双缓冲。这意味着每个窗口都有两个渲染缓冲区。
		//前缓冲区和后缓冲区。前缓冲区是要显示的缓冲区,后缓冲区是要渲染的缓冲区。
		//渲染完整个帧后,需要相互交换缓冲区,因此后缓冲区将变为前缓冲区,反之亦然。
		glfwSwapBuffers(window);
		//检查有没有触发什么事件(比如键盘输入、鼠标移动等)
		//并调用对应的回调函数(可以通过回调方法手动设置)
		glfwPollEvents();
	}

	//删除顶点数组对象
	//void glDeleteVertexArrays(GLsizei n​, const GLuint *arrays​);
	//参数1为数量,参数2为顶点数组 数组
	glDeleteVertexArrays(1, &VAO);
	//删除命名的缓冲区对象
	//void glDeleteBuffers(GLsizei n​, const GLuint * buffers​);
	//参数1为数量,参数2为缓冲区数组
	glDeleteBuffers(1, &VBO);

	//销毁窗口
	glfwDestroyWindow(window);
	//使用GLFW的操作后,需要终止GLFW
	glfwTerminate();
	return 0;
}

void error_callback(int error, const char* description)
{
	std::cout << "error:" << error << std::endl;
	std::cout << "description:" << description << std::endl;
}

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
	//Esc按下则触发WindowShouldClose为true
	if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
		glfwSetWindowShouldClose(window, GLFW_TRUE);
}

void bufferresize_callback(GLFWwindow* window, int width, int height)
{
	//glViewport函数前两个参数控制渲染窗口左下角的位置
	glViewport(0, 0, width, height);
}

教程三角形在这里:https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/2.1.hello_triangle/hello_triangle.cpp 

3.索引缓冲对象

索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。

假设我们不再绘制一个三角形而是绘制一个矩形,可以使用两个三角形来组成一个矩形,但是OpenGL主要处理三角形,这就需要六个点:

LearnOpenGL学习笔记:第一个三角形_第4张图片

而矩形本身只有四个点,六个太多余了,这时候就需要用到EBO了:

LearnOpenGL学习笔记:第一个三角形_第5张图片

(内容详情参见教程这一节)

索引缓冲对象和顶点缓冲对象的创建和配置类似,只是渲染时用glDrawElements来替换glDrawArrays函数,来指明我们从索引缓冲渲染。

LearnOpenGL学习笔记:第一个三角形_第6张图片

我的代码在这里(first_ebo.cpp):https://github.com/gongjianbo/LearnTheOpenGL.git

教程EBO代码在这里:https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/2.2.hello_triangle_indexed/hello_triangle_indexed.cpp

4.参考

LearnOpenGL:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/

(后面的附加链接和练习一定要看)

OpenGL API:https://www.khronos.org/opengl/wiki/GLAPI/

着色器概念:https://www.jianshu.com/p/16271a827095

博客(VAO VBO的绑定):https://blog.csdn.net/Blues1021/article/details/51460498

你可能感兴趣的:(OpenGL)