OGL现代图形(顶点/纹理/状态命令/Shader)和底层渲染顺序(CommandBuffer协同并行/帧前后/drawcall过程)-持续更新

一、OGL显卡中的帧缓存区(非输入的原始顶点和像素数据)

1) 颜色缓存,  左前,右前,左后,右后和任意数量的辅助颜色缓存; OGL 3.1辅助颜色缓存没有,不支持立体观察(VR), 是使用左颜色缓存(启用双缓存是左前,左后颜色缓存,启用单缓存是左前颜色缓存)
2) 深度缓存
3) 模板缓存
4) 累积缓存(累计存储,拷贝回源颜色缓存的,OGL 3.1后已废弃)

OGL中用全局指针和结构体字段接口访问修改API参数

OGL中结构体申请名字,申请空间和填充值,切换绑定对象(类型指针全局唯一,类型结构体众多)
enum { Cube, Cone, NumVAOs };
GLuint VAO[NumVAOs];
glGenVertexArrays(NumVAOs, VAO);
glBindVertexArray(VAO[Cube]);
  glBindVertexArray(VAO[Cone]);


//Create the storage for the object
GLuint objectName;
glGenObject(1, &objectName);

//Put data into the object.
glBindObject(GL_MODIFY, objectName);
glObjectParameteri(GL_MODIFY, GL_OBJECT_COUNT, 5);
glObjectParameterf(GL_MODIFY, GL_OBJECT_OPACITY, 0.4f);
glObjectParameters(GL_MODIFY, GL_OBJECT_NAME, "Some String");

OpenGL拥有所有变量的存储空间。因此,用户只能通过引用来访问这些变量。几乎所有的OpenGL变量都通过无符号整型值(GLuint)来引用。变量通过像glGen*之类的函数创建,这里*是变量的类型。第一个参数是要创建变量的个数,第二个是一个GLuint*的数组用来接受新创建的变量的名字。

要修改这些变量,他们首先必须被绑定到上下文(context)对象。许多的对象可以被绑定到上下文中的不同地址。这样允许同一个对象以不同方式使用。不同的地址称作目标(targets)。所有的变量都有一个合法的目标列表,而有一些仅有一个。在上述例子中,这个虚构的“GL_MODIFY”  就是objectName变量绑定到的地址,GL_MODIFY目标地址类似一个全局指针,所以同一个类型的当前操作缓存指针只有一个。

枚举常量 GL_OBJECT_*表示变量中可以被设置的有名字的值域。glObjectParameter函数族的函数设置绑定到目标的变量的参数。

鉴于OpenGL是C API,因此它需要为不同类型声明不同版本函数。因此glObjectParameteri 对应整数参数,glObjectParameterf 对应浮点型参数版本,依次类推。

OpenGL context

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.


OGL状态机工作模式

几乎所有的OpenGL函数设置或者获取状态。仅有的不改变状态的函数是那些使用当前状态来渲染图像的函数。

你可以将状态机想象成一个有着很多值域的大型结构体。这个结构体称为OpenGL context,其中的每个值域对于渲染图像都有用。

OpenGL中的变量是在结构体中定义的值域列表,他们可以保存和恢复。绑定一个对象到上下文中,导致上下文中的状态被该对象中的数据替换。因此,绑定后,如果有函数调用读取或者修改了上下文的状态将会读取或者修改这个对象的状态。

对象通常有GLuint来代表,他们是实际的OpenGL对象的句柄。0是个特别的变量,它和NULL指针的类似。绑定对象0意味着解除当前绑定对象的绑定,这样绑定之前的状态将会成为当前状态并起作用。

举个例子,下面的context代表OpenGL的上下文状态:

struct Values
{
    int iValue1;
    int iValue2;
};

struct OpenGL_Context
{
    ...
    Values *pMainValues;
    Values *pOtherValues;
    ...
};

OpenGL_Context context;


创建一个Values 对象,你需要调用glGenValues。你可以将 Values 绑定到GL_MAIN_VALUES(代表着context.pMainValues)或者GL_OTHER_VALUES(代表着context.pOtherValues)。你可以调用glBindValues函数来绑定对象,向这个对象传递两个目标之一。这样目标的指针就指向为你创建的那个对象。

这里也会有一个设置对象值得函数,例如, glValueParam。它将对象的目标作为一个参数,这个参数代表了上下文中的指针;同时使用一个代表对象中值域的枚举常量作为参数。枚举常量GL_VALUE_ONE代表iValue1,GL_VALUE_TWO代表iValue2。

OGL Shader对象和Shader着色器程序Program的创建存储在显存中,渲染时可以激活和去激活

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(); 

