OpenGL入门教程之 深入三角形

news2024/11/18 11:26:24

一、引言

 本教程使用GLEWGLFW库。
 通过本教程,你能轻松的、深入的理解OpenGL如何绘制一个三角形
 如果你不了解OpenGL是什么,可以阅读OpenGL深入理解。

二、基本函数和语句介绍

 通过阅读以下的函数,你的大脑里能留下关于OpenGL基本函数的一个大致印象。(不需深入理解,可忽略此节)

  • glfwInit ()
     初始化glfw,为一些空指针赋值 。
  • glfwWindowHint(name, value)
     设置glfw库的一些属性,name为属性字段,value为设置的值。
     GLFW_CONTEXT_VERSION_MAJOR指OpenGL主版本号。
     GLFW_CONTEXT_VERSION_MINOR指OpenGL次版本号。
     GLFW_OPENGL_PROFILE指渲染模式, GLFW_OPENGL_CORE_PROFILE为核心模式。
     GLFW_RESIZABLE表示窗口可调性, GL_FALSE代表不可调。
  • glfwCreateWindow(width, height, title, NULL, NULL)
     创建一个glfw窗口,参数为:宽、高、窗口标题。返GLFWwindow* 类型。
  • glfwMakeContextCurrent(window)
     通知GLFW将窗口window的上下文设置为当前线程的主上下文。
  • glfwSetKeyCallback(window, callback)
     通过GLFW注册函数至合适的回调,callback为函数名。
  • glewExperimental = GL_TRUE
     让GLEW在管理OpenGL的函数指针时更多地使用现代化的技术,如果把它设置为GL_FALSE的话可能会在使用OpenGL的核心模式时出现一些问题。
  • glewInit()
     初始化glew。
  • glfwGetFramebufferSize(window, &width, &height);
     获取窗口window的大小width和height。
  • glViewport(x, y, width, height)
     设置视口位置,x、y表示视口左下角在窗口中的位置,width和height表示视口大小。
  • glCreateShader(shader)
     创建一个shader着色器,返回shader的引用。
     shader值有GL_VERTEX_SHADER和GL_FRAGMENT_SHADER。
  • glShaderSource(shader, n, &shaderSource, NULL)
     绑定shader着色器的源码,n为源代码中字符串数量,shaderSource为源代码字符串。
  • glCompileShader(shader)
     编译shader着色器。
  • glGetShaderiv(shader, GL_COMPILE_STATUS, &success)
     检查着色器shader编译是否成功编译,success为GLint类型。
  • glGetShaderInfoLog(shader, length, NULL, infoLog)
     获取错误信息并存储与infoLog字符数组中,length为数组长度。
  • glCreateProgram()
     创建一个着色器程序,返回其引用。
  • glAttachShader(shaderProgram, shader)
     将着色器shader添加到着色器程序shaderProgram中。
  • glLinkProgram(shaderProgram)
     链接着色器程序。
  • glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success)
     检测链接着色器程序是否成功。
  • glGetProgramInfoLog(shaderProgram, length, NULL, infoLog)
     获取链接着色器程序
  • glUseProgram(shaderProgram)
     激活着色器程序,激活后每个着色器调用和渲染调用都会使用这个程序对象。
  • glDeleteShader(shader)
     删除着色器对象。当把着色器对象链接到程序对象后,要删除着色器对象。
  • glGenVertexArrays(n, &VAO)
     返回一个顶点数组对象(Vertex Array Object, VAO)的引用。同样当n≥2时,返回的是一个引用的数组。
  • glGenBuffers(1, &VBO)
     返回一个顶点缓冲对象(Vertex Buffer Objects, VBO)的引用。同样当n≥2时,返回的是一个引用的数组。
  • glBindVertexArray(VAO)
     绑定VAO。
  • glBindBuffer(GL_ARRAY_BUFFER, VBO)
     绑定VBO。
  • glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW)
     专门用来把用户定义的数据复制到当前绑定缓冲的函数。
     第一个参数是目标缓冲的类型,第二个参数指定传输数据的大小(以字节为单位),第三个参数是我们希望发送的实际数据,第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
     GL_STATIC_DRAW : 数据不会或几乎不会改变。
     GL_DYNAMIC_DRAW:数据会被改变很多。
     GL_STREAM_DRAW : 数据每次绘制时都会改变。
  • glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0)
     进行顶点缓冲对象与顶点属性的链接。
  • glEnableVertexAttribArray(0)
     以顶点属性位置值作为参数,启用顶点属性(顶点属性默认是禁用的)。
  • glfwWindowShouldClose(window)
     检查GLFW是否被要求退出。
  • glfwPollEvents()
     检查事件
  • glClearColor(0.2f, 0.3f, 0.3f, 1.0f)
     设置清空屏幕所用的颜色。
  • glClear(GL_COLOR_BUFFER_BIT)
     清空屏幕的颜色缓冲。
  • glfwSwapBuffers(window)
     交换窗口的缓冲区。
  • glDeleteVertexArrays(1, &VAO)
     删除对应的VAO对象。
  • glDeleteBuffers(1, &VBO)
     删除对于的VBO对象。
  • glfwTerminate()
     释放GLFW分配的内存

