opengl基础学习(3)

目前的工程放置在github上Link

继上章

在上一个章节里,我们定义了监听主动关闭窗口的while循环,现在我们需要在每次循环里绘制一个简单的三角形

while (!glfwWindowShouldClose(window))
{
    // m_curSession = new OpenGL_Session1();
	m_curSession->drawView();
    glfwPollEvents();
	glfwSwapBuffers(window);
}

通过一个对象OpenGL_Session1来绘制这个三角形,同时也是为了以后方便进行其他的内容; 首先创建一个OpenGL_Session1的C++对象,添加器公开方法drawView,我们在while里每帧调起drawview去绘制;

glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等),然后调用对应的回调函数(可以通过回调方法手动设置)。我们一般在游戏循环的开始调用事件处理函数

事件监听

GLFW提供了glfwSetKeyCallback(window, key_callback);给我们绑定窗口的按钮事件,key_callback的格式 void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
eg:

void key_callback(GLFWwindow* window, int key, int scancode, int >> action, int mode) { // 当用户按下ESC键,我们设置window窗口的WindowShouldClose属性为true // 关闭应用程序 if(key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) glfwSetWindowShouldClose(window, GL_TRUE); }

glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色的大缓冲),它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上。

双缓冲(Double Buffer)

应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。

三角形的前提

前面说了基本事件监听,以及buffer的交换,这里就大概讲一下渲染中的三角形是怎么一回事:

首先要明确的是,在GPU里,万物皆数据,而一个占空间的物体(2D或3D)都是需要通过一系列的定点来确定基本的形状的,而GPU下,所有的定点都是用来构建三角形的,然后通过一个个三角形构建出更复杂的几何体;比如一个4边形就由两个三角形构成; png

上面的流程中,还省略了几个比较重要的节点

png

顶点输入

前面已经提到过,图形的形状都是由顶点的数据组成的,而opengl的坐标体系都是3D的(x,y和z),该坐标的值只有在[-1~1]的范围的时候opengl才会处理。所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。

标准化设备坐标(Normalized Device Coordinates, NDC)

一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴):

png

与通常的屏幕坐标不同,y轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。最终你希望所有(变换过的)坐标都在这个坐标空间中,否则它们就不可见了。

你的标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport函数提供的数据,进行视口变换(Viewport Transform)完成的。所得的屏幕空间坐标又会被变换为片段输入到片段着色器中。

下面讲一下如何将顶点数组输入到GPU里

常用的有两种VAO(顶点数组对象)和VBO(顶点缓冲对象)

顶点数组对象:Vertex Array Object,VAO

顶点缓冲对象:Vertex Buffer Object,VBO

VAO

// 数组对象的唯一id
GLuint VertexArrayID;
// 生成一个VAO对象放置到指定的id下
glGenVertexArrays(1, &VertexArrayID);
// 新创建的数组绑定到GL_ARRAY_BUFFER目标
glBindVertexArray(VertexArrayID);
// 顶点数据复制到数组的内存
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

VBO

// 数组缓冲的唯一id
unsigned int VBO;
// 生成一个VBO对象放置到指定的id下
glGenBuffers(1, &VBO);
// 新创建的缓冲绑定到GL_ARRAY_BUFFER目标
glBindBuffer(GL_ARRAY_BUFFER, VBO); 
// 顶点数据复制到缓冲的内存

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

显卡管理给定的数据有三种形式:

  • GL_STATIC_DRAW :数据不会或几乎不会改变。
  • GL_DYNAMIC_DRAW:数据会被改变很多。
  • GL_STREAM_DRAW :数据每次绘制时都会改变。

至于VBO和VAO之间的关系,这里不谈,先记住这两个怎么使用先!

三角形

前文已经说过了定点是什么,现在我们定义一下要绘制的定点数据

float vertices[] = {
	-0.5f, -0.5f, 0.0f,
	 0.5f, -0.5f, 0.0f,
	 0.0f,  0.5f, 0.0f
};

定点数据声明完成后,我们需要把这些数据存放到显存里

// 声明一下要使用的VAO和VBO
// 使用无符号short性能更优
GLuint VAO,VBO;
void OpenGL_Session1::Start(ApplicationStart* application)
{
	// 生成VAO
	glGenVertexArrays(1, &VAO);
	// 绑定VAO
	glBindVertexArray(VAO);
	// 生成VBO
	glGenBuffers(1, &VBO);
	// 把顶点数组复制到缓冲中供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);
}

上面的数据绑定和生成我们只需要执行一次就可以,当前还不需要每帧对数据进行修改。

下面开始绘制三角形

// 绘制
void OpenGL_Session1::drawView() {
	glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);
	// 启动数组
	glEnableVertexAttribArray(0);
	// 指定VAO
	glBindVertexArray(VAO);
	// 绘制三角形
	glDrawArrays(GL_TRIANGLES, 0, 3);

	glDisableVertexAttribArray(0);
}

需要注意的是,glEnableVertexAttribArrayglDisableVertexAttribArray通常是配对使用的,当然如果在现在这个例子中也可以不在渲染循环中使用的,但是为了良好的习惯,还是使用比较好!

代码

png

补充说明一下glEnableVertexAttribArray

glEnableVertexAttribArray可以直接理解成允许gpu读取哪些数据;