【unity shader 入门精要】CH9 更复杂的光照

Unity的渲染路径

在Unity里,渲染路径(Rendering Path)决定了光照是如何应用到UnityShader中的。

如果要和光源打交道,我们需要为每个Pass指定它使用的渲染路径。

大多数情况下,一个项目只使用一种渲染路径,因此我们可以为整个项目设置渲染时的渲染路径。我们可以通过在Unity的Edit → Project Settings →Player → Other Settings → Rendering Path中选择项目所需的渲染路径。默认情况下,该设置选择的是前向渲染路径。

但有时,我们希望可以使用多个渲染路径,例如摄像机A渲染的物体使用前向渲染路径,而摄像机B渲染的物体使用延迟渲染路径。这时,我们可以在每个摄像机的渲染路径设置中设置该摄像机使用的渲染路径,以覆盖ProjectSettings中的设置。

如果当前的显卡并不支持所选择的渲染路径,Unity会自动使用更低一级的渲染路径。例如,如果一个GPU不支持延迟渲染,那么Unity就会使用前向渲染。

完成了上面的设置后,我们就可以在每个Pass中使用标签来指定该Pass使用的渲染路径。这是通过设置Pass的LightMode标签实现的。不同类型的渲染路径可能会包含多种标签设置。

在这里插入图片描述
该Pass使用前向渲染路径中的ForwardBase路径。而前向渲染路径还有一种路径叫做ForwardAdd。

在这里插入图片描述
Unity支持多种类型的渲染路径。在Unity 5.0版本之前,主要有3种:前向渲染路径(Forward Rendering Path)、延迟渲染路径(DeferredRendering Path)和顶点照明渲染路径(Vertex Lit Rendering Path)。但在Unity 5.0版本以后,顶点照明渲染路径已经被Unity抛弃;新的延迟渲染路径代替了原来的延迟渲染路径。

前向渲染路径

前向渲染路径的原理

每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元,并计算两个缓冲区的信息:一个是颜色缓冲区,一个是深度缓冲区

我们利用深度缓冲来决定一个片元是否可见,如果可见就更新颜色缓冲区中的颜色值。我们可以用下面的伪代码来描述前向渲染路径的大致过程:

在这里插入图片描述
对于每个逐像素光源,我们都需要进行上面一次完整的渲染流程。如果一个物体在多个逐像素光源的影响区域内,那么该物体就需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合起来得到最终的颜色值。假设,场景中有N个物体,每个物体受M个光源的影响,那么要渲染整个场景一共需要N*M个Pass。可以看出,如果有大量逐像素光照,那么需要执行的Pass数目也会很大。因此,渲染引擎通常会限制每个物体的逐像素光照的数目。

Unity中的前向渲染

事实上,一个Pass不仅仅可以用来计算逐像素光照,它也可以用来计算逐顶点等其他光照。这取决于光照计算所处流水线阶段以及计算时使用的数学模型。

当我们渲染一个物体时,Unity会计算哪些光源照亮了它,以及这些光源照亮该物体的方式。

在Unity中,前向渲染路径有3种处理光照(即照亮物体)的方式:逐顶点处理、逐像素处理,球谐函数(Spherical Harmonics, SH)处理。而决定一个光源使用哪种处理模式取决于它的类型渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否是重要的(Important)。如果我们把一个光照的模式设置为Important,意味着把它当成一个逐像素光源来处理。我们可以在光源的Light组件中设置这些属性。

在前向渲染中,当我们渲染一个物体时,Unity会根据场景中各个光源的设置以及这些光源对物体的影响程度(例如,距离该物体的远近、光源强度等)对这些光源进行一个重要度排序。其中,一定数目的光源会按逐像素的方式处理,然后最多有4个光源按逐顶点的方式处理,剩下的光源可以按SH方式处理。Unity使用的判断规则如下:

  1. 场景中最亮的平行光总是按逐像素处理的。
  2. 渲染模式被设置成Not Important的光源,会按逐顶点或者SH处理。
  3. 渲染模式被设置成Important的光源,会按逐像素处理。
  4. 如果根据以上规则得到的逐像素光源数量小于Quality Setting中的逐像素光源数量(Pixel Light Count),会有更多的光源以逐像素的方式进行渲染。

在Pass里进行光照计算。前向渲染有两种Pass:Base Pass和Additional Pass:
在这里插入图片描述