三、重要语句模块

OpenGL渲染程序的大体框架是固定的,在本节你将会学习到一个个完整且独立的模块
 当我们学习完这些基本模块后,我们把模块拼接起来即可构成一个完整的OpenGL的程序。
 如果你对于文章整体脉络感到困惑,可以参考文章目录。
 如果你对片段化的模块代码感到头疼,可以往下阅读,在每个阶段我会展现完整的代码。
 如果在你读到模块代码时感到看困惑,请不要停止阅读,因为模块代码后会有小节专门解释。

1 GLFW和GLEW头文件的引用

宏定义表示glew是静态链接的

// GLEW的导入
#define GLEW_STATIC
#include <GL/glew.h>

// GLFW的导入
#include <GLFW/glfw3.h>

OpenGL中的基元类型

 使用OpenGL时,建议使用OpenGL定义的基元类型比如使用float时我们加上前缀GL(因此写作GLfloat)。int、uint、char、bool等等也类似。OpenGL定义的这些GL基元类型的内存布局是与平台无关的,而int等基元类型在不同操作系统上可能有不同的内存布局。使用GL基元类型可以保证你的程序在不同的平台上工作一致。

2 GLFW的初始化和配置

2.1 模块代码

 	// 初始化 GLFW
    glfwInit();
    
    // 设置GLFW的使用 OpenGL的版本 为3.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	// 设置GLFW使用OpenGL的 核心模型
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    // 设置GLFW窗口大小的可调整性为:否
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

    // 创建一个宽为WIDTH、高为HEIGHT、标题为"LearnOpenGL"的窗口
    GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
    // 将窗口window的上下文设置为当前线程的主上下文
    glfwMakeContextCurrent(window);

2.2 运行结果和完整代码


 当你正确导入GLEW和GLFW,并对GLFW进行初始化和配置后,运行程序你将会看到如下图的一个纯白色GLFW窗口。在这里插入图片描述

 到此处的完整代码为:

#define GLEW_STATIC
#include <GL/glew.h>

#include <GLFW/glfw3.h>

const GLuint WIDTH = 800, HEIGHT = 600;

int main()
{
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);
	glfwWindowHint(GLFW_RESIZABLE,GL_FALSE);

	GLFWwindow* window = glfwCreateWindow(WIDTH,HEIGHT,"LearnOpenGL",NULL,NULL);
	if (window == NULL)
	{
		// 当window为NULL时说明GLFW窗口创建失败
		std::cout << "Failed to create GLFW window" << std::endl;
		// 窗口创建失败则释放GLFW分配的内存,结束程序返回-1表示错误。
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	// 循环避免程序结束、窗口关闭
	while (1);
}

3 GLEW的初始化和配置

	// 设置GLEW为现代化的
    glewExperimental = GL_TRUE;
    // 初始化GLEW
    glewInit();

4 视口配置

4.1 模块代码

    // 视口位置和大小的设置
    glViewport(x, y, width, height);

 到此处时如果你运行程序,就会发现只能看到和上文中一样的白窗口,你或许疑惑你的视口在哪?视口是展示渲染后画面的地方,之所以没有画面是因为你没有调用过OpenGL的渲染函数,渲染函数是一种状态应用函数,会基于OpenGL的状态做出对应的操作。

4.2 渲染函数介绍

 由于你实在是太想有一些画面了,所有现在"超纲"的介绍一个渲染函数:

glClear(GL_COLOR_BUFFER_BIT)

 其作用是使用特定颜色清空屏幕的颜色缓冲,其参数表示缓冲类型暂时不用深究,这是一个状态应用函数,会基于OpenGL的状态做相应的渲染操作。你可能觉得情况屏幕变成全白就行了,为什么还要指定特定颜色?
 因为不是每一个程序窗口的背景都是白色,有些可能是暗黑色、蓝绿色,所以需要指定特定的背景颜色去"刷屏"。你可能又有疑问,你并没有指定特定的颜色呀?这是因为OpenGL的函数分为状态应用函数和状态设置函数,所谓特定颜色其实是指清空屏幕颜色缓存所使用的颜色,这种颜色是OpenGL的一个状态,所以我们应该使用状态设置函数去指定那种特定颜色。状态设置函数如下:

