老子不信我学不会OpenGL系列!003 绘图的操作!
关于渲染管线,这个博主讲的比我好多了,具体参看下面的文章:
【《Real-Time Rendering 3rd》 提炼总结】(二) 第二章 · 图形渲染管线 The Graphics Rendering Pipeline
【《Real-Time Rendering 3rd》 提炼总结】(三) 第三章 · GPU渲染管线与可编程着色器 The Graphics Processing Unit
——————分割线——————
OpenGL的所有东西都是3D的。而我们的显示器是2D的(……废话)。所以很大一部分工作就是将数据从3D转成2D。OpenGL里干这个事的,叫做graphics pipeline(中文应该是 渲染管线)。graphics pipeline又可以分为两大部分:3D→2D,2D→像素色块。
具体来说,graphics pipeline是分了好几步来完成这件事的,第一步接收了很多三维空间中的点(点里面不止有坐标,还会有其他的数据,比如颜色之类的),之后每一步的输出,都会成为下一步的输入,直到最后一步执行完输出一个位图。这里每一步都相当的独立,都是一个独立的函数,这些函数被显卡执行(而不是CPU),被称为shader。而我们可以对其中一些步骤,编写自己的函数,实现更精细的控制。(另外一些不让修改的步骤,就是OpenGL要求厂商为你提供的服务。我个人认为,理论上你可以完全不管他是怎么实现的,只要记住功能就行了,但是……所有的书上可都讲了这块……【待更正】)
下图是graphics pipeline中各个步骤的一个示意图(源自我看的那个英文教程),蓝色部分是可以被我们控制的部分。
基本名词:
vertex(顶点):一个点,以及这个点上所包含的信息。逻辑上就是(位置,信息)这种东西。
vertex attribute:(位置,信息)里包含了很多的vertex attributes,如:位置,颜色等。
Vertex Data:vertex的集合,graphics pipeline接收的就是这个东西。
primitive(图元):用来告诉OpenGL如何利用vertex画图。你是想把vertex画成散点、折线、三角形?这种信息叫primitive。(大概这么理解,primitive也可以理解成一个图形,反正我是没理解……)
渲染管线的过程:
【待补,先自己去网上搜搜吧,我真的不是很了解,这里有一篇 ,我随便找的】
1.构造原始数据:
(内存)写出坐标,用float型的数组:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
(显卡)告诉显卡内存有一片空间(buffer)需要用:(这些空间都有唯一的ID,所以通过ID来管理这些空间)
unsigned int VBO;
glGenBuffers(1, &VBO);
glGenBuffers( 个数,ID地址 ):要产生几个buffer,用来存储ID的地方。
(显卡)为这片空间指定数据类型:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBindBuffer( 类型,ID)
(显卡)将数据赋给这个buffer:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData( 类型,数据的字节数,数据,存取方法 ):存取方法由:这个数据是否要经常发生改变,而决定。
- GL_STATIC_DRAW:这个数据压根就不需要改,或者基本不变(比如一个形状不会改变的石头)
- GL_DYNAMIC_DRAW:可能会经常改变。
- GL_STREAM_DRAW:时时刻刻都在变……。
【在后面会解释什么是VertexAttribPointer,先跳过去,待会回来再看这里】关于GL_ARRAY_BUFFER这类东西,可以理解成是一个显存对外的接口(一条可以走的路,其他地方都进不去,无法访问)。每种类型只有一个接口。如果你想访问显存,要么走这个接口(直接将VBO绑定到GL_ARRAY_BUFFER上),要么自己进去,为特定的区域(一个特定的VBO)修一条路,即设置一个VertexAttribPointer(你先得进去,就是在修路的时候(即,配置VertexAttribPointer的时候),必须要绑定VBO),修好之后(配置好之后)就可以解绑了。
如果想解绑的话,用0绑定就可以了,如下:(其他的绑定也是一样)
glBindBuffer(GL_ARRAY_BUFFER, 0);
2.写Vertex Shader程序代码:
做了3件事:1.告诉编译器OpenGL的版本,2.从显存(应该是显存吧)中导入数据,3.为这个着色器的输出变量(一个预制变量 gl_Position)赋值。
//版本声明
#version 330 core
//vertex attributes 即 输入
layout (location = 0) in vec3 aPos;
void main()
{
// 输出,一个 vec4 类型的预制变量 gl_Position
gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0);
}
上面是GLSL,语言形式很类似于C。在输入的地方,要确定这个属性用用哪个指针读入,就是 location = 0 的作用。至于具体的从内存的什么位置读入,怎么读入,就是那个指针的事情了,下面会说。详细内容会在下一节讲。
3.编译Vertex Shader:
有3件事要做:1.创建shader对象,同样用ID来管理,2.将上面写的代码,赋给这个shader对象,3.编译这个shader对象
GLuint vshader;
vshader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vshader, 1, &vsl, NULL);
glCompileShader(vshader);
这玩意应该相当于中间文件,最后是可以删掉的(一般会删掉,节省空间嘛)
4.写Fragment Shader代码:
差不多,详细内容后面会说。
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f,0.5f,0.2f,1.0f);
}
5.编译Fragment Shader:
与VertexShader几乎完全一样,只是改变了一下类型:
GLuint fshader;
fshader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fshader, 1, &fsl, NULL);
glCompileShader(fshader);
6.链接各个shader,并**,并删除中间文件:
3件事:1.创建一个shader program对象,2.将编译好的vshader,fshader都加进去,3对shader program进行链接。
GLuint shadProg;
shadProg = glCreateProgram();
glAttachShader(shadProg,vshader);
glAttachShader(shadProg,fshader);
glLinkProgram(shadProg);
shader program应该就是类似于.exe执行文件,可以直接用的。不过用的时候要**一下(就像你要双击.exe一样)。当我们**了这个shader program的时候,graphics pipeline就会用这个来画所有的图像。同样当你想换的时候,就**另一个就好了。下面是如何**shader program:
glUseProgram(shadProg);
在链接完之后,就可以删掉了~,下面的代码是删除中间文件:
glDeleteShader(vshader);
glDeleteShader(fshader);
7.配置vertex attribute格式:
到现在为止,graphics pipeline已经配置好了。显卡部分,唯一有问题的就是,graphics pipeline还不知道vertex attribute的格式(哪里放的是坐标,哪里放的是颜色),就是整个 graphics pipeline的输入部分。我们通过一个指针告诉它这部分信息,同样,这个指针也需要**。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
这个指针是指向GL_ARRAY_BUFFER 的。
glVertexAttribPointer( 指针序号,数据维度,数据类型,正规化,移动步长,起点 ):
- 指针序号:就是location那里对应的序号,相当于一个ID。
- 数据维度:这里位置数据,是vec3,所以就是3……
- 数据类型:每一维的数据是什么类型的,我们是用的float数组,所以就是GL_FLOAT。
- 正规化:是否希望将数据映射到[0,1](对于负数是[-1,1]【待确认】)之间。
- 移动步长(stride):下一个点的同一个属性在多少个字节之后。
- 起点(offset):第一点的这个属性在哪里。
8.建立VAO:
首先你得知道VAO,vertex array object是个啥:
一个VAO可以将这些保存下来:
- 对于0号,1号,...指针的配置,即glVertexAttribPointer()这个函数的设置。
- 这些指针的**状态,即glEndableVertexAttribArray()。
- 这些指针所指向的VBO。
(图片来自这里)
实际上,可以把一个VAO对应成一个可以传给graphics pipeline的物体。它实际上比VBO多了配置格式的部分。VBO只是提供了数据,而VAO不止提供数据,还告诉graphics pipeline,这组数据中,每6个是一个点,其中前三个是位置,后三个是颜色。
你要做5件事:1.建立VAO(还是用ID管理),2.绑定VAO,配置VAO:3.绑定他的VBO,4.配置他的指针,5.**他的指针。
GLuint VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
9.画图~~~
啊,终于可以画图了~~~,这部分就贼轻松了~~~
3件事:1.启用你想用的shader program,2.绑定你想画的VAO,3.画图~~~
glUseProgram(shadProg);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawArrays( 图元,从哪个点开始画,要画几个点) :【待补】这个函数应该是在对图元装配那一步进行设置。
啊~终于搞定了~
到这里终于画出来了第一个三角形,真的刺激,待会就继续进入下一章,讲GLSL。
下面是源代码,可以参考一下。
// 1.头文件:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
// 5.OpenGL与窗口(main之前):
void CBK_framebuffer_size(GLFWwindow* window, int w, int h)
{
glViewport(0, 0, w, h);
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
int main()
{
// 2.在创建窗口之前……:
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// 3.创建窗口:
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// 4.在OpenGL之前……:
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// 5.OpenGL与窗口:
glViewport(0, 0, 800, 600);
glfwSetFramebufferSizeCallback(window, CBK_framebuffer_size);
// OpenGL的配置:
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);//设置背景色
// 绘图需要用的数据:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
GLuint VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);//可以解绑,不会影响结果,因为VAO主要是有指针决定的,他“存储”的VBO,是由指针来直接访问的【待更正】。
glBindVertexArray(0);//可以解绑……反正待会还要再绑定上。
// Shader:
const char *vsl = "#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 *fsl = "#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";
GLuint vshader;
vshader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vshader, 1, &vsl, NULL);
glCompileShader(vshader);
GLuint fshader;
fshader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fshader, 1, &fsl, NULL);
glCompileShader(fshader);
GLuint shadProg;
shadProg = glCreateProgram();
glAttachShader(shadProg,vshader);
glAttachShader(shadProg,fshader);
glLinkProgram(shadProg);
glDeleteShader(vshader);
glDeleteShader(fshader);
// 6.Render Loop:
while (!glfwWindowShouldClose(window))
{
processInput(window);
glClear(GL_COLOR_BUFFER_BIT);//绘制背景色
// 这下面就可以写绘图的代码了
glUseProgram(shadProg);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// 这上面是绘图的代码
glfwSwapBuffers(window);
glfwPollEvents();
}
// 7.程序结束之前:
glfwTerminate();
return 0;
}
下面是运行结果(控制台就不截图了,反正是空的):