节点对象学习:
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的,在下一节中我们再花更多时间研究它。
图形渲染管线的初步了解:
● 在 OpenGL 中,任何事物都在 3D 空间中,而屏幕和窗口却是 2D 的空间,这导致 OpenGL 的大部分工作都是关于把 3D 坐标转变为适应你屏幕的 2D 坐标。
● 3D 坐标转为 2D 坐标的处理过程是由 OpenGL 的图形渲染管线管理的。(Graphics Pipeline,大多译为管线,实际上指的是 一堆原始图形数据途经一个输送管道,期间经过各种变化处理,最终出现在屏幕的过程 )
● 为什么叫 “管线” 呢? 因为图形的渲染过程就像是:管的一端输入,管的另一端输出。输入是一些 “顶点坐标、画图规则”,而输出就是 “实际图画”。而中间那条 “管子” ,就像是一个通用的 “容器”,什么样的 “输入” 都能接受。就像生活中的水管,即能传输水,也能传输油,还能传输天然气。
● 它们具有并行执行的特性,也就是说,就像自来水厂向你家输水一样,可以多条水管一起输过来。而当今大多数大脑的显卡上,都有成千上万的小处理核心(这个就像是那成千上万的“水管”),它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
● 图形渲染管线可以被划分为两个主要部分:第一部分把 3D 坐标转换为 2D 坐标,第二部分把 2D 坐标转变为实际有颜色的像素。
● 图形渲染管线的阶段流程图:
① 图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标(后面会解释)。
② 图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入,并将所有的顶点装配成指定图元的形状。
③ 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它会通过新产生的顶点构造出新的图元来生成其他形状(但变形不变样)。
④ 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。
⑤ 片段着色器(也称为片元着色器)的主要目的是计算一个像素的最终颜色。
从0开始的OpenGL学习(二)-渲染三角形_章小京的博客-CSDN博客
唯一ID:OpenGL中有太多的东西,我们需要一个唯一的ID,就像身份证一样来标识出哪个是哪个。这个ID不能我们自己来定,只能告诉OpenGL说,我需要一个唯一ID,你给我一个吧。然后,OpenGL就会给你一个没用过的唯一ID,这个过程是由glGenBuffers来实现的。
GLuint VBO_ID;
glGenBuffers(1,&VBO_ID);
指明缓存对象类型:OpenGL中有很多缓存对象,虽然它给了我们一个ID,但不知道这个ID是用来表示什么缓存对象的。我们要明确告诉它,这是一个顶点缓存对象。绑定的作用,是将原来有的东西替换成我们新的东西,这样,接下来对GL_ARRAY_BUFFER的操作都是对我们新东西的操作了。
// 指定为顶点缓冲区
glBindBuffer(GL_ARRAY_BUFFER,VBO);
// 将顶点数据传到VBO:
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
从OpenGL 3.3开始,OpenGL的渲染方式就从固定功能管线转向了可编程功能管线。而可编程的意思,就是可以通过许许多多的着色器来实现多种多样的效果,这些效果要比固定的效果好的多。顶点着色器就是这些许许多多的着色器中的一种。
#version 430
layout (location = 0) in vec3 aPos;
void main(void){
gl_Position = vec4(aPos.x,aPos.y,aPod.z,1.0);
}
gl_Position是GLSL内置变量,GPU将从这个位置读取数据并显示点线.
代码自然是要编译链接之后才能执行的,这里我们先说编译,链接的部分等讲完片元着色器再一起说,先不用纠结。
首先,创建一个着色器对象,返回值是这个对象的唯一ID
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
源码附加到着色器对象上并且编译这个着色器对象
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
#version 430
out vec4 color;
void main(void){
color = vec4(0.2745, 0.1569, 0.2353, 1.0);
}
这里FragColor是GLSL内置的一个变量,用于指定上述定点内部的颜色。
类似的,我们输出一个颜色供片元使用,这个颜色是橙色。一个片元,包含了OpenGL用来渲染一个像素的所有信息
我们同样需要创建一个着色器对象,然后将源码附加到着色器对象上,然后编译。
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
将着色器都连接到程序对象上时,OpenGL会将上一个阶段的输出连接到下一个阶段的输入上。
我们需要三步来使用着色器程序对象,现在已经很熟悉了。创建、附加、链接。
glCreateProgram函数创建一个程序,并返回新创建程序对象的ID引用
GLuint program_id;
program_id = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
激活这个函数,之后每个着色器调用都活使用这个程序对象,将着色器对象连接到程序对象之后,记得删除着色器对象
glUseProgram(shaderProgram);
// 删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer函数的参数非常多,所以我会逐一介绍它们:
layout(location = 0)
定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0
。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0
。vec3
,它由3个值组成,所以大小是3。vec*
都是由浮点数值组成的)。float
之后,我们把步长设置为3 * sizeof(float)
。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。void*
,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,代码会像是这样:
// 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();
每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(这其实并不罕见)
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中
OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。
一个顶点数组对象会储存以下这些内容:
创建一个VAO和创建一个VBO很类似:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看起来像这样:
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
就这么多了!前面做的一切都是等待这一刻,一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。
要想绘制我们想要的物体,OpenGL给我们提供了glDrawArrays函数,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型。由于我们在一开始时说过,我们希望绘制的是一个三角形,这里传递GL_TRIANGLES给它。第二个参数指定了顶点数组的起始索引,我们这里填0
。最后一个参数指定我们打算绘制多少个顶点,这里是3
(我们只从我们的数据中渲染一个三角形,它只有3个顶点长)。
#include
#include
#include
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);
//GLFW显示窗口宽度和高度
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 640;
//顶点着色器代码
const char * vertexShaderSource = "#version 460 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 460 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\0";
float vertices[] =
{
-0.5f, -0.5f, 0.0f, //左
0.5f, -0.5f, 0.0f, //右
0.0f, 0.5f, 0.0f //上
};
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow *window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (!window)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
//创建\编译我们的着色器程序
//定点着色器
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
//片元着色器
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
//链接着色器
int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
//别忘了释放着色器
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void *)0);
glEnableVertexAttribArray(0);
while(!glfwWindowShouldClose(window))
{
processInput(window);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLE_FAN, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
glViewport(0, 0, width, height);
}