写在前面
啦啦啦,搞了非常久的Unity Shaders。越学越觉得基础知识非常重要。
学Unity Shader的时候,总会想。shader究竟是什么呢?shader的pipeline是什么呢?它们是怎么工作的?有哪些限制?等等问题。但这些问题。Unity是不负责告诉你的。它专注于how,而不是what和why。想要深入理解一些问题,感觉还是要从GL或者DX学起。
后面会学习GL龙书第八版~当然Unity我也不会放弃的。
这篇文章旨在回答一些基本问题。
We always rant about them...
什么是OpenGL
这个问题非常easy,它就是应用程序接口,也就是API。用于訪问图形硬件中的可编程特性。OpenGL和DX相比有一个非常大的特点就是跨平台的特性。
换句话说,它是不依赖硬件的接口,能够运行在各种不同类型的图形硬件系统上,甚至全然是一个软件(而没有图形硬件)。
OpenGL是一种client-server(client-server)类型的系统。我们编写的程序就是一个client。而我们的计算机图形硬件制造商提供的OpenG的实现就是server。在一些OpenGL的实现里(比如一些和X Window System相关的应用),client和server可能会在不同的机器上运行。中间用网络连接。在这样的情况下,client能够发起OpenGL命令,然后转换成窗体系统特定的协议,再发送给server,终于在server上运行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)
上图中,蓝色的方块表示是可编程的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()。我们后面会讲到。
这个绘制的过程意味着我们把顶点数据传递给OpenGLserver。
Vertex Processing
对于通过绘制命令绘制的每个顶点,OpenGL将调用一个vertex shader来处理关于这个顶点的相关信息。Vertex shader接受从上个阶段传递来的attribute作为输入,然后把每个输入顶点转换成一个输出顶点(这个关系是1对1的。不会多也不会少)。和输入的顶点信息不同。输出的顶点数据有一些必需的要求——vertex shader必须填充一个位置信息。
Vertex shaders的复杂性能够变化非常大,有的非常easy,就是仅仅复制数据然后传递给下一个流水线阶段,我们称这样的为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。