glClearColor(0.2f, 0.3f, 0.3f, 1.0f)

其作用是设置清空屏幕所用的特定颜色,即改变OpenGL的状态,四个参数是RGBA
 需要了解的是:现代屏幕画面的呈现使用双缓冲技术。如果当屏幕需要显示内容时才一个个像素的在屏幕上绘制,绘制速度不同步会导致屏幕画面的撕裂。解决方法是我们提前将需要呈现的完整画面生成出来,把即将要展现的一整幅画面存储于显卡的缓冲区中,当需要交换屏幕内容时,将缓冲区中完整的一幅画面展现在屏幕上,这就是双缓冲技术。在OpenGL中交换缓冲区这个步骤需要我们使用函数明确指定

glfwSwapBuffers(window)

 这是一个状态应用函数,它将window窗口的两个缓冲区进行交换。这两个缓冲区是指双缓冲技术中的前缓冲区和后缓冲区:前缓冲区用于展示屏幕上的内容,而后缓冲区就用来绘制,当每一帧开始的时候,将两个缓冲区交换,这样后缓冲区又可以画新的内容

4.3 运行结果和完整代码

 通过以上代码,可以得到下图的窗口内容:
在这里插入图片描述 上图对应代码如下:

// GLEW
#define GLEW_STATIC
#include <GL/glew.h>

// GLFW
#include <GLFW/glfw3.h>

#include <iostream>

const GLuint WIDTH = 800, HEIGHT = 600;

int main()
{
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);
	glfwWindowHint(GLFW_RESIZABLE,GL_FALSE);

	GLFWwindow* window = glfwCreateWindow(WIDTH,HEIGHT,"LearnOpenGL",NULL,NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);

	glewExperimental = true;
	glewInit();

	GLint width, height;
	glfwGetFramebufferSize(window, &width, &height);
	glViewport(0, 0, 100, 100);

	while (1)
	{
		// 指定清空屏幕颜色缓冲的颜色(改变状态后其会保存于OpenGL中)
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		// 清空屏幕颜色缓冲,这里生成的屏幕画面还存储于后缓冲区中
		glClear(GL_COLOR_BUFFER_BIT);
		// 交换前缓冲区和后缓冲区
		glfwSwapBuffers(window);
	}
	
	glfwTerminate();
	return 0;
}

4.4 拓展

1.glClear()是针对整个窗口而非视口的,如上代码中所示glViewport(0, 0, 100, 100)设置的视口大小非常小,但是清空屏幕颜色缓存的蓝绿色还是布满了整个窗口。
2.如注释所言,glClearColor会设置OpenGL的状态,状态保存于OpenGL中,或许你感觉设置一次就行了,但这是一个初学OpenGL者常见但错误的想法。当以后你的渲染代码越来越庞大,渲染情况越来越多,你可能会多次改变清空屏幕颜色缓冲所用的颜色,这时你应当在每一次循环(每一帧)中判断并使用所需颜色所以每一帧都需要设置渲染所需的各式各样的状态,这是必须要做的。
3.将如上代码中的"glClearColor(0.2f, 0.3f, 0.3f, 1.0f);"和"glClear(GL_COLOR_BUFFER_BIT);"删除,你会得到一个纯黑色的窗口。这很容易理解,因为没有配置过的后缓冲区中是一幅的纯黑画面,所以不断交换缓冲区使得前缓冲和后缓冲都是一幅纯黑的画面了。
4.将如上代码的整个循环替换为如下代码,你会得到一个闪烁的窗口.

	glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);
	while (1)
	{
		// 交换前缓冲区和后缓冲区
		glfwSwapBuffers(window);
	}

 这很容易理解。你通过"glClearColor(0.2f, 0.3f, 0.3f, 1.0f);"和"glClear(GL_COLOR_BUFFER_BIT);"将蓝绿色背景的一幅画面存储到后缓冲区中,而前缓存区中最初是没有内容的(无渲染指令所以呈现全白),当你第一次交换时,后缓冲区中的蓝绿色背景交换到前缓冲区中,而此时的后缓区中的内容没有配置(没有执行过清空屏幕颜色,没有写入后缓冲区)为默认的黑色因此后续每一帧缓冲区交换,蓝绿色和黑色会不断在屏幕上交替,导致屏幕的闪烁