Base Pass中渲染的平行光默认是支持阴影的(需要开启光源的阴影功能),而Additional Pass中渲染的光源在默认情况下是没有阴影效果的,即便我们在它的Light组件中设置了有阴影的Shadow Type。
但我们可以在Additional Pass中使用 #pragmamulti_compile_fwdadd_fullshadows 代替 #pragmamulti_compile_fwdadd 编译指令,为点光源和聚光灯开启阴影效果,但这需要Unity在内部使用更多的Shader变种。

环境光和自发光也是在Base Pass中计算的。这是因为,对于一个物体来说,环境光和自发光我们只希望计算一次即可,而如果我们在AdditionalPass中计算这两种光照,就会造成叠加多次环境光和自发光,这不是我们想要的。

在Additional Pass的渲染设置中,我们还开启和设置了混合模式。这是因为,我们希望每个Additional Pass可以与上一次的光照结果在帧缓存中进行叠加,从而得到最终的有多个光照的渲染效果。如果我们没有开启和设置混合模式,那么Additional Pass的渲染结果会覆盖掉之前的渲染结果,看起来就好像该物体只受该光源的影响。通常情况下,我们选择的混合模式是 Blend One One

对于前向渲染来说,一个Unity Shader通常会定义一个Base Pass(BasePass也可以定义多次,例如需要双面渲染等情况)以及一个AdditionalPass一个Base Pass仅会执行一次(定义了多个Base Pass的情况除外),而一个Additional Pass会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行一次Additional Pass

内置的光照变量和函数

根据我们使用的渲染路径(即Pass标签中LightMode的值),Unity会把不同的光照变量传递给Shader。

前向渲染可以使用的内置光照变量:

在这里插入图片描述

前向渲染可以使用的内置光照函数:
在这里插入图片描述

顶点照明渲染路径

顶点照明渲染路径是对硬件配置要求最少、运算性能最高,但同时也是得到的效果最差的一种类型,它不支持那些逐像素才能得到的效果,例如阴影、法线映射、高精度的高光反射等。

实际上,它仅仅是前向渲染路径的一个子集,也就是说,所有可以在顶点照明渲染路径中实现的功能都可以在前向渲染路径中完成。顶点照明渲染路径只是使用了逐顶点的方式来计算光照。

我们在上面的前向渲染路径中也可以计算一些逐顶点的光源。但如果选择使用顶点照明渲染路径,那么Unity会只填充那些逐顶点相关的光源变量,不可以使用一些逐像素光照变量。

Unity中的顶点照明渲染

顶点照明渲染路径通常在一个Pass中就可以完成对物体的渲染。在这个Pass中,我们会计算我们关心的所有光源对该物体的照明,并且这个计算是按逐顶点处理的。这是Unity中最快速的渲染路径,并且具有最广泛的硬件支持。

延迟渲染路径

前向渲染的问题是:当场景中包含大量实时光源时,前向渲染的性能会急速下降。例如,如果我们在场景的某一块区域放置了多个光源,这些光源影响的区域互相重叠,那么为了得到最终的光照效果,我们就需要为该区域内的每个物体执行多个Pass来计算不同光源对该物体的光照结果,然后在颜色缓存中把这些结果混合起来得到最终的光照。然而,每执行一个Pass我们都需要重新渲染一遍物体,但很多计算实际上是重复的。

延迟渲染是一种更古老的渲染方法,但由于上述前向渲染可能造成的瓶颈问题,近几年又流行起来。除了前向渲染中使用的颜色缓冲深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区也被统称为G缓冲(G-buffer)(Geometry)。G缓冲区存储了我们所关心的表面(通常指的是离摄像机最近的表面)的其他信息,例如该表面的法线、位置、用于光照计算的材质属性等

延迟渲染的原理

延迟渲染主要包含了两个Pass

在第一个Pass中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到G缓冲区中

在第二个Pass中,我们利用G缓冲区的各个片元信息,例如表面法线、视角方向、漫反射系数等,进行真正的光照计算

延迟渲染使用的Pass数目通常就是两个,这跟场景中包含的光源数目是没有关系的。换句话说,延迟渲染的效率不依赖于场景的复杂度,而是和我们使用的屏幕空间的大小有关。这是因为,我们需要的信息都存储在缓冲区中,而这些缓冲区可以理解成是一张张2D图像,我们的计算实际上就是在这些图像空间中进行的。

Unity中的延迟渲染