一个DrawCall(batch)和N个DrawCall的一帧图像

一次图形渲染过程(会渲染一些物体)就是一次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绘制


VBO数据缓存位置和GPU driver command buffer

VBO和数据的存储

没有使用VBO,而是老式的VAO存放数据或者是传统立即模式的那么数据时存储在CPU RAM中的,每次draw call前需要传递数据给GPU(显示列表却不同)。
而使用了VBO, vertex buffer object那么数据储存在CPU RAM,还是AGP RAM,或GPU RAM是根据声明vertex buffer时候的usage flagged来指定的,dynamic类型的会在CPU RAM, managed类型的按照OGL调度来存储,static类型的会常驻VBO。DX中是按照这样的指定来存储的。OGL更加简单些没有暴露更多的存储位置声明的话,那么是Managee类型,调度存储。当在CPU RAM中时候,从CPU发送数据到GPU也不会很慢,因为GPU使用了在OS内核态DMA IO传输模式,不需要中断CPU即可进行。
频繁更新的数据,类似粒子系统,动画数据不要放在GPU端,而尽量在而是用Dynamic(dynamic类似DX的dynamic和Managed)的VBO中,因为只需要每次draw call发送改变的数据到GPU中即可,所以使用VBO是OGL提倡的模式。
Does its data get sent to GPU memory only once and sit there forever?
Usually yes, but the driver is free to do what is "optimal", the data might be stored at VRAM or RAM or could just be cached here is an atricle that explains what actually happens with the VBO flow.
For example if it was flagged as a dynamic openGL buffer (e.g. VBO), it's more likely to be stored in RAM. GPU use direct memory access (DMA) to access the ram directly without the CPU intervention, this is controlled by the DMA controller in the graphics card and graphics driver and is executed in kernel mode.
usage表示数据的读取和写入方式,提供给OGL优化存取。
STREAM 加载一次,并且使用次数较少;STATIC加载一次,可多次频繁使用;DYNAMIC可加载多次(后面加载主要是修改),可多次频繁使用。
DRAW是表示数据从CPU RAM传递到GPU缓存区对象中用于渲染, READ表示数据从GPU缓存区对象读入到CPU中, COPY从GPU缓存区对象读取数据并作为渲染。
例如:
glGenBuffers(1, &vboId);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
// GL_STREAM_DRAW, GL_STATIC_DRAW, GL_STATIC_DRAW

CPU和GPU的生产消费模式协同工作(三个command buffer, GPU两个驱动),单帧内一般不打断

CPU Command Buffer,GPU UMD和KMD中的Command buffer。

图形应用程序系统分为:图形应用程序、RUNTIME、Hardware/software DRIVER和GPU。
RUNTIME有个最重要的Command Buffer将绘图命令转换为与设备无关的命令并存放起来; 一般所有的图形应用程序绘图指令数据(包括变换渲染命令,Shader程序,顶点数据和像素数据),都是先发送到这个缓存而已(单缓存下在draw call打断点可以简单验证,原因是提高协调工作效率(GPU在高速并行计算不应该马上改变绘图指令数据中断GPU),io传输效率; 方便优化渲染指令; 在这里控制阻塞和非阻塞便于协同工作也是glFlush/glFinish C-S架构设计的本意;双缓存下如果GPU处理的是当前帧,那么显示的是当前前一帧,CPU处理的是当前后一帧),当RUNTIME Command Buffer满(一般3个帧的图形指令才会满)或者调用了Present/Flush/SwapBuffer时候或Lock一些影响表面资源时候,才一下全部提交RUNTIME Command Buffer中的图形命令, 提交的图形命令会转换为硬件指令Flush到User Driver然后是Kernal模式下的驱动DRIVER的Command buffer中(DRIVER也有Command Buffer存放硬件绘图指令,Driver还分为UMD(user model driver)对应不同app, KMD(kernal model driver)系统只有一个(尽管是多GPU)他们都是动态库形式的,OS可以终止和重新加载动态库);DRIVER中的指令会被划分为一个个的Draw Call图形指令集合分别驱动GPU产生绘图操作调用对于测定帧率,还要看Present/Flush/SwapBuffer函数是否被阻塞,要看绘图指令数量的多少,如果超过了额度(具体多少不清楚)那么会阻塞会导致CPU逻辑快于GPU渲染,如果没有超过额度那么可以良好协同运行。
这样使用VBO存放数据在GPU中,相比传统的glBegin/glEnd减少了很多指令和数据,可以有效的降低了Runtime, Driver Command buffer中的IO消耗和导致阻塞的可能。