5.将如上循环部分代码改为如下代码,你会发现窗口停止闪烁。

	glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);
	glfwSwapBuffers(window);
	glClear(GL_COLOR_BUFFER_BIT);
	while (1)
	{
		// 交换前缓冲区和后缓冲区
		glfwSwapBuffers(window);
	}

 这是因为前缓冲区在第一次交换后被替换为蓝绿色背景的,而第二次glClear将后缓冲区也替换为蓝绿色背景。而后在循环中交换前后缓冲区都是同一个颜色,所以不会出现闪烁。(这个故事告诉我们默认缓冲区中背景是黑色的当没有执行渲染函数时,前后缓冲区中的内容不会改变

5 Shader着色器的创建和编译

 OpenGL中我们可以编写的着色器有三个:
  顶点着色器 VetrtexShader
  片段着色器 FragmentShader
  几何着色器 GeometryShader
顶点着色器和片段着色器是我们必须要编写的,因为OpenGL中没有提供默认的这两类着色器,而几何着色器有默认的着色器提供

5.1 模块代码

	// 顶点着色器源代码
const GLchar* vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 position;\n"
    "void main()\n"
    "{\n"
    "gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
    "}\0";
    // 片段着色器源代码
const GLchar* fragmentShaderSource = "#version 330 core\n"
    "out vec4 color;\n"
    "void main()\n"
    "{\n"
    "color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
    "}\n\0";
    
    // 顶点着色器的创建和编译
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    
    // 顶点着色器编译状态检查
    GLint success;
    GLchar 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;
    }
    
    // 片段着色器的创建和编译
    GLuint 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;
    }

5.2 VertexShader着色器源代码

#version 330 core
layout(location = 0) in vec3 position;
void main()
{
	gl_Position = vec4(position.x, position.y, position.z, 1.0);
}

 一个最简单的顶点着色器代码如上所示,首先要声明着色器使用的OpenGL版本,330即对应OpenGL3.3版本
vec3 position代表定义一个有xyz分量的对象,in 关键字代表这个对象是输入到顶点着色器的,layout(location = 0)表示这个输入对象的位置标识
gl_Position指的是需要渲染的顶点的位置坐标,在顶点着色器中我们需要告诉它有哪些位置的顶点需要渲染,即给gl_Position赋值
顶点着色器的输入比较特殊,它从顶点数据中直接接收输入

5.3 FragmentShader着色器源代码

#version 330 core
out vec4 color;
void main()
{
	color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}

 一个最简单的片段着色器代码如上所示,片段着色器中同样要声明OpenGL版本
 片段着色器中定义了一个vec4类型的color对象,并用out关键字将它声明为输出变量。需要注意的是,片段着色器需要输出光栅化后每一个像素上的颜色值也就是说我们需要在片段着色器中定义一个RGBA四分量类型即vec4类型的对象,并使用out关键字将它声明为输出变量,并且在main函数中给它赋值。这里将输出的颜色值固定为一种颜色。

5.4 着色器的创建和编译

有了着色器源代码还不够,我们需要在程序中创建着色器对象,需要绑定着色器对象对应的源代码并编译。具体过程如下:

GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

 首先使用"glCreateShader"返回对应着色器对象的引用,这里传入的参数为"GL_VERTEX_SHADER",因此返回的是顶点着色器引用。如果要创建片段着色器,传入"GL_FRGEMENT_SHADER"即可。
 其次使用"glShaderSource"函数将着色器源代码vertexShaderSource绑定到着色器对象vertexShader上,其中参数1表示源代码仅包含一个字符串。
 最后使用"glCompileShader"函数编译着色器对象包含的着色器源代码。

5.5 着色器源码编译检测

 着色器的源代码并不是每次都能成功编译,原因是它可能出现各种各样的问题。由于着色器在GPU中编译,即使出错也不会显示在Vis Stdio中,我们需要手动获取着色器的编译状态。具体的过程如下:

	GLint success;
    GLchar 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;
    }

使用success记录编译是否成功,使用infoLog记录错误信息。"glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success)"会将着色器vertexShader编译成功与否存储在变量success中。如果编译错误,"glGetShaderInfoLog(vertexShader, 512, NULL, infoLog)"会将着色器vertexShader的编译错误信息存储于infoLog中。

6 着色器程序的创建

模块代码

	// 创建着色器程序
 	GLuint shaderProgram = glCreateProgram();
	
	// 添加顶点着色器和片段着色器到程序中
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    
    // 链接着色器程序中的着色器
    glLinkProgram(shaderProgram);
    
    // 检查程序链接状态
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
    	// 如果链接失败则获取错误信息存储于infoLog中
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    
	// 着色器程序中的着色器链接成功后就可以删除了,节省显存
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

 渲染流程包括一整套着色器,仅有几个我们自定义的着色器还不够。我们需要创建一个着色器程序,这个着色器程序创建后就包含许多我们不能自定义的着色器,我们将我们自定义的着色器加入这个着色器程序,然后链接着色器程序中的各个着色器,这样我们就得到了一个完整的着色器程序
 得到的结果就是一个程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象