对于延迟渲染路径来说,它最适合在场景中光源数目很多、如果使用前向渲染会造成性能瓶颈的情况下使用。而且,延迟渲染路径中的每个光源都可以按逐像素的方式处理。但是,延迟渲染也有一些缺点。

  1. 不支持真正的抗锯齿(anti-aliasing)功能。
  2. 不能处理半透明物体。
  3. 对显卡有一定要求。如果要使用延迟渲染的话,显卡必须支持MRT(Multiple Render Targets)、Shader Mode 3.0及以上、深度渲染纹理以及双面的模板缓冲。

当使用延迟渲染时,Unity要求我们提供两个Pass:

  1. 第一个Pass用于渲染G缓冲。在这个Pass中,我们会把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲区中。对于每个物体来说,这个Pass仅会执行一次
  2. 第二个Pass用于计算真正的光照模型。这个Pass会使用上一个Pass中渲染的数据来计算最终的光照颜色,再存储到帧缓冲中。

默认的G缓冲区包含了以下几个渲染纹理(Render Texture, RT):

  1. RT0:格式是ARGB32, RGB通道用于存储漫反射颜色,A通道没有被使用
  2. RT1:格式是ARGB32, RGB通道用于存储高光反射颜色,A通道用于存储高光反射的指数部分。
  3. RT2:格式是ARGB2101010, RGB通道用于存储法线,A通道没有被使用
  4. RT3:格式是ARGB32(非HDR)或ARGBHalf(HDR),用于存储自发光+lightmap+反射探针(reflection probes)。
  5. 深度缓冲和模板缓冲。

当在第二个Pass中计算光照时,默认情况下仅可以使用Unity内置的Standard光照模型。如果我们想要使用其他的光照模型,就需要替换掉原有的Internal-DeferredShading.shader文件。

Unity的光源类型

Unity一共支持4种光源类型:平行光点光源聚光灯面光源(arealight)。

面光源仅在烘焙时才可发挥作用,因此不在本节讨论范围内。

由于每种光源的几何定义不同,因此它们对应的光源属性也就各不相同。这就要求我们要区别对待它们。

最常使用的光源属性有光源的位置、方向(更具体说就是,到某点的方向)、颜色、强度以及衰减(更具体说就是,到某点的衰减,与该点到光源的距离有关)这5个属性。

平行光

平行光可以照亮的范围是没有限制的,它通常是作为太阳这样的角色在场景中出现的。

在这里插入图片描述

平行光之所以简单,是因为它没有一个唯一的位置,也就是说,它可以放在场景中的任意位置(回忆一下,我们小时候是不是总感觉太阳跟着我们一起移动)。它的几何属性只有方向,我们可以调整平行光的Transform组件中的Rotation属性来改变它的光源方向,而且平行光到场景中所有点的方向都是一样的,这也是平行光名字的由来。除此之外,由于平行光没有一个具体的位置,因此也没有衰减的概念,光照强度不会随着距离而发生改变

点光源

点光源的照亮空间则是有限的,它是由空间中的一个球体定义的。

点光源可以表示由一个点发出的、向所有方向延伸的光。

在这里插入图片描述
球体的半径可以由面板中的Range属性来调整,也可以在Scene视图中直接拖拉点光源的线框(如球体上的黄色控制点)来修改它的属性。

【以下属性参照生活中的灯泡理解】

点光源是有位置属性的,它是由点光源的Transform组件中的Position属性定义的。

对于方向属性,我们需要用点光源的位置减去某点的位置来得到它到该点的方向。

而点光源的颜色和强度可以在Light组件面板中调整。

点光源也是会衰减的,随着物体逐渐远离点光源,它接收到的光照强度也会逐渐减小。

点光源球心处的光照强度最强,球体边界处的最弱,值为0。其中间的衰减值可以由一个函数定义。

聚光灯

聚光灯的照亮空间同样是有限的,但不再是简单的球体,而是由空间中的一块锥形区域定义的。

聚光灯可以用于表示由一个特定位置出发、向特定方向延伸的光。

unity中的聚光灯:
在这里插入图片描述
生活中的聚光灯:
在这里插入图片描述
这块锥形区域的半径由面板中的Range属性决定,而锥体的张开角度由SpotAngle属性决定。我们同样也可以在Scene视图中直接拖拉聚光灯的线框(如中间的黄色控制点以及四周的黄色控制点)来修改它的属性。

聚光灯的位置同样是由Transform组件中的Position属性定义的。

