Unity的渲染路径
RenderingPath (渲染路径) 决定了光照在UnityShader中的应用,需要为每个Pass 都设置合适的渲染路径,但是呢一般一个项目只使用一种渲染路径,可以在项目里设置默认的。
Unity5之前有三种渲染路径:前向渲染路径(Forward Rendering Path),延迟渲染路径(Deferred Rendering Path)和顶点照明渲染路径(Vertex Lit Rendering Path)。在5之后的版本 顶点照明渲染被舍弃 但是还是兼容支持,而且使用了新的延迟渲染路径。
前面说到 一般一个项目就一个渲染路径 所以可以在Unity中设置一个默认的
有时需要使用多个路径的时候,比如两个相机A用前向渲染,B用延迟渲染。那么可以在单独的摄像机上设置来覆盖ProjectSetting的设置
Project Settings 中的设置。需要注意的是,如果当前的显卡并不支持所选择的渲染路径,Unity 会自动使用更低一级的渲染路径。例如,如果一个GPU 不支持延迟渲染,那么Unity 就会使用前向渲染。
完成了上面的设置后,我们就可以在每个Pass 中使用标签来指定该Pass 使用的渲染路径。这是通过设置Pass 的 LightMode 标签实现的。不同类型的渲染路径可能会包含多种标签设置。
例如,在们之前在代码中写的:
Pass {
Tags { "LightMode"="ForwardBase" }
告诉Unity 该Pass使用前向渲染中的ForwardBase路径,还有一种ForwardAdd路径。
这个渲染路径的指定还是很必要的,这样会通知Pass提供合适正确的光照属性。不然很有可能光照变量不能正确赋值,最后计算出来的效果也很有可能错误。如果不指定合适的标签话,Unity5会当成一个和顶点照明渲染等同的pass(可是这样已经放弃~)
前向渲染路径
1 . 前向渲染路径的原理
每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元,并计算两个缓冲区的信息:一个是颜色缓冲区, 一个是深度缓冲区。我们利用深度缓冲来决定一个片元是否可见,如果可见就更新颜色缓冲区中的颜色值。我们可以用下面的伪代码来描述前向渲染路径的大致过程:
对于每个逐像素光源,我们都需要进行上面一次完整的渲染流程。如果一个物体在多个逐像素光源的影响区域内,那么该物体就需要执行多个Pass,每个Pass 计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合起来得到最终的颜色值。假设,场景中有N个物体,每个物体受M 个光源的影响,那么要渲染整个场景一共需要 N*M 个Pass 。可以看出,如果有大量逐像素光照, 那么需要执行的Pass 数目也会很大。因此,渲染引擎通常会限制每个物体的逐像素光照的数目。
2. Unity 中的前向渲染
Pass不仅可以计算逐像素光照还可以逐顶点等其它光照,取决于光照计算所处流水线阶段以及计算时使用的数学模型。
这个计算过程是需要 光照和物体的shader一同决定的。不同的灯光告诉Unity我是怎样的(前向或者延时渲染等等)。物体告诉Unity我只需要什么样的灯光(LightMode标签),我拿到这些灯光以后怎么计算。这两部分实现。处理的光照方式有3种:逐顶点处理,逐像素处理,球谐函数(SphericalHarmonics)处理。实际到底应该用哪种取决于它的类型和渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否是重要的(Important )。如果我们把一个光照的模式设置为Important , 意味着我们告诉Unity,“嘿老兄,这个光源很重要,我希望你可以认真对待它,把它当成一个逐像素光源来处理!”我们可以在光源的Light 组件中设置这些属性
Unity有自己的一套标准来判断光照的重要程度,会根据各个光影的位置以及对物体的影响程度(如:物体的远近,光源强度等)对光源进行排序。最后的结果就是一定数目的光源按照逐像素的方式处理,然后最多有4个光源按照逐顶点的方式处理。剩下的按照球谐(SH)方式。标准如下
· 场景中最亮的平行光总是按逐像素处理的。
· 渲染模式被设置成Not Important 的光源,会按逐顶点或者SH 处理。
· 渲染模式被设置成Important 的光源,会按逐像素处理。
· 如果根据以上规则得到的逐像素光源数量小于Quality Setting 中的逐像素光源数量(Pixel Light Count),会有更多的光源以逐像素的方式进行渲染。
具体的计算过程和公式当然是在Pass里面实现,前向渲染有两种Pass: Base Pass 和Additional Pass。通常来说,这两种Pass 进行的标签和渲染设置以及常规光照计算如图
BasePass 就是一次计算把所以影响到的光都计算好。 AdditionalPass 是类似一种叠加的效果会计算多次比较耗
1. 首先,可以发现在渲染设置中,我们除了设置了Pass 的标签外,还使用了#pragma multi_compile_fwdbase 这样的编译指令。虽然#pragma multi_compile_fwdbase 和 # pragma multi_compile_fwdadd 在官方文档中还没有给出相关说明,但实验表明,只有分别为Bass Pass 和Additional Pass 使用这两个编译指令,我们才可以在相关的Pass 中得到一些正确的光照变量,例如光照衰减值等。
2. Base Pass 旁边的注释给出了Base Pass 中支持的一些光照特性。例如在Base Pass 中,我们可以访问光照纹理( lightmap )。
3. Base Pass 中渲染的平行光默认是支持阴影的(如果开启了光源的阴影功能〉,而Additional Pass 中渲染的光源在默认情况下是没有阴影效果的,即便我们在它的Light 组件中设置了有阴影的Shadow Type。但我们可以在Additional Pass 中使用#pragma multi_compile_fwdadd_ fullshadows 代替 #pragma multi_compile_fwdadd 编译指令,为点光源和聚光灯开启阴影效果,但这需要Unity 在内部使用更多的Shader 变种。
4. 环境光和自发光也是在Base Pass 中计算的。这是因为,对于一个物体来说,环境光和自发光我们只希望计算一次即可,而如果我们在Additional Pass 中计算这两种光照,就会造成叠加多次环境光和自发光,这不是我们想要的。
5. 在Additional Pass 的渲染设置中,我们还开启和设置了混合模式。这是因为,我们希望每个Additional Pass 可以与上一次的光照结果在帧缓存中进行叠加,从而得到最终的有多个光照的渲染效果。如果我们没有开启和设置混合模式,那么Additional Pass 的渲染结果会覆盖掉之前的渲染结果, 看起来就好像该物体只受该光源的影响。通常情况下,我们选择的混合模式是Blend One One。
6.对于前向渲染来说,一个Unity Shader 通常会定义一个Base Pass ( Base Pass 也可以定义多次,例如需要双面渲染等情况〉以及一个Additional Pass。一个Base Pass 仅会执行一次(定义了多个Base Pass 的情况除外),而一个Additional Pass 会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行一次Additional Pass 。
上图给出的光照计算是通常情况下我们在每种Pass 中进行的计算。实际上,渲染路径的设置用于告诉Unity 该Pass 在前向渲染路径中的位置,然后底层的渲染引擎会进行相关计算并填充一些内置变量(如 _LightColor0 等),如何使用这些内置变量进行计算完全取决于开发者的选择。例如,我们完全可以利用Unity 提供的内置变量在Base Pass 中只进行逐顶点光照;同样,我们也完全可以在Additional Pass 中按逐顶点的方式进行光照计算, 不进行任何逐像素光照计算。
3.内置的光照变量和函数
根据我们使用的渲染路径(即Pass 标签中LightMode 的值) , Unity 会把不同的光照变量传递给Shader 。
在Unity 5 中,对于前向渲染(即LightMode 为ForwardBase 或ForwardAdd )来说,表9.2给出了我们可以在Shader 中访问到的光照变量。
这里列出了一些常用前向渲染可以使用的内置光照函数
顶点照明渲染路径
顶点照明渲染路径是对硬件配置要求最少、运算性能最高,但同时也是得到的效果最差的一种类型,它不支持那些逐像素才能得到的效果,例如阴影、法线映射、高精度的高光反射等。它仅仅是前向渲染路径的一个子集,所以,所有在顶点照明渲染中的功能都可以在前向渲染完成(好尴尬,要的有啥用。就是为了一些比较粗的效果节省性能)。只是使用了逐顶点的方式来计算。声明这个顶点照明渲染路径时,Unity只会填充其中的一部分逐顶点相关光照变量。就没法逐像素计算了。
1.Unity中的顶点照明渲染
这种渲染路径通常在一个pass中完成所有渲染。在这个Pass 中,我们会计算我们关心的所有光源对该物体的照明,并且这个计算是按逐顶点处理的。这是Unity 中最快速的渲染路径,并且具有最广泛的硬件支持(但是游戏机上并不支持这种路径〉。由于顶点照明渲染路径仅仅是前向渲染路径的一个子集,因此在Unity 5 发布之前, Unity 在论坛上发起了一个投票,让开发者选择是否应该在Unity 5.0 中抛弃顶点照明渲染路径。在这个投票中,很多开发人员表示了赞同的意见。结果是, Unity 5 中将顶点照明渲染路径作为一个遗留的渲染路径,在未来的版本中,顶点照明渲染路径的相关设定可能会被移除。
2.可访问的内置变量
在Unity 中,我们可以在一个顶点照明的Pass 中最多访问到8 个逐顶点光源。如果我们只需要渲染其中两个光源对物体的照明,可以仅使用表9.4 中内置光照数据的前两个。如果影响该物体的光源数目小于8,那么数组中剩下的光源颜色会设置成黑色。
延迟渲染路径
前向渲染的问题是: 当场景中包含大量实时光源时,前向渲染的性能会急速下降。例如,如果我们在场景的某一块区域放置了多个光源,这些光源影响的区域互相重叠,那么为了得到最终的光照效果,我们就需要为该区域内的每个物体执行多个Pass 来计算不同光源对该物体的光照结果,然后在颜色缓存中把这些结果混合起来得到最终的光照。然而,每执行一个Pass 我们都需要重新渲染一遍物体,但很多计算实际上是重复的。
延迟渲染是一种更古老的渲染方法,但由于上述前向渲染可能造成的瓶颈问题,近几年又流行起来。除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区也被统称为G 缓冲(G-buffer ),其中G 是英文Geometry 的缩写。G 缓冲区存储了我们所关心的表面(通常指的是离摄像机最近的表面〉的其他信息,例如该表面的法线、位置、用于光照计算的材质属性等。
1. 延迟渲染的原理
延迟渲染主要包含了两个Pass。在第一个Pass 中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到G 缓冲区中。然后,在第二个Pass 中,我们利用G 缓冲区的各个片元信息,例如表面法线、视角方向、漫反射系数等,进行真正的光照计算。
延迟渲染的过程大致可以用下面的伪代码来描述:
延迟渲染使用的Pass就两个,和场景中包含的光源数目没有关系。延迟渲染的效率不依赖场景的复杂度,而是和我们使用的屏幕空间大小有关。因为:需要的信息都存储在缓冲区中,而缓冲区可以理解成一张张2D图像,计算实际上就是在这些图像空间进行的。 屏幕空间越大 计算的越多。(类似RT)
2. Unity 中的延迟渲染
Unity 有两种延迟渲染路径, 一种是遗留的延迟渲染路径,即Unity 5 之前使用的延迟渲染路径,而另一种是Unity5.x 中使用的延迟渲染路径。如果游戏中使用了大量的实时光照,那么我们可能希望远择延迟渲染路径,但这种路径需要一定的硬件支持。
新旧延迟渲染路径之间的差别很小,只是使用了不同的技术来权衡不同的需求。例如,较旧版本的延迟渲染路径不支持Unity 5 的基于物理的Standard Shader。
对于延迟渲染路径来说,它最适合在场景中光源数目很多、如果使用前向渲染会造成性能瓶颈的情况下使用。而且,延迟渲染路径中的每个光源都可以按逐像素的方式处理。但是,延迟渲染也有一些缺点。
1.不支持真正的抗锯齿(anti-aliasing)功能
2.不能处理半透明物体。(因为深度检测和写入)
3.对显卡有一定的要求,如果要使用延迟渲染,显卡必须支持MRT(Multiple Render Targets),ShaderMode3.0以上,深度渲染纹理以及双面的模板缓冲。
当使用延迟渲染时, Unity 要求我们提供两个Pass 。
( 1)第一个Pass 用于渲染G 缓冲。在这个Pass 中,我们会把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的 G 缓冲区中。对于每个物体来说,这个Pass 仅会执行一次。
( 2 )第二个Pass 用于计算真正的光照模型。这个Pass 会使用上一个Pass 中渲染的数据来计算最终的光照颜色,再存储到帧缓冲中。
默认的G 缓冲区(注意,不同Unity 版本的渲染纹理存储内容会有所不同)包含了以下几个渲染纹理(Render Texture , RT )。
RT0 : 格式是ARGB32 , RGB 通道用于存储漫反射颜色, A 通道没有被使用。
RT1 :格式是ARGB32 , RGB 通道用于存储高光反射颜色, A 通道用于存储高光反射的指数部分。
RT2 : 格式是ARGB2101010, RGB 通道用于存储法线, A 通道没有被使用。
RT3 : 格式是ARGB32 (非HDR)或ARGBHalf ( HDR ),用于存储自发光+lightmap+反射探针(reflection probes )。
深度缓冲和模板缓冲。
当在第二个Pass 中计算光照时,默认情况下仅可以使用Unity 内置的Standard 光照模型。如果我们想要使用其他的光照模型,就需要替换掉原有的 lnternal-DeferredShading.shader 文件。更详细的信息可以访问官方文档
( http ://docs.unity3d.com/Manual/RenderTech-DeferrdShading.html )。
3. 可访问的内置变量和函数
表9.6 给出了处理延迟渲染路径可以使用的光照变量。这些变量都可以在 UnityDeferredLibrary.cginc 文件中找到它们的声明。
Unity的光源类型
前面的例子都只用了一个平行光。Unity一共支持4种光源 平行光,点光源,聚光灯和面光源(area light)。面光源只在烘焙时才发生作用。
1.光源类型有什么影响
常用的光源属性有 光源的位置,方向(具体说就是到某点的方向),颜色,强度以及衰减(更具体说,到某点的衰减,与该点到光源的距离有关)这些属性和它的几何定义息息相关。
1平行光
可以照亮范围没有限制。一般作为太阳这样的角色。Unity中的组件面板。它的属性只有方向,可以任意位置。没有衰减
2.点光源
照亮空间是有限的。它由空间中的一个球体定义。需要提醒读者的一点是,我们需要在Scene 视图中开启光照才能看到预览光源是如何影响场景中的物体的。图9.7 给出了开启Scene 视图光照的按钮。点光源是有位置属性的,它是由点光源的Transform组件中的Position 属性定义的。对于方向属性,我们需要用点光源的位置减去某点的位置来得到它到该点的方向。而点光源的颜色和强度可以在Light 组件面板中调整。同时,点光源也是会衰减的,随着物体逐渐远离点光源,它接收到的光照强度也会逐渐减小。点光源球心处的光照强度最强,球体边界处的最弱,值为0 。其中间的衰减值可以由一个函数定义。
3.聚光灯
聚光灯是这3 种光源类型中最复杂的一种。它的照亮空间同样是有限的,但不再是简单的球体,而是由空间中的一块锥形区域定义的。聚光灯可以用于表示由一个特定位置出发、向特定方向延伸的光。光的衰减值可以由一个函数定义,这个函数相对于点光源衰减计算公式要更加复杂,因为我们需要判断一个点是否在锥体的范围内。
前向渲染中处理多光源
我们在这里使用Blinn-Phone光照模型,和前文说的一样使用两个Pass来处理光源。这里只写一些关键代码。先来Bass Pass。在这个里面我们需要声明LightMode,还需要多加一个编译指令#pragma multi_compile_fwdbase 这个可以保证正确使用光照衰减等光照变量。两个都是不可缺少的。
Pass{
Tags{ "LightMode" = "ForwardBase"} //定义渲染路径 用的Bass 方式 主要计算环境光,平行光等
CGPROGRAM
#pragma multi_compile_fwdbase //这个是保证光照衰减,位置等变量
}
在Bass Pass的片元着色器中,如前文说到 有些光照只想计算一次都在这里实现。先计算环境光,平行光,一些情况还有自发光等。
这里平行光 有些特殊如果场景里有多个平行光,Unity会选择最亮的平行光传递给BassPass逐像素处理,其他的平行光 就会按照逐顶点或者在Additional Pass中按照逐像素的方式处理。如果没有任何平行光,那么BasePass会当做全黑光源处理。在BasePass中它处理的逐像素光源类型一定是平行光。我们可以用_WorldSpaceLightPos0来得到平行光的方向(位置没有意义),使用LightColor0 来得到平行光的颜色和强度(这已经是颜色和强度相乘后的结果),由于平行光没有衰减,这里的衰减值设问1.0
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb *max(0, dot(worldNormal, worldLightDir)); //漫反射
fixed specular = _LightColor0.rgb *_Specular.rgb *pow(max(0,dot(worldNormal, halfDir)),_Gloss); //高光
fixed atten =1.0 // 衰减
return fixed4(ambient + (diffuse + specular) *atten,1.0);
下面完成Additional Pass 一样先声明LightMode和对应需要的编译指令 #pragma multi_compile_fwdadd.这和BassPass 不同的是这里需要开启Blend 命令。因为这个Pass会进行多次运算,我们希望的是效果的叠加,不开启Blend的话就会每次都覆盖上一次的结果。至于融合因子根据需要改变。
这里的代码和BasePass基本类似,可以直接把顶点和片元的代码拷贝过来。去掉里面环境光,自发光,逐顶点光照,SH光照的部分。这里处理的光源有可能平行光,点光源或者聚光灯。
Pass{
Tags { "LightMode"="ForwardAdd" } //Pass d都是逐像素处理的方式
Blend One One //融合方式 必须的
CGPROGRAM
#pragma multi_compile_fwdadd
}
至于光源的属性(位置,方向,颜色,强度,衰减),颜色和强度仍然可以使用_LightColor0得到,但方向,位置,衰减需要根据光源类型判断.。类似C#的宏。如果是点光源或聚光灯,那么 _WorldSpaceLightPos0.xyz 表示的是世界空间下的光源位置,而想要得到光源方向的话,我们就需要用这个位置减去世界空间下的顶点位置。
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
方向解决,就是衰减的问题,这里处理的几个光源类型都有衰减而且还有公式可以计算,但这个计算经常涉及到开方、除法等运算。因此Unity 选择了使用一张纹理作为查找表( Lookup Table, LUT ),以在片元着色器中得到光源的衰减。我们首先得到光源空间下的坐标,然后使用该坐标对衰减纹理进行采样得到衰减值。(典型的查表解决速度问题)
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
#if defined (POINT)
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined(SPOT)
float4 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1));
fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#else
fixed atten = 1.0;
#endif
#endif
这是是伪代码
这两个Pass的实际调用,我们创建一个光源默认他的RenderMode是Auto(这个可以在light组件设置)。这样Unity会自动判断哪些光照执行逐像素处理。还记得前面设置的Edit → Project Settings → Quality →Pixel Light Count 中的数值(最多多少个逐像素光照,默认5个)。默认情况下一个物体可以接收除了最亮平行光以外的4个逐像素光照。 有些示例中假如有一个平行光, 其余的都是4个点光源。符合默认数量情况,那么他就会平行光用BasePass 剩下的4个点光源 依次调用AdditonalPass。
我们可以用 帧调试器(Frame Debugger)来查看场景的绘制过程。在Window -> Frame Debugger 中打开帧调试器
从帧调试器中可以看出,渲染这个场景Unity 一共进行了6 个渲染事件,由于本例中只包含了一个物体,因此这6 个渲染事件几乎都是用于渲染该物体的光照结果。我们可以通过依次单击帧调试器中的渲染事件,来查看Unity 是怎样渲染物体的。图 9.12 给出了本例中Unity 进行的 6个渲染事件。
从图9.12 可以看出, Unity 是如何一步步将不同光照渲染到物体上的:在第一个渲染事件中,Unity 首先清除颜色、深度和模板缓冲,为后面的渲染做准备: 在第二个渲染事件中, Unity 利用Chapter9-ForwardRendering 的第一个Pass, 即 Base Pass,将平行光的光照渲染到帧缓存中; 在后面的4 个渲染事件中, Unity 使用Chapter9-ForwardRendering 的第二个Pass, 即 Additional Pass,依次将4 个点光源的光照应用到物体上,得到最后的渲染结果。
可以注意到, Unity 处理这些点光源的顺序是按照它们的重要度排序的。在这个例子中,由于所有点光源的颜色和强度都相间,因此它们的重要度取决于它们距离胶囊体的远近,因此图9.12中首先绘制的是距离胶囊体最近的点光源。但是,如果光源的强度和颜色互不相同,那么距离就不再是唯一的衡量标准。例如,如果我们把现在距离最近的点光源的强度设为0.2 ,那么从帧调试器中我们可以发现绘制顺序发生了变化,此时首先绘制的是距离胶囊体第二近的点光源,最近的点光源则会在最后被渲染。Unity 官方文档中并没有给出光源强度、颜色和距离物体的远近是如何具体影响光源的重要度排序的,我们仅知道排序结果和这三者都有关系。对于场景中的一个物体,如果它不在一个光源的光照范围内, Unity 是不会为这个物体调用Pass 来处理这个光源的。前面说到RenderMode设置为Auto就unity自动判断,我们也可以设置成Not Important 来使该光源不用当做逐像素处理。但是这样需要你在BasePass设置 如何进行逐顶点和SH光源的计算。
Unity的光照衰减
我们提到Unity 使用一张纹理作为查找表来在片元着色器中计算逐像素光照的衰减。这样的好处在于,计算衰减不依赖于数学公式的复杂性,我们只要使用一个参数值去纹理中采样即可。但使用纹理查找来计算衰减也有一些弊端。
1.需要预处理得到采样纹理,而且纹理的大小也会影响衰减的精度(所有采样的处理方式都有这个问题)
2. 不直观,同时也不方便,因此一旦把数据存储到查找表中,我们就无法使用其他数学公式来计算衰减
但由于这种方法可以在一定程度上提升性能,而且得到的效果在大部分情况下都是良好的,因此Unity 默认就是使用这种纹理查找的方式来计算逐像素的点光源和聚光灯的衰减的。
用于光照衰减的纹理
Unity 在内部使用一张名为 _LightTexture0 的纹理来计算光源衰减。需要注意的是,如果我们对该光源使用了cookie,那么衰减查找纹理是 _LightTextureB0,但这里不讨论这种情况。我们通常只关心 _LightTexture0 对角线上的纹理颜色值,这些值表明了在光源空间中不同位置的点的衰减值。例如,(0, 0)点表明了与光源位置重合的点的衰减值,而( 1, 1 )点表明了在光源空间中所关心的距离最远的点的衰减。
为了对 _LightTexture0 纹理来样得到给定点到该光源的衰减值,我们首先需要得到该点在光源空间中的位置,这是通过 _LightMatrix0 变换矩阵得到的。在9.1.1 节中,我们已经知道 _LightMatrix0 可以把顶点从世界空间变换到光源空间。因此,我们只需要把 _LightMatrix0 和世界空间中的顶点坐标相乘即可得到光源空间中的相应位置:
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1)).xyz;
然后,我们可以使用这个坐标的模的平方对衰减纹理进行采样,得到衰减值:
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
可以发现,在上面的代码中,我们使用了光源空间中顶点距离的平方(通过dot 函数来得到)来对纹理采样,之所以没有使用距离值来采样是因为这种方法可以避免开方操作。然后,我们使用宏UNITY_ATTEN_CHANNEL 来得到衰减纹理中衰减值所在的分量,以得到最终的衰减值。
使用数学公式计算衰减
尽管纹理采样的方法可以减少计算衰减时的复杂度,但有时我们希望可以在代码中利用公式来计算光源的衰减。例如,下面的代码可以计算光源的线性衰减:
float distance = length( _WorldSpaceLightPosO.xyz - i.worldPosition.xyz);
atten = 1.0 / distance; // linear attenuation
Unity的阴影
阴影的实现
实时渲染中,使用一张叫做Shadow Map的技术,其实就是在光源的位置放一个摄像机,那么阴影就是摄像机看不到的区域。Unity使用的就是 这种技术。
在前向渲染路径中,如果重要的平行光开启了阴影,Unity就会为该光源计算阴影映射纹理(shadowMap). 这张纹理本质上也是一张深度图,它记录了从该光源位置出发能看懂的场景中距离它最近的表面位置(深度信息)。
这个纹理的计算过程有两个办法一个是按照前面的办法在BasePass和Additional Pass中来计算。但是这样多余的计算太多。不需要光照等计算。 还有一种就是再单独声明一个Pass只计算阴影纹理。Unity用的就是这个。在这个Pass中 LightMode 标签设置为ShadowCaster, 这个Pass的渲染目标不是帧缓存,而是阴影映射纹理(或是深度纹理)。
Unity 首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。因此,当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的Unity Shader 中找到LightMode 为ShadowCaster 的Pass , 如果没有, 它就会在Fallback 指定的Unity Shader 中继续寻找,如果仍然没有找到, 该物体就无法向其他物体投射阴影〈但它仍然可以接收来自其他物体的阴影)。当找到了一个LightMode 为ShadowCaster 的Pass 后, Unity 会使用该Pass 来更新光源的阴影映射纹理。
在传统的阴影映射纹理的实现中,我们会在正常渲染的Pass 中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后,我们使用xy 分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息。如果该深度值小于该顶点的深度值(通常由z 分量得到〉,那么说明该点位于阴影中。但在Unity 5 中, Unity 使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术( Screenspace Shadow Map )。屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法。需要注意的是,并不是所有的平台Unity 都会使用这种技术。这是因为,屏幕空间的阴影映射需要显卡支持MRT,而有些移动平台不支持这种特性。
当使用了屏幕空间的阴影映射技术时, Unity 首先会通过调用LightMode 为ShadowCaster 的Pass 来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后, 根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在Shader 中对阴影图进行采样。由于阴影图是屏幕空间下的, 因此,我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。
总结一下,一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程。
PS:上面的这段 过程解释,说明白点就是。取一个阴影纹理和深度纹理。两个图叠起来就是正确的阴影图,而且还是最终结果。别的物体要取阴影 直接过来采样就可以。
如果我们想要一个物体接收来自其他物体的阴影,就必须在Shader 中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。在Unity中,这个过程是通过为该物体执行LightMode为ShadowCaster的Pass 来实现的。如果使用了屏幕空间的投影映射技术, Unity 还会使用这个Pass 产生一张摄像机的深度纹理。
在下面的章节中,我们会学习如何在Unity 中实现上面两个过程。
不透明物体的阴影
首先要设置光源让平行光可以收集阴影信息,Light组件中开启阴影。
让物体投射阴影:通过设置MeshRender 组件中的Cast Shadows 和Receive Shadows属性来实现
Cast Shadows 可以被设置为开启(On)或关闭(Off)。如果开启,就会把该物体加入到光源的阴影映射纹理计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。这个过程是通过为该物体执行LightMode为ShadowCaster的Pass来实现的。Receive Shadows则可以选择是否让物体来接受其他物体的阴影。如果没有开启ReceiveShadows ,那么就不会在内部进行阴影计算。
图9.17 中还有一个有意思的现象,就是右侧的平面并没有向最下面的平面投射阴影,尽管它的Cast Shadows 已经被开启了。在默认情况下,我们在计算光源的阴影映射纹理时会剔除掉物体的背面。但对于内置的平面来说,它只有一个面,因此在本例中当计算阴影映射纹理时,由于右侧的平面在光源空间下没有任何正面(frontface),因此就不会添加到阴影映射纹理中。我们可以将
Cast Shadows 设置为Two Sided 来允许对物体的所有面都计算阴影信息。图9.18 给出了当把右侧平面的Cast Shadows 设置为Two Sided 后的结果。
// Pass to render object as a shadow caster
Pass {
Name ”ShadowCaster”
Tags {”LightMode” = ”ShadowCaster”}
CG PROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#include ”UnityCG.cginc”
struct v2f {
V2F SHADOW_CASTER;
};
v2f vert( appdata_base v) {
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET (o)
return o; }
float4 frag( v2f i) : SV_Target {
SHADOW_CASTER_FRAGMENT(i) }
ENDCG
}
2.让物体接受阴影
代码实现,想把前面的前向渲染的代码考过了,这里只写差异的地方
在Base Pass中包含一个新的内置文件,所需要的阴影宏都在这个文件中有声明
#include "AutoLight.cginc"
在顶点着色器的输出结构体v2f 添加一个内置宏 SHADOW_COORDS: 这个宏的作用很简单,就是声明一个用于对阴影纹理采样的坐标。需要注意的是, 这个宏的参数需要是下一个可用的插值寄存器的索引值,在上面的例子中就是2 。
struct v2f{
float4 pos :SV_POSITION;
float3 worldNormal :TEXCOORD0;
float3 worldPos: TEXCOORD1;
SHADOW_COORDS(2) //内置宏,参数是可用的插值寄存器的索引值
}
在顶点着色器返回之前 加另一个内置宏 TRANSFER_SHADOW:这个宏用于在顶点着色器上计算上一步中声明的阴影纹理坐标
v2fvert(a2v v){
v2f o;
……
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o);
returno;
}
在片元着色器中国 计算阴影值,同样使用了一个内置宏 SHADOW_ATTENUATION:
// Use shadow coordinates to sample shadow map
fixed shadow = SHADOW_ATTENUATION(i);
SHADOW_ COORDS 、TRANSFER_SHADOW 和SHADOW_ATTENUATION 是计算阴影时的“三剑客”。这些内置宏帮助我们在必要时计算光源的阴影。我们可以在AutoLight.cginc 中找到它们的声明:
这里很多的宏判断只是为了Unity处理不同光源的不同计算方法,定义了多个平台和多个宏。
宏SHADOW_COORDS实际上就是声明了一个名为_ShadowCoord的阴影纹理坐标变量。而TRANSFER_SHADOW 的实现会根据平台不同而有所差异,如果当前平台可以使用屏幕空间的阴影映射技术(通过判断是否定义了UNITY_NO_SCREENSPACE_SHADOWS来得到) , TRANSFER_SHADOW 会调用内置的ComputeScreenPos 函数来计算_ShadowCoord ;如果该平台不支持屏幕空间的阴影映射技术, 就会使用传统的阴影映射技术,TRANSFER_SHADOW 会把顶点坐标从模型空间变换到光源空间后存储到ShadowCoord 中。然后, SHADOW_ATTENUATION 负责使用ShadowCoord 对相关的纹理进行采样,得到阴影信息。
注意到,上面内置代码的最后定义了在关闭阴影时的处理代码。当关闭了阴影后,SHADOW_COORDS和TRANSFER_SHADOW实际没有任何作用,而SHADOW_ATTENUATION会直接等同于数值1.
为了让这些宏可以正常使用,我们必须遵守一些命名规则,我们需要保证: a2f 结构体中的顶点坐标变量名必须是vertex, 顶点着色器的输出结构体v2f必须命名为v,且v2f 中的顶点位置变量必须命名为pos。
在完成了上面的所有操作后, 我们只需要把阴影值shadow 和漫反射以及高光反射颜色相乘即可。保存文件, 返回Unity 我们可以发现,现在正方体也可以接收来自右侧平面的阴影了,如图9.19所示。
这整个绘制过程 同样可以通过FrameDebugger来查看。
绘制该场景共需要花费20 个渲染事件。这些渲染事件可以分为4 个部分: UpdateDepthTexture , 即更新摄像机的深度纹理; RenderShadowmap , 即渲染得到平行光的阴影映射纹理; CollectShadows , 即根据深度纹理和阴影映射纹理得到屏幕空间的阴影图; 最后绘制渲染结果。
我们可以发现,Unity 调用了Shader: Unity Shader Book/Chapter9 Shadow pass #3 来更新深度纹理,即Chapter9-Shadow 中的第三个Pass 。尽管Chapter9-Shadow 中只定义了两个Pass,但正如我们之前所说, Unity 会在它的Fallback 中找到第三个Pass ,即LightMode 为ShadowCaster 的Pass 来更新摄像机的深度纹理。同样,在第二个部分,即渲染得到平行光的阴影映射纹理的过程中, Unity也是调用了这个Pass 来得到光源的阴影映射纹理。
在第三个部分中, Unity 会根据之前两步的结果得到屏幕空间的阴影图,
具体过程真机体验统一管理光照衰减和阴影
核心是UNITY_LIGHT_ATTENUATION宏。前向渲染中BasePass 平行光衰减因子为1,Additional Pass会根据光照类型,使用内置变量和宏计算衰减。光照衰减和阴影对物体都是把光照因子和阴影值及光照结果相乘得到最终的渲染结果。 这样这两个信息就可以同时计算避免消耗。
(1)首先要包含头文件
#include "Lighting.cginc"
#include "AutoLight.cginc"
(2)在v2f 中使用内置宏SHADOW_COORDS 声明阴影的采样坐标(用阴影贴图的方式都需要这个变量) 这个地方参数是2 是因为 Texcoord0 和TEXCOORD1 使用了两个插值寄存器 所有索引变成了2。
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
SHADOW_COORDS(2)
};
(3)顶点着色器 使用内置宏 TRANSFER_SHADOW 计算并向片元着色器传递阴影坐标
v2f vert(a2v v) {
v2f o; ……
// Pass shadow coordinates to pixel shader
TRANSFER_SHADOW(o); //上一节有内置代码实现
return o;
}
(4)这里是关键,片元着色器用内置宏UNITY_LIGHT_ATTENUATION 来计算光照衰减和阴影
fixed4 frag(v2f i) : SV_Target {
……
// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
UNITY_LIGHT_ATTENUATION是Unity 内置的用于计算光照衰减和阴影的宏,我们可以在内置的AutoLight.cginc 里找到它的相关声明。它接受3 个参数, 它会将光照衰减和阴影值相乘后的结果存储到第一个参数中。注意到,我们并没有在代码中声明第一个参数atten,这是因为UNITY_LIGHT_ATTENUATION会帮我们声明这个变量。它的第二个参数是结构体v2f,这个参数会传递给9.4.2 节中使用的SHADOW_ATTENUATION , 用来计算阴影值。而第三个参数是世界空间的坐标, 正如我们在9.3 节中看到的一样, 这个参数会用于计算光源空间下的坐标, 再对光照衰减纹理采样来得到光照衰减。我们强烈建议读者查阅AutoLight.cginc 中UNlTY_LIGHT_ATTENUATION的声明,读者可以发现, Unity 针对不同光源类型、是否启用cookie 等不同情况声明了多个版本的UNITY_LIGHT_ATTENUATION。 这些不同版本的声明是保证我们可以通过这样一个简单的代码来得到正确结果的关键。
Unity为我们做了很多事情,需要仔细看源码
( 5 )由于使用了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中的shader有可以使用的ShadowCasterPass 但是透明的物体就不可以了,这样使用得到和不透明的没差别。这里的主要差别就是透明度需要透明度测试和透明度混合。很多片元就会被舍弃,没法得到正确效果。
即使我们fallback使用了一些有透明度测试的ShadowCasterPass 也不一定能保证显示正确。比如Transparent/Cutout/VertexLit, 这里还需要注意一样 物体在渲染时Mesh Render的CastShadows 选择TwoSided。不然就是单面接受阴影计算也不对。这个前面已经提过了。
这个后面再具体实现。要正确的透明度的阴影 也需要和前面透明物体的渲染一样特别注意渲染顺序。
网友评论