Unity Shader入门精要学习笔记 - 第9章 更复杂的光照

47 篇文章 10 订阅

Unity Shader入门精要学习笔记 - 第9章 更复杂的光照

本系列为UnityShader入门精要读书笔记总结,
原作者博客链接:http://blog.csdn.net/candycat1992/article/
书籍链接:http://product.dangdang.com/23972910.html

第9章 更复杂的光照

在实际的游戏幵发过程中, 我们往往需要处理数目更多、 类型更复杂的光源。
更重要的是, 我们想要得到阴影。
在学习这些之前,我们有必要知道Unity到底是如何处理这些光源的。当我们在场景里放置了各种类型的光源后,Unity的底层渲染引擎是如何让我们在Shader中访问到它们的。如何处理更多不同类型的光源,如点光源和聚光灯。光照的衰减处理,实现距离光源越远光强越弱的效果。Unity中阴影的实现方法,并学习在UnityShader中如何为不同类型的物体实现阴影效果。

9.1 Unity 的渲染路径

在Unity里,渲染路径决定了光照是如何应该到Unity Shader 中的。因此,如果要和光源打交道,我们需要为每个Pass指定它使用的渲染路径,只有这样才能让Unity知道,“哦,原来这个程序想要这种渲染路径,那么好的,我把光源和处理后的光照信息都放在这些数据里,你可以访问啦!”也就是说,我们只有为Shader 正确地选择和设置了需要的渲染路径,该Shader的光照计算才能被正确执行。
Unity支持多种类型的渲染路径。在Unity5.0版本之前,主要有3中:前向渲染路径(Forward Rendering Path)、延迟渲染路径(Deferred Rendering Path)和顶点照明渲染路径(Vertex Lit Rendering Path)。但在Unity 5.0 版本以后,Unity 做了很多更改,主要有两个变化:首先,顶点照明渲染路径已经被Unity抛弃(但目前仍然可以对之前使用了顶点照明渲染路径的Unity Shader兼容);其次,新的延迟渲染路径代替了原来的延迟渲染路径。
大多数情况下,一个项目只使用一种渲染路径,因此我们可以为整个项目设置渲染时的渲染路径。我们可以通过在Unity 的 Edit -> Project Srtting -> Player -> Other Setting -> Rendering Path 中选择项目所需的渲染路径。默认情况下,该摄者选择的是前向渲染路径,如下图所示。
这里写图片描述
但有时,我们希望可以使用多个渲染路径,例如摄像机A渲染的物体使用前向渲染路径,而摄像机B渲染的物体使用延迟渲染路径,这时,我们可以在每个摄像机的渲染路径设置中设置该摄像机使用的渲染路径,以覆盖Projec Setting 中的设置。如下图所示。
这里写图片描述
在上面的设置中,如果渲染了Use Player Setting,那么这个摄像机会使用Project Setting 中的设置;否则就会覆盖掉Project Setting 中的设置。需要注意的是,如果当前的显卡并不支持所选择的渲染路径,Unity 会自动使用更低一级的渲染路径。例如,一个GPU不支持延迟渲染,那么Unity就会使用前向渲染。
完成了上面的设置后,我们就可以在每个Pass 中使用标签来指定该Pass 使用的渲染路径。这是通过设置Pass 的LightMode 标签实现的。不同类型的渲染路径可能会包含多种标签设置。例如,我们之前在代码中写的:

Pass{  
    Tags {"LightMode" = "ForwardBase"}  
}  

上面的代码告诉Unity,该Pass 使用前向渲染路径中的ForwardBase路径。而前向渲染路径还有一种路径叫做ForwardAdd。下表给出了Pass的LightMode 标签支持的渲染路径设置选项。
这里写图片描述

那么指定渲染路径到底有什么用呢?如果一个Pass没有指定任何渲染路径会有什么问题呢?通俗来讲,指定渲染路径是我们和Unity的底层渲染引擎的一次重要的沟通。
例如,如果我们为一个Pass设置了前向渲染路径的标签,相当于告诉Unity:“我准备使用前向渲染了,你把那些光照属性都按前向渲染的流程给我准备好,我一会儿要用!”随后,我们可以通过Unity提供的内置光照变量来访问这些属性。如果我们没有指定任何渲染路径(实际上,在Unity5.x版本中如果使用了前向渲染又没有为Pass指定任何前向渲染适合的标签,就会被当成一个和顶点照明渲染路径等同的Pass),那么一些光照变量很可能不会被正确赋值,我们计算出的效果也就很有可能是错误的。
前向渲染路径是传统的渲染方式,也是我们最常用的一种渲染路径。