对于方向属性,我们需要用聚光灯的位置减去某点的位置来得到它到该点的方向。

聚光灯的衰减也是随着物体逐渐远离点光源而逐渐减小,在锥形的顶点处光照强度最强,在锥形的边界处强度为0。其中间的衰减值可以由一个函数定义,这个函数相对于点光源衰减计算公式要更加复杂,因为我们需要判断一个点是否在锥体的范围内。

在前向渲染中处理不同的光源类型

在Unity Shader中访问不同光源类型的5个属性:位置、方向、颜色、强度以及衰减。

在这里插入图片描述

如果场景中包含了多个平行光,Unity会选择最亮的平行光传递给Base Pass进行逐像素处理,其他平行光会按照逐顶点或在Additional Pass中按逐像素的方式处理。如果场景中没有任何平行光,那么Base Pass会当成全黑的光源处理。

Unity处理这些点光源的顺序是按照它们的重要度排序的。若所有点光源的颜色和强度都相同,则它们的重要度取决于它们距离胶囊体的远近,首先绘制的是距离胶囊体最近的点光源。

对于场景中的一个物体,如果它不在一个光源的光照范围内,Unity是不会为这个物体调用Pass来处理这个光源的。

Unity的光照衰减

用于光照衰减的纹理

Unity在内部使用一张名为 _LightTexture0 的纹理来计算光源衰减。需要注意的是,如果我们对该光源使用了cookie,那么衰减查找纹理是 _LightTextureB0 ,但这里不讨论这种情况。我们通常只关心_LightTexture0对角线上的纹理颜色值,这些值表明了在光源空间中不同位置的点的衰减值。

(0, 0)点表明了与光源位置重合的点的衰减值,而(1, 1)点表明了在光源空间中所关心的距离最远的点的衰减。

使用数学公式计算衰减

Unity的阴影

为了让场景看起来更加真实,具有深度信息,我们通常希望光源可以把一些物体的阴影投射在其他物体上。

阴影是如何实现的

推荐阅读:阴影映射 - LearnOpenGL CN

生活中的阴影:当一个光源发射的一条光线遇到一个不透明物体时,这条光线就不可以再继续照亮其他物体(这里不考虑光线反射)。因此,这个物体就会向它旁边的物体投射阴影,那些阴影区域的产生是因为光线无法到达这些区域。

在实时渲染中,我们最常使用的是一种名为Shadow Map的技术。
阴影映射(Shadow Mapping)背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。

在这里插入图片描述
蓝线代表光源可以看到的fragment。黑线代表被遮挡的fragment:它们应该渲染为带阴影的。

在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的阴影映射纹理(shadowmap)。这张阴影映射纹理本质上也是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。


计算阴影映射纹理的一种方法是,先把摄像机放置到光源的位置上,然后按正常的渲染流程,即调用Base Pass和Additional Pass来更新深度信息,得到阴影映射纹理。但这种方法会对性能造成一定的浪费,因为我们实际上仅仅需要深度信息而已,而BasePass和Additional Pass中往往涉及很多复杂的光照模型计算。

因此,Unity选择使用一个额外的Pass来专门更新光源的阴影映射纹理,这个Pass就是LightMode标签被设置为ShadowCaster的Pass。这个Pass的渲染目标不是帧缓存,而是阴影映射纹理(或深度纹理)

Unity首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。

当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的Unity Shader中找到LightMode为ShadowCaster的Pass,如果没有,它就会在Fallback指定的Unity Shader中继续寻找,如果仍然没有找到,该物体就无法向其他物体投射阴影(但它仍然可以接收来自其他物体的阴影)。当找到了一个LightMode为ShadowCaster的Pass后,Unity会使用该Pass来更新光源的阴影映射纹理。


Unity使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术(Screenspace Shadow Map)。屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法。Unity首先会通过调用LightMode为ShadowCaster的Pass来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在Shader中对阴影图进行采样。由于阴影图是屏幕空间下的,因此,我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。

不透明物体的阴影

为了让场景中可以产生阴影,我们首先需要让平行光可以收集阴影信息。这需要在光源的Light组件中开启阴影。

让物体投射阴影

在Unity中,我们可以选择是否让一个物体投射或接收阴影。这是通过设置Mesh Renderer组件中的Cast Shadows和Receive Shadows属性来实现的。

在这里插入图片描述

如果开启了Cast Shadows属性,那么Unity就会把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。

