【OpenGL】理解一些基本问题


写在前面


啦啦啦,搞了很久的Unity Shaders,越学越觉得基础知识很重要。学Unity Shader的时候,总会想,shader到底是什么呢?shader的pipeline是什么呢?它们是怎么工作的?有哪些限制?等等问题。但这些问题,Unity是不负责告诉你的。它专注于how,而不是what和why。想要深入理解一些问题,感觉还是要从GL或者DX学起。后面会学习GL龙书第八版~当然Unity我也不会放弃的。


这篇文章旨在回答一些基本问题。We always rant about them...



什么是OpenGL


这个问题很简单,它就是应用程序接口,也就是API,用于访问图形硬件中的可编程特性。OpenGL和DX相比有一个很大的特点就是跨平台的特性。换句话说,它是不依赖硬件的接口,可以运行在各种不同类型的图形硬件系统上,甚至完全是一个软件(而没有图形硬件)。


OpenGL是一种客户端-服务器(client-server)类型的系统。我们编写的程序就是一个客户端,而我们的计算机图形硬件制造商提供的OpenG的实现就是服务器。在一些OpenGL的实现里(例如一些和X Window System相关的应用),客户端和服务器可能会在不同的机器上运行,中间用网络连接。在这种情况下,客户端可以发起OpenGL命令,然后转换成窗口系统特定的协议,再发送给服务器,最终在服务器上执行OpenGL进行图像显示。



为什么OpenGL不提供窗口操作


这个问题我经常会问。。。为什么写个GL还要用这么多第三方库!连个窗口都不能自己画吗!这其实不能怪OpenGL,这正是它的优点——跨平台的特点造成的。因为它可以不依赖硬件和系统,因此就不会包含执行窗口任务的函数,或者处理用户输入等。这些函数是由我们使用的应用或系统来提供。



为什么OpenGL没有读取三维模型或者图片的函数


我以前经常抱怨,发展这么多年的OpenGL,怎么连读取三维模型这么简单的画图需求都不提供呢!!!好吧,这也是它的跨平台特性造成的。和上一点一样,这些操作是和系统存储格式密切相关的,OpenGL不管的~我们必须从点、线、三角形和patches这样的几何图元集合中自己构建三维对象。



什么是Shader


这是一类在图形硬件上执行的特殊函数。我们可以理解成,Shader是一些为图形处理单元(GPU)编译的小程序。OpenGL包含了编译工具来把我们编写的Shader源代码编译成可以在GPU上运行的代码。在OpenGL中,我们可以使用四种shader阶段。最常见的就是vertex shaders——它们可以处理顶点数据;以及fragment shaders,它们处理光栅化后生成的fragments。


vertex shaders和fragment shaders是每个OpenGL程序必不可少的部分。



Shader有什么用


请直接看下一节~



OpenGL的渲染流水线(Rendering Pipeline)


渲染流水线,就是一系列有序的处理阶段的序列,用于把我们应用中的数据转化到OpenGL生成一个最终的图像。下图是OpenGL4.3使用的流水线。(跟早期的相差很大)(左图来源:OpenGL Wiki)


【OpenGL】理解一些基本问题_第1张图片      



上图中,蓝色的方块表示是可编程的shader阶段。


OpenGL从我们提供的几何数据(顶点和几何图元)出发,首先使用了一系列shader阶段来处理它:vertex shading,tessellation shading(它本身就包含了两种shaders),接着是geometry shading,然后再传递给光栅化程序(rasterizer);光栅化程序将会为每个在裁剪区域(clipping region)内部的图元生成fragments,然后再为每个fragment执行一个fragment shader。


如你所见,shaders真是无处不在啊!不是所有的阶段都是需要的。如上面所说,只有vertex和fragment shaders是我们必须实现的。Tessellation和geometry shaders都是可选的。


下面,我们对每个阶段进行更深入地解释。下面的内容难度系数五颗星(对新手。)!但是,请坚持看下去!它们很重要!