glUseProgram(shaderProgram)

在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个着色器程序对象

7 顶点的输入

 在OpenGL中着色器是有顺序的执行的,每一个着色器把上一个着色器的输出当作输入。但在5.2中我们提到顶点着色器的输入比较特殊,它直接从顶点数据中读取输入
 那什么是顶点数据呢?顶点数据就是存储顶点各种各样信息的数据,它最初存在于内存之中,由于渲染时GPU中的顶点着色器需要顶点数据,所以我们应该把内存中的顶点数据传输到显存中。
 我们要做的事情是:将顶点数据发送给显卡配置OpenGL解析顶点数据
顶点着色器已经在GPU上创建内存用于储存我们的顶点数据,我们需要通过顶点缓冲对象(Vertex Buffer Objects, VBO)来管理这个内存

模块代码

// 定义顶点数据(三个顶点,每个顶点包含x y z属性)
GLfloat vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};
// 创建VBO为缓冲类型的对象
GLuint VBO;
glGenBuffers(1, &VBO);
// 绑定VBO为GL_ARRAY_BUFFER缓冲类型
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 将我们定义的数据vertices复制到GL_ARRAY_BUFFER缓冲类型当前绑定的缓冲对象VBO上
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

 首先我们定义顶点数据,它可以是任何自定义形式,我们在例中定义顶点数据为:包含9个GLfloat类型变量的数组。我们认为它应该被理解为:三个顶点,每个顶点三个坐标xyz
顶点着色器已经在显存中创建了缓存区域,这个缓存区域是专门用来存储顶点数据的,顶点数据对应的缓冲类型为GL_ARRAY_BUFFER。
 我们创建一个缓冲类型的VBO对象,将对象VBO的缓冲类型绑定为GL_ARRAY_BUFFER。
绑定GL_ARRAY_BUFFER的表示将该缓冲区对象VBO用于顶点属性数据的,当顶点着色器需要顶点数据时将会从VBO中读取
glBufferData会将数据复制到 绑定对应缓冲类型 的缓冲对象中
 使用glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW)函数将顶点数据vertices复制到 绑定了GL_ARRAY_BUFFER缓冲类型 的缓冲对象VBO中。
 你是否感觉上文是否有些别扭,为什么不将顶点数据直接复制到VBO中呢?因为我们是基于OpenGL这个状态机去操作,你无法直接将顶点数据复制到显存VBO,而OpenGL模型中并不知道哪个VBO(我们可以定义多个VBO)对应的显存应该存储顶点数据,所以这个函数将以绑定了GL_ARRAY_BUFFER缓冲类型的缓冲对象为目标进行复制数据的操作
经过以上操作,我们的顶点着色器就知道在定义的VBO中读取顶点数据了

8 链接顶点属性

 顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性所以,我们必须在渲染前指定OpenGL该如何读取顶点数据

模块代码

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);

 以上的模块中使用glVertexAttribPointer函数通知OpenGL解析顶点数据的方式
 我们传入VBO的是一个float类型的数组,包含9个float类型的变量,我们知道每三个变量代表一个顶点的属性,现在我们要做的就是告诉OpenGL这件事
OpenGL规定,顶点位置属性VertexPosition的位置值layout为0,顶点颜色属性VertexColor的位置值layout为1。顶点位置属性的索引值为0,颜色的index为1,这是规定的属性
因此上文中我们的顶点着色器中有定义:layout(location = 0),location = 0表示这个输入属性是顶点位置属性,而在此处第一个参数为0表示这个函数解析出的数据是顶点位置属性,因此解析出的顶点位置属性后将会输入到顶点着色器中location=0的属性
glVertexAttribPointer函数的第一个参数表示我们要配置的顶点属性,我们现在要配置的是顶点的位置属性,所以传入0。第二个参数指定顶点属性的大小,我们传入的每个顶点的属性由3个 GLfloat 值构成,所以每个顶点属性的大小为3,代表由三个值组成第三个参数指定数据的类型,即每个值是什么类型呢?传入对应类型GLfloat。第四个参数定义我们是否希望数据被标准化,如果设置为GL_TRUE,则所有的数据会被映射到【-1,1】之间。第五个参数叫步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于每个不同的顶点属性相差三个GLfloat的"距离",所以我们传入3* sizeof(GLfloat)。
 你可能觉得这个参数一点用都没有,毕竟每个顶点包含三个GLfloat,那不是一定相差三个GLfloat吗?其实这只是对于这个例子而言,如果我们对每个顶点添加三个GLfloat值代表其颜色RGB值呢?这时我们配置顶点位置的读入时,每个顶点包含6个GLfloat值,前三个变量依旧是位置,2但是步长就变为6个GLfloat了,毕竟这个顶点属性开头到下个顶点属性开头相差了6个GLfloat值,所以现在你还觉得它没有用吗?
 最后一个参数的类型是GLvoid*,所以我们需要进行这个奇怪的强制类型转换,它表示读入的数据在缓冲中其实位置的偏移量,你可能又觉得没用,但是同样是对于刚才提到的添加3个颜色后每个顶点包含6个GLfloat属性值的情况,如果要读取颜色必须要从每个顶点属性的第三个值开始,这怎么指定呢?就使用这个参数指定偏移即可,具体值为:(GLvoid*)(3* sizeof(GLfloat))。
