OpenGL_Learn
基本知识
1:
顶点数组对象:Vertex Array Object,VAO
顶点缓冲对象:Vertex Buffer Object,VBO
索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO
2:
在OpenGL中,任何事物都在3D空间,但是屏幕和窗口是2D像素数组。
3D坐标—>2D坐标的处理过程是由OpenGL的图形渲染管线管理的。它被分为两部分:
- 3D—>2D
- 2D坐标转变为实际的有颜色的像素
3 管线:
接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
各个阶段:
a: 以数组形式传3个3D坐标作为管线输入,这个数组称为顶点数据。它是一系列顶点的集合。一个顶点是一个3D坐标的数据的集合。而顶点数据是用顶点属性表示的,它可以包含任何我们想用的数据,但是简单起见,还是假定每个顶点只由一个3D位置(译注1)和一些颜色值组成。
注: 为了让OpenGL知道坐标和颜色值构成的到底是什么,OpenGL需要指定这些数据所表示的渲染类型。做出的这些提示叫做图元图元、顶点、片元、像素,任何一个绘制指令的调用都将把图元传递给OpenGL。这是其中的几个:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。
b: 管线的第一部分就是顶点着色器,它把一个单独顶点作为输入。其目的是把3D坐标转化为另一种3D坐标,同时允许为顶点属性进行一些基本处理。
c: 图元装配阶段是将顶点着色器的输出的所有顶点作为输入,将所有的点装配成指定图元形状。
d: 将图元装配阶段的输出传递给几何着色器,它把图元形式的一系列顶点的集合作为输入。它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状,如上图所示。
e: 几何着色器的输出会被传入光栅化阶段,这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段。在片段着色器运行之前会执行裁切,丢弃超出你的视图以外的所有像素,用来提升执行效率。
f: 片段着色器的主要目的是计算一个像素的最终颜色。
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。
顶点输入
渲染一个三角形,需要三个顶点。定义一个float
数组:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
然后将其作为输入发送给管线第一个处理阶段:顶点着色器,它在GPU上创建内存用于存储顶点数据,还需要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着处理内存中指定数量的顶点。
通过**顶点缓冲对象(VBO)**来管理这个内存,它会在显存中储存大量顶点。使用glGenBuffers函数
和一个缓冲ID生成一个VBO对象:
unsigned int VBO;
glGenBuffers(1,&VBO);
如果要生成多个VBO对象 ,用:unsigned int VBO[];
VBO缓冲类型是GL_ARRAY_BUFFER
,OpenGL允许同时绑定多个缓冲,只要它们是不同的缓冲类型。使用glBindBuffer
函数把新创建的缓冲绑定到GL_ARRAY_BUFFER
目标上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
此刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后调用glBufferData函数
:负责把之前定义的顶点数据复制到缓冲的内存中。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData()
是一个专门用来把用户自定义的数据复制到当前绑定缓冲的函数:
- 第一个参数:目标缓冲的类型 VBO绑定到
GL_ARRAY_BUFFER
目标上。 - 第二个参数:指定传输数据的大小(以字节为单位)。
- 第三个参数:发送的实际数据。
- 第四个参数:指定用户希望显卡如何管理给定的数据
GL_STATIC_DRAW:数据不会改变。
GL_DYNAMIC_DRAW:会改变很多。
GL_STREAM_DRAW:每次绘制时都会改变。
VAO:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
顶点着色器(Vertex Shader)
可编程着色器之一,做渲染,OpenGL需至少一个顶点着色器和一个片段着色器,使用GLSL编写顶点着色器。
const char* vertexShaderSource =
"#version 330 core \n" //core代表核心模式,330是GLSL版本号3.3
"layout (location = 0) in vec3 aPos; \n" //location用于设定输入变量的位置值 in是关键字
"void main(){ \n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);}\n" //把位置数据赋值给gl_Position;
in关键字
:顶点着色器中声明所有顶点属性。- 顶点是3D坐标:创建一个
vec3
输入变量apos
。 - gl_Position是
vec4
类型(vec.x, vec.y, vecc.z, vec.w),所以需要转换,将w分量的值设为1.0f。
编译着色器
为了OpenGL能用顶点着色器源码,必须在运行时动态编译他的源码。
首先用glCreateShader
创建一个着色器对象,还是用ID来引用:
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
把需要创建的着色器类型(此时创建的是顶点着色器)以参数形式(GL_VERTEX_SHADER
)传递。接下来把这个着色器源码附加到着色器对象上,然后编译它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
glShaderSource函数
:
- 第一个参数:要编译的着色器对象
- 第二个参数:指定传递的源码字符串数量
- 第三个参数:顶点着色器真正的源码
片段着色器(Fragment Shader)
计算像素最后的颜色输出。
const char* fragmentShaderSource =
"#version 330 core \n "
"out vec4 FragColor; \n "
"void main(){ \n "
" FragColor = vec4(0.0f, 1.0f, 1.0f, 1.0f);} \n "; //RGB+透明度(alpha)
接着就是编译片段着色器了,和编译顶点着色器类似:
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
两个着色器都编译好之后,将两个着色器对象链接到一个用来渲染的着色器程序(Shader Program) 中。
着色器程序(Shader Program)
着色器程序对象(Shader Program Object) 是多个着色器合并之后并最终链接完成的版本,先将刚才编译的两个着色器链接为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。
第一步一样先创建一个程序对象:
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
然后把之前编译的着色器附加到程序对象上,再链接它们:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
得到的结果就是一个程序对象,通过调用glUseProgram函数
,将刚创建的程序对象作为它的参数,用以激活这个程序对象:
gluseProgram(shaderProgram);
现在已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它,但是OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。
链接顶点属性
顶点着色器允许指定任何以顶点属性为形式的输入,但必须手动指定输入数据的每一个部分对应的顶点着色器的顶点属性。顶点缓冲数据会被解析为下面这样子:
- 位置数据被存储为32位(4字节)浮点值
- 每个位置包含三个这样的值
- 每个值之间紧密排列
- 数据中的第一个值在缓冲开始的位置
使用glVertexAttribPointer函数
告诉OpenGL该如何解析顶点数据:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void)*0);
glEnableVertexAttribArry(0);
glVertexAttribPointer
参数:
- 第一个参数:指定要配置的顶点属性。
layout (location = 0)
定义了顶点属性的位置值,location是多少,这个参数就设置为多少。 - 第二个参数:指定顶点属性的大小。这里是
vec3
,三个值组成,所以大小为3 - 第三个参数:指定数据类型,GLSL中
vec*
都是由浮点数值组成的。 - 第四个参数;是否希望数据被标准化。如果设为
GL_TURE
,所有数据都会被映射到0到1之间。 - 第五个参数:步长(Stride)。连续顶点属性组之间的间隔,因为这里设置的是每三个float为一组,所以将步长设置为
3*sizeof(float)
。如果知道数组是紧密排列,也可以设置为0让OpenGL自行判断。 - 第六个参数:
void*
表示位置数据在缓冲中起始位置的偏移量。因为这里位置数据在数组开头,所以为0。
然后使用glEnableVertexAttribArray
,以顶点属性位置值作为参数,启用顶点属性。
最后一步:glDrawArrays函数
,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元.
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
绘制结果
整体代码
#include<stdio.h>
//#include<iostream>
#define GLEW_STATIC
#include<GL/glew.h>
#include<GLFW/glfw3.h>
//#include <glad/glad.h>
void processInput(GLFWwindow* window);
float vertices[] = {
-0.5f, -0.5f, 0.0f, //0
0.5f, -0.5f, 0.0f, //1
0.0f, 0.5f, 0.0f, //2
};
const char* vertexShaderSource =
"#version 330 core \n"
"layout (location = 0) in vec3 aPos; \n"
"void main(){ \n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);} \n";
const char* fragmentShaderSource =
"#version 330 core \n "
"out vec4 FragColor; \n "
"void main(){ \n "
" FragColor = vec4(0.0f, 1.0f, 1.0f, 1.0f);} \n ";
int main()
{
/*实例化glfw窗口*/
glfwInit(); //初始化函数库
/*OpenGL版本号*/
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//Open GLFW Window
GLFWwindow* window = glfwCreateWindow(800, 600, "My OpenGL", NULL, NULL);
if (window == NULL)
{
printf("Open window failed.");
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
//Init GLEW
glewExperimental = true;
if (glewInit() != GLEW_OK)
{
printf("Init GLEW failed.");
glfwTerminate();
return -1;
}
glViewport(0, 0, 800, 600);
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
//渲染循环
while (!glfwWindowShouldClose(window))
{
//input
processInput(window);
//
glClearColor(1.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBindVertexArray(VAO);
glUseProgram(shaderProgram);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}