1.1,状态机-上下文-对象
GPU渲染流程
OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。
状态机:变量(描述该如何操作)的大集合
OpenGL的状态通常被称为OpenGL上下文(Context)。
对象(Object):类似一个结构体
在OpenGL中一个对象是指一些选项的集合,它代表OpenGL状态的一个子集
1.2,创建一个窗口
1.2.1 GLFW-GLAD
GLFW解决操作系统层面的不同
GLAD使代码可以可以用于不同的OpenGL驱动
注意头文件顺序,glad的include中包含了glfw所需的gl.h文件
测试代码:
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
//用户调整窗口大小之后的回调
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
//实现输入控制
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//检查用户是否按下了返回键(Esc)
{
glfwSetWindowShouldClose(window, true);
}
}
int main()
{
glfwInit(); //初始化GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号(Major)
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号(Minor)
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//核心模式(Core-profile)
GLFWwindow* window = glfwCreateWindow(800, 600, "learnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window!\n" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);//创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了
//初始化glad
//glad:加载所有OpenGL的函数指针
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
//告诉OpenGL渲染窗口的大小
glViewport(0, 0, 800, 600);
//设置窗口大小变化之后的回调
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//渲染循环
//在窗口结束之前,不断绘制图像,并且能够接收用户的输入
while (!glfwWindowShouldClose(window))//检查gldw是否被要求退出
{
/*
双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,
而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步
生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲
保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指
令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
前缓冲区:屏幕上显示的图片
后缓冲区:正在渲染的图片
*/
/*
当调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
*/
// 输入
processInput(window); //检查是否键盘输入
//渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);//设置状态
glClear(GL_COLOR_BUFFER_BIT);//使用状态
// 检查并调用事件,交换缓冲
glfwSwapBuffers(window); //交换颜色缓冲(他是一个存储glfw窗口每一个像素颜色值的大缓冲),他在这一迭代中被用来绘制,并且将作为输出显示在屏幕上
glfwPollEvents(); //检查有没有触发什么事件,并且调用对应的回调
}
glfwTerminate();
return 0;
}
2.1,渲染管线-VBO-VAO
在学习此节之前,建议将这三个单词先记下来:
- 顶点数组对象:Vertex Array Object,VAO
- 顶点缓冲对象:Vertex Buffer Object,VBO
- 元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO
图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)
图形渲染管线可以被划分为两个主要部分:
第一部分把你的3D坐标转换为2D坐标,
第二部分是把2D坐标转变为实际的有颜色的像素。
渲染和shader
渲染由一套渲染管线来管理
shader用来设置一些点线面的位置信息,以及映射图片到纹理这些逻辑,从而拿到图像数据,然后如何处理图像,也可以通过shader来处理。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
图形渲染管线的各个阶段的作用:
1,顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。
2,图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状;本节例子中是一个三角形。
3,图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。
4,几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据。
5,片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
6,在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
什么是VAO,VBO?
VBO用于存放数据,VAO用于描述数据的属性(如何将数据取出来)
我们的顶点缓冲数据会被解析为下面这样子:
- 位置数据被储存为32位(4字节)浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
- 数据中第一个值在缓冲开始的位置。
元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)。
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, // 右下角 -0.5f, -0.5f, 0.0f, // 左下角 -0.5f, 0.5f, 0.0f // 左上角 };
就可以转化成:
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 // 第二个三角形 };
着色器程序
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
函数理解:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
- 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用
layout(location = 0)
定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0
。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0
。- 第二个参数指定顶点属性的大小。顶点属性是一个
vec3
,它由3个值组成,所以大小是3。- 每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性
0
现在会链接到它的顶点数据。
代码:画一个矩形
// sample_1_1_HelloWindow.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
//顶点着色器
const char* vertexShaderSource ="#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
//片段着色器
const char * fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
//用户调整窗口大小之后的回调
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
//实现输入控制
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//检查用户是否按下了返回键(Esc)
{
glfwSetWindowShouldClose(window, true);
}
}
int main()
{
glfwInit(); //初始化GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号(Major)
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号(Minor)
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//核心模式(Core-profile)
GLFWwindow* window = glfwCreateWindow(800, 600, "learnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window!\n" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);//创建完窗口我们就可以通知GLFW将我们窗口的上下文设置为当前线程的主上下文了
//glad:加载所有OpenGL的函数指针
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
//告诉OpenGL渲染窗口的大小
glViewport(0, 0, 800, 600);
//设置窗口大小变化之后的回调
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
/************************************************************************/
/*画一个三角形 */
//顶点着色器创建
//顶点着色器附加源码,并且运行时动态编译它的源代码
unsigned int vertexShader=glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
int success = -1;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILEED\n" << infoLog << std::endl;
}
//片段着色器
unsigned int 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::FRAGMEMT::COMPILATION_FAILEED\n" << infoLog << std::endl;
}
//着色器程序,链接着色器
unsigned int 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::PROGRAM::LINK\n" << infoLog << std::endl;
}
//把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
/*
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};*/
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
unsigned int indices[] = { // note that we start from 0!
0, 1, 3, // first Triangle
1, 2, 3 // second Triangle
};
//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
unsigned int VBO = 0;
unsigned int VAO = 0;
unsigned int EBO = 0;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
//OpenGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。
// OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用
// glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
//任何对GL_ARRAY_BUFFER的操作,都是对VBO的操作
//把之前定义的顶点数据复制到缓冲的内存
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
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);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
/*画一个三角形 end */
/************************************************************************/
//渲染循环
//在窗口结束之前,不断绘制图像,并且能够接收用户的输入
while (!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
//渲染指令
//激活,在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象
//(也就是之前写的着色器)了。
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
//glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// 检查并调用事件,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glDeleteProgram(shaderProgram);
glfwTerminate();
return 0;
}