前向渲染路径原理:每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元,并计算两个缓冲区的信息:一个是颜色缓冲区,一个是深度缓冲区。我们利用深度缓冲来决定一个片元是否可见,如果可见就更新颜色缓冲区中的颜色值。我们可以用下面的伪代码来描述前向渲染路径的大致过程:

Pass {  
    for(each primitive in this model){  
        for(each fragment coverd by this primitive){  
            if(failed in depth test){  
                //如果没有通过深度测试,说明该片元是不可见的  
                discard;  
            }  
            else{  
                //如果该片元可见,就进行光照计算  
                float4 color = Shading(materialInfo,pos,normal,lightDir,viewDir);  
                //更新帧缓冲  
                writeFrameBuffer(fragment,color);  
            }  
        }  
    }  
}  

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

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

实际上,渲染路径的设置用于告诉Unity 该Pass 在前向渲染路径中的位置,然后底层的渲染引擎会进行相关计算并填充一些内置变量(如_LightColor0等),如何使用这些内置变量进行计算完全取决于开发者的选择。

根据我们使用的渲染路径(即Pass标签中的LightMode 的值),Unity 会把不同的光照变量传递给Shader。
在Unity 5 中,对于前向渲染(即LightMode 为ForwardBase或forwardAdd)来说,下表给出了我们可以在Shader 中访问到的光照变量和一些光照函数。
这里写图片描述

这里写图片描述

顶点照明渲染路径是对硬件配置要求最少、运算性能最高、但同时也是得到的效果最差的一种类型,它不支持那些逐像素才能得到的效果,例如阴影、法线映射、高精度的高光反射等。就如它的名字一样,顶点照明渲染路径只是使用了逐顶点的方式来计算光照。实际上,我们在上面的前向渲染路径中也可以计算一些逐顶点的光源。但如果选择使用顶点照明渲染路径,那么Unity会只填充那些逐顶点相关的光源变量,意味着我们不可以使用一些逐像素光照变量。
Unity中的顶点照明渲染:顶点照明渲染路径通常在一个Pass 中就可以完成对物体的渲染。在这个Pass中,我们会计算我们关心的所有光源对该物体的照明,并且这个计算是按逐顶点处理的。这是Unity中最快速的渲染路径,并且具有最广泛的硬件支持。
这里写图片描述
可以看出,一些变量我们同样可以在前向渲染路径中使用,例如unity_LightColor。但这些变量数组的维度和数值在不同渲染路径中的值是不同的。
下表给出了顶点照明渲染路径中可以使用的内置函数。

这里写图片描述

延迟渲染是一张更古老的渲染办法,但由于上述前向渲染可能造成的瓶颈问题,近几年又流行起来。除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区也bei称为G缓冲。G缓冲区存储了我们所关心的表面的其他信息,例如该表面的法线、位置、用于光照计算的材质属性等。
前向渲染的问题是:当场景中包含大量实时光源时,前向渲染的性能会急速下降。每执行一个Pass我们都需要重新渲染一遍物体,但很多计算实际上是重复的。
延迟渲染的原理:延迟渲染主要包含了两个Pass。在第一个Pass中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到G缓冲区中。然后,在第二个Pass中,我们利用G缓冲区的各个片元信息,例如表面法线、视角方向、漫发射系数等,进行真正的光照计算。
延迟渲染的过程大致可以用下面的伪代码来描述:

Pass1{  
    //第一个Pass不进行真正的光照计算  
    //仅仅把光照计算需要的信息存储到G缓冲中  
    for (each primitive in this model){  
        for (each fragment covered by this primitive){  
            if(failed in depth test){  
                discard;  
            }  
            else{  
                //如果该片元可见  
                //就把需要的信息存储到G缓冲中  
                writeGBuffer(materialInfo,pos,normal,lightDir,viewDir);  
            }  
        }  
    }  
}  

