C++ OpenGL学习——创建一个三角形

本文参考
opengl学习网站
opengl学习网站中文版
对应教程 OpenGL自製3D遊戲引擎
1、专业名称
顶点数组对象:Vertex Array Object,VAO
顶点缓冲对象:Vertex Buffer Object,VBO
索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO

2、原理
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D的,故OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(graphics pipeline)管理的

3、图形渲染管线可以被划分为两个主要部分:
第一部分把你的3D坐标转换为2D坐标
第二部分是把2D坐标转变为实际的有颜色的像素

4、图形渲染管线工作流程

在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)

(1)顶点输入,并把顶点数据储存在显卡的内存中,用VBO这个顶点缓冲对象管理

float vertices[] = {
    
        //定义顶点数据
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

//创建顶点缓冲对象(Vertex Buffer Objects, VBO),用于管理内存(储存顶点数据的内存)
unsigned int VBO;   
glGenBuffers(1, &VBO);

//使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
glBindBuffer(GL_ARRAY_BUFFER, VBO); 

//调用glBufferData函数,把之前定义的顶点数据复制到缓冲的内存中
//glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:顶点缓冲
//对象当前绑定到GL_ARRAY_BUFFER目标上。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶
//点数据大小就行。第三个参数是我们希望发送的实际数据。
//第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
/*
    GL_STATIC_DRAW :数据不会或几乎不会改变。
    GL_DYNAMIC_DRAW:数据会被改变很多。
    GL_STREAM_DRAW :数据每次绘制时都会改变
*/

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

(2)用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,顶点着色器把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。

#version 330 core                   //GLSL版本号
layout (location = 0) in vec3 aPos; //在顶点着色器中声明所有的输入顶点属性

void main()
{
    
    
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);    //把位置数据赋值给预定义的gl_Position变量,最后,gl_Position设置的值会成为该顶点着色器的输出
}

(3)将上一步代码储存到一个字符串当中,然后编译这个着色器

//创建着色器对象
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

//glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,即刚才的字符串
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

//检测编译是否成功
//定义一个整型变量来表示是否成功编译,还定义了一个储存错误消息(如果有的话)的容器。
int  success;
char infoLog[512];
//用glGetShaderiv检查是否编译成功
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
//如果编译失败,用glGetShaderInfoLog获取错误消息,然后打印它。
if(!success)
{
    
    
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

(4)定义片段着色器并进行编译,用做计算像素最后的颜色输出

#version 330 core
out vec4 FragColor; #用out关键字声明输出变量

void main()
{
    
    
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);  #最后会返回FragColor
} 

编译片段着色器的过程与顶点着色器类似

//定义着色器
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//编译着色器
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

(5)把两个着色器对象链接到一个用来渲染的着色器程序(Shader Program)中

//创建一个着色器程序对象
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
//把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

//检测是否链接失败
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
    
    
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    std::cout << "ERROR::LINK_FAILED\n" << infoLog << std::endl;
}

//激活程序对象
glUseProgram(shaderProgram);

//在把着色器对象链接到程序对象后,删除着色器对象,已经不再需要它们
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

通过(1)~(4)我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点着色器和片段着色器中处理它

(5)链接顶点属性,解析顶点数据

//使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

glVertexAttribPointer函数的参数
第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec都是由浮点数值组成的)。
第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
最后一个参数的类型是void
,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。

使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。

(6)在OpenGL中绘制一个物体

//  复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();

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

//创建VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);

// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 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();

(8)绘制三角形

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

最后源代码:

// 创建第一个窗口
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);


const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

//顶点数据
float vertices[] = {
    
    
	-0.5f, -0.5f, 0.0f,
	 0.5f, -0.5f, 0.0f,
	 0.0f,  0.5f, 0.0f
};

//顶点着色器数据(版本号330,in代表输入,layout(location = 0)设置输入变量的位置值)
const char* vertexShaderSource =
"#version 330 core \n"
"layout (location = 0) in vec3 aPos; \n"
"void main(){\n"
"	gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0);}";

//创建片段着色器(out代表输出)
const char* fragmentShaderSource =
"#version 330 core \n"
"out vec4 FragColor;\n"
"void main(){\n"
"	FragColor = vec4(1.0f,0.5f,0.2f,1.0f);}";