RUNTIME会自己管理VBO缓存存放在RAM, AGP, VAM中的位置,当VBO是Dynamic的时候,就会被RUNTIME调度放置在VAM, AGP,或CPU RAM中的位置。静态的时候RUNTIME就会将VBO放置在VAM中。

绑定一个Buffer就是将该类型的全局指针指向这块数据,供后面使用且不再需要指定了。RUNTIME Command Buffer提交时候,处理每个Draw call会将所有绑定的数据,状态命令,贴图解码传输缓存进行一个渲染一个流水线过程(无论是固定的还是可编程的Shader)。
CPU和GPU之间CPU的Runtime Command buffer, GPU Driver也是有个Command buffer的:
There's sending the data to the GPU and there's setting/binding the buffers as current. Does the latter cause any data flow?
GPUs contain a command buffer, and all API commands are submited to this buffer, notice that this can happen simultaneously with the data being copied to the GPU. The command ring buffer is a communication queue between the CPU and GPU, any command that needs to be executed needs to be submitted to the queue so it can be execulated by the GPU. Just like any operation binding new buffers needs to be submitted to the gpu so it can access some memory location.
That's one of the reasons glBegin/glEnd was deprecated, submitting new commands needs queue synchronization (using memory fences/barriers).
GPU Kernal model driver下的command buffer环形队列图,CPU作为生产者,GPU为消费者(CPU去GPU取数据时候反过来),由KMD中的Command Processor来调度和维护读写指针:
OGL现代图形(顶点/纹理/状态命令/Shader)和底层渲染顺序(CommandBuffer协同并行/帧前后/drawcall过程)-持续更新_第1张图片

CPU和GPU,GPU和GPU之间的同步机制都是: if event X happens, do Y,先做完自己的事情等下才受理新的事务。
如下情况可能清空RUNTIME COMMAND BUFFER,并引起一个模式切换:
1.Lock method(某些条件下和某些LOCK标志)
2.创建设备、顶点缓冲、索引缓冲和纹理
3.完全释放设备、顶点缓冲、索引缓冲和纹理资源
4.调用ValidateDevice
5.调用Present
6.COMMAND BUFFER已满
7.用D3DGETDATA_FLUSH调用GetData函数

D3D中当GPU处理完D3DQUERYTYPE_EVENT类型查询在CB中加入的D3DISSUE_END标记后,会将查询对象状态置SIGNALED状态,所以CPU等待查询一定是异步的。为了效率所以尽量少在PRESENT之前使用BEGINSCENE ENDSCENE对,为什么会影响效率?原因只能猜测,可能EndScene会引发Command buffer flush这样会有一个执行的模式切换,也可能会引发D3D RUNTIME对MANAGED资源的一些操作。而且ENDSCENE不是一个同步方法,它不会等待DRIVER把所有命令执行完才返回。

不合理的 LOCK 会严重影响程序性能,因为一般 LOCK 需要等待 COMMAND BUFFER 前面的绘制指令全部执行完毕才能返回,否则很可能修改正在使用的资源,从 LOCK 返回到修改完毕 UNLOCK 这段时间 GPU 全部处于空闲状态,没有合理使用 GPU CPU 的并行性, DX8.0引进了一个新的LOCK标志 D3DLOCK_DISCARD,表示不会读取资源,只会全写资源,这样驱动和RUNTIME配合来了个瞒天过海,立即返回给应用程序另外块VM地址指针,而原指针在本次UNLOCK之后被丢弃不再使用,这样CPU LOCK无需等待GPU使用资源完毕,能继续操作图形资源(顶点缓冲和索引缓冲),这技术叫VB IB换名(renaming)。

很多困惑来源于底层资料的不足,相信要是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技术。

显卡驱动访问GPU并显示图形工作流程

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开发者更专注,作为图形软件程序员就暂时没有多大必要去了解了。

二、Vertex Shader 变换和光照


顶点Shader从模型坐标系顶点数据,到MVP 4D空间中(ogl 内建的gl_Position位置就是这个4D坐标位置),可以在观察坐标系中进行光照计算,这个步骤为转换和光照(光照需要的表面法线,光源位置,材料属性,环境光等进行实时光照计算,非实时的可以在法线贴图中在片元Shader中进行光照着色),曲面细分Shader可以在这里进行插入。

顶点Shader的输入数据一般是从VAO索引到的VBO中加载的(VBO也在显存中),其中顶点数据是单独并行处理的,单个顶点内部的各种数据(位置,uv, 颜色,法向量,雾坐标,边缘标志)是通过顶点属性索引一个完整的顶点进行处理的(顶点属性索引到的VBO位置可能不连续需要分开读取,并行的从不同地方拿到数据)。