glEnableVertexAttribArray(0)的作用是启用顶点在location=0处的属性值,因为顶点属性默认是禁用的(为了节省性能)

9 VAO的使用

 在往后的应用中,我们渲染场景中可能存在很多物体,在渲染一个物体前我们需要重新使用glVertexAttribPointer函数指定顶点数据的解析方式,毕竟在它之前我们可能渲染了其他解析方式不同的物体。当以后每个物体不仅要设置位置解析方式,还要设置颜色、纹理解析方式时,这毫无疑问是非常麻烦的有没有一种办法可以创建一个变量,将我们对物体的解析方式(包含位置、颜色、纹理解析)存储起来,当我们需要配置解析方式时,直接使用它即可?这当然就是VAO了。

模块代码

// 创建VAO并绑定
GLuint VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);

// 设置解析方式(会保存到VAO中)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 解绑VAO
glBindVertexArray(0);

上文代码使用glBindVertexArray(VAO)将我们定义的VAO对象绑定到顶点数组对象(Vertex Array Object, VAO)中然后我们使用"glVertexAttribPointer"函数设置解析方式,"glVertexAttribPointer"函数会自动将设置的解析方式 绑定到 目前绑定 顶点数组对象Vertex Array的VAO对象中如此我们便使用VAO将解析方式存储了起来。当解析方式设置完毕,我们暂时就不需要指定VAO的的解析方式了,因此我们将它解绑,这样"glVertexAttribPointer"函数就影响不到我们已经设置好的解析方式了,注意解绑后解析方式任然存在于VAO中
当需要使用VAO这种解析方式时,我们需要将它绑定,这样OpenGL才会使用VAO中的解析方式。
需要注意的是,无论是对VAO还是VBO,我们都是通过将它们绑定到对应的类型上,然后设置对应的类型,以达到设置VAO和VBO的目的。当VAO和VBO设置好后,为了避免我们后来对其的错误设置,应该解绑VAO和VBO

10 渲染模块

模块代码

// 只要窗口不应关闭就继续循环
while (!glfwWindowShouldClose(window))
    {
        // 检查窗口事件
        glfwPollEvents();

        // 清空屏幕缓存
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 使用着色器程序进行渲染
        glUseProgram(shaderProgram);
        // 绑定VAO指定解析顶点属性的方式
        glBindVertexArray(VAO);
        // 绘制图形(基于VBO、VAO调用函数)
        glDrawArrays(GL_TRIANGLES, 0, 3);
        // 使用完VAO后应该解绑VAO
        glBindVertexArray(0);

        // 交换前后缓冲区
        glfwSwapBuffers(window);
    }
    // 渲染程序结束,释放GPU中VAO和VBO的内存
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    // 释放GLFW申请的内存
    glfwTerminate();
    return 0;

四、运行结果和完整代码

渲染结果:三角形

 将以上模块代码按顺序拼接,运行程序可以得到下图结果:
在这里插入图片描述

完整代码

#include <iostream>

// GLEW
#define GLEW_STATIC
#include <GL/glew.h>

// GLFW
#include <GLFW/glfw3.h>

// 定义窗口大小
const GLuint WIDTH = 800, HEIGHT = 600;

// 定义字符串:着色器源代码(这种形式会自动拼接,分行只是好看)
const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"}\0";
const GLchar* fragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