Vertex Specification


准备顶点数组数据(vertex array data)


即进行顶点规格定义。应用将会建立一个有序的顶点列表,然后发送给OpenGL。这些顶点定义了图元的边界。图元是基本的绘制形状,如点、线、三角形。这些顶点列表是如何被组织成一个个图元的会在后面的阶段里进行处理。


这个阶段会处理一些顶点数组对象(Vertex Array Objects)顶点缓存对象(Vertex Buffer Objects)。VAO定义了每个顶点包含的信息,而VBO则是顶点本身的数据所在。


一个顶点的数据就是一系列顶点属性(vertex attributes)。每一个attribute都是一个数据的集合,用于后面的阶段进行处理。虽然这些attributes定义了一个顶点,但没有要求说一个顶点的attributes集合中必须包含了位置和法线信息。实际上,attribute数据是完全任意的,它们仅仅是“数据”,在这个准备阶段不会有人在意你传递的到底是什么attribute,它们的真正含义会在顶点处理阶段进行解读。


OpenGL要求所有的数据都必须存储在缓存对象中(buffer objects)。缓存对象是OpenGL管理的一些内存空间。“想要让我办事,请先把你的东西放到我的地盘!”把数据放到这些缓存里有很多方法,但最常见的是使用glBufferData()来实现。当然这里面还有一些其他步骤,我们后面会讲到。



向OpenGL发送数据


在我们定义了顶点相关信息后,我们可以通过调用OpenGL的绘图操作来要求按一个个几何图元绘制到屏幕上。这些操作例如有glDrawArrays()。我们后面会讲到。这个绘制的过程意味着我们把顶点数据传递给OpenGL服务器。



Vertex Processing


对于通过绘制命令绘制的每一个顶点,OpenGL将调用一个vertex shader来处理关于这个顶点的相关信息。Vertex shader接受从上个阶段传递来的attribute作为输入,然后把每一个输入顶点转换成一个输出顶点(这个关系是1对1的,不会多也不会少)。和输入的顶点信息不同,输出的顶点数据有一些必需的要求——vertex shader必须填充一个位置信息。


Vertex shaders的复杂性可以变化非常大,有的很简单,就是仅仅复制数据然后传递给下一个流水线阶段,我们称这种为pass-through shader;有的很复杂,会执行很多操作来计算顶点的屏幕位置(通常使用变换矩阵来完成,后面会讲到),还可能会进行光照计算来计算顶点的颜色,或者其他技术。


通常,一个应用会包含多个vertex shader,但同一时间只有一个会被激活(active)。



Primitive Assembly


Primitive assembly就是把vertex shader输出的顶点数据集合在一起,并把它组合成一个图元的过程。用户渲染的图元的类型决定了这个过程是如何工作的。


这个过程的输出是一个有序的简单图元(点、线或者三角形)序列。


Tessellation Shading


Tessellation,读 泰斯类什,可以翻译成曲面细分。在vertex shader处理了每一个顶点的相关信息后,如果tessellation shader阶段被激活的话,它就会继续处理这些数据。在后面我们会看到tessellation使用patchs来描述一个对象的形状,并且允许细化(tessellate)相对简单的patch集合,来增加几何图元的数量,提高模型的平滑度和真实度。Tessellation Shading阶段可以使用两个shaders来控制patch数据,以及中间一个固定函数的tessellator来生成最终的形状。


更多内容可以参见这篇文章(虽然是DirectX的。。。)



Geometry Shading


这一阶段允许在光栅化之前处理单独的几何图元,包括创建新的图元。这一阶段同样是可选的,但是会很有用!后面会讲到。


Geometry shader会处理每一个输入的图元,然后返回0个或更多的输出图元。它的输入是primitive assembly的输出图元。因此如果我们按triangle strip看待一个图元,那么geometry shader看到的就会使一系列三角形。