int main()
{
    
    
	//初始化GLFW
	glfwInit();

	//配置GLFW,glfwWindowHint的第一个参数表示要配置哪个选项,第二个参数是要设置的该配置选项的值
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	//在Mac OS X上需要添加glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);的初始化代码。
#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

	// 创建宽800,高600,名称为LearnOpenGL的窗口
	GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
	if (window == NULL)
	{
    
    
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);

	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	// 初始化glad
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
    
    
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}


	//创建顶点着色器
	unsigned int vertexShader;
	vertexShader = glCreateShader(GL_VERTEX_SHADER);

	//绑定着色器源码到顶点着色器对象
	glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);

	//编译顶点着色器
	glCompileShader(vertexShader);

	//判断顶点着色器编译是否成功
	int success;
	char infoLog[512];
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
    
    
		glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADE::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
	}

	//创建片段着色器
	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::SHADE::FRAGMENT::COMPILATION_FAILD\n" << infoLog << std::endl;
	}


	//创建程序对象
	unsigned int shaderProgram;
	shaderProgram = glCreateProgram();

	//绑定着色器到程序对象上
	glAttachShader(shaderProgram, vertexShader);
	glAttachShader(shaderProgram, fragmentShader);

	//链接两个着色器
	glLinkProgram(shaderProgram);

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

	//激活程序对象
	glUseProgram(shaderProgram);

	//删除着色器
	glDeleteShader(vertexShader);
	glDeleteShader(fragmentShader);




	//创建顶点缓冲对象
	unsigned int VBO;
	glGenBuffers(1, &VBO);

	//创建顶点数组对象
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);

	//绑定VAO
	glBindVertexArray(VAO);

	//将VBD绑定到GL_ARRAY_BUFFER上
	glBindBuffer(GL_ARRAY_BUFFER, VBO);

	//加载顶点数据到VBO
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	/*
	解析顶点数据的方法:
		第一个参数指定我们要配置的顶点属性,0即代表layout(location = 0)中的0,即为顶点的位置值
		第二个参数指定顶点属性的大小,vec3即为3
		第三个参数指定数据的类型,这里是GL_FLOAT
		第四个参数为是否希望数据被标准化,如果设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间
		第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔
		最后一个参数的类型是void*,所以需要进行强制类型转换,它表示位置数据在缓冲中起始位置的偏移量
	*/
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);



	// 让窗口一直渲染
	while (!glfwWindowShouldClose(window))
	{
    
    
		//输入
		processInput(window);

		// 渲染

		//使用glClearColor指定了清除屏幕的颜色。每当我们调用glClear和clear颜色缓冲区时,整个颜色缓冲区将填充glClearColor配置的颜色即蓝绿色。
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		//准备开始绘制
		//1、使用程序对象
		glUseProgram(shaderProgram);
		//2、绑定配置好的VAO
		glBindVertexArray(VAO);
		//3、绘制三角形,逆时针绘制,0号开始,总共3个点
		glDrawArrays(GL_TRIANGLES, 0, 3);


		//glfwSwapBuffers将交换颜色缓冲区(一个大型2D缓冲区,它包含GLFW窗口中每个像素的颜色值),这个颜色缓冲区在这个渲染迭代期间用于渲染,并将其作为输出显示在屏幕上
		glfwSwapBuffers(window);

		//检查是否触发了任何事件(如键盘输入或鼠标移动事件),更新窗口状态,并调用相应的函数(可以通过回调方法注册这些函数)。
		glfwPollEvents();
	}

	// 清除所有分配的GLFW资源.
	glfwTerminate();
	return 0;
}

//  查询GLFW是否按下/释放了相关键并做出相应的反应
void processInput(GLFWwindow *window)
{
    
    
	//检查用户是否按了esc键(如果没有按,glfwGetKey返回GLFW_RELEASE)。如果用户确实按了esc键,那么我们通过使用glfwSetwindowShouldClose将其WindowShouldClose属性设置为true来关闭GLFW。主while循环的下一个条件检查将失败,应用程序将关闭。
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);
}


//窗口大小改变时,调用该函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    
    
	// 确保视窗与新的视窗尺寸相匹配
	glViewport(0, 0, width, height);
}

输出结果:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/peixin_huang/article/details/104394812