Pass2{  
    //利用G缓冲中的信息进行真正的光照计算  
    for(each pixel in the screen){  
        if(the pixel is valid){  
            //如果该像素是有效的  
            //读取它对应的G缓冲中的信息  
            readGBuffer(pixel,materialInfo,pos,normal,lightDir,viewDir);  
            //根据读到的信息进行光照计算  
            float4 color = Shading(materialInfo,pos,normal,lightDir,viewDir);  
            //更新帧缓冲  
            writeFrameBuffer(pixel,color);  
        }  
    }  
}  

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

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

  • 不支持真正的抗锯齿功能。
  • 不能处理半透明物体。
  • 对显卡有一定要求。如果要使用延迟渲染的话,显卡必须支持MRT、Shader Mode
    3.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,用于存储自发光+lightmap+反射探针
  • 深度缓冲和模板缓冲。

当在第二个Pass中计算光照时,默认情况下仅可以使用Unity内置的Standard 光照模型。
下表给出了处理延迟渲染路径可以使用的光照变量。这些变量都可以在UnityDeferredLiabrary.cg.inc文件中找到它们的声明。
这里写图片描述
Unity的官方文档中给出了4中渲染路径(前向渲染路径、延迟渲染路径、遗留的延迟渲染路径和顶点照明渲染路径)的详细比较,包含它们的特性比较(是否支持逐像素光照、半透明物体、实时阴影等)、性能比较以及平台支持。
总体来说,我们需要根据游戏发布的目标平台来选择渲染路径。如果当前显卡不支持所选渲染路径,那么Unity会自动使用比其低一级的渲染路径。
我们主要使用前向渲染路径。

9.2 Unity的光源类型

Unity一共支持4种光源类型:平行光、点光源、聚光灯和面光源。面光源仅在烘焙时才可发挥总用,因此我们不作讨论。由于每种光源的几何定义不同,因此它们对应的光源属性也就各不相同。
我们来看一下光源类型的不同到底会给Shader 带来哪些影响。我们可以考虑Shader 中使用了光源的哪些属性。最常用的光源属性有光源的位置、方向、颜色、强度以及衰减这5个属性。而这些属性和它们的几何意义息息相关。

平行光:对于我们之前使用的平行光来说,它的几何定义是最简单的。平行光可以照亮的范围是没有限制的,它通常是作为太阳这样的角色在场景中出现的。下图给出了Unity中平行光在Scene视图中的表示以及Light组件的面板。
平行光之所以简单,是因为它没有一个唯一的位置,也就是说,它可以放在场景中的任意位置。它的几何属性只有方向,我们可以调整平行光的Transform组件中的Rotation属性来改变它的光源方向,而且平行光到场景中所有点的方向都是一样的,这也是平行光名字的由来。除此之外,由于平行光没有一个具体的位置,因此也就没有衰减的概念,也就是说,光照强度不会随着距离而发生改变。

点光源:点光源的照亮空间是有限的 ,它是由空间中的一个球体定义的。点光源可以表示由一个点发出的、向所有方向延伸的感觉。下图给出了Unity中点光源在Scene视图中的表示以及Light组件的面板。
这里写图片描述
聚光灯:聚光灯是这3种光源类型中最复杂的一种。它的照亮空间同样是有限的,但不再是简单的球体,而是由空间中的一块锥形区域定义的 。聚光灯可以用于表示由一个特定位置触发、向特定方向延伸的光。下图给出了Unity中聚光灯在Scene视图中的表示以及Light组件的面板。
这里写图片描述

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