这个过程是通过为该物体执行LightMode为ShadowCaster的Pass来实现的。

Receive Shadows则可以选择是否让物体接收来自其他物体的阴影。
如果没有开启Receive Shadows,那么当我们调用Unity的内置宏和变量计算阴影(在后面我们会看到如何实现)时,这些宏通过判断该物体没有开启接收阴影的功能,就不会在内部为我们计算阴影。


在前向渲染中,宏 SHADOW_COORDS 实际上就是声明了一个名为 _ShadowCoord 的阴影纹理坐标变量。而 TRANSFER_SHADOW 的实现会根据平台不同而有所差异。如果当前平台可以使用屏幕空间的阴影映射技术(通过判断是否定义了 UNITY_NO_SCREENSPACE_SHADOWS 来得到),TRANSFER_SHADOW会调用内置的ComputeScreenPos函数来计算_ShadowCoord;如果该平台不支持屏幕空间的阴影映射技术,就会使用传统的阴影映射技术,TRANSFER_SHADOW会把顶点坐标从模型空间变换到光源空间后存储到_ShadowCoord中。然后,SHADOW_ATTENUATION负责使用_ShadowCoord对相关的纹理进行采样,得到阴影信息。

统一管理光照衰减和阴影

在Unity Shader的前向渲染路径中计算光照衰减——在Base Pass中,平行光的衰减因子总是等于1

而在Additional Pass中,我们需要判断该Pass处理的光源类型,再使用内置变量和宏计算衰减因子。

实际上,光照衰减和阴影对物体最终的渲染结果的影响本质上是相同的——我们都是把光照衰减因子和阴影值及光照结果相乘得到最终的渲染结果。

为了同时计算两个信息,Unity在Shader里提供了这样的功能,主要是通过内置的 UNITY_LIGHT_ATTENUATION 宏来实现的。

UNITY_LIGHT_ATTENUATION 是Unity内置的用于计算光照衰减和阴影的宏,我们可以在内置的 AutoLight.cginc 里找到它的相关声明。它接受3个参数,它会将光照衰减和阴影值相乘后的结果存储到第一个参数中

注意到,我们并没有在代码中声明第一个参数atten,这是因为UNITY_LIGHT_ATTENUATION会帮我们声明这个变量。它的第二个参数是结构体v2f,这个参数会传递给上节中使用的SHADOW_ATTENUATION,用来计算阴影值。而第三个参数是世界空间的坐标,这个参数会用于计算光源空间下的坐标,再对光照衰减纹理采样来得到光照衰减。

Unity针对不同光源类型、是否启用cookie等不同情况声明了多个版本的UNITY_LIGHT_ATTENUATION。这些不同版本的声明是保证我们可以通过这样一个简单的代码来得到正确结果的关键。

由于使用了 UNITY_LIGHT_ATTENUATION ,我们的Base Pass和Additional Pass的代码得以统一——我们不需要在Base Pass里单独处理阴影,也不需要在Additional Pass中判断光源类型来处理光照衰减,一切都只需要通过 UNITY_LIGHT_ATTENUATION 来完成即可。这正是Unity内置文件的魅力所在。如果我们希望可以在Additional Pass中添加阴影效果,就需要使用 #pragma multi_compile_fwdadd_fullshadows 编译指令来代替Additional Pass中的 #pragma multi_compile_fwdadd 指令。这样一来,Unity也会为这些额外的逐像素光源计算阴影,并传递给Shader。

透明度物体的阴影

想要在Unity里让物体能够向其他物体投射阴影,一定要在它使用的Unity Shader中提供一个LightMode为ShadowCaster的Pass。在前面的例子中,我们使用内置的VertexLit中提供的ShadowCaster来投射阴影。VertexLit中的ShadowCaster实现很简单,它会正常渲染整个物体,然后把深度结果输出到一张深度图或阴影映射纹理中。

对于大多数不透明物体来说,把Fallback设为VertexLit就可以得到正确的阴影。

但对于透明物体来说,我们就需要小心处理它的阴影。透明物体的实现通常会使用透明度测试或透明度混合,我们需要小心设置这些物体的Fallback。透明度测试的处理比较简单,但如果我们仍然直接使用VertexLit、Diffuse、Specular等作为回调,往往无法得到正确的阴影。这是因为透明度测试需要在片元着色器中舍弃某些片元,而VertexLit中的阴影投射纹理并没有进行这样的操作。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值