三、图元组装Geometry Shader

所有的图元会在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信息,和顶点信息一起最后形成片元,但是片元是没有着色的。

 五、Fragment Shader 片元像素Shader

基于像素的绘制和基于顶点的绘制在这里汇合(片元信息整合处汇合),后面基于顶点和基于像素的操作会一起进行

像素Shader在片元上进行信息整合为输出像素的计算,位置就不用指定输入了,根据颜色,法向量计算光照法线贴图的计算,uv纹理计算,多重采样抗锯齿计算,雾计算也可以在这里进行。输出片元像素上的颜色, 经过一些列测试才能写入源颜色缓存中,源颜色缓存还要和目标后台颜色缓存进行 混合 抖动 逻辑操作得 目标颜色缓存最新颜色。

在VertexShader和FragmentShader之间:

如果VertexShader中的变量要在FragmentShader中使用相同的名称,那么用smooth修饰。

例如:

const std::string vertexStr(
        "#version 330\n"
        "in vec4 pos;\n"
        "in vec4 incolor;\n"
         "smooth out vec4 thecolor;\n"
        "void main()\n"
        "{gl_Position = pos;\n"
        "thecolor = incolor;}\n"
        );
    const std::string fragmentStr(
        "#version 330\n"
        " smooth in vec4 thecolor;\n"
        "out vec4 outputColor;\n"
        "void main()\n"
        "{ outputColor = thecolor;}\n"
        );
其中, FragmentShader中的thecolor是定点Shader输出后,经过光栅化插值后的片元上的thecolor。

着色器之间共享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)雾的计算在所有着色和光照之后进行。

六、逐像素测试scissor, alpha, stencil ,depth Test是否写入深度模板颜色缓存

接着可能进行的是雾计算(全局雾计算,全局抗锯齿计算可能也在这里进行?)。

然后进行对着色好的像素进行写入颜色缓存区的测试。

测试顺序是:
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:

void  glScissor​(GLint  x​, GLint  y​, GLsizei  width​, GLsizei  height​);


2. alpha 测试 
3. stencil 模版测试
4. depth 深度测试,被遮挡的物体剔除。

如果用ogl 3.1之前的累计缓存区(实时会有大内存开销),会用GL_RETURN 写入到当前的颜色缓存区的源像素中(这里只是过了像素Shader,颜色裁剪,alpha,stencil, depth测试),还需要进行混合抖动逻辑操作与目标颜色缓存区组合得到输出。

七、写入,硬件将源颜色缓存和目标颜色缓存(之前的所有Draw Call本区域结果),进行混合抖动逻辑操作


通过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处理过程见经典的渲染过程图:

OGL现代图形(顶点/纹理/状态命令/Shader)和底层渲染顺序(CommandBuffer协同并行/帧前后/drawcall过程)-持续更新_第2张图片
DX11 shader model 5的Draw call渲染过程:

OGL现代图形(顶点/纹理/状态命令/Shader)和底层渲染顺序(CommandBuffer协同并行/帧前后/drawcall过程)-持续更新_第3张图片

拓展阅读和参考文章:

学习着色器,并理解着色器的工作机制,就要对OpenGL的固定功能管线有深入的了解。

首先要知道几个OpenGL的术语

渲染(rendering):计算机根据模型(model)创建图像的过程。
模型(model):根据几何图元创建的物体(object)。
几何图元:包括点、直线和多边形等,它是通过顶点(vertex)指定的。

最终完成了渲染的图像是由在屏幕上绘制的像素组成的。在内存中,和像素有关的信息(如像素的颜色)组织成位平面的形式,位平面是一块内存区域,保存了屏幕上每个像素的一个位的信息。例如,它指定了一个特定像素的颜色中红色成分的强度。位平面又可以组织成帧缓冲区(framebuffer)的形式,后者保存了图形硬件为了控制屏幕上所有像素的颜色和强度所需要的全部信息。

OpenGL的固定功能管线

理清了基本的概念,下面了解了一些关于OpenGL渲染管线的知识.看了这个之后对于OpenGL的学习我想应当是很有帮助.关于这么一篇的原文则是GLSL-LIGHTSOURCE 教程一个开篇部分.点击这里访问原文。原文是英文的,以下是中文的翻译,点击访问下文的原文地址。

关于渲染管线将什么呢?无非就是在OpenGL的管道当中各个部分的功能以及如何在管道当中形成了我们想要的最终的一幅图.(像素).而管线当中的操作可分为以下几个部分:

阶段1. 指定几何对象.