Shader "Unlit/Chapter9-MyForwardRendering"
{
    Properties {
        _Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
        _Specular ("Specular", Color) = (1, 1, 1, 1)
        _Gloss ("Gloss", Range(8.0, 256)) = 20
    }
    SubShader {
        Tags { "RenderType"="Opaque" }

        Pass {
            // Pass for ambient light & first pixel light (directional light)
            Tags { "LightMode"="ForwardBase" }

            CGPROGRAM

            //pragma multi_compile_fwdbase可以保证我们在Shader中使用光照衰减等  
            //光照变量可以被正确赋值。这是不可缺少的  
            #pragma multi_compile_fwdbase   

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                fixed3 worldNormal = normalize(i.worldNormal);
                fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                //处理场景中的最重要的平行光  漫反射高光计算
                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                //由于平行光可以认为是没有衰减的,因此这里我们直接令衰减值为1.0
                fixed atten = 1.0;

                return fixed4(ambient + (diffuse + specular) * atten, 1.0);
            }

            ENDCG
        }

        //通常来说,Addtional Pass的光照处理和Base Pass的处理方式是一样的,因此我们只需要把  
        //Base Pass的顶点和片元着色器代码复制到Additional Pass中,稍微修改一下即可。  
        Pass {
            // Pass for other pixel lights
            Tags { "LightMode"="ForwardAdd" }
            //开启和设置了混合模式  
            //希望Additional Pass 计算得到的光照结果与之前的光照结果进行叠加。  
            //没有使用Blend命令的话,Additional Pass会直接覆盖掉之前的光照结果  
            //我们也可以选择其他Blend命令,如Blend SrcAlpha One  
            Blend One One

            CGPROGRAM

            //这个指令保证我们再Additional Pass中访问正确的光照变量  
            #pragma multi_compile_fwdadd

            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            fixed4 _Diffuse;
            fixed4 _Specular;
            float _Gloss;

            struct a2v {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float3 worldNormal : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                o.worldNormal = UnityObjectToWorldNormal(v.normal);

                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                fixed3 worldNormal = normalize(i.worldNormal);

                //计算不同光源的方向  如果是平行光Unity底层会定义USING_DIRECTIONAL_LIGHT
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
                #else
                    fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
                #endif

                fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

                fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                fixed3 halfDir = normalize(worldLightDir + viewDir);
                fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

                //处理不同光源的衰减
                #ifdef USING_DIRECTIONAL_LIGHT
                    fixed atten = 1.0;
                #else
                    #if defined (POINT)
                        float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
                        fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
                    #elif defined (SPOT)
                        float4 lightCoord = mul(unity_WorldToLight, 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

                return fixed4((diffuse + specular) * atten, 1.0);
            }

            ENDCG
        }
    }
    FallBack "Specular"
}

我们新建一个场景。新建4个点光源,调整它们的颜色为相同的红色。可以得到类似下图的结果。
这里写图片描述

**疑问第二个pass是怎么进行的,比如同时又4个光源,如何判断现在该计算哪一个?
BasePass和AdditionalPass的调用是怎么进行的?**
那么,这个效果是怎么来的呢?当我们创建一个光源时,默认情况下它的Render Mode(可以在Light组件中设置)是Auto。这意味着,Unity会在背后为我们判断哪些光源会按逐像素处理,而哪些按逐顶点或SH的方式处理。由于我们没有更改Edit -> Project Setting ->Quality ->Pixel Light Count 中的数值,因此默认情况下一个物体可以接受最亮的平行光外的4个逐像素光照。在这个例子中,场景中共包含了5个光源,其中一个是平行光,它会在Chapter9-Forward Rendering 的Base Pass 中按逐像素的方式被处理:其余4个都是点光源,由于它们的Render Mode 为Auto且数目正好等于4,因此都会在Chapter9-ForwardRendering 的Additional Pass 中逐像素的方式被处理,每个光源会调用一次Additional Pass。此处我们会调用4次Additional Pass。
在Unity 5中,我们还可以使用帧调试器工具来查看场景的绘制过程。具体过程可以查看原书籍讲解,我们现在只需要知道结论。
如果逐像素光源的数目很多的话,该物体的Additional Pass就会被调用很多次,影响性能。我们可以通过把光源的Render Mode 设为Not Important 来告诉Unity,我们不希望把该光源当成逐像素处理。

9.3 Unity的光照衰减

