注:
1.本篇文章仅作为个人学习总结与知识梳理备份之用,旨在记录个人在技术学习过程中的所思所悟,其内容不具备教学意义,不保证内容的完整性、准确性及适用性,读者在参考时应谨慎判断。
2.本文中可能会引用他人制作的图片、PPT 等资料。若存在侵权行为,请与我私信联系,我将在核实后第一时间删除侵权内容。
目录
一、渲染管线的定义
渲染管线(Rendering Pipeline),也称为渲染流水线,是计算机图形学中用于生成二维图像以表现三维场景的处理流程。它描述了一系列连续的步骤,这些步骤从原始的三维模型数据出发,经过一系列计算和处理,最终转化成能在屏幕上看到的像素。
《Real-Time Rendering, Fourth Edition》一书中,定义渲染管线的基本结构包含一下四个阶段:应用阶段、几何处理阶段、光栅化阶段和像素处理阶段。其中,应用处理阶段主要在CPU上运行,另外三个阶段在GPU上运行。
二、应用阶段
应用阶段主要在CPU上运行,为GPU的渲染管线做准备工作。主要功能有:
1.数据准备与管理
CPU从硬盘等存储设备读取场景所需的各类数据,如3D模型文件(包含顶点、面片等几何信息)、纹理贴图、Shader等,并将这些数据加载到系统的主内存(RAM)中。为了使GPU能够高效地访问和处理这些数据,CPU需要将加载到RAM中的数据传输到显存(VRAM)中。显存是专门供GPU使用的高速存储设备,其访问速度远高于RAM,能够满足GPU在渲染过程中对数据的快速读取需求。CPU还负责计算MVP矩阵、对纹理贴图进行预处理(如进行压缩、格式转换和生成MipMap、RipMap贴图等)。这些数据也将一并被打包传输到显存之中。
同时,CPU还会进行视椎体剔除,通过判断物体的AABB包围盒与视锥体的相交情况,来决定是否进行保留,以减少需要处理的图元数量,显著减少GPU的工作量,提高渲染效率。
2.设置渲染状态
《UnityShader入门摘要》一书中,通俗的解释渲染状态就是定义了场景中的网格是怎样被渲染的,例如,使用哪个顶点\片元Shader、光源属性、材质,渲染到哪个目标上等多种渲染状态。而Shader中又包含渲染队列(RenderQueue)、渲染类型(RenderType)等诸多Tags对渲染状态进行更详细的设置。
3.调用Draw Call
Draw Call实际上就是一个命令,它的发起方是CPU,接受方式GPU,这个Draw Call会指向本次调用需要渲染的图元列表,然后GPU就会根据传输过来的数据和渲染状态进行计算,最终输出屏幕上显示的像素。
优化Draw Call:在渲染管线的应用阶段,CPU通过合并相似物体、使用实例化渲染以及优化渲染命令组织来减少Draw Call数量,从而降低CPU和GPU之间的通信开销,提高渲染效率。具体来说,CPU会将具有相同材质、纹理和渲染状态的物体合并为一个渲染批次,通过一次Draw Call完成它们的渲染;对于具有相同几何形状但不同属性的物体,采用实例化渲染技术,允许GPU在一次Draw Call中渲染多个实例,每个实例的属性通过顶点或实例化属性指定;此外,CPU还会合理安排渲染顺序,按物体属性排序以减少状态切换,按距离摄像机的远近排序以及时进行深度测试和剔除操作,避免不必要的渲染计算。
三、几何阶段
CPU传输数据到GPU上后,首先会进入几何阶段。几何阶段主要负责处理和转换几何图元(如点、线、三角形等),并为后续的光栅化阶段准备数据。几何着色器主要分为以下阶段:顶点着色器、曲分细面着色器、几何着色器、裁剪、屏幕映射。
其中,曲面细分着色器和几何着色器是是两个可选的着色器,前者用于细分图元,后者可用于产生更多的图元或对图元进行修改。因为这两个着色器是可选的,因此不再过多赘述。接下来我们将关注于完全可编程控制的顶点着色器和进行变换的裁剪、屏幕映射。
1.顶点着色器
顶点着色器主要具有以下功能:坐标系变换(最重要)、逐顶点着色、顶点偏移计算
坐标系变换
模型的数据传入到顶点着色器后,对于模型的每一个顶点都会调用一次顶点着色器对顶点及顶点相关信息(如顶点切线、顶点法线等)计算其在某一坐标系下的位置或向量,以方便我们在合适的坐标系中进行渲染的相关计算。最起码也要将模型空间下的顶点坐标变换到裁剪空间(必做)。
逐顶点着色
在早些时候,物体的光照是逐顶点计算的,通过将光源应⽤于每个顶点的位置和法线,从而计算并存储最终的顶点颜色;然后再通过对顶点颜色进⾏插值,来获取三⻆形内部像素的颜色,因此这个可编程的顶点处理单元被命名为顶点着色器(vertex shader)。随着现代 GPU 的出现,以及几乎全部的着色计算都在逐像素的阶段进行,因此顶点着⾊阶段变得越来越通用,甚⾄可能并不会在该阶段中进行任何的着色计算,当然这也取决于开发⼈员的意图,我们仍然可以在顶点着⾊器中进行着色计算。
顶点偏移计算
在将顶点转换到裁剪空间之前,我们可以对顶点进行一定规则或随机的偏移,使得模型具有顶点动画效果,如旗帜飘扬、水上下波动等渲染效果就是通过这种方法实现的。这里不做过多赘述
下面是UnityShader(CG)顶点着色器代码示例
2.裁剪
经过顶点着色器的变换,现在整个视椎体已经被变换到xyz都是【-1,1】的裁剪空间中,在这个裁剪空间中,对于哪些不再摄像机视野范围内的物体将会被裁剪(即图元不会继续向下一个流水线传递),对于部分在裁剪空间的图元则会生成新的顶点,而在外面的顶点会被舍弃。
3.屏幕映射
该阶段的任务是将每个图元的xy坐标值映射到屏幕坐标系中。屏幕坐标系是一个二维的坐标系,它和我们的显示器分辨率相关,假设我们的显示器分辨率是1920乘1080,那么以OpenGL为例,左下角坐标是(0,0),右上角的坐标就是(1920,1080),而DirectX的坐标系以左上角为原点。
裁剪空间系下坐标的z值将会被用作深度进行深度测试判断遮挡关系,这在之后会提及。
四、光栅化阶段
经过几何阶段,我们已经得到了被正确变换和正确投影的顶点数据,以及它们的着色数据(顶点着色器),光栅化阶段的目标就是将二维屏幕空间坐标的点转换到屏幕上像素的过程,其中每个顶点对应一个深度值(z值),并计算每个图元到底覆盖了哪些像素。光栅化阶段分为两个子阶段:三角形设置(也叫做图元装配)和三角形遍历。
1.三角形设置
在三角形设置阶段,主要计算了三角形边界信息,包括确定边界像素坐标和计算三角形的边界框,以便三角形遍历阶段确定三角形覆盖的像素区域。同时,为插值准备数据,计算线性插值的参数以及准备与着色相关的三角形表面数据,这些数据用于后续遍历阶段的属性插值和像素着色阶段的着色计算。此外,还计算了三角形的微分信息,以更准确地计算像素属性值和进行光照计算等。最后,根据顶点数据组装图元,将点连接起来形成三角形,为后续的光栅化处理做好准备。
2.三角形遍历
确定三角形覆盖像素区域的方法
确定三角形覆盖像素区域的方法:对于每个像素,我们都取它的中心点为一个采样点,判断该点是否在三角形内部,若在,则视作该三角形图元覆盖了该中心点所在像素。我们可以通过叉积判断像素中心点是否在三角形内部:叉积判断点是否在三角形内部
而且,我们并不需要遍历完全部像素,只需要用一个AABB包围盒将需要计算覆盖区域的三角形图元包裹起来,遍历包围盒的像素即可。
但同时,我们也会发现,在这个阶段会产生一些问题:三⻆形会以像素的形式显示出来,⼀个网格像素要么被覆盖,要么不被覆盖,绘制出来的线也有类似的问题。由于这个原因,因此三⻆形和线段的边界会呈现出锯齿状,这个视觉瑕疵被称作锯齿,正式点也叫做走样。这里不做过多赘述,会另写一篇文章分析锯齿的产生及抗锯齿的方法。
插值
接着,对于每一个被三角形覆盖的像素区域都会生成一个对应的片元,然后根据三⻆形三个顶点上的属性进行插值,来获得每个三角形片元的属性,这些属性包括片元的深度,以及几何阶段输出的相关着色数据等。在此,我们常用的插值方法为三角形重心坐标插值法。
重心坐标是一种表示三角形内部点的方法。对于三角形 ΔABC,其内部的任意一点 P 可以表示为三个顶点 A,B,和 C 的线性组合: P=αA+βB+γC 其中,α,β,和 γ 是权重系数,它们满足以下条件: α+β+γ=1 并且每个系数都是非负的。如果 α,β,和 γ 都是非负数且和为1,则点 P 位于三角形内部。由于三个权值相加得1,所以实际上我们只需要计算出两个权值即可对三角形内的任意点进行表达。
一旦计算出点 P 的重心坐标 (α,β,γ),可以使用这些坐标来插值三角形顶点的属性。例如,如果顶点 A,B,和 C 分别有属性 IA,IB,和 IC,则点 P 的属性 IP 可以通过以下公式计算: IP=αIA+βIB+γIC
五、像素处理阶段
1.片元着色器
片元着色器是另一个可编程控制的着色器。前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是产生一系列的数据信息用于,用来表述三角形网格是如何覆盖每个像素的,而片元就负责储存这些数据。
片元着色器的输入是上一阶段对顶点信息插值得到的结果,输出是一个或多个颜色值。这一阶段可以实现很多重要的渲染算法,其中最重要且基础的技术是纹理采样。顶点信息中携带有纹理对应的uv坐标值,而每个片元通过顶点插值来获得自己的uv坐标值,并通过uv对纹理贴图进行查找,找到对应的颜色。
这一阶段也会产生诸多问题,如纹理贴图过小or过大,这里不过多赘述,会另写文章进行分析
2.输出合并阶段
这个阶段主要决定了每个片元的可见性问题,需要进行一些列测试;若片元通过了所有测试,就需要将该片元的颜色与原来已存在于颜色缓冲区中的颜色进行混合,然后更新颜色缓冲区中的颜色值。
Alpha测试
Alpha测试用于根据片元的alpha值(透明度)决定是否丢弃该片元。alpha值通常在0到1之间,表示透明度。如果alpha值小于某个预设的阈值,则该片元被丢弃,不会进行后续的处理。
模板测试
模板测试是一种高级的像素选择机制,用于根据模板缓冲区中的值来决定是否更新帧缓冲区中的像素。模板缓冲区中的每个像素都有一个模板值,通过比较新片元的模板值与模板缓冲区中的值,决定是否更新该像素。具体来说,模板测试会根据预设的模板测试函数(如等于、不等于、小于、大于等)来比较片元的模板值和模板缓冲区中的值,如果比较结果满足条件,则该片元可以通过模板测试,继续进行后续处理;否则,该片元将被丢弃。不管一个片元有没有通过深度测试,我们都可以根据测试结果来修改模板缓冲区的值,这个更新逻辑也是可以由开发者指定的。通过模板测试,可以实现复杂的渲染效果,如多层遮挡、阴影绘制等。
深度测试(重要)
深度测试会将该片元的深度值(z值)与相同位置已经存在于深度缓冲区的深度值进行比较,与模板测试相同,深度测试的比较函数也是可以自定义。一般默认为小于等于,若片元通过了深度测试,则可认为该片元比已经存在于深度缓冲区片元更靠近相机,就会遮挡原来的片元,因此就应该进行更新,当然,与深度缓冲区一样,这里的更新逻辑也可以由开发者自定义,可以选择通过测试但不进行深入写入(ZWrite Off),也可以选择始终通过深度测试(ZTest Always)等。通过深度测试,我们就可以确定物体的前后遮挡关系,渲染出正确的画面。
颜色混合
颜色混合是将新生成的片元颜色与帧缓冲区中已有像素的颜色进行混合。混合操作可以通过多种模式进行,具体来说,颜色混合会根据预设的混合模式(如替换、加法、减法、乘法、alpha混合等)来计算最终的颜色值。例如,在alpha混合模式下,根据新片元的alpha值(透明度)将新颜色与旧颜色进行加权混合,公式为:Final Color = SrcColor × SrcAlpha + DstColor × (1 -SrcAlpha),其中SrcColor和SrcAlpha是新片元的颜色和透明度,DstColor是帧缓冲区中已有像素的颜色。通过颜色混合,可以实现透明效果、特效渲染等复杂的视觉效果。
太多了,可以单独写一篇文章进行总结
帧缓冲区
帧缓冲区是图形系统中的一个关键组件,用于存储屏幕上每个像素的颜色和属性信息。它就像是屏幕的“草稿纸”,GPU在上面绘制图像,然后将结果呈现到屏幕上。帧缓冲区的工作原理包括图像渲染和刷新屏幕两个主要步骤。GPU计算每个像素的颜色值并存储在帧缓冲区的相应位置,一旦一帧图像在帧缓冲区中完全渲染完毕,它就会被“刷新”到显示器上,这就是我们看到的画面。
帧缓冲区渲染完毕的图像,还可以重回GPU渲染管线中做一些后处理效果,然后就可以真正滴输出到我们的渲染目标上了!