如:点 线 三角形.等一些几何图元..OpenGL绘制几何图元的方法有以下三种:

  • <1> 一次一个顶点.即使用glBegin()  glVertex() glEnd() 指定几何对象.
  • <2> 使用顶点数组..如glDrawArrays.glDrawElements.等.一次性的绘制大量图元.

上面这两种模式则是立即模式.即指定完图元之后会被立即渲染.即将所有数据发往渲染管线后立即被渲染.

  • <3>显示列表模式.它存储于OpenGL服务端 (接收OpenGL命令的一端),操作函数有 glNewList、 glEndList、 glCallList .

阶段2   顶点处理操作:



不管以上的几何对象是如何指定的,所有的几何数据都将会经过这个阶段,这个阶段负责的则是逐个顶点的操作.

在这个阶段能做的工作则是:

  1.  顶点变换:根据模型视图和投影矩阵变换
  2. 光照计算和法线变换(法线矩阵 是模型矩阵的左上角3*3的逆矩阵)和法线规格化
  3.  纹理坐标变换.(纹理矩阵)
  4. 材质状态:纹理坐标生成

而最重要的则是变换以及光照. 每个顶点在这个阶段分别是单独处理的.

这个阶段所接收到的数据则是每个顶点的属性特征..输出则是变换后的顶点数据.

阶段3  图元组装



在顶点处理之后,顶点的全部属性都已经被确定。在这个阶段顶点将会根据应用程序送往的图元规则如GL_POINTS 、GL_TRIANGLES 等将会被组装成图元。

阶段4 图元处理(裁剪 消隐)




  • <1>这个步骤第一个所做的应当是裁剪操作,会将图元与用户定义的裁剪平面,即glClipPlane 和模型投影矩阵所建立的视景比较. 这将会裁剪且丢弃位于视景和裁剪平面外部的图元.不在予以处理.
  • <2> 其次.若是采用透视投影 那么.将会对每个顶点的x,y z坐标分别除以w.
  • <3>紧接着,则是由视口变换将顶点坐标变换至窗口坐标.
  • <4> 执行消隐操作

阶段5  栅格化操作




  • <1>由图元处理传递过来的图元数据.在此将会被分解成更小的单元并对应帧缓冲区的各个像素.这些单元被称之为片元. 一个片元可能包含窗口左边、深度、颜色、纹理坐标等属性.
  • <2> 片元的属性则是图元上顶点数据等经过插值而确定的..这里生成的片元将会包含主颜色和次颜色.   glShadeMode() 函数的作用将会这里体现.即使用插值(平滑着色) 或者使用最后一个顶点颜色(平面着色)
  • <3> 点宽 线宽.多边形模式,正面背面等一些特征也将是这阶段发生作用.
  • <4> 反走样也是这个阶段起作用.

阶段6 片元处理




  • <1>上纹理:通过纹理坐标取得纹理内存中相对应的颜色。
  • <2> 雾化:通过片元距离当前视点位置修改颜色.
  • <3> 颜色汇总..这个与混合完全不同概念.将纹理,主定义的颜色,雾化的颜色,次颜色光照阶段计算的颜色 汇总一起.

阶段7  逐个片元的操作




  • <1> 所有的一些测试 像素所有权 剪切(glScissor) Alpha测试(glAlphaFunc) 模版测试(glStencilFunc) 深度测试 (glDephtFunc) 混合(glBlendFunc)

这些操作将会最后影响其在帧缓冲区的颜色值.

阶段8  帧缓冲操作




  • <1>这个阶段执行帧缓冲的写入等操作等..最后产生了显示出来的像素.

glColorMask、glStrncilMask、glDepthMask、glClearDepht、glClearStencil、glClearColor 等.将在这个阶段影响写入的值.

以上只是讨论OpenGL 图元绘制的基本过程 那么基于像素图像绘制.几乎形同之上..只是在光栅化处理前的操作不一样.即经过像素解码 像素传输.栅格化 最后形成片元...片元之后的处理完全一样..

可编程管线可以替换的功能

在着色器编程领域..你将可实现

  • Vertex Shader(顶点着色器) 替换 顶点处理阶段
  • Fragment Shader(片元着色器,又叫像素着色器) 替换 片元处理阶段
  • Geometry Shader(几何着色器) 替换 图元组装阶段..

因为这三个阶段所决定都是最重要效果的阶段..对于这些的可编程将带来非常大的好处以及可控制的渲染!!

在前面的固定功能管线提到了,在阶段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://traxnet.wordpress.com/2011/07/16/understanding-modern-gpus-1/
https://traxnet.wordpress.com/2011/07/18/understanding-modern-gpus-2/

https://fgiesen.wordpress.com/category/graphics-pipeline/

你可能感兴趣的:(OpenGL图形学)