1) 颜色缓存, 左前,右前,左后,右后和任意数量的辅助颜色缓存; OGL 3.1辅助颜色缓存没有,不支持立体观察(VR), 是使用左颜色缓存(启用双缓存是左前,左后颜色缓存,启用单缓存是左前颜色缓存)
2) 深度缓存
3) 模板缓存
4) 累积缓存(累计存储,拷贝回源颜色缓存的,OGL 3.1后已废弃)
OpenGL拥有所有变量的存储空间。因此,用户只能通过引用来访问这些变量。几乎所有的OpenGL变量都通过无符号整型值(GLuint)来引用。变量通过像glGen*之类的函数创建,这里*是变量的类型。第一个参数是要创建变量的个数,第二个是一个GLuint*的数组用来接受新创建的变量的名字。
要修改这些变量,他们首先必须被绑定到上下文(context)对象。许多的对象可以被绑定到上下文中的不同地址。这样允许同一个对象以不同方式使用。不同的地址称作目标(targets)。所有的变量都有一个合法的目标列表,而有一些仅有一个。在上述例子中,这个虚构的“GL_MODIFY” 就是objectName变量绑定到的地址,GL_MODIFY目标地址类似一个全局指针,所以同一个类型的当前操作缓存指针只有一个。
枚举常量 GL_OBJECT_*表示变量中可以被设置的有名字的值域。glObjectParameter函数族的函数设置绑定到目标的变量的参数。
鉴于OpenGL是C API,因此它需要为不同类型声明不同版本函数。因此glObjectParameteri 对应整数参数,glObjectParameterf 对应浮点型参数版本,依次类推。
OGL context代表了当前的OGL进程中的一个线程渲染实例集合(一个线程渲染实例渲染到一个帧缓存中,而不是帧缓存对象),集合内包含了这个OGL实例所有的图形数据集合,像素数据集合,渲染状态集合,GLSL显存着色器命令集合,和相关OGL硬件抽象对象例如设备对象等;Context销毁了一个OGL实例就销毁了。OGL进程可以拥有多个OGL Context,context之间可以共享非容器的数据类型例如同步对象,GLSL对象。OGL Context是线程安全的,当前OGL Context是线程本地化的,不能由多个线程共享,所以Unity中都是单线程渲染。
一个渲染流水线只有一个ogl-context所以直接绑定glBindVertexArray,GL_ARRAY_BUFFER,GL_ELEMENT_ARRAY_BUFFER就可以绑定到全局的指针中。在多GPU中,多GPU会抽象为一个逻辑显卡和OGL Context对应,具体在驱动层分配工作。
An OpenGL context represents many things. A context stores all of the state associated with this instance of OpenGL. It represents the (potentially visible)default framebuffer that rendering commands will draw to when not drawing to a framebuffer object. Think of a context as an object that holds all of OpenGL; when a context is destroyed, OpenGL is destroyed.
Contexts are localized within a particular process of execution (an application, more or less) on an operating system. A process can create multiple OpenGL contexts. Each context can represent a separate viewable surface, like a window in an application.
Contexts can share many kinds of objects between each other. Any OpenGL object types which are not containers are sharable, as well as Sync Objects and GLSL Objects (excluding program pipeline objects). All container objects are not shared between contexts.
Any object sharing must be made explicitly, either as the context is created or before a newly created context creates any objects. However, contexts do nothave to share objects; they can remain completely separate from one another.
In order for any OpenGL commands to work, a context must be current; all OpenGL commands affect the state of whichever context is current. The current context is a thread-local variable, so a single process can have several threads, each of which has its own current context. However, a single context cannot be current in multiple threads at the same time.
几乎所有的OpenGL函数设置或者获取状态。仅有的不改变状态的函数是那些使用当前状态来渲染图像的函数。
你可以将状态机想象成一个有着很多值域的大型结构体。这个结构体称为OpenGL context,其中的每个值域对于渲染图像都有用。
OpenGL中的变量是在结构体中定义的值域列表,他们可以保存和恢复。绑定一个对象到上下文中,导致上下文中的状态被该对象中的数据替换。因此,绑定后,如果有函数调用读取或者修改了上下文的状态将会读取或者修改这个对象的状态。
对象通常有GLuint来代表,他们是实际的OpenGL对象的句柄。0是个特别的变量,它和NULL指针的类似。绑定对象0意味着解除当前绑定对象的绑定,这样绑定之前的状态将会成为当前状态并起作用。
举个例子,下面的context代表OpenGL的上下文状态:
创建一个Values 对象,你需要调用glGenValues。你可以将 Values 绑定到GL_MAIN_VALUES(代表着context.pMainValues)或者GL_OTHER_VALUES(代表着context.pOtherValues)。你可以调用glBindValues函数来绑定对象,向这个对象传递两个目标之一。这样目标的指针就指向为你创建的那个对象。
这里也会有一个设置对象值得函数,例如, glValueParam。它将对象的目标作为一个参数,这个参数代表了上下文中的指针;同时使用一个代表对象中值域的枚举常量作为参数。枚举常量GL_VALUE_ONE代表iValue1,GL_VALUE_TWO代表iValue2。
GLuint shader = glCreateShader(eShaderType);//根据类型创建shader
glShaderSource(shader, 1, &strFileData, NULL);//绑定shader对象到Shader代码字符串
glCompileShader(shader);//编译shader对象
// 检查编译的Shader编译是否成功
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE)
{ }
//构造着色器程序对象
GLuint programId = glCreateProgram();//创建program
//绑定多个shader到着色器程序
glAttachShader(programId, shaderList[iLoop]);
//链接shader
glLinkProgram(programId);
// 检查Shader程序对象链接是否成功
GLint status;
glGetProgramiv(programId, GL_LINK_STATUS, &status);
if (status == GL_FALSE)
{ }
// 得到着色器内部的变量
offsetLocationId = glGetUniformLocation(programId, "offset");
// Draw call渲染时候激活着色器程序
glUseProgram(programId);
// 更新着色器中的变量
glUniform2f(offsetLocationId, fXOffset, fYOffset);//偏移量发送到顶点着色器
// 指定解析VBO数据
glBindBuffer(GL_ARRAY_BUFFER, vboId);
//启用顶点位置属性索引
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
// 绘制几何图元
glDrawArrays(GL_TRIANGLES, 0, 3);
// 去激活着色器程序
glBindBuffer(GL_ARRAY_BUFFER, 0);
glUseProgram(0);
glDisableVertexAttribArray(0);
glutSwapBuffers();
一次图形渲染过程(会渲染一些物体)就是一次Draw call(dx ogl等底层api draw函数的调用)也叫一个batch, 一个draw call走完整个图形流程。顶点变换和光照,组装,材质贴图像素资源的解码传输,顶点和纹理的光栅化,片元着色,各种测试,融合抖动逻辑运算写入,会消耗很多时间。所以尽量减少draw call,(减少绘制的面片是从一次的量的维度),可以有效提高性能,blend 融合操作是在不同的draw call之间的,所以减少透明物体的使用,进行透明物体的排序,可以有效的减少draw call。
一帧图像包含了N个draw call调用绘制成的图像的集合,一帧图像开始的标志是绘制循环的开始,结束是device->present, glFlush, gluSwapBuffer,提交后台颜色缓存中的一帧图像到前台颜色缓存进行video monitor绘制。
CPU Command Buffer,GPU UMD和KMD中的Command buffer。
很多困惑来源于底层资料的不足,相信要是MS开放D3D源码,开放驱动接口规范,NV / ATI显示开放驱动和硬件架构信息,这些东西就很容易弄明白了。
图形API(OGL, DX)封装了显卡驱动层(或者调用了驱动层)来完成绘图指令发送和控制GPU的运作。
显卡驱动分为UMD(user model driver), KMD(kernal model driver) ring buffer就是在KMD中的。
图形绘图的顺序是:App, App Runtime command buffer(flush/present, 满,Lock, create buffer时候提交), Api User model driver buffer, Api Kernal model driver ring buffer and GPU Command processor(负责调度装载数据和翻译Command,Shader等指令), GPU High Compute execute。
DMA(direct access memory)直接内存访问,不需要频繁的中断CPU,而是DMA掌控这IO总线。无论是写入CPU RAM还是写出都需要先获得DMA控制权,然后进行数据的传输。
PCIE(PCI Express)总线,英特尔提出的新一代IO传输标准,现在新的设备都有PCIE插槽但是老设备很多不支持,PCIE总线专门是因为图形数据的传输效率不满足要求而提出的下一代IO技术。
1)初始化CPU, API RUNTIME,UMD, KMD, IO,GPU:
显然是驱动封装了GPU硬件接口指令集合,CPU初始化GPU进行图形绘制时候会中断GPU(KMD)关联初始化一个API Runtime, 驱动的UMD, 在CPU和GPU,IO端都初始化所需要的资源(例如用MMIO 映射GPU的空间到CPU RAM中);
2)写入CB(直接或MMIO),中断,传输,写入GPU寄存器(命令和顶点纹理数据):通过中断GPU(提交一次Runtime Command buffer时候)通过PCIE总线(PCI Express 代替了PCI AGP IO接口,原因是能提供双通道的,专属的线路,高速传输数据(声卡,网卡,磁盘也逐渐使用PCIE,总线都是由DMA控制器掌控), MMIO也是通过该总线)发送数据包(指令集和顶点相关纹理数据)到GPU提供的寄存器写入指令集和装载数据。
3)GPU在GPU KMDriver的Command processor下,它的工作模式下,对每一个Draw call 装载顶点相关数据,解码纹理资源,对顶点进行变换&光照, 图元组装,裁剪透视除法设备规范化,视口转换背面消隐;顶点和纹理数据进行光栅化,Fragment Shader, 图元测试,图元融合到后台缓存,当所有Draw call完成就完成了一帧图像绘制提交监视器渲染;然后CPU, GPU, 显示器继续协同工作。
4)CPU读取,GPU通过DMA方式不用中断CPU,可以写入数据到CPU内存中,但是读取还是会比写入的速度慢。
1)中断初始化外设相关资源;
2)CPU端写入,DMA传输数据到外设寄存器,先写入到CPU缓存中,当刷新或缓存满或其它相关事件发生(Batch处理,GPU和磁盘,网络都是有Buffer队列 方便协同并行工作),而且双方都有最近传输的缓存区避免不必要的传输(磁盘特别有用,GPU在GPU端缓存,网络数据可能例外), 如果发送的数据对自己或对方没有缓存那么中断外设KMD(kernal model driver)将指令和数据通过数据包的形式传输写入外设寄存器;
3)外部设备用自己的KMD调度程序(两个设备可以抽象为一个逻辑实体),进行数据解析,指令翻译,进入自己的处理流程(GPU是draw call流程,磁盘是寻道旋转写入或读取传输),很可能也是若干个处理流程完成(例如多个draw call)一个总的流程(例如一帧),然后进行数据的扬声器输出,显示器输出,磁盘文件输出,网络数据包发送。
4)外设用中断或不中断(阻塞或不阻塞)的方式通知CPU,请求任务执行完毕(GPU flush可能阻塞或不阻塞看数据量,磁盘控制器会中断CPU)。
重复执行上述的2)3)4)并行工作(释放CPU和外设无用的等待),直到结束与外设的通信。
从设备写入数据到CPU中类似上述流程,也要经过获取DMA 控制IO所有权,初始化,写入外设,中断传输,写入CPU,DMA调度处理,读入到CPU中.
关于硬件底层更多的细节那是硬件工程师和OS开发者更专注,作为图形软件程序员就暂时没有多大必要去了解了。
顶点Shader从模型坐标系顶点数据,到MVP 4D空间中(ogl 内建的gl_Position位置就是这个4D坐标位置),可以在观察坐标系中进行光照计算,这个步骤为转换和光照(光照需要的表面法线,光源位置,材料属性,环境光等进行实时光照计算,非实时的可以在法线贴图中在片元Shader中进行光照着色),曲面细分Shader可以在这里进行插入。
顶点Shader的输入数据一般是从VAO索引到的VBO中加载的(VBO也在显存中),其中顶点数据是单独并行处理的,单个顶点内部的各种数据(位置,uv, 颜色,法向量,雾坐标,边缘标志)是通过顶点属性索引一个完整的顶点进行处理的(顶点属性索引到的VBO位置可能不连续需要分开读取,并行的从不同地方拿到数据)。
所有的图元会在4D裁剪空间中(2D透视是在ndc坐标空间中了)这里进行组装,组装时候可以进行几何重新划分,曲面细分。
在顶点处理之后,顶点的全部属性都已经被确定。在这个阶段顶点将会根据应用程序送往的图元规则如GL_POINTS 、GL_TRIANGLES 等将会被组装成图元。
Geometry Shader(几何着色器) 替换 图元组装阶段
图元组装后,才进行裁剪,视口转换,到达屏幕坐标后便于光栅化处理。
硬件在裁剪4D坐标空间中进行视锥裁剪,OGL中x, y, z值都要满足[-w,w]中值才能保留(DX是z属于[0,w]);接着硬件进行/w透视除法到3D DNC 规范化坐标系中(正方体空间中);然后硬件进行视口转换到屏幕坐标系中,深度写入深度缓存。在屏幕坐标中,光栅化之前进行背面消隐(有的背面消隐是在摄像机坐标空间中关照计算和裁剪之前进行)。
硬件对屏幕上的顶点进行光栅化插值(根据着色模式,直线宽度,点的大小,是否光栅化启用了抗锯齿计算方式),形成片元,每个片元在光栅化插值后,有自己的片元信息(位置,颜色,法向量,光照材质信息,uv或多重纹理uv,雾等), 但是这些片元是没有进行着色的。
像素处理:光栅化之前需要,进行纹理贴图装配好传输到了GPU显存中,或者从显存中拷贝纹理像素数据到了当前使用的纹理贴图对象中,这里是一个非常大数据量的操作。需要经过像素解码 像素传输,像素光栅化(栅格化)取得每个片元上的uv信息,和顶点信息一起最后形成片元,但是片元是没有着色的。
基于像素的绘制和基于顶点的绘制在这里汇合(片元信息整合处汇合),后面基于顶点和基于像素的操作会一起进行。
像素Shader在片元上进行信息整合为输出像素的计算,位置就不用指定输入了,根据颜色,法向量计算光照法线贴图的计算,uv纹理计算,多重采样抗锯齿计算,雾计算也可以在这里进行。输出片元像素上的颜色, 经过一些列测试才能写入源颜色缓存中,源颜色缓存还要和目标后台颜色缓存进行 混合 抖动 逻辑操作得 目标颜色缓存最新颜色。
在VertexShader和FragmentShader之间:
如果VertexShader中的变量要在FragmentShader中使用相同的名称,那么用smooth修饰。
例如:
着色器之间共享uniforms
只设置了一个fElapsedTime,会在两个着色器中生效吗?
OpenGL编译模型的一大优势就是,在连接顶点和片元着色器时把他们集成到一个对象中去时,名称和类型相同的uniform变量将会被连接起来。因此,这里也就只有一个fElapsedTime 的uniform变量,它即指向两个着色器中的uniform变量(即共享同一个uniform变量)。这一特性的负面是,如果你在一个着色器中创建了一个与另一个着色器中同名但类型不同的uniform变量,那么OpenGL在产生程序对象时会给出链接错误。而且,偶然将两个uniforms链接成一个也是有可能的。在我们的案例中,给两个着色器的Loop duration取了两个不同的名字,就是为了避免共享该变量。
更新VBO中的数据,可以CPU计算结果传递给Shader, 也可以传递基本的参数给GPU,让GPU端在Shader中计算结果,具体看具体场景和数据量。
GLSL中的全局变量可以使用几种限定符来定义:const
,uniform
,in
, 和 out
.
const变量就像C99和C++中工作一样,他们保持不变,他们必须被初始化;
没有限定符的变量像C/C++里一样工作,他们是全局变量,可以被更改;
GLSL着色器可以调用函数,全局变量可以再函数之间共享。
但是,不像in、out和uniforms,非常量和常量在渲染各个阶段之间不可共享。
Fragment末尾的其它重要操作:
1)抗锯齿,多重纹理采样的抗锯齿操作,应该也是在Fragment Shader后对纹理进行多重采样(启用了alpha混合,那么抗锯齿也会影响混合阶段操作)。
2)镜面高光的辅助颜色,在FragmentShader后进行颜色组合。
3)雾的计算在所有着色和光照之后进行。
接着可能进行的是雾计算(全局雾计算,全局抗锯齿计算可能也在这里进行?)。
然后进行对着色好的像素进行写入颜色缓存区的测试。
测试顺序是:
1. scissor 裁剪测试 // 只是自定义的矩形裁剪,
To activate the scissor test, first enable the GL_SCISSOR_TEST enumerator. Once enabled, pixels outside of the scissor box will be discarded. To define the scissor box, use this function:
2. alpha 测试
3. stencil 模版测试
4. depth 深度测试,被遮挡的物体剔除。
如果用ogl 3.1之前的累计缓存区(实时会有大内存开销),会用GL_RETURN 写入到当前的颜色缓存区的源像素中(这里只是过了像素Shader,颜色裁剪,alpha,stencil, depth测试),还需要进行混合抖动逻辑操作与目标颜色缓存区组合得到输出。
通过sissor test, alhpa test, stencil test, depth test后的像素(不通过直接丢弃该draw call的该位置的像素),模板缓存会直接替换写入;深度缓存也会直接替换写入;颜色缓存却可能进行如下的处理和当前不同位置的源像素或目标像素(之前draw call在帧缓存中的)进行组合写入。
5. 混合:模板缓存区和深度缓存区通过掩码直接覆盖写入,颜色混合 写入后台缓存区
操作对象是当前源像素,和颜色缓存中的目标像素(上一次draw call在目标缓存区形成的数据),已经在一次Draw call以外。如果太多的透明物体,u3d提交数据时候会分开为几个draw call,因此大大提高draw call的开销。
6. 抖动
抖动允许只有少量离散颜色的显示系统来模拟更宽范围的颜色(不是一个像素,而是一个像素块中混合不同的颜色的像素实现想要的颜色)。例如,灰色可以通过白点和黑点的混合来模拟。白点多于黑点呈现浅灰色,黑点多于白点呈现深灰色。这种技巧对于只支持8位和16位的显示系统非常有用。抖动的效果可以大幅度地改善低端颜色系统的图像质量。在默认情况下,抖动是打开的。可以通过glEnable(GL_DITHER)/glDisable(GL_DITHER)来打开或关闭它。在高颜色分辨率的显示系统中,OpenGL的实现可能不需要抖动,会禁用抖动来避免性能的开销。
7. 各种写入掩码和逻辑操作
glColorMask、glStrncilMask、glDepthMask
void glLogicOp(GLenum opcode);
选择需要执行的逻辑操作.
整个draw call处理过程见经典的渲染过程图:
DX11 shader model 5的Draw call渲染过程:
学习着色器,并理解着色器的工作机制,就要对OpenGL的固定功能管线有深入的了解。
渲染(rendering):计算机根据模型(model)创建图像的过程。 模型(model):根据几何图元创建的物体(object)。 几何图元:包括点、直线和多边形等,它是通过顶点(vertex)指定的。
最终完成了渲染的图像是由在屏幕上绘制的像素组成的。在内存中,和像素有关的信息(如像素的颜色)组织成位平面的形式,位平面是一块内存区域,保存了屏幕上每个像素的一个位的信息。例如,它指定了一个特定像素的颜色中红色成分的强度。位平面又可以组织成帧缓冲区(framebuffer)的形式,后者保存了图形硬件为了控制屏幕上所有像素的颜色和强度所需要的全部信息。
理清了基本的概念,下面了解了一些关于OpenGL渲染管线的知识.看了这个之后对于OpenGL的学习我想应当是很有帮助.关于这么一篇的原文则是GLSL-LIGHTSOURCE 教程一个开篇部分.点击这里访问原文。原文是英文的,以下是中文的翻译,点击访问下文的原文地址。
关于渲染管线将什么呢?无非就是在OpenGL的管道当中各个部分的功能以及如何在管道当中形成了我们想要的最终的一幅图.(像素).而管线当中的操作可分为以下几个部分:
如:点 线 三角形.等一些几何图元..OpenGL绘制几何图元的方法有以下三种:
上面这两种模式则是立即模式.即指定完图元之后会被立即渲染.即将所有数据发往渲染管线后立即被渲染.
不管以上的几何对象是如何指定的,所有的几何数据都将会经过这个阶段,这个阶段负责的则是逐个顶点的操作.
在这个阶段能做的工作则是:
而最重要的则是变换以及光照. 每个顶点在这个阶段分别是单独处理的.
这个阶段所接收到的数据则是每个顶点的属性特征..输出则是变换后的顶点数据.
在顶点处理之后,顶点的全部属性都已经被确定。在这个阶段顶点将会根据应用程序送往的图元规则如GL_POINTS 、GL_TRIANGLES 等将会被组装成图元。
这些操作将会最后影响其在帧缓冲区的颜色值.
glColorMask、glStrncilMask、glDepthMask、glClearDepht、glClearStencil、glClearColor 等.将在这个阶段影响写入的值.
以上只是讨论OpenGL 图元绘制的基本过程 那么基于像素图像绘制.几乎形同之上..只是在光栅化处理前的操作不一样.即经过像素解码 像素传输.栅格化 最后形成片元...片元之后的处理完全一样..
在着色器编程领域..你将可实现
因为这三个阶段所决定都是最重要效果的阶段..对于这些的可编程将带来非常大的好处以及可控制的渲染!!
在前面的固定功能管线提到了,在阶段5:栅格化操作 过程中, 片元的属性会由图元上顶点数据等经过插值而确定。在顶点着色器处理完毕后,OpenGL都会将顶点与顶点之间的片元(基本上可以理解为像素)的属性(如位置坐标、纹理坐标)进行线性插值。所以,在纹理坐标为(1,0)和(0,0)中间的片元会得到一个(0.5,0)的纹理坐标,在纹理坐标为(0,0)和(1,1)之间的片元会得到一个(0.5,0.5)的纹理坐标。然后将这些经过差值处理之后的片元交给片元着色器处理。片元着色器确定最终的片元颜色。
原文地址http://guzhou.me/glsl%E5%AD%A6%E4%B9%A0%E7%AC%AC%E4%B8%80%E8%AF%BE%EF%BC%9Aopengl%E7%9A%84%E6%B8%B2%E6%9F%93%E7%AE%A1%E7%BA%BF/
https://fgiesen.wordpress.com/2011/07/01/a-trip-through-the-graphics-pipeline-2011-part-1/
https://fgiesen.wordpress.com/2011/07/02/a-trip-through-the-graphics-pipeline-2011-part-2/
https://fgiesen.wordpress.com/category/graphics-pipeline/