OpenGL实现了渲染管线(rendering pipeline),实际上是一系列处理数据的过程,将应用程序的数据转换到最终渲染的图像。OpenGL首先接收用户提供的几何数据(顶点数据和图元数据,一般是顶点数据),将其输入到一系列的着色器中进行处理,这些着色器一共有6个阶段,一般刚需的有两个着色器:点着色器(vertex shader)和片元着色器(fragment shader)。其它四个分别是:细分着色器(其中就包含两个着色器)以及几何着色器。着色器处理中间还有图元装配的过程。经过数据处理后以后,数据将会被输入到光栅化单元(rasterizer),光栅化单元负责对所有裁剪区域内的图元生成片元数据,然后对所有的片元执行片元着色器。下面是OpenGL管线的流程框架:
void glNamedBufferStorage (GLuint buffer, GLsizeiptr size, const void *data, GLbitfield flags);同时设置缓存的大小和内容。
缓存经过初始化(创建对象和分配内存),顶点数据就需要传输到OpenGL的缓存中进行存储。我们将一个顶点视为需要处理的数据包(应该是对每个顶点定义属性数据),通常始终都会包含position数据,其它的属性数据用来决定顶点最终呈现在屏幕上的颜色数据。
对于绘制命令传输的每个顶点,OpenGL都会调用一个顶点着色器来对顶点进行数据处理,在顶点着色器阶段顶点会经过一系列的空间变换转换为齐次坐标。
顶点着色器处理了每个顶点关联数据之后,若果被激活了的话,细分着色器将会进一步处理数据,细分着色器会使用面片(patch)来描述物体,将会使用相对简单的面片几何体连接完成细分,结果将使得几何图元数量增加,模型会更加平滑。
如果被激活的话,几何着色阶段允许光栅化之前对每个图元更进一步处理。
这些顶点构成的几何图元的所有信息也会被传输到OpenGL,图元装配阶段将这些顶点与相关的几何图元之间组织起来,准备下一阶段的裁剪和光栅化工作。
裁剪以后接下来需要对顶点和图元数据进行光栅化操作,将数据传输到光栅化单元,生成相应的片元,光栅化做了什么工作呢?判断每个几何体所覆盖的屏幕像素位置,一个片元可以视为一个候选的像素,也就是可以被放置到帧缓存中的像素。
最后一个阶段是片元着色,片元着色器是可编程控制屏幕上显示颜色的着色器。
最后一步就是独立片元处理过程,这个阶段片元会经过多个测试:深度测试,模板测试,以及融混等操作;如果一个片元经过了所有的测试就可以直接被绘制到帧缓存了。
接下来我们就可以开始码代码了,上来一个简单的OpenGL主程序:
int main( int argc, char** argv )
{
glfwInit();//初始化GLFW库,这个函数必须是所有GLFW函数调用之前调用
GLFWwindow* window = glfwCreateWindow(800, 600, "Triangles", NULL, NULL);//创建窗口
glfwMakeContextCurrent(window);//创建了一个与某窗口关联的OpenGL环境,实际上我们也可以创建多个窗口关联多个OpenGL环境,而用户所做的所有操作指令都只会传到当前设备环境,应用程序中每个线程都会对应一个当前设备环境
gl3wInit();//辅助库用来简化获取函数地址的过程,比如加载动态链接库啥的。
init();//真正的OpenGL程序的所有相关数据的初始化
while (!glfwWindowShouldClose(window))//这里我们需要设置一个无限循环,一直等待处理用户输入或者窗口操作等,这里的判断条件就是一直等候是否关闭当前窗口
{
display();//这就是场景绘制部分代码入口函数
glfwSwapBuffers(window);//我们需要双缓冲技术来加速帧率
glfwPollEvents();//检查操作系统返回的任何信息
}
glfwDestroyWindow(window);//销毁掉已经退出的窗口
glfwTerminate();//停止并退出GLFW库
}
以上就是一个OpenGL的主程序的框架,几乎所有的OpenGL都是这样的框架,下面我们需要做一些花花架子啦!
下面是最重要的OpenGL整个程序的数据初始化的代码:(这就是最简单的初始化过程啦)
void init()
{
glGenVertexArrays(1,vao); //创建顶点数组对象
glBindVertexArray(vao[0]);//绑定顶点数组对象到当前OpenGL环境
struct VertexData//定义一个顶点信息包结构体,其中包含了position和color属性信息
{
GLubyte color[4];
GLfloat position[4];
};
VertexData vertices[6] = {//定义应用程序客户端的顶点数据
{{255,0,0,255},{-0.9,-0.9}},
{{0,255,0,255},{0.85,-0.9}},
{{0,0,255,255},{-0.9,0.85}},
{{10,10,255,255},{0.9,-0.85}},
{{100,100,100,255},{0.9,0.9}},
{{255,255,255,255},{-0.85,0.9}}
};
glGenBuffers(1,vbo);//创建缓存对象
glBindBuffer(GL_ARRAY_BUFFER,vbo[0]);//绑定缓存对象到当前目标GL_ARRAY_BUFFER,也就是说对象绑定到数组缓目标上。
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);//这时候就该传送数据了,实际上这个函数的目的是为将要传送到OpenGL硬件服务端显存的顶点数据分配显存空间以及将数据从应用程序客户端对应的计算机内存传送到OpenGL显存的一个函数
ShaderInfo shader[] = {//这个ShaderInfo的数组就是存储各个着色器的程序的,这些着色器程序里面的代码是以字符串的形式存储的
{GL_VERTEX_SHADER,"gouraud.vert"},//这是最重要的也是第一个进行处理的顶点着色器
{GL_FRAGMENT_SHADER,"gouraud.frag"},//片元着色器
{GL_NONE,NULL}//这句话主要为了LoadShaders函数中读取着色器方便用的
};
GLuint program = LoadShaders(shader);//这个函数就是我们自定义的一个函数,用来加载着色器程序的,返回一个GLuint变量,用来标识程序的id
glUseProgram(program);//这个标识加载了这个程序,也就是开始将这个着色器程序投入使用
glVertexAttribPointer(1,4,GL_UNSIGNED_BYTE,GL_TRUE,sizeof(VertexData),BUFFER_OFFSET(0));//设置顶点属性,其设置的目标(是顶点位置属性还是颜色属性)主要和第一个参数,也就是这个属性在显存中的保存位置决定的,主要和着色器中的变量数据进行交互
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(VertexData),BUFFER_OFFSET(sizeof(vertices[0].color)));//设置颜色属性
glEnableVertexAttribArray(1);//使能这个顶点属性设置(序号为1的是color,我猜测就是根据数据排列,在VertexData中color排列第一位,position排列第二位)
glEnableVertexAttribArray(0);//position排列第二位
}
下面我们看看这个OpenGL渲染程序过程的数据处理是怎样的:
1、初始化顶点数组对象
init()函数初始化了几乎所有的应用程序数据,我们需要创建顶点数组对象,并且将它绑定到OpenGL环境中。
2、分配顶点缓存对象
顶点数组对象负责管理(书上说‘保存’,还是管理比较合适?)一系列的顶点数据,这些顶点数据保存在缓存对象中,并且有当前的顶点数组对象进行管理,几乎所有的数据传入到OpenGL都是保存在缓存对象中的。对于缓存管理,我们需要创建缓存对象名称,利用以下函数:
glGenBuffers(1,vbo);
分配了顶点缓存对象名称后,就可以用glBindBuffer(GL_ARRAY_BUFFER,vbo[0])来将缓存对象绑定到OpenGL环境,绑定缓存对象要指定其绑定类型名称,这里是GL_ARRAY_BUFFER,也称为绑定目标。
3、数据加载到缓存
创建并绑定缓存对象之后,需要让OpenGL分配缓存对象所管理的显存(缓存/帧缓存)空间,并且需要将顶点数据传输到缓存对象中,可以通过函数:
glNamedBufferStorage (GLuintbuffer, GLsizeiptrsize, constvoid *data, GLbitfieldflags);
去实现这个过程,它完成的任务是①分配顶点的缓存空间②将数据从应用程序的数组拷贝到OpenGL服务端内存中。
4、初始化顶点与片元着色器
OpenGL渲染管线中必不可少的两个着色器是顶点和片元着色器。我们需要将着色器程序加载并存储到一个结构体对象ShaderInfo中,我们是通过字符串的形式将GLSL语言传输到OpenGL。
总说的要写shader呢,shader程序长什么样子呢?下面我们来看看最简单的顶点着色器,也称为传递着色器。
#version 400 core //指定GLSL语言的版本 使用核心模式
layout( location = 0 ) in vec4 vPosition;
void
main()
{
gl_Position = vPosition;
}
layout( location = 0 ) in vec4 vPosition;
这一句代码就蕴含很多深意,in关键字表示该变量是输入变量,vec4表示变量类型,vPosition的’v’前缀表示该变量是一个顶点属性,所以该变量所保存的信息是顶点位置属性信息,layout( location = 0 )就是布局限定符,目的是为变量提供元数据,也就是该变量的值在缓存中的位置(有待考量),类似的有片元着色器。
#version 450 core
Layout(location = 0)out vec4 fColor;
void main()
{
fColor = vec4(0.5, 0.4, 0.8, 1.0);
}
Layout(location = 0)out vec4 fColor;
这句代码也有深意,它使用了out限定符,表示输出数据,fColor中的’f’表示这个变量是片元属性,该变量就是输出片元的颜色了。而且它也有布局限定符。
最终的代码,设置顶点属性的函数glVertexAttribPointer指定了顶点着色器变量与我们存储在缓存对象中数据的关系,也就是着色器管线装配的过程,将应用程序与着色器以及不同的着色器之间的数据通道连接起来。使用glVertexAttribPointer将着色器中的变量关联到一个顶点属性数组。
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(VertexData),BUFFER_OFFSET(sizeof(vertices[0].color)));
第一个参数index代表顶点着色器中输入变量location的值,也就是布局限定符0的指定指定的color的值。size就是每个顶点属性的元素数目,每个顶点有2个元素,也就是说position的坐标信息只有x,y坐标两个值。最后一个参数pointer指的是数据从缓存对象的第一个字节开始读。初始化数据的最后一项工作就是开启顶点属性设置,glEnableVertexAttribArray可以完成。将glVertexAttribPointer初始化的属性数组指针索引传入该函数,这两个顶点属性数组设置函数设置的状态保存到最开始绑定好的顶点数组对象中的。
下面就是开始渲染咯,首先我们需要清除帧缓存的数据,也就是上次渲染的图像要清除了,才能绘制这次的,不然就像素重叠了啊。
void display( void )
{
static const float black[] = { 0.0f, 0.0f, 0.0f, 0.0f };
glClearBufferfv(GL_COLOR, 0, black);
glBindVertexArray(vao[0]);
glDrawArrays( GL_TRIANGLES, 0, NumVertices );
}
清除帧缓存的函数是glClearBufferfv,这里可以清除三种缓存,GL_COLOR、GL_DEPTH、GL_STENCIL,实际上就是颜色缓存、深度缓存、模板缓存。
接下来就要真正开始绘制啦,glBindVertexArray用来选择作为顶点数据进行绘制的顶点数组数据,因为顶点数组对象时管理顶点数组数据的变量,然后就调用glDrawArrays来实现顶点数据传输到OpenGL。
然后就结束啦,这时候就是让程序跑起来看渲染结果的时候。
纯属自己记录学习,可能比较呆板抄书,也可能菜鸡,有什么错误请评论哈,一起学习交流!