渲染管线
渲染管线
- 渲染的三个阶段一般分为:
应用阶段
、几何阶段
和光栅化阶段
, 如下图
- 三个阶段的操作对象的流程图可参考下图
应用阶段
- 这一阶段由CPU处理,主要任务是为接下来GPU的渲染操作提供所需要的几何信息,即输出
渲染图元(rending primitives)
以供后续阶段的使用。渲染图元就是由若干个顶点构成的几何形状,点,线,三角形,多边形面都可以是一个图元
数据的准备
- 第一步应先将不需要的数据剔除出去,如以包围盒为单位的视锥体(粗粒度)剔除,遮挡剔除,层级剔除等等
- 第二步根据UI对象在Herachy面板深度值的顺序(DFS深度优先搜索)设置渲染的顺序,其余物体大体可以按照离摄像机先近后远的规则为后续循环绘制所有对象制定排队顺序
- 第三步先将所有需要的渲染数据从硬盘读取到主存中,再把GPU渲染需要用到的数据打包发给显存(GPU一般没有对主存的访问权限,且与显存进行交换速度较快)
- 打包的数据详情见下图
设置渲染状态
- 渲染状态包括着色器(Shader),纹理,材质,灯光等等。
- 设置渲染状态实质上就是,告诉GPU该使用哪个Shader,纹理,材质等去渲染模型网格体,这个过程也就是SetPassCall。当使用不同的材质或者相同材质下不同的Pass时就需要设置切换多个渲染状态,就会增加SetPassCall 所以SetPassCall的次数也能反映性能的优劣。
发送DrawCall
- 当收到一个DrawCall时,GPU会按照命令,根据渲染状态和输入的顶点信息对指定的模(网格)进行计算渲染。
- CPU通过调用图形API接口( glDrawElements (OpenGl中的图元渲染函数) 或者 DrawIndexedPrimitive (DirectX中的顶点绘制方法) )命令GPU对指定物体进行一次渲染的操作即为DrawCall。此过程实质上就是在告诉GPU该使用哪个模型的数据(图形API函数的功能就是将CPU计算出的顶点数据渲染出来)。
在应用阶段有三个衡量性能指标非常重要的名词
DrawCall
CPU每次调用图形API接口命令GPU进行渲染的操作称为一次DrawCall
SetPassCall
设置/切换一次渲染状态
Batch
把数据加载到显存,设置渲染状态,CPU调用GPU渲染的过程称之为一个Batch
注意:一个Batch包含至少一个DrawCall
详情见 UGUI的优化方案(2)
几何阶段
- 几何阶段由GPU进行处理,其几乎要处理所有和几何相关的绘制事情, 如绘制的对象,位置,形状
- 几何阶段处理对象时渲染图元, 进行逐顶点和逐多边形的操作, 主要任务是把顶点坐标变换到屏幕空间中, 以供给接下来的光栅器进行处理
- 具体输出的信息有,变换后的屏幕二位顶点坐标,顶点的深度值,着色,法线等等信息
接下来对几何阶段的主要流水线阶段进行一下解释:
顶点着色器
- 流水线的第一个阶段,可以通过编程进行控制。
- 输入来自CPU发送的顶点信息,每个顶点都会调用一次顶点着色器。其主要工作为:坐标转换和逐顶点光照(可选,计算输出顶点的颜色值)。
- 坐标转换是必须完成的一个任务。它把顶点坐标从模型空间转换到齐次裁剪空间。(齐次裁剪空间不是屏幕空间,是xyz均放缩到-1到1的空间),具体过程可以参考下图
- 注意:此时GPU处理的顶点并不清楚顶点之间的关系,只是无差别的对待每个顶点,能很好的体现各个部件的分离,降低耦合性
- 一句话简单说,确定形状的点
裁剪
- 顾名思义,就是将不需要的数据对象剔除出去的过程。由于场景一般很大,摄像机的视野范围可能不会覆盖所有的场景物体,裁剪就是为了将那些在摄像机视野范围外的物体剔除出去而被提出来的
- 一个图元和摄像机的关系有三种:完全在视野内、部分在视野内、完全在视野外。完全在视野内的就传递给下一个流水线阶段,完全在视野外的就不会向下传递,而部分在视野内的就需要进行一次处理,就是裁剪。具体过程可以参考下图
- 由上图可清楚的看出,除完全在空间内外的图元被保留和舍弃以外,部分在空间内的图元(黄色三角形)会被裁剪,新的顶点将在空间的边界处生成,原来在外部的顶点会被舍弃
屏幕映射
- 通过计算将实际场景的对象映射到屏幕上,本质上就是对坐标的放缩
光栅化阶段
- 此阶段仍然由GPU进行处理。这一阶段将会使用上个阶段传递的数据(屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等。)来产生屏幕上的像素,并渲染出最终的图像。光栅化的主要任务是决定渲染图元中的哪些像素应该被绘制在屏幕上,然后对其颜色进行合并混合。
- 一句话简单说,将图转化为一个个实际屏幕像素
三角形设置
- 主要任务是为后续光栅化提供所需要计算的信息。例如,后续阶段需要判断像素点是否被三角形网格覆盖,只靠上个阶段得到的顶点信息无法确定边界的覆盖情况,还需要三角形网格边的信息,所以在这个阶段需要计算出边的表达式以供后续判断的使用。其输出都是为了给下一阶段做相应的准备。
三角形遍历
- 三角形遍历阶段会根据上一个阶段的计算结果判断一个三角形网格覆盖了哪些像素,并使用三角网格三个顶点的顶点信息对整个覆盖区域进行插值。
- 此阶段会遍历所有的像素点,判断其是否被三角网格所覆盖 (用3-1计算的结果) ,如果被覆盖,则在此像素点上生成一个片元。片元不是单纯的像素点,其还包含很多状态的集合,这些状态用来最终计算检测筛选每个像素点最终的颜色。(部分状态包括:屏幕坐标,深度值,从几何阶段继承来的法线,纹理等等)。
- 片元状态的信息是由其所在三角形网格的三个顶点的信息的插值得到的,例如计算三角形网格重心位置片元的深度。如下图:
- 最终输出的是包含多个片元的片元序列
片元着色器
- 非常重要的可编程着色器阶段。片元着色器的输入是上一个阶段对顶点信息插值得到的结果,输出为每个片元的颜色值。这一阶段可以按需完成很多重要的渲染技术,最重要的技术之一就是纹理采样。
纹理采样
- 为了在片元着色器中进行纹理采样,先在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角形网格的三个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。
- 其局限在于仅可以影响单个片元。即执行片元着色器时,不能将结果直接发给旁边的邻居。片元着色器输出颜色的具体过程如下图
可参考 Unity Texture 基础 中的纹理采样章节
逐片元操作
- 这是OpenGL中的说法,在DirectX中,这阶段被称为输出合并阶段(Output-Merger)。
该阶段是对每一片 片元 进行操作,主要任务有:
- 决定每个片元的可见性,如深度测试、模板测试
- 如果一个片元通过了所有测试,就把这个片元的颜色值和已经存储在颜色缓冲区的颜色进行合并,混合。
该阶段是高度可配置的,我们可以设置每一步的操作细节。该阶段首先解决的是,每个片元的可见性问题。这需要进行一系列测试,只有通过了才能和颜色缓冲区进行合并。若没通过任意一个测试,片元都会被丢弃。
- 测试过程是很复杂的,不同接口实现细节也不同,下面笔者将讲述一些常用的测试
模板测试
- 开启了模板测试,GPU就会使用读取掩码读取模板缓冲区中该片元的模板值,将该值和读取到的参考值进行比较。这个比较函数可以是开发者指定的,例如小于模板值时则舍弃该片元或者大于模板值时舍弃该片元。片元无论有没有通过模板测试都可以根据模板测试和下面的深度测试结果来修改模板缓冲区。这个修改操作也是由开发者指定的。模板测试通常用于限制渲染的区域。
深度测试
- 通过模板测试后,片元就会进行深度测试。其同样是高度可配置的。
- 开启后,GPU会把该片元深度值和已存在与深度缓冲区的深度值进行比较,这个比较函数也是开发者设置的。例如小于缓冲区深度值时舍弃该片元,或者大于缓冲区深度值等于时舍弃该片元。通常人们更希望显示离摄像机最近的物体,所以一般比较函数设置为当前片元深度值要小于缓冲区深度值,深度值大无法通过测试。如果片元没有通过测试,则会被丢弃掉。
- 与模板测试不同,只有通过之后开发者才能指定是否用该片元的深度值覆盖原有缓冲区的深度值。这是通过开启/关闭深度写入做到的
合并操作
- 通过了所有测试后,片元就来到了合并操作。
- 每个像素的信息被存储在一个名为颜色缓冲区的地方,因此执行此次渲染时,颜色缓冲区中往往已经有了上次渲染之后的结果。所以需要合并的方式使其达到一种均衡状态。
- 对于不透明物体,开发者可以选择关闭混合操作。这样片元着色器计算得到的颜色值就会直接覆盖原来颜色缓冲区中的像素值。
- 对于半透明物体,需要使用混合操作来让这个物体看起来是透明的。
- 混合操作也是可以高度配置的。开启了混合,GPU会取出源颜色和目标颜色将两者混合。
- 源颜色是片元着色器得到的颜色,目标颜色是已经存在于颜色缓冲区中的颜色值。
提前测试
- 提前测试的目的主要是为了提高性能。
- 虽然逻辑上这些测试是在片元着色器之后进行的,但对于大多数GPU来说,他们会尽可能在执行片元着色器之前进行这些测试。
- 尽可能早知道哪些片元会被舍弃可以提高性能,比如Unity的渲染流水线中的深度测试就在片元着色器之前。这种将深度测试提前的技术被称为Early-Z技术。
- 但如果将这些测试提前, 检验结果可能会与片元着色器中一些操作产生冲突。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SleepyLoser's Blog!
评论