然而也有一些输入的图元类型是专门为geometry shaders定义的。这些相邻的图元可以让GS了解关于相邻顶点的信息。


GS的输出可以是0个多更多的简单图元。GS可以移除图元,或者根据一个输入图元来输出更多的图元来细分(tessellate)它们。GS甚至可以改变图元的类型,比如把点图元编程三角形,把线图元变成点。



Transform Feedback


Geometry shader或者primitive assembly的输出会被写入一系列的缓存对象。这被称为transform feedback模式。它允许我们通过vertex和geometry shaders来变换数据,然后再等待后续使用。


通过舍弃光栅化的结果,流水线可以在这步就停止了。这允许transform feedback成为渲染的唯一输出。



裁剪(Clipping)和剔除(Culling)


然后就进入图元裁剪和适当的剔除阶段了。


裁剪以为着,如果有图元处于视野的边界上,即一部分在内部一部分在外部,那么它就会被裁剪成一些小的图元。而且,vertex shader可以在空间内定义一些裁剪平面,这些裁剪平面又会引起额外的裁剪。


三角面的剔除同样在这一阶段完成。处于视野范围以外,或者在裁剪平面的边界内部的图元,都会被提出。



光栅化(Rasterization)


在裁剪完成后,更新后的图元就会被发送给光栅化程序去生成fragments。那么什么是fragment呢?一个fragment可以看成是一个“候选像素”。这类像素在帧缓存中的一块区域中。一个fragment仍可以被拒绝(reject),并且永远不会更新它的相关像素位置。


Wiki上把fragment成为是一个状态的集合,用于计算一个像素的最终数据。一个fragment的状态包含了它在屏幕空间的位置信息,样本覆盖(sample coverage,如果开启了multisampling的话),以及一些其他由vertex或者geometry shader输出的数据。这些数据集合是通过该fragment对应的顶点数据进行插值计算而得的。这个插值计算是由输出这些数据的shader定义的。


处理fragments是后面两个阶段的任务——fragment shading和per-fragment操作。



Fragment Processing


最后一个我们可以编程控制颜色的阶段就是fragment shading。在这个阶段,我们使用一个shader来决定该fragment的最终颜色(其实下个阶段,per-fragment操作仍可以进行最后的颜色修改)和它的深度值(depth value)。在fragment shaders里我们可以进行非常强大的纹理映射的工作。如果一个fragment shader认为某个fragment不应该绘制出来,它还可以终结一个fragment的处理过程。这个过程称为fragment discard。


一个fragment shader会输出一个颜色列表、一个深度值和一个stencil值。Fragment shaders不可以为一个fragment设置stencil,但是它们可以控制颜色和深度值。


我们可以想来区分vertex shading(包括tessellation和geometry shading)和fragment shading:vertex shading决定了一个图元在屏幕上的位置,而fragment shading使用这些信息来决定该fragment的颜色。



Per-Fragment操作


从fragment处理器输出的fragment数据会再通过一系列的步骤。


第一个步骤就是各种剔除检验(culling tests)。如果开启了stencil test,如果一个fragment没有通过检验它就会被剔除,而不会写入到帧缓存中;如果开启了depth test,如果一个fragment没有通过检验它就会被剔除,而不会写入到帧缓存中。只要没有通过任何一个检验,fragments都会被剔除,不会添加到帧缓存中。


如果一个fragment成功通过了所有的检测,它就会直接写入帧缓存中,更新它的像素颜色(也可能是深度值)。如果blending被开启了,该fragment的颜色会和当前的像素颜色进行混合去产生一个新的颜色,再写入帧缓存中。


最后,fragment数据被写入帧缓存中。Masking operation允许用户避免写入特定的值。写颜色、深度和stencil都可以被mask成on或者off;单独的颜色通道也可以。


更多内容请参考OpenGL Wiki。





你可能感兴趣的:(【OpenGL】理解一些基本问题)