int main()
{
    // GLFW的初始化
    glfwInit();
    // GLFW的配置
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

    // 创建glfw窗口并设置窗口的状态为当前线程的主状态
    GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
    glfwMakeContextCurrent(window);
    
    // 设置glew更加现代化
    glewExperimental = GL_TRUE;
    // 初始化glew
    glewInit();

    // 定义视口左下角在窗口的位置 和 视口的大小	
    glViewport(0, 0, 800, 600);

    // 顶点着色器的创建、绑定源代码、编译
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // 检查顶点着色器编译是否成功
    GLint success;
    GLchar 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;
    }
    
    // 片段着色器的创建、绑定源代码、编译
    GLuint 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;
    }
    
    // 创建着色器程序、添加着色器后链接着色器
    GLuint 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_FAILED\n" << infoLog << std::endl;
    }
    
	// 链接着色器后就删除着色器,节省显存
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    // 定义顶点属性数据(格式任意自定义)
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f  // Top   
    };

	// 创建VAO、VBO
    GLuint VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // 绑定VAO
    glBindVertexArray(VAO);
	// 绑定VBO
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // 将顶点数据vertices复制到GL_ARRAY_BUFFER绑定的对象VBO中
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	
	// 设置解析顶点数据的方式到绑定VertexArray的VAO中
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    // 启用顶点属性0号位置
    glEnableVertexAttribArray(0);

	// 解绑VBO(因为暂时不需要再向这个VBO传入数据了)
    glBindBuffer(GL_ARRAY_BUFFER, 0); 
	// 解绑VAO(因为暂时不需要再配置这个VAO的解析方式了)
	// 要用的时候再绑定需要的VAO
    glBindVertexArray(0); 

    // 渲染循环
    while (!glfwWindowShouldClose(window))
    {
        // 检查窗口事件
        glfwPollEvents();

        // 清空屏幕缓存
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 使用着色器程序
        glUseProgram(shaderProgram);
        // 需要使用VAO解析顶点数据
        glBindVertexArray(VAO);
        // 根据传入VBO显存中的顶点数据和VAO解析方式渲染
        glDrawArrays(GL_TRIANGLES, 0, 3);
        // 使用完解除VAO绑定
        glBindVertexArray(0);

        // 交换缓冲区
        glfwSwapBuffers(window);
    }
    // 删除VAO、VBO释放显存
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    // 释放glfw申请的内存
    glfwTerminate();
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/441868.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通过CSIG—走进合合信息探讨生成式AI及文档图像处理的前景和价值

一、前言 最近有幸参加了由中国图象图形学学会&#xff08;CSIG&#xff09;主办&#xff0c;合合信息、CSIG文档图像分析与识别专业委员会联合承办的“CSIG企业行——走进合合信息”的分享会&#xff0c;这次活动以“图文智能处理与多场景应用技术展望”为主题&#xff0c;聚…

安全防御第四天:防病毒网关

一、恶意软件 1.按照传播方式分类 &#xff08;1&#xff09;病毒 病毒是一种基于硬件和操作系统的程序&#xff0c;具有感染和破坏能力&#xff0c;这与病毒程序的结构有关。病毒攻击的宿主程序是病毒的栖身地&#xff0c;它是病毒传播的目的地&#xff0c;又是下一次感染的出…

尚融宝21-整合springcloud

目录 一、整合注册中心nacos 二、整合openFeign &#xff08;一&#xff09;准备工作 &#xff08;二&#xff09;导入依赖 &#xff08;三&#xff09;接口的远程调用 &#xff08;四&#xff09;配置超时控制和日志打印 三、整合Sentinel 四、整合gateway服务网关 …

【Spring从成神到升仙系列 五】从根上剖析 Spring 循环依赖

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小黄&#xff0c;独角兽企业的Java开发工程师&#xff0c;CSDN博客专家&#xff0c;阿里云专家博主&#x1f4d5;系列专栏&#xff1a;Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙…

基于SpringBoot+Vue家乡特色推荐系统

您好&#xff0c;我是码农飞哥&#xff08;wei158556&#xff09;&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精…

【李老师云计算】HBase+Zookeeper部署及Maven访问(HBase集群实验)

索引 前言1. Zookeeper1.1 主机下载Zookeeper安装包1.2 主机解压Zookeeper1.3 ★解决解压后文件缺失1.4 主机配置Zookeeper文件1.4.1 配置zoo_sample.cfg文件1.4.2 配置/data/myid文件 1.5 主机传输Zookeeper文件到从机1.6 从机修改Zookeeper文件1.6.1 修改zoo.cfg文件1.6.2 修…

一文带你了解MySQL的前世今生,架构,组成部分,特点,适用场景

文章目录 一、MySQL的由来二、MySQL的架构2.1 客户端2.2 服务器 三、 MySQL的主要组成部分3.1 连接管理器3.2 查询缓存3.3 解析器3.4 查询优化器3.5 执行器3.6 存储引擎 四、MySQL的特点五、MySQL的应用场景六、总结 一、MySQL的由来 MySQL最初是由瑞典公司MySQL AB的Michael …

4年功能测试,我一进阶python接口自动化测试,跳槽拿了20k......

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 很多人在这求职市…

让ChatGPT告诉你Java的发展前景

