概述
通过之前的教程,我们已经拥有了开发环境,但是在真正开发程序之前,我们首先了解下Opengl的基本概念。
Opengl是什么?
通常网上会说Opengl是一种规范,一种接口,但是这种说法有点抽象,我们不妨先看看下面这个简单的gl流程
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
using namespace std;
int main()
{
//glfw的初始化和设置
// -----------------------------------------------------
//调用glfwInit函数来初始化GLFW
glfwInit();
//配置GLFW,第一个参数代表选项的名称,第二个参数接受一个整型,用来设置这个选项的值
//此处设置表示使用的OpenGL版本号3.3
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
//glfwWindowHint作为窗口创建的一种提示,可以设置窗口的多种属性,包括透明度等等,感兴趣的可以在glfw文档中查询
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
//使用流水线配置模式
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//glfw窗口创建
// -----------------------------------------------------
//glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数,第三个参数表示这个窗口的名称,最后两个参数我们暂时忽略,它返还一个GLFWwindow对象
GLFWwindow* window = glfwCreateWindow(800, 800, "OpenGL", NULL, NULL);
//创建完窗口,通知GLFW将我们窗口的上下文设置为当前线程的主上下文了
glfwMakeContextCurrent(window);
while (!glfwWindowShouldClose(window))
{
//OpenGL采用双缓冲来渲染窗口
glfwSwapBuffers(window);
//处理按键事件
glfwPollEvents();
}
return 0;
}
代码中可能有人对GLFW_OPENGL_PROFILE这类参数感到疑惑,或者对glfwWindowHint的更多设置感兴趣,你可以在glfw窗口官方文档中具体了解
运行代码,会获得一个如图的结果
在上面流程中,我们通过glfwInit()、glfwWindowHint()、glfwCreateWindow()等等就创建了一个窗口对象,并没有写具体逻辑,也就是说,我们是通过调用一系列的接口去实现了这个窗口对象,这是opengl作为接口的解释。
那么具体逻辑是谁实现的呢?我们首先要了解一个背景,Khronos组织一直在维护opengl接口,如果Khronos定义了一个glDrawLIne的接口,那么支持opengl的GPU设备厂商会让自己的程序员在硬件产品上用图像逻辑去实现这个接口,而当前主流的GPU设备厂商都是支持opengl的。
所以是Khronos和GPU设备厂商帮我们实现了具体逻辑,而我们要做的就是遵循这一系列接口的规范,去得到想要的内容。
Opengl的流程
在理解opengl的概念后,可能大家对GPU和opengl的关系有所了解,接下来不妨看看下面这张图。
opengl渲染管线
简单来说opengl的流程,首先调用opengl的接口,将Attributes、textturedata、uniform等数据输入到vetexshader等着色器中,如果是attributes数据,顶点着色器会处理它,并把结果流转到下一个primitive assembly中实现光栅化,紧接着配合textturedata通过片元着色器进行处理,最终渲染出结果,你可以对照着opengl渲染管线图进行参考(本文流程中并未介绍几何着色器,因为它并非是必须的,也许以后我们会详细解释它)。
opengl常见的名词
这里提及到着色器,光栅化等词汇,这些都是opengl常见的名词,我们不妨先了解下其含义:
Attributes | 一种变量类型,用来接收经常发生改变的数据,比如颜色数据、顶点数据、纹理数据、光照法线等;Attributes只能传给顶点着色器里面,不能直接传递到片元着色器,需要通过GLSL代码间接传递给片元着色器。 |
着色器(Shader) | 在GPU上执行的单独程序,用来处理顶点和执行光栅化任务。 |
GLSL | 全称,( openGL Shading Language,Opengl shader语言,一种语言规范,就像C++一样,它属于一种语言,shader在GPU上运行去处理Attributes、Uniforms等数据,那么shader就是通过GLSL编写。 |
Uniforms | 一种变量类型,用来接收比较统一,不经常发生改变的数据,比如旋转矩阵、视频的颜色空间YUV数据;Uniforms既可以传给顶点着色器也可以传递给片元着色器里面。 |
顶点(Vertex) | OpenGL顶点是4个分量(x, y, z,w),w为0时代表空间中的点,w为1时代表方向,x,y,z为三维空间坐标数据。 |
纹素(texture element) | 全程纹理元素,纹素可以由图像范围来定义,它是计算机图形纹理空间中的基本单元。如同图像是由像素排列而成,纹理是由纹素排列表示的。 |
纹理数据(Texture Data) | 简单理解可以视它为一张图片数据,模型往往会贴图,那么TextureData能够配合片元着色器决定纹理图像中的哪一个纹素赋予哪个顶点。 |
顶点着色器(Vertex Shader) | 可以接收Uniforms、Attributes、Texture Data等数据,它用来处理顶点坐标,例如模型的三维空间坐标的缩放、移动等。(Texture Data通常直接装配到fragment shader处理) |
图元(Primitives) | 一维或二维的实体或表面(点,直线,多边形)。 |
光栅化(Rasterization) | 将一个图元转变成一个二维图像的过程,简单来说就是把三维世界中的物体转换成屏幕上像素的过程。 |
图元装配(Primitive Assembly) | 图元装配有两部分,第一步进行装配,装配即将顶点着色器输出的所有顶点作为输入,将所有的点装配成指定图元的形状,第二步实现光栅化,将图元转换为一组二维片元。 |
片元(Fragment ) | 二维图像上每个点都包含了颜色、深度和纹理数据。将该点和相关信息叫做一个片元,在opengl流程中片元代表可以在屏幕上绘制的像素。 |
片元着色器(Fragment Shader) | 可以接收由光栅化阶段生成的每个片元数据和纹理数据,用来计算出每个像素的最终颜色。 |
在了解了一些opengl名词的含义和opengl的流程后,可能大部分人对GLSL,着色器等概念有点困惑,可以先看看下面这个实际运用的例子。
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
using namespace std;
//Shader创建
// -----------------------------------------------------
//GLSL编写的用于顶点着色器的shader
const char* vertexShaderSource = "#version 330 coren"
"layout (location = 0) in vec3 aPos;n"
"void main()n"
"{n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);n"
"} ";
//用于片段着色器的shader,rgba格式
const char* fragmentShaderSource = "#version 330 coren"
"out vec4 FragColor;n"
"void main()n"
"{n"
" FragColor = vec4(0.41f, 0.35f, 0.80f, 1.0f);n"
"}n ";
//顶点数据
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.4f, 0.0f,
-0.4f, -0.5f, 0.0f
};
//索引数据
unsigned int indices[] = {
0, 2, 3,
1, 4, 5
};
int main()
{
//glfw创建
// -----------------------------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 800, "OpenGL", NULL, NULL);
glfwMakeContextCurrent(window);
//glad初始化
// -----------------------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
//着色器创建和链接
// -----------------------------------------------------
//顶点着色器创建
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
//片段着色器创建
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
//链接着色器
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
//创建数据对象和绑定数据对象
// -----------------------------------------------------
//顶点数组对象:Vertex Array Object,VAO
//顶点缓冲对象:Vertex Buffer Object,VBO
//索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO
unsigned int VBO, VAO, EBO;
//unsigned int VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
//绑定VAO数据对象
glBindVertexArray(VAO);
//复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//复制索引数组到缓冲中供OpenGL使用
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), 0);
//启用顶点属性
glEnableVertexAttribArray(0);
//解绑VAO
glBindVertexArray(0);
while (!glfwWindowShouldClose(window))
{
//按照rgba格式设置窗口背景颜色
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
//将当前设置的color写入启用的缓冲区
glClear(GL_COLOR_BUFFER_BIT);
//加载shdaer
glUseProgram(shaderProgram);
//绑定VAO数据对象
glBindVertexArray(VAO);
//获取索引,根据GL_TRIANGLES类型绘制图像
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();
}
return 0;
}
执行结果:
上文代码实现的流程,简单的将opengl渲染过程进行了介绍,窗口的创建、shader的编写、着色器的创建及链接、VAO,VBO,EBO创建及绘图。
Opengl功能分析
窗口的创建在本文开头已提过
shader的编写
shader是采用GLSL语言编写的,你可能发现了这个语法和C语言非常类似,而且是用字符串格式标注的,简单理解可以说是这样的,在此基础上多了一些例如:opengl的版本声明,数据location声明,及专用属性,in,out、vec3等等,同时shader实际上是运行在GPU里的,这意味着,我们无法编写shader的log,但是我们可以在shader编译的时候进行错误检查,具体方法会在以后补充。
着色器的创建及链接
着色器的创建及编译
前文提到的着色器,写成代码,还是比较容易理解,通过专用的语法glCreateShader及参数,我们便创建shader及获取其对应的id,通过对id操作glShaderSource,我们便将前文编写的shader和对应着色器的进行了绑定,仅仅绑定还不够,还需要glCompileShader来进行编译。
在opengl中,我们有且必有的两个着色器:顶点着色器和片段着色器(又名:片元着色器),两者都是通过相同的三步:glCreateShader、glShaderSource、glCompileShader进行创建。
着色器的链接
我们创建并编译好了顶点着色器和片段着色器,接下来我们还需要将它们合并链接到着色器程序对象,我们在opengl中进行的着色器处理,使用的就是着色器程序对象(需要注意的是,着色器的链接到着色器程序对象有严格顺序,上一个着色器的输出,对应下一个着色器的输入,数据对不上就会出错!),着色器程序对象的创建与着色器类似,通过glCreateProgram创建并获取id,根据id,我们使用glAttachShader将各个着色器对象附加上去,最后通过glLinkProgram完成整体链接。
总结来讲通过三步:glCreateProgram、glAttachShader、glLinkProgram
VAO,VBO,EBO/IBO创建及绘图
VAO,全称Vertex Array Object,顶点数组对象,VBO全称Vertex Buffer Object,顶点缓冲对象,EBO全称Element Buffer Object,IBO全称Index Buffer Object,都指索引数组对象
在opengl中,我们创建好着色器程序对象,紧接着就需要输入数据,数据的类型如何定义呢?
顶点坐标数据
opengl中的顶点坐标采用的是3D坐标,简单来讲就是空间直角坐标系,对应x,y,z三个方向,你可能发现代码中我们实际顶点数据为(x,y,z,w),w向量是用于透视法处理,作为基础知识,就不展开详谈,但以后我们会具体介绍,如代码中,vertices实际上就装载了我们要绘制的各个点坐标数据,z置为0,就是2D平面坐标,对应平面直角坐标系(需要注意的是,opengl中坐标范围为0-1,所有输入的坐标都需要进行归一化处理)
索引数组数据
代码中的indices数组,可能有人会对此有疑惑,这就是索引点,每一列数据如(0,1,2),对应着顶点坐标数据中的第一行,第二行,第三行数据,这样opengl绘制的时候就会去寻找这三行数据对应的顶点按照绘制图像进行链接,以此类推,我们绘制下一个图像可以反复使用,例如(0,1,3),openlg绘制就寻找第一行,第二行,第四行数据,索引数组在实际项目中经常使用。
VBO的含义及创建流程
我们定义的数组,必须绑定到VBO中才能输入到着色器中
创建流程:我们通过glBindBuffer来绑定对象类型及地址,然后使用glBufferData,将值输入到VBO中
VAO的含义及创建流程
在实际项目中,我们不可能仅仅绘制两个图像,数量过大的时候,每次绘制一个图像,我们都需要实现VBO的调用,绘制会非常复杂,而VAO可以保存VBO对象,比如VAO1对应VBO1,VAO2对应VBO2,在绘制的时候,我们调用VAO1,VAO2...我们就可以将对象的VBO数据都绘制出来
创建流程:通过glGenVertexArrays创建VAO1,再在需要绑定的VBO1之前,使用glBindVertexArray绑定VAO1,这样VAO1会对应上后文的VBO1,在绘制的时候,直接调用glBindVertexArray便调用了VBO1的内容
glVertexAttribPointer函数
glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据,该函数默认是关闭的,需要通过glEnableVertexAttribArray来开启,其参数对应关系如下:
glEnableVertexAttribArray(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0)
第一个参数 0 | 指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0 |
第二个参数 3 | 指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。 |
第三个参数 GL_FLOAT | 指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。 |
第四个参数 GL_FALSE | 定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。 |
第五个参数 3 *sizeof(float) | 步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。 |
第六个参数 (void*)0 | 它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。 |
EBO/IBO的含义及创建流程
索引数组对象是实际开发中,我们经常使用的对象,顶点坐标数据可能有几万,或者几十万,这仅仅指的是各个独立点的个数,实际绘制的时候,我们可能需要连接第1、2、3这三个点,然后连接1、2、4这三个点,在代码中我们使用了glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);来告诉opengl我们的顶点数据三个一组,进行后文的glDrawElements中的GL_TRIANGLES以三角形绘制,这里有个问题,opengl仅知道使用三个点,以三角形绘制,那么当绘制有重复点的三角形的时候,它需要重复输入这些顶点数据,我们的顶点数据例如:(0.1f, 0.2f, 0.3f),每多输入一个顶点,就需要多一组顶点数据,在几十万个点的模型中,可能导致多上十几万个点的数据,这将导致内存空间膨胀,和计算过程复杂,所以我们需要引入索引数组对象,它能够告知opengl如何重复利用顶点数组对象里的数据。
其创建流程与VBO类似:通过glGenBuffers创建EBO及获取id,通过glBindBuffer和glBufferData,复制索引数组到缓冲中供OpenGL使用,使用glDrawElements,告知opengl以索引绘制图像。
总结:本文是对opengl整体流程的简单介绍,通过代码来描述,并具体介绍opengl的相关图形知识,便于初学者对opengl整体上有大概印象,后面的opengl知识会在此基础中进行深度和广度的扩展。
最后
以上就是英俊招牌为你收集整理的Opengl入门基础-基础知识Opengl功能分析的全部内容,希望文章能够帮你解决Opengl入门基础-基础知识Opengl功能分析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复