OPENGL学习(2) 万物之始三角形
把最简单的多边形,作为opengl绘制的第一个对象,从而讲述从顶点数据到输出一个橙色三角形的过程。
窗口
首先,需要一个窗口来承载三角形的渲染。
初始化
// 对glfw进行预处理
void _glfw_init()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,6);
glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT,GL_TRUE);
}
解析
glfwInit对glfw进行初始化。
glfwWindowHint可以对opengl进行一些配置。
窗口创建和终止
通过glfwCreateWindow来进行window的创建。
glfwTerminal进行窗口的终结。
GLFWwindow* window = glfwCreateWindow(height,width,head,NULL,NULL)
判断是否创建成功
if(window==NULL)
{
//失败
}
将窗口加载到当前环境
glfwMakeContextCurrent(window)
GLAD的初始化
在正式调用glad中封装的函数之前,要对glad进行初始化。
从而让glad获取到正确的函数指针供你使用。
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
//pass GLAD the function to load the adress of the OpenGL function pointers which is OS-specific
//glfwGetProcAddress that defines the correct function based on the OS
cout<< "Failed to initialize GLAD" << endl;
return -1;
}
回调函数
窗口的大小会被改变(放大、缩小等)。若窗口改变视口不改变,则会出现不正常的现象。
可以对窗口注册一个回调函数,在窗口被调整大小时,更改视口至对应的值。
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
tips:除此之外,窗口还可以获得更多类型的回调函数,比如设备输入输出、处理错误等等。
交互做的好不好,有相当一部分原因在回调函数的质量上。
完成着色器
如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器
顶点着色器
//起始版本与模式说明
#version 460 core
//in 表示输入顶点属性 vec--glsl的数据类型 vecn表示一个有n个分量的向量
//layout设定了输入变量的位置值
layout (location=0) in vec3 aPos;
void main()
{
//把位置数据赋值给预定义的gl_Position变量
gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0);
}
片段着色器
片段着色器所做的是计算像素最后的颜色输出。
#version 460 core
//片段着色器只需要一个输出变量
out vec4 FragColor;
void main()
{
FragColor = vec4((1.0f, 0.5f, 0.2f, 1.0f);
}
绑定着色器的步骤
graph TD
Start(开始)
-->S[unsigned int声明某Shader]
-->A[glCreateShader创造指定类型着色器并绑定到变量]
-->B[glShaderSource将实现代码与shader绑定]
-->C[glCompileShader编译指定shader]
-->D(结束流程)
绑定顶点着色器
unsigned int vertex_shader;
vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader,1,&vertex_shader_source,NULL);
glCompileShader(vertex_shader);
绑定片段着色器
unsigned int fragment_shader;
fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);
glCompileShader(fragment_shader);
绑定程序对象
现在获得了定点着色器对象和片段着色器对象,然而我们需要一个着色器程序来绑定这两个对象,从而实现一个完整的shader。
流程图
graph TD
A(start)-->B[声明unsigned int来承载程序对象]
-->C[glCreateProgram创建程序对象并绑定]
-->D[glAttachShader来绑定自定义shader]
-->E[glLinkPrograme链接指定程序]
-->END(end)
可以意识到 opengl中大部分对象都是通过glCreateXxx来创建并绑定到一个unsigned int变量上进行管理。
代码
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
使命完成
在把着色器对象链接到程序对象以后,着色器对象已经不再需要,通过glDeleteShader删除。
激活程序对象
glUseProgram(shaderProgram);
在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。
链接顶点属性
glVertexAttribPointer
vertex shader表明了数据的输入格式和转换方法,
但与此同时,
我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个定点属性。
假设你接受到了一定大小的一些数据,那么需要什么信息来解析这些数据?
- 一个顶点的数据数量
- 数据类型(只有数量不知道类型仍无法解析)
- 是否要对其标准化?
- 连续的顶点属性之间间隔多大
- 在接受到的数据中需要偏移多少才到达顶点数据的部分?
glVertexAttribPointer就用来设定这些信息。
函数原型
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void * pointer);
glEnableVertexAttribArray
默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU,由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的属性数据。
顶点缓冲对象 Vertex Buffer Object
VBO在GPU内存(通常被称为显存)中储存大量顶点,可以一次性的发送一大批数据到显卡上,从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
流程图
graph TD
A(start)-->
B[unsigned int VBO]-->
C[glGenBuffers生成缓冲并绑定到vbo]-->
D[把特定的VBO绑定到GL_ARRAY_BUFFER]-->
E[通过glBufferData将数据复制到GL_ARRAY_BUFFER]-->
END(end)
代码
unsigned int VBO;
glGenBuffers(1,&VBO);
glBindBuffe(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(data),data,GL_STATIC_DRAW);
意义
通过VBO的创建和绑定,已经把顶点数据储存在显卡的内存中,用VBO来对其进行管理。
和VertexAttrib的关系
每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。
应用
如果不考虑VAO,那么绘制一个自定义物体对象的代码将会如下。
// 0. 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
顶点数组对象 Vertex Array Object
VAO可以像VBO那样被绑定,任何随后的顶点属性调用(glVertexAttribPointer)都会存储在这个VAO中。
这样当频繁切换顶点属性时,会降低大部分重复代码。减轻工作难度。
内容
VAO会存储以下内容
- glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
- 通过glVertexAttribPointer设置的顶点属性配置。
- 通过glVertexAttribPointer调用与顶点属性关联的VBO。
 万物之始三角形.assets/vertex_array_objects.png)
流程图
graph TD
Start[uint声明vao]
-->A[glGenVertexArrays生成vao]
-->B[glBindVertexArray将VAO绑定]
-->C[处理不同VBO对象]
-->End(end)
总流程
graph TD
A(strat)-->
B[申请窗口]-->
C[完善着色器]-->
D[设置render progarm]-->
E[申请VAO管理VBO]-->
F[设置VBO]-->
G[进入渲染循环]-->
H(end)
完整代码:learnopengl.com/code_viewer…
索引缓冲对象(Element Buffer Object,EBO)
顾名思义,其也是一个缓冲,只不过专门存储索引。OpenGL调用这些顶点的索引来决定该绘制哪个顶点,来解决大量顶点重复导致的资源浪费。