Java版电商购物系统项目实战 最近很多人问我Java的发展前景怎么样&#xff1f;该怎么学Java基础&#xff1f;java这么卷还该不该学等等。那今天老王以电商场景为例&#xff0c;再结合ChatGPT的回答和大家聊的一下Java有哪些应用前景和技术层面的落地方案。&#xff08;在收获干…

【Spring】-- 02 -- Spring中Bean的配置、作用域

一、Bean的配置 Spring用于生产和管理Spring容器中的Bean&#xff0c;需要开发者对Spring的配置文件进行配置。在实际开发中&#xff0c;最常采用XML格式的配置方式&#xff0c;即通过XML文件来注册并管理Bean之间的依赖关系。 在Spring中&#xff0c;XML配置文件的根元素是…

iOS问题记录 - Xcode 14.3版本运行项目报错

文章目录 前言开发环境问题描述问题分析解决方案最后 前言 看到Xcode有新版本&#xff0c;没忍住点了升级&#xff0c;然后问题来了。 开发环境 macOS 13.3Xcode: 14.3 问题描述 Xcode 14.2版本运行项目一切正常&#xff0c;升级到14.3版本后运行报错。 运行到模拟器的报…

【PWN刷题__ret2text】——CTFHub之 简单的 ret2text

萌新第一阶段自然是了解做题的套路、流程&#xff0c;简单题要多做滴 目录 前言 一、checksec查看 二、IDA反汇编 三、exp编写 前言 经典的ret2text流程 一、checksec查看 64位程序&#xff0c;什么保护都没有&#xff0c;No canary found——可以栈溢出控制返回 二、IDA反汇…

“MySQL5.6”、“索引优化”,其实都是索引下推

如果你在面试中&#xff0c;听到“MySQL5.6”、“索引优化” 之类的词语&#xff0c;你就要立马get到&#xff0c;这个问的是“索引下推”。 什么是索引下推 索引下推(Index Condition Pushdown&#xff0c;简称ICP)&#xff0c;是MySQL5.6版本的新特性&#xff0c;它能减少回…

学习实践-Alpaca-Lora (羊驼-Lora)(部署+运行+微调-训练自己的数据集)

Alpaca-Lora模型GitHub代码地址 1、Alpaca-Lora内容简单介绍 三月中旬&#xff0c;斯坦福发布的 Alpaca &#xff08;指令跟随语言模型&#xff09;火了。其被认为是 ChatGPT 轻量级的开源版本&#xff0c;其训练数据集来源于text-davinci-003&#xff0c;并由 Meta 的 LLaMA …

OpenAI对实现强人工智能AGI的规划:《Planing for AGI and beyond》

OpenAI对实现AGI的长期和短期的计划&#xff1a;《Planing for AGI and beyond》 返回论文和资料目录 原文地址 1.导读 OpenAI最近这些年发布了很多令人印象深刻的模型&#xff0c;毫无疑问&#xff0c;OpenAI已经走在了人工智能领域的最前沿。但是很多人只注意到这些模型&…

Nacos Docker Kubernetes ⽣态

博主介绍&#xff1a;✌全网粉丝4W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战、定制、远程&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面…

概率密度函数的非参数估计方法

概率密度函数的非参数估计方法 1. Parzen窗方法2. kn近邻估计 \qquad 直接由样本来估计概率密度 p ( x ) p(\boldsymbol{x}) p(x) 的方法&#xff0c;称为非参数方法 (non-parametric method) \text{(non-parametric method)} (non-parametric method)。 \quad ● \quad 概率…

数学建模第三天:数学建模算法篇之线性规划及matlab的实现

目录 一、前言 二、线性规划简介 1、线性规划模型介绍与特征 2、线性规划模型的一般形式 三、单纯形法 1、标准化 2、单纯形法解题 四、matlab解决问题1、matlab线性规划函数 2、解题代码 一、前言 数学建模&#xff0c;本意就是用来解决生活中的问题&#xff0c;我们今…

二叉树的前中后序遍历写法归纳

如题&#xff0c;对应力扣题目如下&#xff1a; 144.二叉树的前序遍历145.二叉树的后序遍历94.二叉树的中序遍历 1.递归 1.1 先序遍历 根 -> 左 -> 右 所以,这个递归函数先打印根节点的值,然后递归地遍历左子树,最后递归地遍历右子树。如果传入的根节点是空,则直接返回…

Linux学习记录—— 이십일 进程间通信(3)信号量和消息队列

文章目录 1、消息队列2、信号量1、了解概念2、信号量理解 3、接口4、理解IPC 1、消息队列 两个进程ab之间系统维护一个队列结构&#xff0c;a进程往队列里放信息&#xff0c;信息编号为1&#xff0c;b进程往队列里放信息&#xff0c;信息编号为2&#xff1b;之后开始读取数据的…