精华内容
下载资源
问答
  • 图形渲染管线是实时渲染的核心组件。渲染管线的功能是通过给定虚拟相机、3D场景物体 以及光源等场景要素来产生或者渲染一副2D的图像。如上图所示,场景中的3D物体通过管 线转变为屏幕上的2D图像。渲染管线是实时渲染...
  • 渲染管线一般是由显示芯片GPU内部处理图形信号的并行处理单元组成,这些并行处理单元之间是独立的,从另一个角度看,渲染管线实际上也是一系列绘制过程,这一系列过程的输入是待绘制物体的相关描述信息,输出的是要...
  • 图元装配是图形学渲染管线中很重要的一环。其目的在于将位于模型坐标系下的顶点,通过矩阵运算,最终变换至标准设备坐标系下(NDC); 具体来说是 本地坐标系->世界坐标系->摄像机坐标系->裁剪坐标系->标准化设备...
  • 渲染管线

    2020-11-14 22:47:09
    渲染管线是实时渲染的重要工具,实时渲染离不开渲染管线。图形渲染管线主要包括两个功能:一是将物体3D坐标转变为屏幕空间2D坐标,二是为屏幕每个像素点进行着色。渲染管线的一般流程如下图所示。分别是:顶点数据的...

    概述 (Introduction)

    图形渲染管线是实时渲染的核心组件。渲染管线的功能是通过给定虚拟相机、3D场景物体以及光源等场景要素来产生或者渲染一副2D的图像。如上图所示,场景中的3D物体通过管线转变为屏幕上的2D图像。渲染管线是实时渲染的重要工具,实时渲染离不开渲染管线。图形渲染管线主要包括两个功能:一是将物体3D坐标转变为屏幕空间2D坐标,二是为屏幕每个像素点进行着色。渲染管线的一般流程如下图所示。分别是:顶点数据的输入、顶点着色器、曲面细分过程、几何着色器、图元组装、裁剪剔除、光栅化、片段着色器以及混合测试。我们会在后文对管线的各个阶段进行详细的介绍。
    渲染管线的一个特点就是每个阶段都会把前一个阶段的输出作为该阶段的输入。例如,片段着色器会将光栅化后的片段(以及片段的数据块)作为输入进行光照计算。除了图元组装和光栅化几个阶段是由硬件自动完成之外,管线的其他阶段管线都是可编程/可配置的。其中顶点着色器、曲面细分相关着色器、几何着色器和片段着色器是可编程的阶段,而混合测试是可高度配置的阶段。管线的可编程/可配置是渲染管理的另一个特点。因为早期的渲染管线采用的是立即渲染模式(Immediate mode,也就是固定渲染管线),不允许开发人员改变GPU渲染的方式,而核心渲染默认(Core-profile mode)允许开发人员定制化GPU的渲染方式。

    我们接下来简单介绍管线各个阶段的功能:

    顶点数据:顶点数据用来为后面的顶点着色器等阶段提供处理的数据。是渲染管线的数据主要来源。送入到渲染管线的数据包括顶点坐标、纹理坐标、顶点法线和顶点颜色等顶点属性。为了让OpenGL明白顶点数据构成的是什么图元,我们需要在绘制指令中传递相对应的图元信息。常见的图元包括:点(GL_POINTS)、线(GL_LINES)、线条(GL_LINE_STRIP)、三角面(GL_TRIANGLES)。

    顶点着色器:顶点着色器主要功能是进行坐标变换。将输入的局部坐标变换到世界坐标、观察坐标和裁剪坐标。虽然我们也会在顶点着色器进行光照计算(称作高洛德着色),然后经过光栅化插值得到各个片段的颜色,但由于这种方法得到的光照比较不自然,所以一般在片段着色器进行光照计算。关于坐标变换以及着色方案的细节,我们会在后面详细介绍。

    曲面细分:曲面细分是利用镶嵌化处理技术对三角面进行细分,以此来增加物体表面的三角面的数量,是渲染管线一个可选的阶段。它由外壳着色器(Hull Shader)、镶嵌器(Tessellator)和域着色器(Domain Shader)构成,其中外壳着色器和域着色器是可编程的,而镶嵌器是有硬件管理的。我们可以借助曲面细分的技术实现细节层次(Level-of-Detail)的机制,使得离摄像机越近的物体具有更加丰富的细节,而远离摄像机的物体具有较少的细节。

    几何着色器:几何着色器也是渲染管线一个可选的阶段。我们知道,顶点着色器的输入是单个顶点(以及属性), 输出的是经过变换后的顶点。与顶点着色器不同,几何着色器的输入是完整的图元(比如,点),输出可以是一个或多个其他的图元(比如,三角面),或者不输出任何的图元。几何着色器的拿手好戏就是将输入的点或线扩展成多边形。下图展示了几何着色器如何将点扩展成多边形。

    图元组装:图元组装将输入的顶点组装成指定的图元。图元组装阶段会进行裁剪和背面剔除相关的优化,以减少进入光栅化的图元的数量,加速渲染过程。在光栅化之前,还会进行屏幕映射的操作:透视除法和视口变换。

    关于透视除法和视口变换到底属于流水线的那个阶段并没有一个权威的说法,某些资料将这两个操作归入到图元组装阶段,某些资料将它归入到光栅化过程,但对我们理解整个渲染管线并没有太大的影响,我们只需要知道在光栅化前需要进行屏幕映射就可以了,所以我们这里将屏幕映射放到了图元组装过程。这两个操作主要是硬件实现,不同厂商会有不同的设计。

    光栅化:经过图元组装以及屏幕映射阶段后,我们将物体坐标变换到了窗口坐标。光栅化是个离散化的过程,将3D连续的物体转化为离散屏幕像素点的过程。包括三角形组装和三角形遍历两个阶段。光栅化会确定图元所覆盖的片段,利用顶点属性插值得到片段的属性信息,然后送到片段着色器进行颜色计算,我们这里需要注意到片段是像素的候选者,只有通过后续的测试,片段才会成为最终显示的像素点。

    片段着色器:片段着色器在DirectX中也成为像素着色器(Pixel Shader)。片段着色器用来决定屏幕上像素的最终颜色。在这个阶段会进行光照计算以及阴影处理,是渲染管线高级效果产生的地方。

    测试混合阶段:管线的最后一个阶段是测试混合阶段。测试包括裁切测试、Alpha测试、模板测试和深度测试。没有经过测试的片段会被丢弃,不需要进行混合阶段;经过测试的片段会进入混合阶段。Alpha混合可以根据片段的alpha值进行混合,用来产生半透明的效果。Alpha表示的是物体的不透明度,因此alpha=1表示完全不透明,alpha=0表示完全透明。测试混合阶段虽然不是可编程阶段,但是我们可以通过OpenGL或DirectX提供的接口进行配置,定制混合和测试的方式。

    值得注意的是,半透明物体的绘制需要遵循画家算法(painter Algorithm)由远及近进行绘制,因为半透明的混合跟物体的顺序有严格的对应关系。从下面两张图我们可以看到,先绘制红色还是先绘制绿色对最终颜色的有这很大的影响。所以,绘制半透明物体之前,我们需要按照距离远近对场景中的物体进行严格排序,然而这是一个非常棘手的问题。比如,我们如何排序下面几个三角形呢?所以当进行半透明物体渲染时,一般会使用顺序无关的半透明渲染技术(Order-independent transparency,OIT)

    渲染管线并非严格这样划分,不同的教材会有不同的划分方法。《Real Time Rendering》一书将渲染管线划分为以下四个阶段:应用程序阶段(Application)、几何处理阶段(Geometry Processing)、光栅化(Rasterization)和像素处理阶段(Pixel Processing)。应用阶段通常是在CPU端进行处理,包括碰撞检测、动画物理模拟以及视椎体剔除等任务,这个阶段会将数据送到渲染管线中;几何处理阶段主要执行顶点着色器、投影变换、裁剪和屏幕映射的功能;光栅化阶段和我们上面讨论的差不多,都是将图元离散化片段的过程;像素处理阶段包括像素着色和混合的功能。我们可以发现,虽然管线的划分粒度不一样,但是每个阶段的具体功能其实是差不多的,原理也是一样的,并没有太大的差异。

    顶点数据(Vertex Data)

    顶点数据在DirectX中成为输入装配阶段(Input Assembler State)。是渲染管线数据的主要来源,输入的数据可以包括顶点坐标、顶点颜色、顶点法线、纹理坐标等数据,利用这些输入数据我们可以在片段着色器计算片段的光照信息,最终输出到颜色缓冲器。顶点数据在流水线中以图元的方式进行处理,常见的图元有:点、线和三角面。在OpenGl中可以使用glGenVertexArrays()glGenBuffers()glBindBuffer()glBindVertexArray()glVertexAttribPointer()等API从应用程序传入数据,并设置顶点对应的属性信息和内存布局。四边形的顶点数据如下所示,包括了顶点坐标、顶点颜色和顶点的纹理坐标。这里我们是手动设置了四边形四个顶点的属性值,试想一下,对于一个具有几万个三角面的复杂模型,我们如何设置这些属性值呢?此时我们就需要使用到建模工具了,常见的3D建模工具有:Maya、Blender、3Ds Max等。 我们可以在程序中加载建模工具产生的这些数据,提高开发效率。


    我们用三角形网格来近似表示物体,用指定的3个顶点来定义三角形。由于相邻三角形会存在顶点共用的情况,尤其在物体网格非常复杂的情况下,冗余数据会非常多。我们可以通过使用索引来避免共享顶点间数据的多余,也就是使用顶点缓存对象(Vertex Buffer Object,VBO)

    顶点着色器(Vertex Shader)

    坐标变换 (Coordinate Transform)

    顶点着色器用来处理输入的顶点数据,主要用来进行顶点坐标变换以及顶点着色。我们知道,从输入的顶点局部坐标到最终的屏幕坐标需要经过一系列的坐标变换,才能最终显示到屏幕上。下面的图片展示了顶点坐标的一系列变换过程。我们从建模工具得到的是物体的局部坐标(Local Coordinate),局部坐标通过模型矩阵Model变换到世界坐标(World Coordinate),世界坐标通过观察矩阵View变换到观察坐标(View Coordinate),观察坐标经过投影矩阵Projection变换到裁剪坐标(Clip Coordinate),裁剪坐标经过透射除法(Perspective Division)得到标准设备空间(Normalized Device Coordinates,NDC),NDC坐标通过视口变换(Viewport Transformation)变换到窗口坐标进行显示。看到这么多变换,是不是一下子有点懵了!我们接下来看下每个变换都做了什么。

    我们知道,光照计算一般都是在世界空间进行的,所以输入的顶点坐标需要通过乘以模型矩阵变换到世界空间。如果物体变换有非均匀缩放,那么在变换法线时就要注意了。我们不能简单的通过乘以模型矩阵来将法线变换到世界空间。下图展示了法线变换可能产生的问题。如果只是存在平移变换(Translation)我们无需对法线进行变换;如果只存在平移和旋转变换(Rotation)我们只需要乘上渲染矩阵;如果存在非均匀缩放变换(Scaling)我们需要使用矩阵的逆的转置来变换法线。关于该过程的推导可以参考OpenGL Normal Vector Transformation这篇文章

    虚拟摄像机定义了我们的观察空间。世界空间和观察空间的关系如下所示,虚拟摄像机的位置是坐标的原点,观察方向沿着Z轴的负方向。我们可以通过摄像机的位置EyePosition、观察目标点FocusPosition和向上的方向向量UpDirection来构建观察矩阵。OpenGL和DIrectX都有对应的API。该方法的实现比较简单,只需要通过两次向量的叉乘就可以构建该矩阵。

    介绍裁剪空间之前,我们需要先来看一个重要的概念:视椎体(Frustum)。视椎体可以通过上下左右远近六个平面来定义。我们通过投影矩阵将物体从观察空间变换到裁剪空间,裁剪空间是一个以原点为中心的立方体,不在该裁剪空间的图元都会被裁剪。根据投影方式的不一样,我们可以定义不同的投影矩阵,常见的投影方法有:正交投影和透视投影。两种不同投影对应的视椎体如下图所示。我们可以看到正交投影的视椎体是长方体,而透视投影的视椎体是台体。我们可以通过近平面(Near)、远平面(Far)、垂直视场角(Vertical Field of View, FOV)和屏幕纵横比(Aspect Ratio,也叫作屏幕宽高比)四个参数来定义视椎体。

    正交投影

    正交投影又叫平行投影。投影视椎体是一个长方体,物体在投影平面的大小与距离远近没有关系。在OpenGL中我们可以通过glm:ortho()这个函数来创建一个正交投影矩阵。正交投影其实是使用如下所以的GL_PROJECTION矩阵进行变换。其中,变量r、l、t、b、n和f是视椎体的上下左右远近平面的边界变量。通过正交矩阵变换后,我们得到了裁剪空间。建筑蓝图绘制和计算机辅助设计需要使用到正交投影,因为这些行业要求投影后的物体尺寸及相互间的角度不变,以便施工或制造时物体比例大小正确。正交投影的示意图如上图右图所示。

    透视投影 

    根据我们的生活经验我们会发现这样的现象,离你越远的物体看起来越小,随着距离的增大,最终会消失在视野中,成为灭点。为了实现这种近大远小的效果,我们需要引入透视投影。在OpenGL中我们可以通过glm:perspective()这个函数来创建一个透视投影矩阵。投影变换使用的是齐次坐标,因为在透视除法阶段需要将XYZ的值除以W分量来获取NDC坐标空间。透视除法可以实现近大远小的视觉效果,该过程由硬件自动执行。这也是正交变换和透视变换最主要的区别,我们在后面会进行具体讨论。下图所示的矩阵是透视投影矩阵,和正交投影一样都是用来进行坐标空间变换的。透视投影示意图如上图左图所示。

    如果你对这些矩阵的推导感兴趣,或者对顶点着色器的坐标变换以及透视除法的原理不是很清楚,推荐大家看下我之前翻译的这篇文章:OpenGL投影矩阵

    由于我们在坐标变换过程中涉及到了很多矩阵:Model、View、Projection。我们可以通过矩阵的乘法将它们联系起来,得到从局部空间到裁剪空间的坐标变换矩阵,这里需要注意矩阵的运算顺序是从右到左。

    平面着色 (Flat Shading)

    根据光照计算的不同,可以将着色方式分为平面着色、高洛德着色和冯氏着色。其中,最简单的着色方式就是平面着色。平面着色是使用一个顶点颜色来代表整个三角面的颜色,默认是使用索引中第一个顶点的颜色,例如,第一个顶点是红色,那么整个三角面都是红色的。上图最左边的图像就是利用平面着色渲染得到的,我们可以看到平面着色几乎无法展示高光效果。

    高洛德着色 (Gouraud Shading)

    高洛德着色就是在顶点着色器中计算顶点的光照信息,所以也叫作逐顶点着色。高洛德着色是平面着色和冯氏着色的折中方案,该方法会在顶点着色器中计算三个顶点的光照信息,然后在光栅化阶段插值得到三角形内部各个片段的光照信息。上图中间的图像就是利用高洛德着色渲染得到的,我们可以看到在高光部分高洛德着色表现的并不是很令人满意,原因在于高光并非是线性变换的,所以通过插值得到的效果比较差。

    冯氏着色 (Phong Shading)

    冯氏着色就是在片段着色器中计算每个片段的光照信息,所以也叫作逐片段着色。冯氏着色是三种着色方式中效果最好的,也是性能消耗最大的着色方式。冯氏着色会在根据输入的顶点法线信息在光栅化阶段插值得到各个片段的法线信息,然后在片段着色器中利用法线、纹理坐标、位置等信息计算每个片段的光照信息。开销最大,同时效果也是最好的,我们可以从上图右边的图像看到冯氏着色渲染得到的,通过对比高洛德着色和冯氏着色的高光部分,我们看发现冯氏着色处理得更加自然真实。

    从上面的讨论我们发现:这三种着色方案中平面着色效果最差(计算量最少),高洛德着色其次(计算量其次),冯氏着色最好(计算量最多)。我们需要根据实际的需求选择对应的着色方式,需要在画面效果和性能开销之间进行取舍。

    曲面细分着色器(Tessellation Shader)

    曲面细分是利用镶嵌化处理技术对三角面进行细分,以此来增加物体表面的三角面的数量,是渲染管线一个可选的阶段。它由外壳着色器(Hull Shader)、镶嵌器(Tessellator)和域着色器(Domain Shader)构成。我们不创建高模(high-poly)来丰富网格信息主要是考虑到以下几个原因:第一,基于GPU可以实现动态的LOD技术,可以根据物体距离摄像机的远近来调整多边形网格的细节,比如说,若物体距离摄像机比较远,则按照高模的规格对它进行渲染会造成浪费,因为我们根本看不清网格的所有具体细节。随着物体和摄像机之间距离的拉近,我们可以实现连续镶嵌化处理,增加物体的细节;第二,节省内存。我们可以在各种存储器中保存低模网格信息,再根据需求用GPU动态地增加物体表面的细节。

    几何着色器(Geometry Shader)

    几何着色器(Geometry Shader)属于渲染管线的一个可选阶段,位于曲面细分(Tessellation)和光栅化(Rasterization)之间。顶点着色器以顶点数据作为输入数据,而几何着色器则以完整的图元(Primitive)作为输入数据。例如,以三角形的三个顶点作为输入,然后输出对应的图元。与顶点着色器不能销毁或创建顶点不同,几何着色器的主要亮点就是可以创建或销毁几何图元,此功能让GPU可以实现一些有趣的效果。例如,根据输入图元类型扩展为一个或更多其他类型的图元,或者不输出任何图元。需要注意的是,几何着色器的输出图元不一定和输入图元相同。几何着色器的一个拿手好戏就是将一个点扩展为一个四边形(即两个三角形)。几何着色器通常用来实现一种叫做公告栏(BillBoards)的视觉效果,几何着色器的一些应用如下图所示。关于几何着色器的具体细节可以参考我之前写的一篇文章:几何着色器

    图元组装(Primitive Assembly)

    裁剪 (Clipping)

    只有当图元部分或全部位于视椎体内时,我们才会将它送到流水线的下个阶段,也就是光栅化阶段。而完全位于视椎体外部的图元会被裁剪掉,不会对它们进行渲染。对于部分位于视椎体的图元,我们需要对他们进行所谓的剪裁操作,例如,线段的两个顶点一个位于视椎体内而另一个位于视椎体外,那么位于外部的顶点将被裁剪掉,而且在视椎体与线段的交界处产生新的顶点。通过之前顶点着色器的投影变换后,视椎体是一个立方体,这有利于我们进行裁剪操作,使得该操作变得更加容易和一致。

    从上图中我们可以看到红色的三角形被丢弃了,而绿色的三角形被送到了光栅化阶段,需要特别留意的是黄色三角形被部分裁剪了,位于视椎体外的顶点被裁剪了,并且在交界处产生了两个新的顶点代替原来的旧顶点。裁剪阶段使用的是4-分量的齐次坐标,通过执行透射除法我们可以得到归一化设备坐标(Normalized Device Coordinates,NDC),然后通过视口变换将NDC坐标变换到屏幕坐标,我们会在下面的屏幕映射具体讨论这两个部分。我们注意到透射除法在裁剪操作之后进行,可以保证透射除法中 w != 0。常见的裁剪算法有Cohen-Sutherland算法Liang-Barsky算法Sutherland-Hodgman多边形裁剪算法

    这里我们顺便说下帧缓存中的裁剪,也就是说我们可以将裁剪操作推迟到屏幕坐标中进行,使用一种叫做裁切(Scissoring)的技术在帧缓存中执行裁剪操作。不过,对于几何实体一般在帧缓存之前进行裁剪更好一些,帧缓存中的裁剪一般只适合于光栅对象,比如像素块。

    背面剔除 (Back-Face Culling) 

    背面剔除指的是剔除那些背对摄像机的图元,如下图所以,图元t1背向摄像机,需要被剔除,而图元t2需要被保留。我们利用三角形顶点的环绕顺序(Winding Order)来确定所谓的正面(front-face)和背面(back-face)。通常情况下,三角形的3个顶点是逆时针顺序(couter-clockwise,ccw)进行排列时,我们会认为是正面,而顺时针(clockwise,cw)排序时,我们会认为是背面。例如t2的三个顶点顺序为逆时针的(v1,v2,v3),所以是正面,需要保留。将t1和t2投影到XY平面后,我们可以清楚的看到它们顶点的环绕顺序。


    从上图可以看到,这里我们可以使用行列式(determinant)来确定投影后的2D三角形到底是CW还是CCW顺序。行列式的第一行由顶点v1和v2坐标确定,而第二行由顶点v1和v3坐标确定。如果行列式的值为负数,那么该三角面是背面朝向;如果为正数,则是正面朝向。

    背面剔除的技术默认是不开启的。我们可以通过glEnable(GL_CULL_FACE)函数来开启背面剔除的优化。开启后,我们还可以通过glCullFace()函数来配置剔除的是正面还是背面。参数为GL_FRONTGL_BACK(默认值)和GL_FRONT_AND_BACK。虽然背面剔除可以大概减少50%的渲染图元,但是在渲染半透明或不透明物体时,不能使用该技术,否则会出现穿帮的情况,因为半透明或不透明物体可以看到物体背后的东西。

    屏幕映射 (Screen Mapping)

    • 透视除法 (Perspective Division)

     

    上面的流程图展示了顶点数据的变换过程。从前面讨论的顶点变换我们知道:经过模型矩阵Model、观察矩阵View和投影矩阵Projection变换后,局部空间被变换到了裁剪空间(-Wc <= XYZ <= Wc)。执行透射除法后(除以w分量),我们可以得到标准设备空间,该空间一般也称作标准视体(Canonical View Volume,CVV)。执行透视除法是为了实现透射投影中近大远小的视觉效果,经过了投影矩阵Projection的变换后,W分量保留了观察空间中物体Z坐标的信息,所以透视除法才能够根据距离摄像机的远近正确实现透视效果。我们注意到透射除法是由硬件自动执行的,也就是说透视除法在正交投影和透视投影中都会被执行,只不过正交投影变换并没有改变W分量的值(W分量的值仍是1),所以透视除法并没有实际的效果。我们从这里也明白了使用齐次坐标的意义,其实就是为了正确记录下投影变换前(观察空间)中物体的深度信息,也就是Z坐标的值。

    如果对坐标变换、投影变换、齐次坐标以及透视除法不是很清楚的话,可以参考我之前翻译的这两篇文章:OpenGL变换OpenGL投影矩阵

    视口变换 (Viewport Transform)

    通过透视除法后,我们得到了NDC坐标,获得NDC坐标是为了实现屏幕坐标的转换与硬件无关。经过视口变换后,我们可以得到窗口坐标(Window Coordinates)。除了窗口坐标,还有屏幕坐标(Screen Coordinates)。一般来说,屏幕坐标是2D的概念,只用于表示屏幕XY坐标,而窗口坐标是2.5D的概念,它还带有深度信息,也就是经过变换后的Z轴的信息。我们可以用glViewport()进行视口变换。Z坐标的数值在OpenGl和DirectX中略有不同,Z坐标在OpenGL中会被映射到[-1,1],在DirectX中会被映射到[0,1],我们使用glDepthRange()来进行深度映射。NDC坐标映射到窗口坐标需要经过平移和缩放两个变换,视口变换矩阵如下所示,其中width、height、farVal和nearVal分别表示视口的宽高远近。关于视口矩阵的推导可以参考YouTube的这个视频

    从上面视口变换的示意图我们注意到屏幕空间和Viewport其实是不一样的。我们可以通过glViewport()来设定视口的坐标和宽高。如果视口小于屏幕空间,那么会造成多余的像素被渲染。例如,glClear()会为整个屏幕空间设定指定颜色值。我们可以通过裁切测试 (Scissor Test)来指定渲染的区域,避免上面出现的渲染浪费的问题。我们会在后面的内容具体讨论裁切测试这种技术。

    扩展:拾取 (Picking)

    • 与投影变换和视口变换相反的一种变换是:拾取(Picking)。也就是根据屏幕坐标反算出对应的3D对象。我们需要做逆于投影视口变换的操作,将屏幕坐标变换到3D坐标。拾取变换的过程如下所示:
    • 通过视口变换矩阵逆矩阵将屏幕坐标变换到NDC坐标
    • 然后通过乘以W分量(投射除法的逆变换)将NDC坐标变换到裁剪坐标
    • 通过投影矩阵逆矩阵将裁剪坐标变换到观察坐标
    • 求出经过原点O以及点的拾取射线
    • 拾取射线位于观察空间,通过将拾取射线变换到局部空间进行相交行检测 (这里将拾取变换到局部空间是为了减少运算量,将物体的每个图元变换到世界坐标效率较低)

    光栅化(Rasterization)

    光栅化也称为扫描转换(scan conversion),该阶段主要是将变换到屏幕空间的图元离散化为片元的过程。离散化过程是利用三角图元所覆盖的片段来表示该图元。这里有个概念需要我们注意,就是如何定义图元覆盖(Overlap)一个片段。如果我们采用最简单的点采样(Point Sampling)而且采样点位于片段的中央位置。当采样点位于图元的内部时,我们认为图元覆盖了该片段。这是最简单的采样方式,除此还有超级采样和多重采样技术,我们会在抗锯齿的部分展开介绍。光栅化主要包括两个过程:三角形的组装和三角形的遍历。

    三角形组装 (Triangle Setup)

    三角形组装会对顶点的输入数据(比如,颜色、法线、纹理坐标)进行插值,得到各个片段对应的数据值,为后面的片段着色提供片段数据。下图所示就是根据顶点输入的颜色值和法线进行插值得到的各个片段的颜色和法线,用于后续的计算。这里对法线的插值进行了可视化处理。不过我们这里只是进行一个简单的示范,一般情况下,我们很少去插值颜色值,通常都是利用片段着色器对片段进行着色。


    三角形遍历 (Triangle Traversal)

    其实三角形遍历的操作我们在前面基本都说过了,通过屏幕空间的坐标组装三角形后,我们遍历这些三角图元覆盖了哪些片段的采样点,随后得到该图元所对应的片元。接下来我们通过顶点的输入数据插值获取片段的数据属性,包括颜色、法线、纹理坐标、深度等信息。对于透射投影,我们需要用到透射校正插值(Perspetive-Correct-Interpolation)来正确插值片段的颜色、纹理等信息。关于透射校正插值的方法可以参考这篇论文

    线段扫描转换

    这部分我们讨论在屏幕绘制线段的方法。线段是最简单的图元,是绘制其他复杂图元的基础。我们从最简单的数字微分画线算法DDA开始讨论扫描转换的方法。然后介绍更加高效绘制线段的Bresenham光栅化算法。讨论完线段的绘制方法后,我们简单介绍多边形填充算法。这里主要介绍最简单最常见的扫描线填充算法!

    • 数字微分画线算法DDA (Digital Differential Analyzer)

    假定一条线段的两个端点分别是(x1,y1)和(x2,y2),斜率m满足:0<=m <= 1。其实这里假定斜率m只是为了方便讨论,我们会在Bresenham算法中讨论如何对一般情况下的m进行处理。当x从x1变化到x2时,y从y1变化到y2。也就是说XY满足如下的增量关系:

    假定x的增量为1,那么y的增量为m。虽然增量为整数,但是由于斜率m为浮点数,所以导致y的值不是整数,为了找到合适的像素位置,我们需要对y的值进行取整操作。DDA算法的伪代码如下:

    我们把斜率最大限制为1的原因可以从下面左边的图看到。DDA算法的基本思想是对于每个x值,算出一个最佳位置的y值。对于斜率大于1的线段,由于两个像素点之间的间隔过大,生成近似的线段产生了较大的误差。对于斜率大于1的线段,我们可以通过交换x和y,即该算法变为:对于每个y值,计算一个最佳的x值,我们可以从下面右边的图看到对应的结果。

    • Bresenham光栅化算法

    DDA算法虽然很简单,编码也非常容易实现,但是由于每生成一个像素都需要一次浮点数的加法运算,效率较低。我们接下来介绍的Bresenham算法可以避免浮点数的运算,只需要通过整数的加法、减法和移位操作,效率很高,所以它已经成为 硬件和软件光栅处理器的标准算法。Bresenhan算法与DDA算法的主要区别在于构造了一个新的判定变量d来避免浮点数的运算,即:

    如何对Bresenham算法不是很熟悉的话,可以参考我的这篇文章Bresenham光栅化算法来认识Bresenham算法是如何利用判定变量d来进行迭代处理每个像素的。

    多边形填充算法

    介绍完线段扫描算法后,我们接下来看多边形的填充算法,相关的算法有很多,比如:奇偶填充、种子填充和扫描线填充算法。我们这里介绍使用最多最常见的扫描线填充算法。

    • 扫描线填充算法(Scanline Filling)

    扫描线算法根据扫描线与多边形的交点来对多边形进行颜色填充。需要注意的是,当多边形的顶点与扫描线相交时,需要对交点进行取舍来保证正确的配对。我们可以通过检查与顶点相邻边的端点Y值来确定顶点的取舍,如下图(c)所示,扫描线 y = e 与三角形的顶点P1相交。我们发现P1相邻边顶点P0和P2的Y值都大于P1的Y值,所以需要舍弃当前相交的顶点,保证顶点能够进行正确的两两配对!

    在扫描线算法中,我们发现扫描线其实只与某些边存在交点,所以我们其实无需对每条边都进行求交的计算。通过使用活性边表(Active Edge Table,AET)这种数据结构可以对该算法进行优化。对于下面的多边形相交测试,我们可以使用该AET链表进行表示,其中每条边Edge的数据结构定义如下:

     

    片段着色器(Fragment Shader)

    Phong光照模型

    计算光照最常用的模型就是大名鼎鼎的phong模型了,该模型其实是经验模型,参数信息都是经验得到的,并没有实际的物理意义,所以利用Phong模型会出现违背物理规则的时候。Phong模型将物体光照分为三个部分进行计算,分别是:漫反射分量、镜面高光和环境光。其中,环境光分量是用来模拟全局光照效果的,其实就是在物体光照信息基础上叠加上一个较小的光照常量,用来表示场景中其他物体反射的间接光照。

    漫反射表示的是光线进入物体内部后重新散射出来的那部分光线,简单起见我们会认为重新散射出来的光线是均匀分布的,如上图所示。因此,无论观察者从哪个方向进行观察,漫反射效果其实是一样的,所以我们认为漫反射和观察位置是无关的。漫反射分量通常利用朗伯余弦定律(Lambert Consine Law)来计算,也就是说漫反射的大小取决于表面法线和光线的夹角。当夹角越大时,漫反射分量越小,当夹角接近90度时,我们认为漫反射几乎为零。

    说到朗伯余弦定律,我们不得不说下半朗伯模型(Half Lambert)。该光照模型是有Valve公司在制作半条命游戏时发明的,由于改进物体较暗区域的光照信息。如下图所示,右边的图是使用半朗伯模型得到的效果。我们可以明显的看到人物被照亮了!半朗伯模型的代码只需要在原来的代码加上float hLambert = difLight * 0.5 + 0.5;一行代码就可以了,其实该代码就是将之前的漫反射系数从[0,1]变到[0.5,1],所以提升了漫反射的亮度信息。


    镜面反射表示光线照射到物体表面后被重新反射的现象,镜面反射遵循反射定律。我们生活中发现金属表面会有高光的现象,就是由于金属对光线有较高的反射率,给人一种金属感,通过镜面反射我们可以模拟金属和非金属物质对光照的反射程度。我们在日常生活中其实也可以发现,高光跟我们观察的方向是有关系的,我们在描述高光性质时需要知道观察者位置信息。


    Blinn-Phong光照模型

    下面我们介绍另一种光照模型:Blinn-Phong光照模型。Blinn-Phong模型是对我们上面讲到的Phong模型的改进,Phong模型在处理高光时会出现光照不连续的情况。我们知道高光跟观察位置密切相关,当观察方向和反射光线夹角大于90度时(如上图所示),Phong模型会出现镜面反射分量被消除的情况,所以出现高光不连续的想象,如下图的第一种图所示。我们可以通过Blinn-Phong模型来对它进行改进,下面两张图对比了这两种模型在处理高光时的差异。

    Blinn-Phong模型在处理镜面反射时不使用观察方向和反射光线的夹角来计算,而是引入了一个新的向量:半角向量(Halfway vector)。半角向量其实很简单,就是光线向量L和观察方向V的中间位置。Blinn-Phong模型计算的就是半角向量H和平面法线的夹角(当视线和反射向量对齐时,有最大的镜面反射),这样无论观察者在那个方向进行观看,半角向量和法线夹角都不会超过90度,不会出现上面说的高光不连续的问题,光照效果更佳真实。

    纹理贴图 (Textures)

    纹理贴图也成为纹理映射,是将图像信息映射到三角形网格上的技术,以此来增加物体表面的细节,令物体更具有真实感。纹理技术有很多,最常见的是凹凸贴图(bump mapping)、法线贴图(normal mapping)、高度纹理(height mapping)、视差贴图(parallax mapping)、位移贴图(displacement mapping)、立方体贴图(cubemap)、阴影贴图(shadowmap)。凹凸贴图、法线贴图等技术都是利用光照信息的明暗对比来产生视觉错误。纹理贴图是片段着色器的主要操作,通过贴图技术可以实现很多高级的效果。我们将贴图上的每个像素成为纹素(texel,纹理像素texture pixel的意思,用于和像素进行区分),纹理映射其实就是进行纹素和像素对应的过程。

    我们一般使用一个二维的坐标(u,v)来表示纹理坐标,其中u是横坐标,v是纵坐标,因此纹理坐标一般也被称为UV坐标。UV坐标一般被归一化到[0,1]之间,但是如果UV超出这个范围,我们就需要指定纹理坐标的寻址方式,也叫作平铺方式。常见的寻址方式有:重复寻址(repeat)、边缘钳制寻址(clamp)和镜像寻址(mirror)。不同的寻址方式说明了UV超出[0,1]时如何访问纹理数据的。下图展示了Unity3d中纹理的重复寻址和钳制寻址方式。

    除了寻址方式外,纹理的采样方式也会觉得最终的显示效果。由于纹素和像素通常不是一一对应的,所以我们需要决定像素所对应的纹素信息时,需要用到纹理的滤波方式。常见的滤波方式有点过滤(point)、线性过滤(linear)、最近领点过滤(nearest neighbor point)和双线性过滤(bilinear)。Unity中还有Trilinear滤波的技术,该滤波和Bilinear差不多,只不过会在多级渐近纹理(mipmapping)之间进行混合。纹理的多级渐进技术是为了解决纹理缩小时产生所谓的摩尔纹现象,由于远处的物体并不需要很高的精度,所以我们在对远处物体进行采样时,会使用分辨率更低的纹理贴图,这就是mipmapping的思想。

    • 法线贴图 (Normal Mapping)

    纹理常见的一种应用是凹凸映射(bumping mapping),该技术由Blinn引入的。凹凸映射一般有两种方式:一种是使用高度纹理(height map),利用高度图来修改表面的法线,这种方式也被称为高度映射(height mapping);还有另一种方式是使用法线贴图(normal map),这种方式一般称为法线映射(normal mapping),该技术由Peercy引入的,法线贴图直接存储的就是物体表面的法线,而高度贴图需要计算物体表面的法线扰动信息,所以说使用法线贴图会比使用高度贴图有更高的性能。尽管我们一般认为凹凸映射和法线映射是相同的技术,但是还是需要知道它们之间的区别。

    法线贴图一般有物体空间(object space)和切线空间(tangent space)两种。它们的区别在于法线存储的坐标空间不同。物体空间的法线贴图是相对于物体的坐标原点进行存储的;而切线空间的法线贴图是相对于顶点坐标进行存储计算的。我们可以发现:当物体进行旋转移动操作时,物体空间的法线贴图会得到错误的光照信息,也就是说该法线贴图不具备一般性,开发人员无法重复利用同一份贴图数据,所以一般采用的是切线空间进行存储计算。我们发现物体空间发现贴图偏向于五颜六色,因为发现相对于物体的朝向是随机的,而切线空间的法线贴图是淡蓝色的,因为Z轴总是朝向(0, 0, 1),经过映射后得到的RGB为浅蓝色。

    切线空间如上图所示。我们会根据顶点本身的法线(N)、切线(T)和副切线(B)三个信息来构建切线空间。其中,法线总是垂直于顶点所在表面向上,通过切线和法线我们可以获得副切线的信息。所以,一般我们都会使用纹理压缩技术来减少纹理的内存开销,常见的纹理压缩技术有:DXT1、DXT5和3Dc。详细的纹理压缩技术可以参考NVIDIA和id Software发表的这篇文章

    此外,我们还需要考虑采样发现贴图后计算光照的坐标空间。我们既可以在世界空间中进行光照计算,也可以在切线空间中进行光照计算,那么我们一般在那个空间计算呢?如果我们在切线空间下计算光照,我们需要将光照方向和观察视角转换到切线空间;如果我们在世界空间下进行光照计算,我们需要将采样到的法线方向转变到世界空间下。从效率上来说,第一种方法比第二种方法要高,因为我们在顶点着色器就可以完成光照方向和观察方向的转换,而第二种方法需要我们将采样到的法线方法变换到世界空间,需要在片段着色器进行矩阵乘法。从通用性来说,第二种方法要优于第一种方法,因为我们有时候需要在世界空间下进行一些计算,例如在使用环境贴图映射时,我们需要使用世界空间下的反射方法对Cubemap进行采样。

    锯齿和抗锯齿 (Aliasing and Anti-aliasing)

    3D场景中的物体是连续的,而2D屏幕空间的像素点是离散的,当我们使用离散的像素点来表示连续的物体时,丢失了物体连续的信息,导致锯齿的产生。我们从前面光栅化阶段已经知道了一个片段是如何产生的,它是抗锯齿技术的基础,接下来主要介绍超级采样和多重采样抗锯齿两种技术。

    • 超级采样抗锯齿 (Super-Sampling Anti-aliasing)

    最直接最好的抗锯齿方法就是SSAA,拿4xSSAA来说,假设屏幕分辨率为800x600,那么4xSSAA会将屏幕渲染到1600x1200的缓冲区上,然后在下采样到800x600,SSAA可以得到非常好的抗锯齿效果,不过SSAA需要的计算量是非常大的,光栅化和片段着色器都是原来的4倍,渲染缓存的大小也是原来的4倍。

    • 多重采样抗锯齿 (Multi-Sampling Anti-aliasing)

    在前面的光栅化阶段,我们知道每个片段都有一个采样点,决定一个片段是否被三角面覆盖的方法就是看是否覆盖了该采样点。在MSAA中我们会使用多个采样点来决定覆盖率的问题(Coverage)。MSAA的原理其实和SSAA差不多,不过在光栅化阶段计算三角面是否覆盖了片段的每个采样点,得到采样点覆盖率的数值,接下来在片段着色器计算这个片段的颜色值(只计算一次),然后最终的颜色会乘上这个覆盖率。以上图第二个图的4xMSAA为例,我们发现三角形覆盖了4个采样点中的两个,所以最终片段得到的颜色值需要乘以采样点覆盖率0.5,得到的是浅红色的颜色。常见的采样模式(sampling schemes)如下图所示。MSAA的主要问题在于它跟延迟渲染(Deferred Rendering)并不兼容,因为延迟渲染需要Geometry和Lighting两个pass,在光照阶段无法通过GBuferf获取片段的覆盖率信息。

    上面讲到的MSAA计算方式在下面这种情况下会出现问题。我们这里使用的是4xMSAA,但是我们在片段着色器中利用的是片段的中心位置(图中的中央的圆点)作为采样点来决定最终的颜色,但是我们发现此时三角面是没有覆盖到该采样点,所以最终输出的颜色时错误的。此时我们会使用重心采样(centroid sampling)来解决这个问题。重心采样是由GPU自动执行的,GPU通过覆盖的采样点的来确定采样的重心点,以此避免上面提到的采样点位于三角面外部的问题。

    除了我们上面提到的SSAA和MSAA之外,还有很多其他的抗锯齿技术,比如:覆盖采样抗锯齿(CSAA)、可编程过滤抗锯齿(CFAA)、快速近似抗锯齿(FXAA)、时间样本抗锯齿(TXAA)、多帧采样抗锯齿(MFAA)、形态抗锯齿(MLAA)、增强型抗锯齿(EQAA)、亚像素形态抗锯齿(SMAA)。

    阴影 (Shadows)

    阴影和光是这个世界的一体两面,有光的地方就会产生影子。阴影对于渲染真实感来说非常重要,它给出了物体在3D世界中的位置信息。对于静态的物体,我们可以使用Lightmap烘焙的方法来获取物体的影子,而对于动态的物体,一般采用的是Shadowmap的技术。该技术其实是一种渲染到纹理的技术,我们得到的这张贴图一般称作阴影贴图。Shadowmap的原理非常简单,首先是从光源的位置渲染一遍场景,将得到的深度信息写入到贴图到,然后再一次正常的渲染场景,利用我们得到的shadowmap来判断哪些片段落在了阴影中。


    在上面的左图中,我们从光源的角度看,像素p的深度值为d(p),但是从阴影贴图中我们发现离光源最近的像素深度值其实是s(p),而且d(p) > s(p)。所以我们可以得到像素p之前有物体挡住了它,因此p点落在了阴影当中。右图中,我们发现像素p的深度值d(p)=s(p),因此像素p没有落在阴影中。


    由于shadowmap精度的问题,阴影会出现阴影粉刺(shadow acen)。对于阴影粉刺我们可以通过偏移量对阴影贴图中的深度值进行调整,我们发现调整前平面会有某些像素深度比深度贴图大,某些像素深度比深度贴图小,所以导致了某些地方有阴影某些没有的粉刺现象,经过了偏移后,所有像素点的深度值都比深度贴图大,没有了粉刺现象。不过我们如果偏移太大的话,会出现阴影偏离物体的失真现象。偏移量的大小和平面的倾斜度有关,平面越倾斜的话,需要的偏移量就越大。

    除了阴影粉刺之外,还有阴影走样(aliasing)的问题,如上图所示。虽然我们可以通过提升shadowmap的分辨率来降低走样的影响,但是同时也增大了显存的消耗。所以通常使用百分比渐进过滤(Percentage Closer Filter, PCF)的方法来实现软阴影效果,如下图所示。我们在采样得到的结果进行插值,不过这里的插值和我们讨论的纹理采样插值有所不同,我们并非对采样得到的深度值的进行插值,而是对得到的阴影因子(shadow factor,不在阴影中为1,在阴影中为0)进行插值,从而使得阴影的边缘能够进行柔和的过渡,不会产生阴影到非阴影的突变。4-tap PCF的过滤方式如下图所示,我们以四个采样结果来计算对应的阴影颜色,得到的阴影因子为0.25,得到的是过渡的阴影颜色。


    其实,shadowmap有非常多的变种,我们上面介绍的技术是最简单的一种,除了这种技术之外,还有PSSM(Parallel-Split Shadow Maps)、CSM(Cascaded Shadow Maps)、VSM(Variance Shadow Maps)、BSM(Bitmap Shadow Maps)、TSM(Trapezoidal Shadow Maps)等等。

    测试和混合(Tests & Blending)

    裁切测试 (Scissor Test)

    在前面视口变换中我们已经提到裁切测试这种技术了。裁切测试可以避免当视口比屏幕窗口小时造成的渲染浪费问题。通常情况下,我们会让视口的大小和屏幕空间一样大,此时可以不需要使用到裁切测试。但当两者大小不一样大时,我们需要用到裁切测试来避免像glClear()出现的问题。裁切测试默认是不开启的,我们可以通过glEnable(GL_SCISSOR_TEST)来开启裁切测试,通过glScissor()来指定裁切区域。下面通过StackExchange的一组图来简单说明裁切测试的原理:(背景颜色使用glClear()设置为白色)

    正常情况下视口窗口和屏幕空间一样大,此时渲染整个屏幕空间,没有多余的渲染,不需要进行裁切。

    视口变换后视口比屏幕空间小,我们应用裁切测试将渲染窗口变成和视口一样大的窗口,没有多余的需要,但是需要用到裁切。

    视口窗口和屏幕窗口一样大,但是进行了裁切测试,裁切的窗口比视口小,只有位于裁切窗口部分的片段进行了渲染。

    裁切窗口和屏幕空间一样大,而且比视口要大,所以真个屏幕空间都被glClear()设置为白色,此时有渲染的浪费,视口外的片段也被渲染了。

    从上面的讨论我们知道裁切的区域可以比视口大,但是不能大于设备的渲染目标(Render Rarget)。如果对裁切测试还不是很明白,可以参考StackExchange上面的这个问题:What is the purpose of glScissor?

    Alpha测试 (Alpha Test)

    颜色一般采用RGBA四分量来进行表示,其中颜色的Alpha值用来表示物体本身的不透明度(alpha=1表示完全不透明,alpha=0表示完全透明)。Alpha测试可以根据片段颜色的Alpha值来裁剪片段。OpenGL和DIrectX都有内置的函数进行裁剪,其中HLSL中使用Clip(),GLSL中使用discard()。如果我们想在片段着色器中丢弃alpha值小于0.1的片段,我们可以使用下面的代码进行简单的alpha测试。不过由于alpha测试本身消耗较大,性能较低,所以只有在必要的情况下才会使用alpha测试。我们下面讨论下alpha测试效率较低的原因。




    • Early-Z Culling

    通常情况下,我们是在片段着色器执行后再进行深度/模板测试,来确定到底哪些片段(Fragments)对屏幕显示有贡献,哪些片段需要被丢弃(Discard),只有哪些通过了深度/模板测试的片段才成为屏幕上显示的像素(Pixels),这也就是片段和像素本质的区别,也就是说片段是最终显示在屏幕上像素的候选者!

    由于深度/模板测试是在片段着色器之后进行的,所以导致着色器计算资源的浪费,因为这些被遮挡的片段对我们最终的画面是没有任何贡献的,而我们还花费了大量的资源对它们进行了复杂的光照等一系列计算。Early-Z Culling正是在这种情况下出现的,不过我们需要注意的是Early-Z Culling本不是管线标准,只是硬件厂商用来加速渲染的一种优化手段,所以在不同的硬件上会有不同的实现,而且Early-Z Culling并不保证一定有效。只有当满足某些条件的情况下,GPU才会开启Early-Z的优化!

    到底需要满足那些条件才会开启呢?我们可以从NVIDIA和AMD的官方文档看到一些说明:只允许硬件使用光栅化插值得到的深度值,也就是说我们不能在片段着色器去修改深度缓冲,否则GPU将无法提前为我们做出优化,因为之前的深度值已经被改变了!这也意味着像alpha测试和texkil (HLSL中为Clip,GLSL中为discard) 这些操作会使得Early-Z Culling失效!这也是alpha测试效率低下的主要原因!

    除了Early-Z Culling之外,AMD的官方文档还提到了另一种相关的优化:层次Z(Hierarchical Z,HiZ)。Early-Z Culling和HiZ的主要区别在于处理片段的粒度不一样。HiZ是以块(Tiles)为单位进行相对粗糙的深度测试(rough depth test)的,以便节省带宽的消耗。一般是在HiZ操作后进行Early-Z Culling的测试。

    模板测试 (Stencil Test)

    模板测试默认是不开启的,可以通过glEnable(GL_STENCIL_TEST);开启模板测试。模板测试其实很简单,我们可以把它理解为一个模子mask,通过mask的值来控制那些片段的可见性,无法通过模板测试的片段将被丢弃,下图给出了模板测试的一个例子。模板测试属于流水线中高度可配置的阶段,可以通过glStencilMask来设置一个掩码,该掩码会将要写入缓存区的值进行AND操作,默认情况下掩码值有1,不影响输出。此外,我们还可以利用glStencilFuncglStencilOp连个函数来设置模板函数,控制在模板测试失败或成功时的行为。我们可以利用模板测试来实现平面镜效果、平面阴影和物体轮廓等功能。

    深度测试 (Depth Test)

    根据我们的日常经验,近处的物体会挡住后面的东西,我们可以通过深度缓冲来实现这样的效果。深度测试的原理很简单:比较当前片段的深度值是否比深度缓冲中预设的值小(默认比较方式),如果是更新深度缓冲和颜色缓冲;否则丢弃片段不更新缓冲区的值。其实我们前面介绍Early-Z Culling也是利用Z-Buffer的技术来进行深度测试的,只不过该测试是在片段着色器之前进行的,而深度测试是在片段着色器之后进行的。的深度测试默认是禁用的,我们可以通过glEnable(GL_DEPTH_TEST)来开启深度测试。深度测试是可配置的阶段,我们可以通过深度函数glDepthFunc()来设置深度比较运算符,默认情况下的深度比较函数是GL_LESS,也就是前面的物体会盖住后面的物体。除了GL_LESS还有其他深度比较函数,比如:GL_ALWAYS、GL_GREATER、GL_NEVER等。我们在渲染半透明物体时,需要开启深度测试而关闭深度写入功能。

    Z-Fighting

    Z-fighting是由于深度缓冲精度不够带来的问题。当同一个位置的片段具有相似的深度值时,由于深度缓冲精度不够无法决定应该显示那个片段,导致片段之间抢占深度的至高点,造成了视觉上的假象, 如下图所示。我们在OpenGL投影矩阵这篇文章已经介绍过了z-fighting的问题,我们发现经过透视除法后的z分量具有了非线性的关系,近平面处有较好的深度精度,而靠近远平面处深度精度较低。

    解决z-fighting的一个常见技巧是让物体之间有一些偏移,不要将物体靠的太近;另一种技巧是使用高精度的深度缓冲。比如使用32bits的深度缓冲,然而这样会占用更多的显存资源。关于如何更好避免z-fighting可以参考StackExchange上面的这个问题:

    Avoiding z-fighting with coincident surfaces

    隐藏面消除 (Hidden Surface Removal, HSR)

    隐藏面消除也叫作可见面确定。其实隐藏面消除的技术我们来说并不陌生,前面讨论的图元组装的裁剪(Clipping)、背面剔除和Z-Buffer技术其实就是隐藏面消除的一种,只不过剔除的粒度有所不同,其中裁剪操作针对的是图元,而Z-Buffer是针对像素点。不同的隐藏面消除技术的主要区别在于剔除的粒度以及不同的剔除目的,但是最终目的都是相同的:减少到达片段着色器的片段的数量,提高渲染的性能。除了裁剪之外,我们下面还将介绍几种比较常见的HSR技术!

    • 视椎体剔除 (Viewing-Frustum Culling)

    视椎体剔除是最常见的一种剔除技术,对于大场景我们根本不可能每帧对每个物体都进行渲染,人们发现我们其实只需要渲染那些摄像机看得到的物体,也就是位于视椎体内的物体,其他位于视椎体外的物体根本不需要渲染,我们可以将其进行剔除,不送入渲染管线,提升我们的渲染效率。一般来说,视椎体剔除是在应用程序阶段完成的,也就是在CPU端进行剔除工作。视椎体剔除利用的是射线检测的方法,根据视椎体的六个平面来检测物体。我们一般利用物体包围盒(Bounding Box)来做交差检测,常见的包围盒有轴对齐包围盒(AABB)和有向包围盒(OBB)两种。由于场景中物体可能非常多,所以一般需要借助高效的数据结构来提升碰撞检测的性能,常见的用于3D场景碰撞检测的数据结构有:八叉树(OcTree)、二分空间划分(Binary Space Partitioning)、四叉树(Quad Tree)、场景图(Scene Graphs)、kd树(K-Dimensional Tree)和层次包围(Bounding Volume Hierarchies)。这些空间数据结构也通常使用在场景管理、光线追踪和相交测试中。




    • 入口剔除 (Portal Culling)

    当我们位于室内时,我们就可以使用入口剔除技术进行裁剪优化了。我们可以将室内的门或者窗户看做视椎体来进行裁剪。不过我们其实看到入口剔除有很大的局限性,一般只能在室内环境下使用,无法再室外场景使用该技术,对于室外的大场景我们一般需要使用下面介绍的遮挡剔除技术。

    • 遮挡剔除 (Occlusion Culling)

    在城市或者森林这种大场景中,我们很容易发现物体之间有很多的遮挡关系,我们需要遮挡剔除技术去掉那些被挡住的物体,来提升渲染效率,如下图所示,左边的图是遮挡剔除前视椎体示意图,右边的图是进行遮挡剔除后需要渲染的物体,可以说大大减少了需要被渲染的物体数量。遮挡剔除的实现方法有很多,既有基于CPU的,也有基于GPU的,也可以混合使用CPU和GPU进行处理。一般进行遮挡剔除时,我们需要通过离线烘焙的犯法来预先计算出潜在可视集合(Potentially Visible Set,PVS)。PVS记录了每个地形块(Tiles)可能看到的物体的集合,用于运行时查找计算。在计算PVS时我们会将场景划分为小的地形块,在每个块上随机选取N个采样点,以这些采样点为起点发出射线来获取场景中相交的物体,记录下物体的ID,求出每个块对应的ID的集合。在运行时根据摄像机的位置获取每个块可见的物体进行渲染。

    提高烘焙的精度通常有两种方法:一是通过减小地形块的大小;二是增加采样点数量。采样的方式有很多,如下面所示。增加采样点的数量可以获得更加精确的结果,减少玩家在块之间切换出现物体闪现或闪失的情况。不过过多的采样点会大大增加离线烘焙的时间,所以需要根据实际情况进行选择采样的数量。我们发现其实基于烘焙的方法缺点还是很明显的:烘焙时间长,需要额外的包体以及无法处理动态的物体。


    Alpha混合 (Alpha Blending)

    讨论完裁切测试、Alpha测试、模板测试和深度测试后,我们接下来看Alpha混合技术。从前面我们已经知道,颜色一般采用RGBA四分量来进行表示,其中颜色的Alpha值用来表示物体本身的不透明度。引入Alpha技术是为了实现半透明的效果,如上图所示。在OpenGL可以通过glEnable(GL_BLEND)来开启混合的功能。Alpha混合也是可配置的阶段,我们根据下面混合方程对混合效果进行调整。

    其中,四个变量分别表示原颜色、原因子值、目标颜色和目标因子值。通过glBlendFuncSeparate()glBlendFunc()glBlendEquation()来设置各种混合效果,常见的选项有GL_ZEROGL_ONEGL_SRC_ALPHAGL_FUNC_ADD等。我们可以从下图看到两种不同的混合效果。

    当场景中既有不透明物体,又有半透明物体时,我们需要先渲染不透明物体,渲染顺序为从前往后(Front-to-Back);然后再渲染半透明物体,渲染顺序为从后往前(Back-to-Front)。我们需要对不透明和半透明物体分开渲染是因为:我们可以透过半透明物体看到半透明物体背后的东西,所以对半透明物体进行渲染时需要后面图层的信息,才能够正确进行混合。

    话说我们对不透明物体按从近到远进行渲染是为了减少深度颜色缓冲器的写入操作,提升性能;而半透明物体需要遵循画家算法由远及近进行渲染是为了渲染的正确性。从上面两张图可以看到,半透明的绘制跟物体的顺序有紧密的关系。从前面的讨论我们知道,我们并不一定能够对场景中的物体按照距离远近进行严格排序,所以怎么正确渲染半透明物体呢?接下来我们介绍顺序无关半透明算法。

    顺序无关半透明算法 (Order-independent transparency,OIT)

    我们知道半透明物体的渲染比较麻烦,需要考虑渲染的顺序,也就是说需要对半透明物体进行排序,这是个比较耗性能的操作,而且很多时候我们并无法严格对半透明物体进行排序。所以,有了一种顺序无关半透明算法(Order-independent transparency,OIT),它可以简化我们对半透明物体的渲染过程,该技术也被称作深度剥离(Depth Peeling)。该技术的思想就是根据物体距离摄像机的远近进行深度信息的逐层剥离,首先记录离相机最近的一层,然后记录距离第二近的层,以此类推,层层递进,如下图所示。我们一般会对剥离的最大层数进行限制,因为我们每一层剥离都需要一个pass进行处理,太多层会影响到运行的效率,比如最大剥离层数限制为4层,那么我们需要4个pass处理这个过程。关于OIT技术可以参考NVIDIA的这篇文章。后来有人提出了双向剥离的算法(Dual Depth Peeling)。其实该算法就是改进我们刚才说到的效率问题,该算法在每个pass可以剥离两个层的信息,即最近和最远两个层。该算法改进了OIT所需要的pass数量,从N变成了N/2 + 1。关于该算法的具体信息可以参考这篇文章


    颜色空间 (Color Spaces)

    常见的颜色空间有:RGB空间、CMY空间、HSV(B)空间和HSL(I)空间。其中RGB和CMY空间主要是用于设备的,而HSV和HSL空间是面向用户的。RGB空间由红Red、绿Green和蓝Blue三种颜色定义,是一种加色的颜色空间,我们所使用的液晶显示器使用的就是RGB颜色空间。我们所使用的颜色表示都是基于RGB空间的,一般每个分量都是归一化到[0,1]之间的数字,对于半透明物体还会使用alpha分量。RGB三颜色混合得到的是白色。

    与RGB空间不同,CMY空间是一种减色系统,所以CMY三种颜色对应的是RGB三种颜色的补色,分别是青Cyan、品红Magenta和黄Yellow。CMY颜色空间通常用于打印设备中。CMY空间和RGB空间的转换非常简单,只需要通过1-RGB便可得到CMY对应的颜色。CMY三颜色混合得到的是黑色。

    上面介绍的RGB和CMY颜色空间其实都是面向设备的,对用户来说并不是很敏感。而接下来介绍的HSV是面向用户的,主要由色调Hue、饱和度Saturation和亮度Value或Brightness三个分量构成。我们通常所使用的调色板其实就是这种颜色模型。其中色调主要用来表征不同的颜色信息,饱和度从来表示颜色的纯度,而亮度表示颜色的明暗程度。


    接下来介绍的颜色空间是HSL,和HSV类似,H表示色调,S表示饱和度,L表示物体的亮度(Lightness或者Intensity)。这两者的数学模型其实都是圆柱。除了上面介绍的几种颜色空间外,还有一种颜色空间叫做Lab颜色空间,是由国际照明委员会CIE引入的。自然界中任何一种颜色都可以在Lab空间中表示出来,它的颜色空间比RGB还要大,另外,这种方式是通过数字化的方式来描述人的视觉感觉,跟设备没有关系,比RGB和CMY更具有普适性。CIE色度图是归一化的坐标空间,我们只需要存储XY两个分量的信息,对应的Z分量可以通过1-X-Y来计算得到。我们介绍的这些颜色空间其实都是可以进行相互转换的。

    抖动处理 (Dithering)

    抖动可以用来增加可用的颜色数量,例如我们可以用8-bits的色深来模拟屏幕上百万颜色的显示,利用黑白两种颜色来模拟多级灰度图像,都是利用抖动的技术。不过,抖动需要用空间分辨率来换取灰度级或者颜色的精度。下图利用红色和蓝色来模拟紫色。印刷业中经常利用黑白两色来模拟多种灰度级。利用的是人眼视觉能够把黑色圆点图案聚集起来的特性,对于一个小区域内的像素的平均灰度,其实正比于其中黑色像素与区域内所有像素个数的比值。

    下面是一个4x4的1-bit的像素数组,从远处看并不是一个具有黑白两色的图案,而是一个具有一定的灰度值的图案。对于这个4x4的数组,尽管有2^16种不同的黑白像素组合模式,但是只有17中可能的灰度值,对应于该像素数组中黑色像素的个数 (0~16个)。OpenGL中默认是开启抖动功能的,我们可以通过glEnable(GL_DITHER)来控制抖动输出功能。

    抖动相关的算法有很多,下面展示了几种算法的效果对比图:

    References

    1. Real Time Rendering,Fourth Edition
    2. Interactive Computer Graphics A Top-Down Approach with Shader-Based OpenGL,Sixth Edition
    3. Fundamentals of Computer Graphics,Third Edition
    4. OpenGL Shading Language
    5. https://developer.tizen.org/development/guides/native-application/graphics/opengl%C2%AE-es/primitive-assembly-and-rasterization?langredirect=1
    6. http://ogldev.atspace.co.uk/
    7. http://www.opengl-tutorial.org/
    8. https://www.scratchapixel.com/index.php?redirect
    9. https://learnopengl.com/
    10. http://www.songho.ca/opengl/index.html
    11. https://en.wikipedia.org/wiki/Hidden-surface_determination
    12. https://www.khronos.org/opengl/wiki/Rendering_Pipeline_Overview
    13. https://zhuanlan.zhihu.com/p/53092784
    14. https://en.wikipedia.org/wiki/Scanline_rendering
    15. https://www.comp.nus.edu.sg/~lowkl/publications/lowk_persp_interp_techrep.pdf
    16. https://gamedev.stackexchange.com/questions/40704/what-is-the-purpose-of-glscissor
    17. Introduction to 3D Game Programming with DirectX 12
    18. 3D Math Primer for Graphics and Game Development
    19. https://en.wikipedia.org/wiki/Order-independent_transparency
    20. https://pubweb.eng.utah.edu/~cs5610/handouts/order_independent_transparency.pdf
    21. https://en.wikipedia.org/wiki/Dither
    22. http://www.openscenegraph.org/index.php/documentation/knowledge-base/36-what-is-a-scene-graph
    23. https://en.wikipedia.org/wiki/Octree
    24. http://archive.gamedev.net/archive/reference/programming/features/bsptree/bsp.pdf
    25. https://pdfs.semanticscholar.org/90e4/c4a65b4b04d9e2374e5753659c102de4c0eb.pdf
    26. https://hal.inria.fr/inria-00537446/file/bounding_volume_hierarchies.pdf
    27. https://www.slideserve.com/lynley/visiblity-culling-and-clipping
    28. https://medium.com/@Umbra3D/introduction-to-occlusion-culling-3d6cfb195c79
    29. https://docs.unity3d.com/Manual/OcclusionCulling.html
    30. https://software.intel.com/en-us/articles/software-occlusion-culling
    31. https://takinginitiative.wordpress.com/2011/05/25/directx10-tutorial-10-shadow-mapping-part-2/
    32. https://www.zhihu.com/search?type=content&q=%E6%8A%97%E9%94%AF%E9%BD%BF
    33. https://fgiesen.wordpress.com/2011/07/10/a-trip-through-the-graphics-pipeline-2011-part-8/

     

     

     

     

     

    展开全文
  • 这组函数说明了如何从 3D 模型中渲染图像。 实现了以下功能: - 3D模型茶壶- 坐标变换- 深度校正- 光栅化(纹理和颜色) - 平面和古罗着色
  • C++软件渲染管线

    2017-10-12 22:16:21
    定义基于左手坐标系的三维数据类型,包括:点(Vector4)、变换矩阵(Tranform Matrix)定义三维模型数据表示:顶点(Vertex)、Camera和三角形(Triangle);实现三维数据操作方法,包括:矢量的加(+)、减(-)、...
  • DirectX12渲染管线 WindowSDK版本:10.0.18362.0 Visual Studion版本:2017 基于Directx12的渲染管道演示。 字体。 基于freetype的文本渲染和3D文本渲染。 一系列的屏幕后期处理效果。 骨架动画。 GPU粒子系统。 ...
  • 细说-图形学渲染管线.zip
  • Unity通用渲染管线的时间反激射 此时间抗锯齿包为Unity的Universal Render Pipeline提供了时间抗锯齿。 它会在整个渲染过程之前抖动相机的投影,从而可以对相邻帧之间的不同表面进行采样。 然后,可以使用提供的可...
  • 渲染管线(流水线,流程) 一、渲染任务 二、三个阶段 1、应用阶段 1-1:数据的准备 1-2:设置渲染状态 1-3:发送DrawCall 2、几何阶段 2-1:顶点着色器 2-2:裁剪 2-3:屏幕映射 3、光栅化阶段 ...

    目录

    渲染管线(流水线,流程)

    一、渲染任务

    二、三个阶段

    1、应用阶段

            1-1:数据的准备

            1-2:设置渲染状态

            1-3:发送DrawCall

    2、几何阶段

            2-1:顶点着色器

            2-2:裁剪

            2-3:屏幕映射

    3、光栅化阶段

            3-1:三角形设置

            3-2:三角形遍历

            3-3:片元着色器

            3-4:逐片元操作

            


    渲染管线(流水线,流程)

    声明:本文章参考自《Real-Time Rendering》书籍加以汇总并引用了其中部分图片

    一、渲染任务

            渲染的任务其实就是从一个三维场景出发,将其进行渲染生成一个二维图像供人眼观察。详细点说,就是CPU和GPU配合,将3D场景中各个对象的坐标,纹理,材质等信息经一系列转换生成人眼可以看见的图像映射到屏幕上。

    二、三个阶段

            渲染的三个阶段一般分为,应用阶段,几何阶段和光栅化阶段,其大致的流程参考下方图1

     图1 渲染管线流程图

    三个阶段的操作对象的流程图大致可以参考一下 图2

     图2 渲染管线操作对象流程图

    1、应用阶段

            这一阶段由CPU处理,主要任务是为接下来GPU的渲染操作提供所需要的几何信息,即输出渲染图元(rending primitives)以供后续阶段的使用。渲染图元就是由若干个顶点构成的几何形状,点,线,三角形,多边形面都可以是一个图元。

            1-1:数据的准备

             第一步应先将不需要的数据剔除出去,如以包围盒为单位的视锥体(粗粒度)剔除,遮挡剔除,层级剔除等等。

             第二步根据UI对象在Herachy面板深度值的顺序(DFS深度优先搜索)设置渲染的顺序,其余物体大体可以按照离摄像机先近后远的规则为后续循环绘制所有对象制定排队顺序。

             第三步先将所有需要的渲染数据从硬盘读取到主存中,再把GPU渲染需要用到的数据打包发给显存(GPU一般没有对主存的访问权限,且与显存进行交换速度较快)。

    打包的数据详细信息见图3

     图3 打包的数据信息

            1-2:设置渲染状态

             渲染状态包括着色器(Shader),纹理,材质,灯光等等。

             设置渲染状态实质上就是,告诉GPU该使用哪个Shader,纹理,材质等去渲染模型网格体,这个过程也就是SetPassCall。当使用不同的材质或者相同材质下不同的Pass时就需要设置切换多个渲染状态,就会增加SetPassCall 所以SetPassCall的次数也能反映性能的优劣。

            1-3:发送DrawCall

             当收到一个DrawCall时,GPU会按照命令,根据渲染状态和输入的顶点信息对指定的模(网格)进行计算渲染。

             CPU通过调用图形API接口( glDrawElements (OpenGl中的图元渲染函数) 或者 DrawIndexedPrimitive (DirectX中的顶点绘制方法) 命令GPU对指定物体进行一次渲染的操作即为DrawCall。此过程实质上就是在告诉GPU该使用哪个模型的数据(图形API函数的功能就是将CPU计算出的顶点数据渲染出来)。

    在应用阶段有三个衡量性能指标非常重要的名词 下面我将再次叙述一下

    DrawCallCPU每次调用图形API接口命令GPU进行渲染的操作称为一次DrawCall。

    SetPassCall:设置/切换一次渲染状态。

    Batch:把数据加载到显存,设置渲染状态,CPU调用GPU渲染的过程称之为一个Batch。

    注:一个Batch包含至少一个DrawCall

      

    2、几何阶段

            几何阶段由GPU进行处理,其几乎要处理所有和几何相关的绘制事情。如绘制的对象,位置,形状。几何阶段处理的对象时渲染图元,进行逐顶点和逐多边形的操作。主要任务是把顶点坐标变换到屏幕空间中,以供给接下来的光栅器进行处理。具体输出的信息有,变换后的屏幕二位顶点坐标,顶点的深度值,着色,法线等等信息。

    接下来对几何阶段的主要流水线阶段进行一下解释:

            2-1:顶点着色器

            流水线的第一个阶段,其可以通过编程进行控制。输入来自CPU发送的顶点信息,每个顶点都会调用一次顶点着色器。其主要工作为:坐标转换和逐顶点光照(可选,计算输出顶点的颜色值)。坐标转换是必须完成的一个任务。其把顶点坐标从模型空间转换到齐次裁剪空间。(齐次裁剪空间不是屏幕空间,是xyz均放缩到-1到1的空间),具体过程可以参考图4

    (提一下:此时GPU处理的顶点并不清楚顶点之间的关系,只是无差别的对待每个顶点,能             很好的体现各个部件的分离,降低耦合性)

    图4 坐标转换

            2-2:裁剪

             顾名思义,就是将不需要的数据对象剔除出去的过程。由于场景一般很大,摄像机的视野范围可能不会覆盖所有的场景物体,裁剪就是为了将那些在摄像机视野范围外的物体剔除出去而被提出来的。

            一个图元和摄像机的关系有三种:完全在视野内、部分在视野内、完全在视野外。完全在视野内的就传递给下一个流水线阶段,完全在视野外的就不会向下传递,而部分在视野内的就需要进行一次处理,就是裁剪。

    下图(图5)展示了一个裁剪的过程:

    图5 裁剪过程

            由图5可清楚的看出,除完全在空间内外的图元被保留和舍弃以外,部分在空间内的图元(黄色三角形)会被裁剪,新的顶点将在空间的边界处生成,原来在外部的顶点会被舍弃 。

            2-3:屏幕映射

                    通过计算将实际场景的对象映射到屏幕上,实质上就是对坐标的放缩,参考图6

    图6 屏幕映射

    3、光栅化阶段

            此阶段仍然由GPU进行处理。这一阶段将会使用上个阶段传递的数据(屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z坐标)、法线方向、视角方向等。)来产生屏幕上的像素,并渲染出最终的图像。光栅化的主要任务是决定渲染图元中的哪些像素应该被绘制在屏幕上,然后对其颜色进行合并混合。

            3-1:三角形设置

             其主要任务是为后续光栅化提供所需要计算的信息。例如,后续阶段需要判断像素点是否被三角形网格覆盖,只靠上个阶段得到的顶点信息无法确定边界的覆盖情况,还需要三角形网格边的信息,所以在这个阶段需要计算出边的表达式以供后续判断的使用。其输出都是为了给下一阶段做相应的准备。

            3-2:三角形遍历

             三角形遍历阶段会根据上一个阶段的计算结果判断一个三角形网格覆盖了哪些像素,并使用三角网格三个顶点的顶点信息对整个覆盖区域进行插值。详细过程见下方介绍:

             此阶段会遍历所有的像素点,判断其是否被三角网格所覆盖 (用3-1计算的结果) ,如果被覆盖,则在此像素点上生成一个片元。片元不是单纯的像素点,其还包含很多状态的集合,这些状态用来最终计算检测筛选每个像素点最终的颜色。(部分状态包括:屏幕坐标,深度值,从几何阶段继承来的法线,纹理等等)。

              片元状态的信息是由其所在三角形网格的三个顶点的信息的插值得到的,例如计算三角形网格重心位置片元的深度 如下图(图7)

     图7 片元状态信息插值

             最终输出的是包含多个片元的片元序列

            3-3:片元着色器

             非常重要的可编程着色器阶段。片元着色器的输入是上一个阶段对顶点信息插值得到的结果,输出为每个片元的颜色值。这一阶段可以按需完成很多重要的渲染技术,最重要的技术之一就是纹理采样

              纹理采样

              为了在片元着色器中进行纹理采样,先在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角形网格的三个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。其局限在于仅可以影响单个片元。即执行片元着色器时,不能将结果直接发给旁边的邻居。片段着色器输出颜色的具体过程如下图(图8)

    图8 计算输出颜色 

            3-4:逐片元操作

             这是OpenGL中的说法,在DirectX中,这阶段被称为输出合并阶段(Output-Merger)。

            该阶段是对每一片 片元 进行操作,主要任务有:

            ①决定每个片元的可见性,如深度测试、模板测试。

            ②如果一个片元通过了所有测试,就把这个片元的颜色值和已经存储在颜色缓冲区的颜色进             行合并,混合。

                    该阶段是高度可配置的,我们可以设置每一步的操作细节。该阶段首先解决的是,每个         片元的可见性问题。这需要进行一系列测试,只有通过了才能和颜色缓冲区进行合并。没通            过任何一 个测试,片元都会被丢弃。见图(9)

    图9 片元测试及合并

             测试过程是很复杂的,不同接口实现细节也不同,下面笔者将讲述一些常用的测试:

             模板测试

                    开启了模板测试,GPU就会使用读取掩码读取模板缓冲区中该片元的模板值,将该值和读取到的参考值进行比较。这个比较函数可以是开发者指定的,例如小于模板值时则舍弃该片元或者大于模板值时舍弃该片元。片元无论有没有通过模板测试都可以根据模板测试和下面的深度测试结果来修改模板缓冲区。这个修改操作也是由开发者指定的。模板测试通常用于限制渲染的区域。

            深度测试

            通过模板测试后,片元就会进行深度测试。其同样是高度可配置的。

            开启后,GPU会把该片元深度值和已存在与深度缓冲区的深度值进行比较,这个比较函数也是开发者设置的。例如小于缓冲区深度值时舍弃该片元,或者大于缓冲区深度值等于时舍弃该片元。通常人们更希望显示离摄像机最近的物体,所以一般比较函数设置为当前片元深度值要小于缓冲区深度值,深度值大无法通过测试。如果片元没有通过测试,则会被丢弃掉。

    与模板测试不同,只有通过之后开发者才能指定是否用该片元的深度值覆盖原有缓冲区的深度值。这是通过开启/关闭深度写入做到的。

            合并操作

            通过了所有测试后,片元就来到了合并操作。

    每个像素的信息被存储在一个名为颜色缓冲区的地方,因此执行此次渲染时,颜色缓冲区中往往已经有了上次渲染之后的结果。所以需要合并的方式使其达到一种均衡状态。

    对于不透明物体,开发者可以选择关闭混合操作。这样片元着色器计算得到的颜色值就会直接覆盖原来颜色缓冲区中的像素值。

    对于半透明物体,需要使用混合操作来让这个物体看起来是透明的。

    混合操作也是可以高度配置的。开启了混合,GPU会取出源颜色和目标颜色将两者混合。

    源颜色是片元着色器得到的颜色,目标颜色是已经存在于颜色缓冲区中的颜色值。

            提前测试

            提前测试的目的主要是为了提高性能

    虽然逻辑上这些测试是在片元着色器之后进行的,但对于大多数GPU来说,他们会尽可能在执行片元着色器之前进行这些测试。

    尽可能早知道哪些片元会被舍弃可以提高性能,比如unity的渲染流水线中的深度测试就在片元着色器之前。这种将深度测试提前的技术被称为Early-Z技术。

    但将这些测试提前其检验结果可能会与片元着色器中一些操作产生冲突。

            至此Unity的渲染管线的大致过程就已经介绍完毕啦~,这是笔者第一次攥写博客部分绘制的流程图和解释还较为粗糙,后续还会继续编写有关Unity的内容,希望能对大家有所帮助(.^◡^.)

            

    展开全文
  • 创建渲染管线资源和渲染管线实例 如果你正在创建你自己的可编程渲染管线(SRP),你的工程必须包含以下: 一个继承自RenderPipelineAsset的脚本,并且还需要重新里边的CreatePipeline()方法,这个脚本定义了你的渲染...

    创建渲染管线资源和渲染管线实例

    如果你正在创建你自己的可编程渲染管线(SRP),你的工程必须包含以下:

    • 一个继承自RenderPipelineAsset的脚本,并且还需要重新里边的CreatePipeline()方法,这个脚本定义了你的渲染管线资源

    • 一个继承自 RenderPipeline的脚本,同时还需要重写 Render()方法, 这个脚本定义了你的渲染管线实例

    • 你从 RenderPipelineAsset 脚本创建的渲染管线资源. 这个资源的行为可以看作是渲染管线实例的工厂类

      因为这些元素之间的联系是很紧密的,所以你应该在同一时间创建它们

    创建一个基本的渲染管线资源和渲染管线实例

    下边的实例展示了如何创建脚本去定义渲染管线实例以及渲染管线资源本身,这包括了用基本的自定义渲染管线资源去实例化渲染管线实例

    1.创建一个名称为:ExampleRenderPipelineAsset.cs的C#脚本

    2.复制和粘贴以下代码到你新创建的脚本中:

    using UnityEngine;
    using UnityEngine.Rendering;
        
    // The CreateAssetMenu attribute 可以让你在Unity编辑器中创建这个类的实例
    [CreateAssetMenu(menuName = "Rendering/ExampleRenderPipelineAsset")]
    public class ExampleRenderPipelineAsset : RenderPipelineAsset
    {
        // unity会在渲染第一帧的时候调用这个方法
        //如果渲染管线资源的设置改变了,unity会销毁当前渲染管线实例,
        //并且会在下一帧渲染之前再次调用这个方法
        protected override RenderPipeline CreatePipeline() {
            //实例化这个自定义SRP用于渲染而使用的渲染管线
            return new ExampleRenderPipelineInstance();
        }
    }
    

    3.创建一个名称为ExampleRenderPipelineInstance.cs的C#脚本

    4.复制和粘贴以下代码到你新创建的脚本中:

    using UnityEngine;
    using UnityEngine.Rendering;
        
    public class ExampleRenderPipelineInstance : RenderPipeline
    {
        public ExampleRenderPipelineInstance() {
        }
        //unity在当前渲染画面中为每个不同类型的摄像机每一帧调用这个方法一次
        protected override void Render (ScriptableRenderContext context, Camera[] cameras) {
            //这里是你可以编写自定义渲染代码的地方,自定义这个方法从而自定义你的SRP
        }
    }
    

    5.在Project视图中,你要么点击加号(+)按钮,要么打开相关联的菜单按钮,然后找到Create按钮,接着选择Rendering->Example Render Pipeline Asset.然后unity会在Project试图下生成一个新的渲染管线资源

    创建可配置的渲染管线资源和渲染管线实例

    默认情况下,渲染管线资源是存储渲染管线实例用于渲染的信息,以及存储默认的材质和shader在编辑器中所使用的数据.在你的RenderPipelineAsset脚本中,你可以扩展你的渲染管线资源,用其来存储额外的数据,你也可以在你的工程中拥有多个配置不同的渲染管线资源.比如说,你可以使用渲染管线资源为每个不同的硬件层保存配置数据.高清晰渲染管线(HDRP)和通用渲染管线(URP)都包含在这里的实例中.

    以下的实例展示了如何创建一个用于定义渲染管线资源的RenderPipelineAsset脚本,并且脚本中含有你可以在Inspector面板中为每个实例设置的公有数据.渲染管线实例在其构造函数中接收渲染管线资源并且使用该管线资源中的数据

    1.创建一个名称为ExampleRenderPipelineAsset.cs的脚本

    2.复制和粘贴以下代码到你新创建的脚本中:

    using UnityEngine;
    using UnityEngine.Rendering;
        
    // The CreateAssetMenu attribute 可以让你在unity编辑器中创建这个类的实例
    [CreateAssetMenu(menuName = "Rendering/ExampleRenderPipelineAsset")]
    public class ExampleRenderPipelineAsset : RenderPipelineAsset
    {
        //为每一个渲染管线资源在Inspector面板中定义的数据
        public Color exampleColor;
        public string exampleString;
        
        	//unity会在渲染第一帧之前调用这个方法
        //如果渲染管线资源的设置改变了,unity会销毁当前渲染管线实例,并且会在下一帧之前再次调用这个方法
        protected override RenderPipeline CreatePipeline() {
            //实例化这个自定义SRP用于渲染的渲染管线,同时给这个渲染管线资源传递一个引用
            //接着渲染管线实例就可以访问上边定义好的配置数据
            return new ExampleRenderPipelineInstance(this);
        }
    }
    

    3.创建一个名为ExampleRenderPipelineInstance.cs的脚本

    4.拷贝和粘贴以下代码到新创建的脚本:

    using UnityEngine;
    using UnityEngine.Rendering;
        
    public class ExampleRenderPipelineInstance : RenderPipeline
    {
        //使用这个变量去引用渲染管线资源,这个渲染管线资源在构造函数中被赋值
        private ExampleRenderPipelineAsset renderPipelineAsset;
        //这个构造函数有一个ExampleRenderPipelineAsset类的实例作为它的参数
        public ExampleRenderPipelineInstance(ExampleRenderPipelineAsset asset) {
            renderPipelineAsset = asset;
        }
        
        //unity在当前渲染画面中为每个不同类型的摄像机每一帧调用这个方法一次
        protected override void Render(ScriptableRenderContext context, Camera[] cameras) {
            //这是一个使用来自渲染管线资源的数据的例子
            Debug.Log(renderPipelineAsset.exampleString);    
            //你可以在这里编写自定义渲染的代码,自定义这个方法以至于自定义你的SRP
        }
    }
    
    

    5.在Project视图中,你既可以点击加号(+)按钮,也可以点击相关的菜单,然后找到Create,再然后选择Rendering->Example Render Pipeline Asset.然后 unity就会在Project视图下创建一个新的渲染管线资源

    展开全文
  • 学习OpenGL CLion CMake _Mac:使用GLFW库在Mac端进行OpenGL开发,开发环境:Clion + CMake + Mac Visual Studio_Windows:使用Windows.h原生API在Windows进行OpenGL开发,开发环境:Visual Studio + Windows
  • Unity 渲染管线

    千次阅读 2020-02-12 10:17:43
    渲染管线1.1 选择一种渲染管线2. 默认管线 Built-in Render Pipeline2.1 渲染通路 Rendering paths2.1.0.1 前向渲染 Forward Rendering2.1.0.2 延迟着色 Deferred Shading2.1.0.3 老版本的延迟着色 Legacy Deferred...

    1. 渲染管线

    Unity中,有多种不同的渲染管线可以选择。简单的说,渲染管线负责从场景中选择物体并渲染到屏幕上,包括三个基本步骤:

    • 裁剪
    • 渲染
    • 后效

    不同的渲染管线有不同的功能和性能特性,分别适应于不同的游戏,应用和平台。

    将项目从一个渲染管线切换到另一个很困难,因为不同渲染管线使用不同的shader进行渲染,而且拥有不同的特性。

    1.1 选择一种渲染管线

    Unity提供了下面的渲染管线:

    • Built-in Render Pipeline 默认渲染管线,是一个通用渲染管线,相对固定,仅可以有限度的进行控制。
    • Universal Render Pipeline (URP) 是可编程渲染管线,可以方便地进行自定义渲染,针对不同的平台进行优化。
    • High Definition Render Pipeline (HDRP) 也是一种可编程渲染管线,可以在高端平台上创建技术领先的,高逼真度的图形渲染。
    • Scriptable Render Pipeline (SRP) 可以基于可编程渲染管线API,创建自定义的渲染管线。你可以从头开始创建一个全新的渲染管线,也可以通过修改URP和HDRP来适应自己的项目。

    2. 默认管线 Built-in Render Pipeline

    默认渲染管线相对于可编程管线,自定义扩展性不高,仅可以通过选择不同的 rendering paths ,并通过command bufferscallbacks 对其功能进行扩展。

    2.1 渲染通路 Rendering paths

    默认管线定义了不同的渲染通路,一个渲染通路是一系列光照和阴影处理。不同的渲染通路的功能和性质特性都不一样,要根据项目特性和目标平台选择合适的渲染通路。

    可以在ProjectSettings>Graphics>Tier Settings种设置不同渲染质量的渲染通路,该设置也可以在摄像机中被覆盖掉。

    如果设置的渲染通路,在硬件设备上不支持,Unity会自动选择低一级的渲染通路。例如,对于不支持延迟着色的硬件,会使用前向渲染。

    2.1.0.1 前向渲染 Forward Rendering

    前向渲染,是默认管线中的默认通路。

    在前向渲染中,实时光照效率非常低。可以通过指定每个像素可以接受的光源数量来调整负载。对场景中其它的光源则会使用逐顶点,或逐对象的高效率,低效果的方式。

    如果你的场景中没有很多的实时光源,或者光照效果没有那么重要,那么选择前向渲染。

    2.1.0.2 延迟着色 Deferred Shading

    延迟着色可以实现更好的光照和阴影渲染效果。

    延迟着色需要GPU的支持,并且有一些限制。不支持半透明物体(这类物体会使用前向渲染),正交投影(这类摄像机使用前向渲染),以及硬件抗锯齿(尽管可以通过后效来实现类似效果)。同时,延迟着色对culling mask不完全支持,而且对 Renderer.receiveShadows 标识按照true(设置无效)来处理。

    如果你的场景有很多的实时光源,需要很高的渲染真实度,并且目标平台硬件支持,则使用延迟着色。

    2.1.0.3 老版本的延迟着色 Legacy Deferred

    Legacy Deferred(light prepass)与Deferred Shading 类似,只是使用了不同的技术,不支持新的PBR standard shader。

    2.1.0.4 老版本的顶点着色 Legacy Vertex Lit

    Legacy Vertex Lit 实现低级效果并且不支持实时阴影,是前向渲染的一部分。基于顶点的光照。

    2.1.0.5 渲染通路比较

    DeferredForwardLegacy DeferredVertex Lit
    Features特性
    逐像素光照(normal maps, light cookies)YesYesYesNo
    实时阴影Yes会有警告?With caveatsYesNo
    反射探针YesYesNoNo
    深度/法线缓冲区Yes需要额外的渲染NoNo
    软边粒子Yes
    半透明对象NoYesNoYes
    抗锯齿NoYesNoYes
    光照裁剪掩码部分支持Yes部分支持
    光照渲染精度逐像素部分逐像素逐像素逐顶点
    性能
    逐像素光照消耗由照亮的像素数决定照亮的像素数*照亮的对象数由照亮的像素数决定
    对象的平均渲染次数1每像素受光照数21
    相对简单场景的消耗中等
    平台支持
    PC(Windows/Mac)Shader Model 3.0+ & MRT(MultiRenderTarget)AllShader Model 3.0+All
    Mobile (iOS/Android)OpenGL ES 3.0 & MRT, Metal (on devices with A8 or later SoC)AllOpenGL ES 2.0All
    ConsolesXB1, PS4AllXB1, PS4, 360All

    2.1.1 前向渲染 Forward rendering path

    前向渲染根据照亮对象的光源的数量,对对象进行一次或多次渲染。光源本身根据其强度等设置,会被进行不同的处理。

    2.1.1.1 实现细节

    前向渲染中,照亮物体,并且一定数量的亮度最高的光源,按照逐像素光照来进行渲染。最多由4个光源按照顶点光照进行渲染。其它的光源,按照球函数(一种特定算法,SH:Spherical Harmonics )计算光照,速度很快但仅仅是一种近似模拟。光源是否被逐像素处理,取决于:

    • 光源的 RenderMode 被设置成 Not Important 的,会被处理成顶点光或球函数模拟光。
    • 最亮的方向光,一定是逐像素光照。
    • Render Mode = Important 的光源是逐像素的。
    • 如果最终对象的逐像素光源数量小于QualitySetting.PixelLightCount设置的值,则会选择更多的光源为逐像素光照,来降低亮度。

    对每个对象的渲染,会执行下面的步骤:

    • Base Pass 来渲染一个逐像素的方向光和所有的逐顶点/SH的光照。
    • 对每个逐像素的光照,额外进行一次渲染。

    例如,对象被一些光源照亮,如下图的圆圈代表的对象,被A-H的8个光源照亮。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BVNLRbxQ-1581473824688)(https://docs.unity3d.com/uploads/Main/ForwardLightsExample.png)]

    这里假定所有的光源,拥有想通过的设置,颜色,强度,默认的RenderMode,因此它们对该对象的影响按照A-H的顺序。最亮的光源按照逐像素渲染(A-D,假定设置为每个对象4个逐像素光源),最多4个光源为逐顶点光照(D-G),剩下的用SH渲染(G-H):

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ASE271PA-1581473824689)(https://docs.unity3d.com/uploads/Main/ForwardLightsClassify.png)]

    注意到光照组之间是重叠的,例如最后一个逐像素的光源同时被混合进了逐顶点光源中,以避免光源或对象的移动导致光照的跳变。

    2.1.1.2 Base Pass

    Base Pass 为每个对象渲染一个逐像素的方向光,所有的逐顶点光源和SH光源。该过程同时会在Shader中处理光照贴图(lightmaps),环境光,自发光。该过程中处理的方向光可以渲染阴影。注意,LightMaps对象不会再渲染SH光照。

    注意,如果Shader中设置了 “Only Directional Light” pass 标识,则该Pass仅渲染主方向光,环境/光照探针和光照贴图(SH和逐顶点光照将不在该Pass中渲染)。

    2.1.1.3 Additional Pass

    Additional Pass 负责渲染影响到对象的一盏光源的逐像素光照。这些pass不会渲染阴影(也就是说前向渲染,仅支持一个带阴影的方向光),除非添加了multi_compile_fwdadd_fullshadows 声明。

    2.1.1.4 效率 Performance Considerations

    Spherical Harmonics光照拥有非常快的效率,CPU占用很小,GPU负载也非常低(base pass总是会计算SH;同时由于其工作原理,SH光源数量的增加,几乎不会增加性能损失)。(SH没有深入研究,所以先按照字面意思解释这部分吧)

    SH光照的缺点:

    • 它们是逐顶点计算的,所以不支持light cookies(简单理解为镂空的灯笼效果,应一张贴图部分遮挡光源)和lightmaps。
    • SH光照将以一个比较低的频率执行。SH光照不会产生强烈的光照过度,它们只会影响漫反射(diffuse)光照(对高光来说频率太低了)。
    • SH光照算法,在光源离表面很近时,渲染效果可能不太正常。

    (译者)以前自研引擎也曾经实现过一种效率很高,但是效果不太好的点光源:

    • 在CPU中计算光照位置,颜色,强度,衰减。这些可以根据参数实时变化,模拟摇曳的灯光等效果。
    • 渲染一个物体时,将照亮该物体的该类光源的参数传给shader
    • 在vertex shader中,按照顶尖距离这些光源的距离,和光源自身的参数,计算该顶点受这些光源的影响,并混合,叠加到顶点颜色中(或者其它的VertexOutput的字段)
    • 在pixel shader中,处理diffuse时混合上面计算的颜色。

    感觉跟SH有点像,基于球形的衰减照亮顶点。

    2.1.2 延迟着色 Deferred Shading rendering path

    延迟着色的技术原理,可以参考Wikipedia: deferred shading

    2.1.2.1 概述

    使用延迟着色时,每个对象能够接收的点光源数量没有限制,所有的光源都将按照逐像素渲染,意味着都能跟光照贴图正确地配合,以及支持cookies和阴影。

    延迟着色地优势是负载跟照亮一个像素的光源数量是成比例的。该负载只跟光源在场景中的体积(照亮范围)有关,而跟它照亮的对象数量无关。因此提升效率的方法之一,就是让光源的照亮范围小一些。延迟着色具有一致性,可以按照我们预想的进行渲染。每个光源都是逐像素的,因此再比较大的三角形上形成颜色块(基于顶点的光照因为只计算了顶点颜色,并再像素阶段进行插值而导致一些色阶)。

    延迟着色的缺点是不支持抗锯齿和半透明物体(这些物体将在前向渲染中处理)。同时不支持对特定对象是否接受阴影的设置,并且culling mask也仅是有限度的支持:最多支持4个culling masks,也就是说culling layer mask 至少要包含所有layers减去4个layers,32-4=28个layers是设置的。

    2.1.2.2 需求

    需要显卡支持Multi Render Target,Shader Mode 3.0+,Depth render texture。2006年以后的PC显卡,从GeForce8XXX以及RadeonX2400,IntelG45开始,都支持。

    在移动端,需要OpenGL ES 3.0。

    注意:延迟着色不支持正交投影,如果摄像机是用正交模式渲染,则会对其启用前向渲染通路。

    2.1.2.3 效率

    延迟着色中光照负载,只跟照亮像素的光源数量有关,而跟场景复杂度无关。所以将点光源或聚光灯光源的范围降低可以降低负载。如果光源整体或部分被场景对象遮挡,则效率更高(照亮的范围更小)。

    如果光源需要渲染阴影,则极大的增加负载。对于每个要投射阴影的对象,每个光源都要进行额外的渲染。同时,渲染阴影的光源,在shader代码效率上也远低于不渲染阴影的shader。

    2.1.2.4 实现细节

    如果对象不支持延迟着色(比如半透明),则它们将在延迟渲染过程完成后,在前向渲染通路中完成。

    G-Buffer(Geometry buffer)对RT0-RT4(render target)的使用如下,数据被渲染到每个render target 的不同通道中:

    • RT0, ARGB32 : Diffuse color 漫反射颜色 (RGB), occlusion(A).
    • RT1, ARGB32 : Specular color 高光颜色 (RGB), roughness 粗糙度(A).
    • RT2, ARGB2101010 : World space normal 世界空间法线 (RGB), unused(A).
    • RT3, ARGB2101010(non-HDR) 或 ARGBHalf(HDR) : Emission + lighting + lightmaps + reflection probes
    • Depth + Stencil buffer 深度+模板缓冲区

    因此,默认的人G-Buffer是每个像素160位(non-HDR)或192位(HDR)。

    如果使用了 ShadowmaskDistance Shadowmask 进行混合渲染,则会用到RT4:

    • RT4, ARGB32 : Light occlusion values(RGBA) 光照遮挡值.

    这时,G-Buffer则是每个像素192位(non-HDR)或224位(HDR)。

    如果硬件不支持5个并行的render target,则使用了shadow masks 的对象,将会由前向渲染管线来渲染。当摄像机未开启HDR时,Emission+lighting buffer (RT3)使用对数编码方式,来提供比普通的ARGB32贴图格式更大的动态范围。

    注意,当摄像机使用HDR渲染时,并不会为Emission+lighting buffer (RT3)单独创建render target,而是将RT3创建为ARGBHalf,每个通道是16位,这样用同一个RT存储。

    (Note that when the Camera is using HDR rendering, there’s no separate rendertarget being created for Emission+lighting buffer (RT3); instead the rendertarget that the Camera renders into (that is, the one that is passed to the image effects) is used as RT3.)

    G-Buffer Pass

    G-Buffer位每个对象渲染一次,将包括漫反射和高光颜色,表面平滑度,世界空间法线,以及emission+ambient+reflections+lightmaps渲染到G-Buffer中。G-Buffer被创建成一个全局的属性,这样后面的shader可以访问(命名为CameraGBufferTexture0 … CameraGBufferTexture3)。

    Lighting Pass

    lighting pass 基于G-Buffer和深度在屏幕空间计算光照,因此负载跟场景复杂度无关,光照被累加仅emission buffer。

    与摄像机的近裁剪面不相交的点光源和聚光灯光源(完全在摄像机近裁剪面的前面),利用场景的Z-buffer测试,进行3D渲染。这样可以将被遮挡的光源过滤掉,提升性能,即对光源进行深度测试,失败了就不进行光照运算了。而对于与近平面相交的方向光,点光/聚光等光源,则渲染成四方形面片。

    如果开启了阴影,则也在该阶段处理。但是这需要对投射阴影的对象额外渲染,并在该阶段使用更加复杂的shader。

    在该渲染管线中,仅可以使用Standard光照模型。如果需要不同的光照模型,则修改lighting pass用的shader,通过将 Built-in shaders 里的Internal-DeferredShading.shader的文件,放到Assets/Resources目录下,并进行修改。然后,在ProjectSettings>Graphics,将Deferred下拉列表,改为Custom Shader,并指定我们改好的shader。

    2.1.3 老版延迟着色 Legacy Deferred rendering path

    关于改着色技术理论,参考 this article

    老版本的延迟着色,是为了兼容Unity5.0版本,主要是因为该版本不支持一些渲染特性(比如standard shader,reflection probes)。新项目不建议再使用该版本管线。

    注意:延迟着色不支持正交投影,如果摄像机是正交视图的,则会使用前向通路渲染。

    2.1.3.1 概述

    使用延迟着色时,每个对象能够接收的点光源数量没有限制,所有的光源都将按照逐像素渲染,意味着都能跟光照贴图正确地配合,以及支持cookies和阴影。

    延迟着色地优势是负载跟照亮一个像素的光源数量是成比例的。该负载只跟光源在场景中的体积(照亮范围)有关,而跟它照亮的对象数量无关。因此提升效率的方法之一,就是让光源的照亮范围小一些。延迟着色具有一致性,可以按照我们预想的进行渲染。每个光源都是逐像素的,因此再比较大的三角形上形成颜色块(基于顶点的光照因为只计算了顶点颜色,并再像素阶段进行插值而导致一些色阶)。

    延迟着色的缺点是不支持抗锯齿和半透明物体(这些物体将在前向渲染中处理)。同时不支持对特定对象是否接受阴影的设置,并且culling mask也仅是有限度的支持:最多支持4个culling masks,也就是说culling layer mask 至少要包含所有layers减去4个layers,32-4=28个layers是设置的。

    2.1.3.2 需求

    需要显卡支持Shade Mode 3.0+,Depth render texture,双面stencil buffers。2004年以后的PC显卡,GeForce FX,Radeon X1300,以及Intel 965/GMA X3100 以后的显卡,都支持。

    2.1.3.3 效率

    延迟着色中光照负载,只跟照亮像素的光源数量有关,而跟场景复杂度无关。所以将点光源或聚光灯光源的范围降低可以降低负载。如果光源整体或部分被场景对象遮挡,则效率更高(照亮的范围更小)。

    如果光源需要渲染阴影,则极大的增加负载。对于每个要投射阴影的对象,每个光源都要进行额外的渲染。同时,渲染阴影的光源,在shader代码效率上也远低于不渲染阴影的shader。

    2.1.3.4 实现细节

    Base Pass

    base pass 对每个对象渲染一次,摄像机空间的法线和高光强度,被渲染到一张ARGB32的Render Texture
    上(法线存储在RBG通道,高管强度存储在A通道)。如果平台或硬件支持将Z-buffer当贴图进行访问,则可以不用渲染深度,否则需要一个额外的渲染通路用shader replacement来渲染深度。

    Base Pass主要是根据场景填充Z-buffer,以及存储像素的法线和高光强度的Render Texture。

    Lighting Pass

    lighting pass 基于深度,法线,高光强度来计算光照。因为计算是在屏幕空间的,所以效率跟场景复杂度没有关系。光照图是一张ARGB32(32位)的render texture,RGB通道用来存储漫反射颜色,A通道存储单色的高光强度。光照计算的值经过对数编码,以达到更大的动态范围。如果摄像机打开了HDR,则缓冲区改用ARGBHalf(64位),也不会执行对数编码了。

    对于点光/聚光灯光源,与摄像机近平面不相交(完全在视锥体内)的,渲染成3D形状(考虑球形),同时进行深度测试,如果像素深度小于光源深度,则受该光源影响。对于与近平面相交的光源(可能光源的位置在摄像机近平面的后面),同样也会渲染3D形状,但是深度测试相反,大于光源深度的像素,受该光源影响。这样部分或全部被遮挡的光源,渲染效率就特别高了。如果一个光源同事与近平面和远平面相交,前面提到的优化就不会被执行了,不会执行深度测试,光照会被渲染成一个面片。

    方向光不会执行上述优化,而是全屏计算。

    如果光源开启了阴影,则也在该阶段处理。但是这需要对投射阴影的对象额外渲染,并在该阶段使用更加复杂的shader。

    该通路下,只能使用Blinn-Phong光照模型,如果需要不同的光照模型,则修改lighting pass用的shader,通过将 Built-in shaders 里的Internal-PrePassLighting.shader的文件,放到Assets/Resources目录下,并进行修改。然后,在ProjectSettings>Graphics,将Lagacy Deferred下拉列表,改为Custom Shader,并指定我们改好的shader。

    Final Pass

    Final Pass 生成最终渲染图像,在这一步中,所有的对象需要再渲染一边,并利用光照图获取光照信息,合并贴图颜色,及其它自发光光照。LightMaps也是再这一步中进行应用。距离摄像机近的,渲染实时光照,并叠加烘焙的方向光光照。距离摄像机远的,过渡到完全的烘焙光照。

    2.1.4 顶点光照渲染 Vertex Lit Rendering Path

    该通路对没个对象渲染一次,并在顶点阶段,对每个顶点执行所有的光源的光照运算。

    该方式速度最快,同时绝大多数的显卡都支持。

    因为所有的光照都是在顶点中完成的,该通路不支持很多逐像素渲染的特性,比如:阴影,法线贴图,light cookies,高细节的镜面高光都不支持。

    2.2 内建渲染管线的硬件需求Hardware Requirements for the Built-in Render Pipeline

    2.2.1 概要

    Win/Mac/LinuxiOS/AndroidConsoles
    Deferred lightingSM3.0, GPU support-Yes
    Forward renderingYesYesYes
    Vertex Lit renderingYesYes-
    Realtime ShadowsGPU supportGPU supportYes
    Image EffectsYesYesYes
    Programmable ShadersYesYesYes
    Fixed Function ShadersYesYes-

    2.2.2实时阴影 Realtime Shadows

    实时阴影在大多数的PC,手机,主机平台上都支持。在Windows系统上(Direct3D API),显卡都要支持阴影映射;多数2003年后的独立显卡,以及2007年以后的集成显卡都支持。技术上,支持Direct3D9的显卡,都要支持D16/D24X8 or DF16/DF24 贴图格式。在OpenGL上,还要支持GL_ARB_depth_texture。

    移动端阴影(iOS/Android)需要OpenGL ES 2.0 以及 GL_OES_depth_texture格式扩展,或者支持OpenGL ES 3.0。因为贴图扩展格式在Tegra3,4上不支持,所以在这些硬件上,也不支持阴影。

    2.2.3 后处理效果 Post-processing Effects

    Post-processing effects 后处理效果,需要render-to-texture功能(向一种特殊的贴图上渲染),目前显卡都是支持的。

    2.2.4 Shaders

    可以编写可编程shader或固定功能shader(这里指的是Unity shader,固定管线没有shader)。在Shader Mode2.0(PC)以后和OpenGL ES 2.0(移动端)以后,都支持可编程shader。如果需要更多的功能,可以选择更高的shader models。固定功能渲染除了主机,所有平台都支持。

    3. 通用渲染管线 Universal Render Pipeline

    Universal Render Pipeline (URP),通用渲染管线是预建的一种可编程渲染管线,URP提供了一种易于美术使用的工作流,快速容易地创建优化地多平台地图形,从移动端,到高端主机,到PC。

    更详细地URP,参考URP package documentation microsite

    4. 高级渲染管线 High Definition Render Pipeline

    HDRP让你可以位高端主机平台创建技术领先地,高逼真度地图形。

    用HDRP创建AAA级地游戏,汽车演示,建筑应用,以及其它需要高逼真度地图形。HDRP使用基于物理地渲染和材质(PBR),支持前向和延迟着色。HDRP使用了compute shader 技术因此需要硬件支持。

    参考:HDRP package documentation microsite

    5. 可编程渲染管线 Scriptable Render Pipeline

    Scriptable Render Pipline允许在C#脚本中控制渲染,进行各种高级地定制。

    Unity提供了2中于定义地SRC,分别是URP和HDRP,这两种都提供了大量定制选项。如果需要对渲染进行更多地控制,可以从头开始写一个全新的SRP。

    可以从头开始创建全新的SRP,也可同通过修改URP和HDRP来满足需求。

    更多信息参考SRP Core package documentation microsite.

    5.1 SRP Batcher 合批

    SRP Batcher,是针对场景里,使用同一个shader的变体的许多materials的对象进行处理,以提升CPU渲染效率的。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lY5ZLGFO-1581473824690)(https://docs.unity3d.com/uploads/Main/SRPBatcher.png)]

    5.1.1开启SRP合批

    只有使用SRP的项目,才能开启SRP Batcher,包括:URP,HDRP,自定义SRP。

    位URP,HDRP开启Batcher:

    1. 在Project窗口,找到UniversalRP-High/Low/MediumQuality.asset并选中。
    2. 在Inspector窗口,在Advanced部分,勾选开启,如下图。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dPbMy3G5-1581473824690)(https://docs.unity3d.com/uploads/Main/SRPBatcher_Asset.png)]

    也可以在代码中修改:

    GraphicsSettings.useScriptableRenderPiplineBatching = true;
    

    如果是XR项目:

    XRSettings.stereoRenderingMode = XRSettings.StereoRenderingMode.SinglePassInstanced;
    

    5.1.2 各个平台,支持合批的Unity版本:

    PlatformMinimum Unity version required
    Windows DirectX 112018.2
    PlayStation 42018.2
    Vulkan2018.3
    OSX Metal2018.3
    iOS Metal2018.3
    Nintendo Switch2018.3
    Xbox One DirectX 112019.2
    OpenGL 4.2 and higher2019.1
    OpenGL ES 3.1 and higher2019.1
    Xbox One DirectX 122019.1
    Windows DirectX 122019.1

    5.1.3 SRP Batcher 工作原理

    在Unity中,可以在任何时候修改任何材质的属性,但这回导致效率损失。例如,DrawCall在使用一个新的材质时,需要做做很多初始化工作。所以场景中的材质越多,CPU需要花费更多的时间位GPU准备数据。传统的方法时通多降低Draw Call调用次数,来降低这方面的CPU消耗。因为真正导致效率问题的,是位调用DrawCall做的准备工作,而DrawCall本身相对较少,主要是向GPU指令缓存提交数据。

    SRP Batcher通过将一系列的Bind和Draw的指令合并到一起,降低设置GPU产生的负载,如下图:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n6hhQ3QZ-1581473824691)(https://docs.unity3d.com/uploads/Main/SROShaderPass.png)]

    如上图,左边是标准的一次DrawCall,需要做的准备工作,远高于右边的一次DrawCall。

    (译者:在自研引擎时,对于GPU的指令也是有缓存的,通过在内存中缓存,每次设置GPU状态时,判断是否改变,如果没有改变,就不设置了,比如Driect3D.SetRenderState(…)。这种缓存是分层的,底层是GPU API级别,如SetRenderState,SetTexture,SetShader,等。上层是材质级别,材质级别需要对渲染对象进行材质排序。细节实际上很复杂,这里只是提一下概念原理。)

    为了达到最大的渲染效率,合批越大越好。位了能将更多的渲染合到一个批次,要尽量使用相同的shader建立不同的material,减少shader变体的数量。

    在内部的渲染循环中,当Unity发现一个新的材质,CPU会收集所有的需要设置到GPU的属性,调用GPU API 在GPU现存中建立常量缓冲区。GPU Buffer的数量,取决于Shader如何声明自己的CBUFFERs。

    位了提升使用了较少的shader变体创建了很多材质使用的场景,SRP在CPU对GPU数据状态进行了缓存。

    SRP Batcher 是一个底层的渲染循环,作用是减少对GPU 显存的修改次数。如果材质没有发生改变,则不会设置和更行GPU现存内容。SRP建立了专门的代码流程,来快速的在一个大的GPU Buffer中更新引擎各种属性,如下图:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lDzUqOTb-1581473824692)(https://docs.unity3d.com/uploads/Main/SRP_Batcher_loop.png)]

    这里,CPU只处理Unity引擎定义的属性,上图中的Per Object large buffer。所有的材质,都在GPU中有固定的CBUFFER,随时可以使用。这对渲染提速在于:所有的材质内容都保留在了显存中,有专门的逻辑来为每个对象属性管理per-object GPU CBUFFER。

    (译者:从该篇幅可以看出,主要还是一种缓存机制,但是能将缓存在内存和显存中进行更加有效的映射,同时所有的材质都在显存中缓存了,很多GPU API 调用都可以省掉了。具体原理,还是只能理解个大概)

    5.1.4 SRP Batcher 的兼容性

    SRP渲染的Object需要:

    • 必须是Mesh或Skinned Mesh,不能是粒子。
    • Shader必须兼容SRP Batcher,HDRP和URP的Lit,Unlit都兼容(除了粒子版本)。

    Shader 兼容性:

    • 需要在名字为“UnityPerDraw”的CBUFFER中声明引擎内建的属性。例如,unity_ObjectToWorld, or unity_SHAr
    • 需要在名字为“UnityPerMaterial”的CBUFFER中声明材质属性。

    可以在Shader Inspector 面板中查看兼容性:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JaQJE6v4-1581473824692)(https://docs.unity3d.com/uploads/Main/SRP_batcher_shader_compatibility.png)]

    Unity会自动识别兼容SRP Batcher的对象,在SRP Batcher 逻辑路径渲染,非兼容对象也会被Standard SRP 逻辑路径渲染。

    Using the SRP Batcher code pathNot using the SRP Batcher code path
    * 使用兼容SRP Batcher 的Sahder的Mesh* 不是mesh的(包括skinned mesh)
    * Shader不兼容的
    * 使用了 Material Property Block 进行渲染的

    5.1.5 SRP Batcher 性能剖析

    为了验证SRP Batcher 对渲染性能的提升,可以向场景中添加 SRPBatcherProfiler.cs 脚本(创建一个空对象,添加脚本),来查看。当该脚本运行时,使用F8键显示/隐藏剖析窗口,F9键开启/停止Batcher。

    界面显示如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GJbXRf7K-1581473824692)(https://docs.unity3d.com/uploads/Main/SRP_Batcher_overlay.png)]

    该系统统计了SRP渲染的CPU时间占用。

    这里的时间累计了一帧的所有的RenderLoop.Draw and Shadows.Draw的时间,无论在哪个线程。例如SRP占用了1.31ms,可以是在主线程draw call占用了0.31ms,1ms是在graphic jobs执行占用的。

    显示信息

    名字描述
    (SRP batcher ON) / (SRP batcher OFF)当前Batcher是否工作。F9键开启/关闭
    CPU Rendering timeSRP循环占用的总CPU时间,无论使用的哪种线程模型,例如single(单线程), client/worker(多线程) or graphics jobs(Job系统)。根据该参数可以看到Batcher的效果,通过F9开启/关闭Batcher来对比该CPU时间,来进行对比。上图例子中是2.11ms。(incl RT idle):表示渲染线程的空闲时间,这意味着是client/worker模式,而没有graphics jobs,渲染线程需要等待主线程的Graphics Commands。该例子中,渲染线程等待了0.36ms。
    - SRP Batcher code path (flushes)b标识了SRP Batcher 逻辑占用的CPU时间。该时间被分成了渲染所有对象(1.18ms),和渲染对象阴影(0.13ms)的时间。如果阴影数量比较多,尝试降低场景中投射阴影的光源的数量,或者将SRP Asset配置中的Cascades参数降低。该例中,是1.31ms。
    flush(s)数量标识了根据shader变体,场景被flush了几次(渲染了多少批次),这里是89次。该值越低越好,越低标识该帧中用到的shader变体越少。
    - Standard code path (flushes)标识了SRP非Batcher逻辑占用的CPU时间。例如骨骼动画模型和粒子。在该例子中,渲染了81个对象,消耗0.8ms,其中影子0.71ms,其它的0.09ms。
    Global Main Loop: (FPS)主循环时间,等于FPS时间。注意:FPS并不是线性的,如果看到FPS提升了20,并不意味着效率提升了。按F9开启/关闭SRP Batcher,对比CPU Rendering time

    5.1.6 Unity Frame Debugger 中的SRP Batcher 数据

    可以在Frame Debugger窗口中查看SRP Batcher的合批情况。

    1. Window > Analysis > Frame Debugger ,左侧列表中,Render Camera > Render Opaques,展开 RenderLoopNewBatcher.Draw 列表,查看合批次数。
    2. 点击任意的 SRP Batch 查看其状态。

    SRP Batch 细节显示了调用了多少次DrawCall,Shader附加的关键字,为什么没有于上个批次合到一起。在下面的例子中,原因是:Node use different shader keywords,即该批次使用了与上个批次不同的shader keywords,因为使用了不同的shader变体,合批被打断了。如果SRP的DrawCall次数较低,说明很可能用到了太多的shader变体。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-foI634iK-1581473824693)(https://docs.unity3d.com/uploads/Main/SRP_Batcher_batch_information.png)]

    如果要实现自己的SRP,而不使用URP或HDRP,尝试写一个使用最少的shader keywords的通用的’uber’ shader (但是可以根据需要使用材质参数和属性,没有太大限制)。

    展开全文
  • Unity 可编程渲染管线, LWRP轻量级渲染管线(Light Weight Render Pipeline)出来有一段时间了, 今天写一遍文章,详细的介绍一下可编程渲染管线与LWRP轻量级渲染管线相关内容,帮助大家搞懂这些概念,并建立起一个学习...
  • 我的GL 纯软件渲染管线模拟器,仅供学习。 基于。 去做 优化 截图
  • Unity内置渲染管线_学习笔记

    千次阅读 2020-09-04 03:00:39
    Unity_内置渲染管线 Unity 的内置渲染管线是通用渲染管线。· 内置渲染管线在自定义扩展方面比可编程渲染管线受到的限制更多,但是您可以在不同的渲染路径之间进行选择,并通过命令缓冲区和回调来扩展其功能。 ...
  • 至2018.1版本,Unity中除了默认渲染管线,还提供了轻量级渲染管线(Lightweight Pipeline)和高清晰渲染管线(HD Pipleline)二个渲染管线。当然也支持自定义渲染管线。与高清晰渲染管线相比,...
  • Unity URP/SRP 渲染管线浅入深出【匠】

    万次阅读 多人点赞 2020-11-25 10:35:44
    SRP全称为Scriptable Render Pipeline(可编程渲染管线/脚本化渲染管线),是Unity提供的新渲染系统,SRP是一个瘦API层,可以在Unity通过C#脚本调用一系列API配置和执行渲染命令的方式来实现渲染流程,SRP将这些命令...
  • PBR 一 渲染管线

    2020-12-09 16:08:32
    渲染管线前言渲染管线GPU渲染管线可编程着色器着色器的历史顶点着色器几何着色器像素着色器 前言 该部分主要是对于参考资料的再次创作。 参考资料: 计算机图形学-光栅渲染概述 前一篇中介绍了图像学的梗概,...
  • 从图形渲染管线谈性能优化
  • 渲染管线简介

    2018-08-07 21:45:49
    渲染管线入门    渲染管线中一些技术名词大部分是英文直译,光看中文很容易被误导。所以我们必须在了解一个技术的具体作用之后,才能了解它到底是什么。这篇博客我主要翻译自Minh Tri Do Dinh的GPUs - Graphics ...
  • OpenGL图形渲染管线(Pipeline)学习1.0 图形渲染管线概述2.0 图形渲染管线处理流程2.1 顶点数据(Vertex Data)2.2 顶点着色器(Vertex Shader)2.2.1 世界坐标2.2.2 局部坐标2.2.3 观察坐标2.2.4 裁剪坐标:2.3 ...
  • Unity渲染流程(渲染管线)(渲染流水线)一 渲染的任务二 三个概念阶段三 应用阶段1 准备好需要被渲染的场景数据,做粗粒度剔除2 设置每个对象的渲染状态。3 发送DrawCall。四 几何阶段1 顶点着色器2 裁剪3 屏幕...
  • 地狱 由GPU驱动的Vulkan渲染器,具有基于Nimble的渲染图实现。
  • 渲染管线理论总结

    2021-06-20 14:42:48
    虽然已经用C语言实现了图形渲染管线的整套流程,但是对于一些理论还是不够熟悉,于是这里总结面试题方面的理论 1.GPU渲染流程 所有渲染,都是将数据从CPU传输到GPU的过程。 从GPU的视角来看渲染的话,非常简洁...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 15,646
精华内容 6,258
关键字:

渲染管线