使用纹理查找计算衰减有一些弊端:
需要预处理得到采样纹理,而且纹理的大小也会影响衰减的精度。
不直观,同时也不方便,因此一旦把数据存储到查找表中,我们就无法使用其他数学公式来计算衰减。
但由于这种方法可以在一定程度上提升性能,而且得到的效果在大部分情况下都是良好的,因此Unity默认就是使用这种纹理查找的方式来计算逐像素的点光源和聚光灯的衰减的。
用于光照衰减的纹理
Unity在内部使用一张名为_LightTexture0的纹理来计算光源衰减。需要注意的是,如果我们对该光源使用了cookie,那么衰减查找纹理是_LightTextureB0,但这里不讨论这种情况。我们通常只关心_LightTexture0对角线上的纹理颜色值,这些值表明了再光源空间中不同位置的点的衰减值。例如(0,0)点表明了与光源位置重合的点的衰减值,而(1,1)点表明了再光源空间中所关心的距离最远的点的衰减。
为了对_LightTexture0纹理采样得到给定点到该光源的衰减值,我们首先需要得到该点在光源空间中的位置,这是通过LightMatrix0变换矩阵得到。在之前,我们已经知道LightMatrix0可以把顶点从世界空间变换到光源空间。因此,我们只需要把_LightMatrix0和世界空间中的顶点坐标相乘即可得到光源空间中的相应位置:

float3 lightCoord = mul(_LightMatrix0,float4(i.worldPos,1)).xyz;  

然后,我们可以使用这个坐标的模的平方对衰减纹理进行采样,得到衰减值:

fixed atten = tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;  

可以发现,在上面的代码中,我们使用了光源空间中顶点距离的平方(通过dot函数来得到)来对纹理采样,之所以没有使用距离值来采样是因为这样可以避免开方操作。然后,我们使用宏UINITY_ATTEN_CHANNEL来得到衰减纹理中的衰减值所在的分量,以得到最终的衰减值。

使用数学公式计算衰减
尽管纹理采样的方法可以减少计算衰减的复杂度,但有时我们希望可以在代码中利用公式来计算光源的衰减。例如,下面的代码可以计算光源的线性衰减。

float distance = length(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz);  
atten = 1.0/distance;  

由于我们无法在Shader中通过内置变量得到光源的范围、聚光灯的朝向、张开角度等信息,因此得到的效果往往在有些时候不尽如人意。

9.4 Unity 的阴影

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

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

在实时渲染中,我们最常用的是一种名为Shadow Map 的技术。这种技术很简单,它会首先把摄像机位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是摄像机看不到的地方。而Unity就是使用这种技术。

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

那么,在计算阴影映射纹理时,我们如何判断距离它最近的表面位置呢?一种方法是,先把摄像机放置到光源的位置上,然后按正常的渲染流程,即调用Base Pass 和 Additional Pass 来更新深度信息,得到阴影映射纹理。但这种方法会对性能造成一定的浪费,因为我们实际上仅仅需要深度信息而已,而Base Pass 和 Additional Pass 中往往涉及很多复杂的光照模型计算。因此,Unity选择使用一个额外的Pass来专门更新光源的阴影映射纹理,这个Pass就是LightMode标签被设置为ShadowCaster的Pass。**这个Pass的渲染目标不是帧缓存,而是阴影映射纹理(或深度纹理)。**Unity首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。

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

在传统的阴影映射纹理的实现中,我们会在正常渲染的Pass中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后,我们使用xy分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息。如果该深度值小于该顶点的深度值(通常由z分量得到),那么说明该点位于阴影中。但在Unity5中,Unity 使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术。屏幕空间的阴影映射原本是延迟渲染中产生阴影的方法。需要注意的是,并不是所有的平台Unity都会使用这种技术。这是因为,屏幕空间的阴影映射需要显卡支持MRT,而有些移动平台不支持这种特性。
当使用了屏幕空间的阴影映射技术时,Unity首先会通过调用LightMode 为 ShadowCaster的Pass来得到可投射阴影是光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却出于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在Shader中对阴影图进行采样。由于阴影图是屏幕空间下的,因此我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。

总结一下,一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程。
如果我们想要一个物体接收来自其他物体的阴影,就必须在Shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。在Unity 中,这个过程通过为该物体执行LightMode 为ShadowCaster 的 Pass 来实现的。如果使用了屏幕空间的投射映射技术,Unity还会使用这个Pass 产生一张摄像机的深度纹理。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值