Unity Shader 阴影系列(2)--内置阴影源码解析

Unity是如何生成阴影的

  • 前言
  • 相关的宏
  • 投射阴影
    • v2f结构定义
      • V2F_SHADOW_CASTER
      • UNITY_VERTEX_OUTPUT_STEREO
    • 顶点函数:TRANSFER_SHADOW_CASTER_NORMALOFFSET
      • UnityClipSpaceShadowCasterPos
      • UnityApplyLinearShadowBias
    • 片元函数 SHADOW_CASTER_FRAGMENT
  • 接受阴影
    • v2f结构定义
    • 顶点处理
      • UNITY_TRANSFER_SHADOW
        • TRANSFER_SHADOW
      • 灯光位置--COMPUTE_LIGHT_COORDS
  • 片元处理
    • UNITY_SHADOW_ATTENUATION
      • 淡入淡出计算-UnityComputeShadowFadeDistance
      • 烘焙阴影计算-UnitySampleBakedOcclusion
      • 阴影采样- unitySampleShadow
      • 阴影混合- UnityMixRealtimeAndBakedShadows
  • 总结

前言

通过上一篇的总结,我们知道了阴影的产生原理。先生成一个已知的模板深度,当计算某个片元时,拿着个片元的深度和模板深度比较,得出是否在阴影。那么我们今天就分两个步骤看看unity是如何完成这两部操作的。

相关的宏

1.VERTEXLIGHT_ON:由灯光的rendermode、点光源还是平行光决定,是逐顶点还是像素光,这个不懂得可以看看shaderlab实战开发一书,这里就不多说了。
2.SHADOW_DEPTH:计算平行光照射的阴影深度图时,此关键字开启。(聚光灯不做考虑,也是我们主要讨论的对象)
3.SHADOW_CUBE:计算点光源照射的阴影深度图时,此关键字开启。
4.SHADOW_SCREEN:主光源为平行光时,此关键字开启。
5.SHADOWS_CUBE_IN_DEPTH_TEX:将点光源产生深度纹理渲染到一张深度图中,次关键字会和 关键字SHADOW_CUBE配合使用,默认情况下SHADOW_CUBE的深度纹理渲染到的是cubemap中。
6.UNITY_REVERSED_Z:近屏幕端Z的值,dx中近屏幕端z为1,远处为0
7.UNITY_NEAR_CLIP_VALUE:剪裁空间中近屏幕面的值,ndc空间z的最小值。dx中为0,opengl中为-1
8.SHDOWS_SHADOWMASK:烘焙光照贴图的LightMode模式
9.SHADOWS_NATIVE:表示硬件是否有原生shadowmap支持。如果支持可以直接使用,不需要再做距离比较。
10.LIGHTMAP_SHADOW_MIXING:灯的模式为mixed.

投射阴影

这一步就是生成深度图的步骤,所以这个操作一定是最先开始的,说到这个渲染顺序,谁知道CameraDepth、Sorting Layer、Order In Layer、RenderQueue的排序规则?年纪大了,我也总是忘.,所以我们渲染深度的相机的depth一定是最先开始的。Unity调用Pass中Tags中“LightMode”=“ShadowCaster"的通道进行深度渲染,但是有时候会发现我这个shader中没有添加这个标签为什么还是会有影子,因为unity的FallBack机制,那一定是你的shader有FallBack,并且FallBack对应的shader里有ShadowCaster这个标签。我们定位一下unity的Mobile-Diffuse.shader,发现没有ShdowCaster,但是他有个Fallback “Mobile/VertexLit” 我们打开"Mobile/VertexLit”,这里我只把ShadowCaster展示出来:

// Pass to render object as a shadow caster
Pass
{
    Name "ShadowCaster"
    //--标识用于投射阴影的通道
    Tags { "LightMode" = "ShadowCaster" }
     //深度写入开启
    ZWrite On 
    //ZTest 小于等于
    ZTest LEqual 
    //关闭剔除 ---如果这几个不知道啥意思的~~有个女神叫冯乐乐
    Cull Off
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma target 2.0
    #pragma multi_compile_shadowcaster
    #include "UnityCG.cginc"

    struct v2f {
       //定义阴影需要的数据结构,顶点向片元传递的结构
        V2F_SHADOW_CASTER;
        UNITY_VERTEX_OUTPUT_STEREO
    };
    v2f vert( appdata_base v )
    {
        v2f o;
        //GPUInstance使用,考虑
        UNITY_SETUP_INSTANCE_ID(v);
        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
        //顶点转换
        TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
        return o;
    }
    float4 frag( v2f i ) : SV_Target
    {
        SHADOW_CASTER_FRAGMENT(i)
    }
    ENDCG
}

v2f结构定义

V2F_SHADOW_CASTER

// Declare all data needed for shadow caster pass output (any shadow directions/depths/distances as needed),
// plus clip space position.
#define V2F_SHADOW_CASTER V2F_SHADOW_CASTER_NOPOS UNITY_POSITION(pos)

这里有重新定义了两个宏,如果你看过之前写的那个链接: Stand源码解析,对第二个就感到很亲切了.当然,说实话要是我,我看不下去。

  1. UNITY_POSITION(pos);//float4 pos : SV_POSITION SV_POSITION和VPOS语义,均为片元使用坐标,GPU使用SV_POSITION中的值。来从剪辑空间位置计算屏幕像素位置。如果使用POSITION语义,我们还要手动处理透视触发,去除w分量。

  2. V2F_SHADOW_CASTER_NOPOS:

     //SHADOWS_CUBE--如果是点光源,并且这个点光源渲染的深度纹理渲染到cubemap中,则申请一个位置结构
     #if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
         // Rendering into point light (cubemap) shadows--申请光源到顶点的向量
         #define V2F_SHADOW_CASTER_NOPOS float3 vec : TEXCOORD0;
     #else
         // Rendering into directional or spot light shadows
         //不进行啥操作
         #define V2F_SHADOW_CASTER_NOPOS
         // 同时生命了一个标识,标识没有对位置进行记录
         #define V2F_SHADOW_CASTER_NOPOS_IS_EMPTY		 
     #endif
    

到此,我们知道这个宏定义了位置信息,如果是点光源同时点光源的深度图为cubemap时,还需要记录光源到顶点的向量信息。但是一般情况下,我们的手机有的不支持cubemap,而且手游实时点光源更是奢侈,所以从轻处理。

UNITY_VERTEX_OUTPUT_STEREO

UNITY_VERTEX_OUTPUT_STEREO来声明该顶点是否位于视线域中,来判断这个顶点是否输出到片段着色器,GPUInstance中见过,

# define UNITY_VERTEX_OUTPUT_STEREO   DEFAULT_UNITY_VERTEX_OUTPUT_STEREO
//看看是啥,尝尝鲜就行了~~再往下就跑偏了
#define DEFAULT_UNITY_VERTEX_OUTPUT_STEREO                         
uint stereoTargetEyeIndexSV :SV_RenderTargetArrayIndex;   
uint stereoTargetEyeIndex : BLENDINDICES0;

顶点函数:TRANSFER_SHADOW_CASTER_NORMALOFFSET

// Vertex shader part, with support for normal offset shadows. Requires
// position and normal to be present in the vertex input.
#define TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) TRANSFER_SHADOW_CASTER_NOPOS(o,o.pos)

#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
 #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) 
    //记录光源到顶点的位置向量
    o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; 
    //顶点转换,不多说
    opos = UnityObjectToClipPos(v.vertex);
#else
  #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos)         
    opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); 
    opos = UnityApplyLinearShadowBias(opos);
#endif

UnityClipSpaceShadowCasterPos

//计算顶点在裁剪空间的位置
float4 UnityClipSpaceShadowCasterPos(float4 vertex, float3 normal)
{
    float4 wPos = mul(unity_ObjectToWorld, vertex);	
    if (unity_LightShadowBias.z != 0.0)
    {
        float3 wNormal = UnityObjectToWorldNormal(normal);
        float3 wLight = normalize(UnityWorldSpaceLightDir(wPos.xyz));
           // apply normal offset bias (inset position along the normal)
        // bias needs to be scaled by sine between normal and light direction
        // (http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/)
        //
        // unity_LightShadowBias.z contains user-specified normal offset amount
        // scaled by world space texel size.

        float shadowCos = dot(wNormal, wLight);
        float shadowSine = sqrt(1-shadowCos*shadowCos);
        //上面两行呢,是为了求sin(灯光和法线夹角),因为1=sin^2+cos^2,所以sin=sqrt(1-con^2)
        //我们为什么要求sin,上一篇我们说过,我们的bias的偏移值要和法线以及法线和灯光的角度相关,
        //那么是什么关系呢?夹角越大,偏移越大。夹角为0偏移为0。所以sin函数的曲线完全符合。
        float normalBias = unity_LightShadowBias.z * shadowSine;	
        //下面就是朝着法线反方向偏移,现在我们是在做渲染深度图,在这里做了深度偏移
        wPos.xyz -= wNormal * normalBias;
    }
    //将偏移后的顶点,转换到投影空间	
    return mul(UNITY_MATRIX_VP, wPos);
}

unity_LightShadowBias:x表示阴影裁切空间中的线性偏移bias,透视相机unity是按照摄像机参数计算的这个值。-------y 对于平行光为1,对于其他光源为0,-----z 代表阴影slope depth bias scale。

UnityApplyLinearShadowBias

咋得,上面不是说都计算完了偏移了么?这咋又有偏移,不给面是不是~~上面我们做的只是法线偏移,但是我们真正做计算比较时候深度是在裁剪空间,所以这里才是真正意义上的阴影偏移,我们进行偏移的时候要避免裁剪空间中的z值造成越界,

float4 UnityApplyLinearShadowBias(float4 clipPos)	
{
    // For point lights that support depth cube map, the bias is applied in the fragment shader sampling the shadow map.
    // This is because the legacy behaviour for point light shadow map cannot be implemented by offseting the vertex position
    // in the vertex shader generating the shadow map.
    //上面的注释说的很清楚了,就是深度图是cubemap的(点光源),不会再这里处理偏移,因为这里无法实现。
#if !(defined(SHADOWS_CUBE) && defined(SHADOWS_CUBE_IN_DEPTH_TEX))
    //如果定义了z是反向,那就是z值的范围是[near,0],所以我们偏移是朝向近平面做的偏移,对于做了z反向的空间,
    //近平面偏移就是减法,没有的近平面就是加法
    #if defined(UNITY_REVERSED_Z)
        // We use max/min instead of clamp to ensure proper handling of the rare case
        // where both numerator and denominator are zero and the fraction becomes NaN.
        clipPos.z += max(-1, min(unity_LightShadowBias.x / clipPos.w, 0));
    #else
        clipPos.z += saturate(unity_LightShadowBias.x/clipPos.w);
    #endif
#endif
   //UNITY_NEAR_CLIP_VALUE 上面有提到dx中为1,opengl中为-1,是为了不让越界
   //接下来的操作就是为了不让我们计算出z值越界
#if defined(UNITY_REVERSED_Z)	
   //----Z值范围为[near,0],是递减的,如果clipPos.z大于clipPos.w*UNITY_NEAR_CLIP_VALUE值,说明越界了
    float clamped = min(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE);
#else
  //---Z值范围为[near,1],是递增的,如果范围小于了clipPos.w*UNITY_NEAR_CLIP_VALUE则越界了。
    float clamped = max(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE);
#endif
//上面说了,点光源不做处理,unity_LightShadowBias.y=0的是时候,非平行光,为1的时候表示平行光。
    clipPos.z = lerp(clipPos.z, clamped, unity_LightShadowBias.y);
    return clipPos;
}

片元函数 SHADOW_CASTER_FRAGMENT

简单粗暴,只有一个宏:SHADOW_CASTER_FRAGMENT(i);让我们看看他的庐山真面目。
哇,好n形,平行光返回0,上面辛辛苦苦算了半天,这边就不管是啥都反回0.没错,我们这是在投射阴影,所以只需要保存深度,不需要颜色。但是使用CubeMap来做阴影缺不一样,需要把z值写入cubemap中。UnityEncodeCubeShadowDepth把浮点数编码成RGBA形式存入图中。EncodeFloatRGBA一般用于2D纹理的浮点数压缩,自定义阴影可以使用这个呦

  #if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
     #define SHADOW_CASTER_FRAGMENT(i) 
     //_LightPositionRange.w=1/range
     return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w);
 #else
   #define SHADOW_CASTER_FRAGMENT(i) return 0;
 #endif

所以,如果我们自定义的shader也需要投影的话,就可以自定义pass来实现,或者直接callback已经有对应pass的shader。知道了原理,我们其实在顶点变换的地方做各式各样的阴影投射。

接受阴影

知道了怎么投射阴影后,接下来,我们看看,unity是如何让物体接受阴影的,为了节省只看平行光的。首先接受投影不需要对应的pass的标签。上一章我们讲到,渲染某一个物体的时候,拿自己的点跟深度缓存的数据做对比,来看看是不是在阴影中,所以我们就需要一个坐标,然后做顶点函数将这个坐标转换到灯光摄像机的裁剪空间。
说到这里,我们要想下,unity中还有什么会影响到阴影?没错,就是光照贴图,所以我们再写shader的时候要注意光照贴图,当然角色渲染不用考虑,但是场景渲染是需要考虑进去的。那投射阴影跟光照贴图有没有关系?没有 ,投射阴影只与对应的pass的标签有关,所以被烘焙的物体,如果有投射阴影的pass依然是可以投射阴影的。
变量解释:
_LightTexture0:平行光 光照衰减纹理,在灯光没有

光源类型 Cookie纹理(打开) 光照衰减纹理(无Cookie) 光照衰减纹理(有Cookie)
平行光 _LightTexture0 不需要 不需要
点光源 _LightTexture0 _LightTexture0 _LightTextureB0
聚光灯 _LightTexture0 _LightTexture0 _LightTextureB0

由于,cookie在手游项目中,使用极少,所以不做讨论,聚光灯不做讨论。主要考虑平行光

v2f结构定义

源码有太多的宏定义了,我们自己在做的时候可以根据自己的项目去写对应的代码。
1.定义_LightCoord,这个lightcoord主要是用来做衰减的,所以平行光,是不需要这个定义的(要是有杠头说平行光加了cookie就会用,那就你说的对)。至于如何计算,在顶点函数我们进行。
2.定义_ShadowCoord,这个用来获取深度信息的,通过他来计算出深度图的uv。这个一看就是都需要。

看看unity定义了多少的这些宏

	//---第一个,使用UNITY_LIGHTING_COORDS声明_LightCoord,使用DECLARE_LIGHT_COORDS声明_ShadowCoord
	#define UNITY_LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) UNITY_SHADOW_COORDS(idx2)
	//---第二个,跟上面的不用之处在于使用SHADOW_COORDS声明了_ShadowCoord
	#define LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) SHADOW_COORDS(idx2)

首先是DECLARE_LIGHT_COORDS的定义,

	#ifdef POINT
	#   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord3 _LightCoord : TEXCOORD##idx;
	#endif
	#ifdef SPOT
	#   define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
	#endif		
	//没有任何操作
	#ifdef DIRECTIONAL
	#   define DECLARE_LIGHT_COORDS(idx)
	#endif  

然后UNITY_SHADOW_COORDS定义,其实我们看到他的分支要么直接定义,要么就使用SHADOW_COORDS进行定义,那么为什么这么操作一下呢,主要是光照贴图做的区分。

// handles shadows in the depths of the GI function for performance reasons
//如果在GI的计算了阴影,详见stander源码解析
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI) 
	#   define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
//如果使用平行光,并且没有光照贴图,不是屏幕空间阴影
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS) 
         // no lightmap uv thus store screenPos instead
	    // can happen if we have two directional lights. main light gets handled in GI code, 
	    //but 2nd dir light can have shadow screen and mask.
	    // - Disabled on ES2 because WebGL 1.0 seems to have junk in .w (even though it shouldn't)
	//如果有SHADOWS_SHADOWMASK,这是Lighting-MixedLighting-LightingMode-Shadowmask进行烘焙设置开启
	//如果是在es2.0的不使用w值了。因为WebGL 1.0的w值有问题(尽管不应该)
	#   if defined(SHADOWS_SHADOWMASK) && !defined(SHADER_API_GLES)
	#       define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;		
	#   else
	#       define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
	#   endif
#else
	#   define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
#endif

我们看看SHADOW_COORDS的定义:

 //平行光,这就尬住了啊,定义一样啊
#if defined (SHADOWS_SCREEN)
    #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
    #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
#endif
	// ---- 聚光灯,一样啊
#if defined (SHADOWS_DEPTH) && defined (SPOT)
	#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
#endif
	// ----点光源
#if defined (SHADOWS_CUBE)
	#define SHADOW_COORDS(idx1) unityShadowCoord3 _ShadowCoord : TEXCOORD##idx1;	
#endif
	
	// ---- 关闭阴影,
#if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE)
	#define SHADOW_COORDS(idx1)		
#endif

看来看去,除了点光源和定义数据类型的不一样和关闭时不需要定义外,其他的定义都是一样的,好费劲。是为了条例更加清晰么?才写这么绕。。。

顶点处理

计算顶点操作,其实从上面定义数据的处理就可以看出来,主要是区分了是否使用光照贴图,以及光源类型进行的区分。所以,顶点计算也是如此,代码如下:

#define UNITY_TRANSFER_LIGHTING(a, coord) 
 	 COMPUTE_LIGHT_COORDS(a) 
	 //这里a是结构v2f,但是这coord是什么?是顶点的uv信息
	 UNITY_TRANSFER_SHADOW(a, coord)
#define TRANSFER_VERTEX_TO_FRAGMENT(a) 
	 COMPUTE_LIGHT_COORDS(a)
	 TRANSFER_SHADOW(a)

很简单,两句话,上面的数据结构定义有两个,_LightCoords和_ShadowCoords,所以这两句从名字也能看出是给这俩东西赋值的,如果我们再自己写shader的时候,明确不需要哪些东西就可以选择的时候,毕竟变体太多不是一般项目可以承受的,

UNITY_TRANSFER_SHADOW

UNITY_TRANSFER_SHADOW计算深度图的uv,由此我们看出对于使用了SHADOWS_SHADOWMASK的物体,是不需要深度图的,而是直接从光照贴图中获取阴影信息:

	// handles shadows in the depths of the GI function for performance reasons
	#if defined(HANDLE_SHADOWS_BLENDING_IN_GI) 
	#   define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)		
	#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS) 
	//如果有SHADOWS_SHADOWMASK,这是Lighting-MixedLighting-LightingMode-Shadowmask进行烘焙设置开启
	//如果是在es2.0的不使用w值了。因为WebGL 1.0的w值有问题(尽管不应该)
	#   if defined(SHADOWS_SHADOWMASK) && !defined(SHADER_API_GLES)
	#       define UNITY_TRANSFER_SHADOW(a, coord) 
	            //xy记录了光照贴图的uv信息,zw则记录了屏幕空间xy的位置信息,这里与我们上一张介绍的
	            //不太一致,
	            //但是这里ComputeScreenPos是获取的当前的场景摄像机
				{a._ShadowCoord.xy = coord * unity_LightmapST.xy + unity_LightmapST.zw;
				 a._ShadowCoord.zw = ComputeScreenPos(a.pos).xy;}	
	#   else		
	#       define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
	#   endif
	#else		
	#   if defined(SHADOWS_SHADOWMASK)
	#       define UNITY_TRANSFER_SHADOW(a, coord) 
	            //只获取光照贴图的uv
				a._ShadowCoord.xy = coord.xy * unity_LightmapST.xy + unity_LightmapST.zw;	
	#   else
	         //---记住这个耻辱---高精度片元处理器,不配在顶点处理做阴影坐标转换,真男人就去片元做。。。
	#       if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
	#           define UNITY_TRANSFER_SHADOW(a, coord)
	#       else
	#           define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
	#       endif	
	#   endif
	#endif

TRANSFER_SHADOW

	//平行光
	#if defined (SHADOWS_SCREEN)		
	    #if defined(UNITY_NO_SCREENSPACE_SHADOWS)
	        #define TRANSFER_SHADOW(a) 
	        a._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, v.vertex ) );
	    #else 
	       // 屏幕空间阴影--这里记录的也是场景相机的ndc空间坐标啊??
	        #define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);		 
	    #endif
	#endif

	// ---- Spot light shadows
	#if defined (SHADOWS_DEPTH) && defined (SPOT)
		#define TRANSFER_SHADOW(a) 
		a._ShadowCoord = mul (unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));
	#endif
	
	// ---- Point light shadows
	#if defined (SHADOWS_CUBE)
		#define TRANSFER_SHADOW(a)
		 a._ShadowCoord.xyz = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz;
	#endif
	
	// ---- Shadows off
	#if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE)		
		#define TRANSFER_SHADOW(a)
	#endif

这里我们有了四种情况,第一种是使用阴影蒙版,要记录光照贴图uv信息,以及屏幕空间坐标。第二种是屏幕空间阴影记录做了屏幕空间坐标。第三种是点光源,记录的是灯光到顶点的向量信息(不考虑)。第四种才是我们上一讲说的灯光空间的位置变换unity_WorldToShadow[0]为世界空间–灯光摄像机裁剪空间的转换矩阵。

灯光位置–COMPUTE_LIGHT_COORDS

这个位置主要是计算距离灯光的相对坐标,用于聚光灯和点光源,平行光不需要,因为平行光只有方向

	#ifdef SPOT
	#   define COMPUTE_LIGHT_COORDS(a)
	    a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex));	
	#endif
	#ifdef DIRECTIONAL
	#   define COMPUTE_LIGHT_COORDS(a)
	#endif
	#ifdef POINT
	#   define COMPUTE_LIGHT_COORDS(a) 
	    a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;
	#endif

这里用到的变换矩阵为unity_WorldToLight。

片元处理

UNITY_SHADOW_ATTENUATION

计算衰减,前面都是数据准备。
在看这里之前,了解一下unity烘焙系统还是很关键的,因为阴影处理不可能一直都是实时的,不然的话,下面的各个宏的关系会让你头皮发麻,生活不能自理,走你:光照贴图LightMode.

//如果在GI中处理的阴影,则使用shadow_attenuation进行处理
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI) 
#   define UNITY_SHADOW_ATTENUATION(a, worldPos) SHADOW_ATTENUATION(a)
//平行光,非光照贴图,非屏幕空间阴影。
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)	
//----不开启光照贴图,怎么开启SHADOWS_SHADOWMASK?--官方翻译我没试出来。。
//当有两个有向平行光时,主有向平行光在全局照明的相关代码中进行处理,第二个有向平行光在屏幕空间中进行
//阴影计算,如果使用了阴影蒙版,且不是在D3D9和OpenGLES平台下,那就从烘焙出来的光照贴图中取得阴影数据。
#   if defined(SHADOWS_SHADOWMASK) && !defined(SHADER_API_GLES)
#       define UNITY_SHADOW_ATTENUATION(a, worldPos) 
         //_ShadowCoord.xy为光照图uv,zw为屏幕空间xy坐标
          UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, float4(a._ShadowCoord.zw, 0.0, UNITY_SHADOW_W(a.pos.w)));
#   else
#       define UNITY_SHADOW_ATTENUATION(a, worldPos) 
          //实时计算-平行光,无lightmap,无shadowmask
           UnityComputeForwardShadows(0, worldPos, a._ShadowCoord)
#   endif
#else
#   if defined(SHADOWS_SHADOWMASK)   //开启了shadowmask模式
#       define UNITY_TRANSFER_SHADOW(a, coord) a._ShadowCoord.xy = coord.xy * unity_LightmapST.xy + unity_LightmapST.zw;
#       if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE) || UNITY_LIGHT_PROBE_PROXY_VOLUME)
            //这些宏都是说明需要进行实时阴影的计算,也是就既要shadowmask也要实时阴影
#           define UNITY_SHADOW_ATTENUATION(a, worldPos) 
                   UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, UNITY_READ_SHADOW_COORDS(a))
#       else  //--不需要实时阴影计算
#           define UNITY_SHADOW_ATTENUATION(a, worldPos) 
                   UnityComputeForwardShadows(a._ShadowCoord.xy, 0, 0)
#       endif
#   else
#       if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE))
#           define UNITY_SHADOW_ATTENUATION(a, worldPos)  //---实时计算,没有烘焙阴影
                  UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
#       else
#           if UNITY_LIGHT_PROBE_PROXY_VOLUME
#               define UNITY_SHADOW_ATTENUATION(a, worldPos) //---实时计算,没有烘焙阴影
                UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
#           else//--没有影子计算
#               define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, 0, 0)
#           endif
#       endif
#   endif
#endif



// -----------------------------
//  Shadow helpers (5.6+ version)
// -----------------------------
// This version depends on having worldPos available in the fragment shader and using that to compute light coordinates.
// if also supports ShadowMask (separately baked shadows for lightmapped objects)
//-----支持计算实时阴影,以及烘焙的阴影。  
//第一个参数lightmapUV用来获取烘焙阴影。
//第二个参数worldPos世界坐标,1.计算淡入淡出需要距离计算。2.烘焙阴影计算衰减 3.spot和shadowcube计算需要
//第三个参数screenPos屏幕空间的位置,用于实时计算。1.获取深度图的uv坐标,以及屏幕空间阴影的shadow的uv坐标。
half UnityComputeForwardShadows(float2 lightmapUV, float3 worldPos, float4 screenPos)
{
    //fade value----ZDist表示当前点在视空间的深度
    float zDist = dot(_WorldSpaceCameraPos - worldPos, UNITY_MATRIX_V[2].xyz);
    //实时转烘焙?马萨卡?没错,就是淡入淡入。如果你看了上面的知识点,就知道,通过shadowdistance
    //我们会完成实时阴影和烘焙阴影的切换,但是直接切会显得不够高大尚,所以这里就是淡入淡出
    float fadeDist = UnityComputeShadowFadeDistance(worldPos, zDist);
    half  realtimeToBakedShadowFade = UnityComputeShadowFade(fadeDist);

    //读取烘焙的阴影数据
    half shadowMaskAttenuation = UnitySampleBakedOcclusion(lightmapUV, worldPos);
    //实时阴影计算,只有平行光
    half realtimeShadowAttenuation = 1.0f;
    //directional realtime shadow
    #if defined (SHADOWS_SCREEN)
        #if defined(UNITY_NO_SCREENSPACE_SHADOWS) && !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
            //非屏幕空间阴影,并且是高精度片元处理器,什么高精度?对,就顶点可以啥都不用处理的那个,在这处理了
            realtimeShadowAttenuation = unitySampleShadow(mul(unity_WorldToShadow[0], unityShadowCoord4(worldPos, 1)));
        #else
            //Only reached when LIGHTMAP_ON is NOT defined (and thus we use interpolator for screenPos rather than lightmap UVs). See HANDLE_SHADOWS_BLENDING_IN_GI below.
            //只有当没有开启LightMap时执行,因此我们使用的是sreeenpos而不是lightmapuv.
            realtimeShadowAttenuation = unitySampleShadow(screenPos);
        #endif
    #endif

    #if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT) && !defined(LIGHTMAP_SHADOW_MIXING)
    //avoid expensive shadows fetches in the distance where coherency will be good
    UNITY_BRANCH
    if (realtimeToBakedShadowFade < (1.0f - 1e-2f))
    {
    #endif	
        //spot realtime shadow--聚光灯是实时计算
        #if (defined (SHADOWS_DEPTH) && defined (SPOT))
            #if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
                unityShadowCoord4 spotShadowCoord = mul(unity_WorldToShadow[0], unityShadowCoord4(worldPos, 1));
            #else
                unityShadowCoord4 spotShadowCoord = screenPos;
            #endif
            realtimeShadowAttenuation = UnitySampleShadowmap(spotShadowCoord);
        #endif

        //point realtime shadow--点光源实时计算
        #if defined (SHADOWS_CUBE)
            realtimeShadowAttenuation = UnitySampleShadowmap(worldPos - _LightPositionRange.xyz);
        #endif

    #if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT) && !defined(LIGHTMAP_SHADOW_MIXING)
    }
    #endif
    //混合实时阴影衰减,烘焙阴影衰减,淡入淡入阴影衰减
    return UnityMixRealtimeAndBakedShadows(realtimeShadowAttenuation, shadowMaskAttenuation, realtimeToBakedShadowFade);
}

淡入淡出计算-UnityComputeShadowFadeDistance

这个没啥说的,就是根据距离,算出当前的衰减值,主要是下面变量的意义
超链接:下面用到的一个变量解析unity_ShadowFadeCenterAndType.

	float UnityComputeShadowFadeDistance(float3 wpos, float z)
	{
	    float sphereDist = distance(wpos, unity_ShadowFadeCenterAndType.xyz);
	    return lerp(z, sphereDist, unity_ShadowFadeCenterAndType.w);
	}		
	// ------------------------------------------------------------------
	half UnityComputeShadowFade(float fadeDist)
	{
	    return saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
	}

烘焙阴影计算-UnitySampleBakedOcclusion

看不懂就走你:光照贴图LightMode.

fixed UnitySampleBakedOcclusion (float2 lightmapUV, float3 worldPos)
{
  //shadowmask模式
    #if defined (SHADOWS_SHADOWMASK)
        #if defined(LIGHTMAP_ON)
           //直接采样
            fixed4 rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
        #else
            fixed4 rawOcclusionMask = fixed4(1.0, 1.0, 1.0, 1.0);
            #if UNITY_LIGHT_PROBE_PROXY_VOLUME
                //x分量为1时表示启用本光照体代理体,为0时表示不启用,使用光照探针计算阴影,反之使用蒙版图
                if (unity_ProbeVolumeParams.x == 1.0)
                    rawOcclusionMask = LPPV_SampleProbeOcclusion(worldPos);
                else
                    rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
            #else
                rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
            #endif
        #endif
        return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));

    #else	
        //In forward dynamic objects can only get baked occlusion from LPPV, light probe occlusion is done on the CPU by attenuating the light color.
        //LPPV 是Light Prober Proxy Volume,动态物体如果要获取烘焙的阴影,只能通过LPPV,lightprobe已经
        //通过灯光颜色衰减做了处理
        fixed atten = 1.0f;
        #if defined(UNITY_INSTANCING_ENABLED) && defined(UNITY_USE_SHCOEFFS_ARRAYS)
            // ...unless we are doing instancing, and the attenuation is packed into SHC array's .w component.
            //如果我们在用instancing,并且衰减保存在了SHC array`s.w	        
            atten = unity_SHC.w;
        #endif

        #if UNITY_LIGHT_PROBE_PROXY_VOLUME && !defined(LIGHTMAP_ON) && !UNITY_STANDARD_SIMPLE
            fixed4 rawOcclusionMask = atten.xxxx;
             //x分量为1时表示启用本光照体代理体,为0时表示不启用,使用光照探针计算阴影
            if (unity_ProbeVolumeParams.x == 1.0)
                rawOcclusionMask = LPPV_SampleProbeOcclusion(worldPos);
            return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
        #endif

        return atten;
    #endif
}

阴影采样- unitySampleShadow

// ---- Screen space direction light shadows helpers (any version)
#if defined (SHADOWS_SCREEN)	
    #if defined(UNITY_NO_SCREENSPACE_SHADOWS)
        UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
        #define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, v.vertex ) );
        inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
        {
          //支持原生lightmap,采样后获取数值,然后进行衰减计算
            #if defined(SHADOWS_NATIVE)
                fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord.xyz);
                shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r);
                return shadow;
            #else
               //不支持原生lightmap,比如es2.0,取出深度信息之后自己比较dist
                unityShadowCoord dist = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, shadowCoord.xy);
                // tegra is confused if we use _LightShadowData.x directly
                 // with "ambiguous overloaded function reference max(mediump float, float)"
                unityShadowCoord lightShadowDataX = _LightShadowData.x;
                unityShadowCoord threshold = shadowCoord.z;
                return max(dist > threshold, lightShadowDataX);
            #endif
        }

    #else 
        //屏幕空间阴影,需要采样的是屏幕--设置屏幕空间阴影
        UNITY_DECLARE_SCREENSPACE_SHADOWMAP(_ShadowMapTexture);
        #define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
        inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
        {
           //---一开始看这个采样真的是把我吓出了翔,怎么shadowmap用的场景相机的屏幕空间坐标,往上看,
           //你会发现,在处理阴影的时候,已经将此事的_ShadowMapTexture设置成屏幕空间了
            fixed shadow = UNITY_SAMPLE_SCREEN_SHADOW(_ShadowMapTexture, shadowCoord);
            return shadow;
        }

    #endif	
    #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
    #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
#endif

阴影混合- UnityMixRealtimeAndBakedShadows

这个函数是将计算出的实时阴影,烘焙阴影,以及淡入淡入的阴影就行混合,都是根据一些宏定义,代码已经很清楚了

// ------------------------------------------------------------------
// Used by both the forward and the deferred rendering path
half UnityMixRealtimeAndBakedShadows(half realtimeShadowAttenuation, half bakedShadowAttenuation, half fade)
{
    // -- Static objects --
    // FWD BASE PASS
    // ShadowMask mode          = LIGHTMAP_ON + SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
    // Distance shadowmask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK
    // Subtractive mode         = LIGHTMAP_ON + LIGHTMAP_SHADOW_MIXING
    // Pure realtime direct lit = LIGHTMAP_ON

    // FWD ADD PASS
    // ShadowMask mode          = SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
    // Distance shadowmask mode = SHADOWS_SHADOWMASK
    // Pure realtime direct lit = LIGHTMAP_ON

    // DEFERRED LIGHTING PASS
    // ShadowMask mode          = LIGHTMAP_ON + SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
    // Distance shadowmask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK
    // Pure realtime direct lit = LIGHTMAP_ON
       // -- Dynamic objects --
    // FWD BASE PASS + FWD ADD ASS
    // ShadowMask mode          = LIGHTMAP_SHADOW_MIXING
    // Distance shadowmask mode = N/A
    // Subtractive mode         = LIGHTMAP_SHADOW_MIXING (only matter for LPPV. Light probes occlusion being done on CPU)
    // Pure realtime direct lit = N/A

    // DEFERRED LIGHTING PASS
    // ShadowMask mode          = SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
    // Distance shadowmask mode = SHADOWS_SHADOWMASK
    // Pure realtime direct lit = N/A

    #if !defined(SHADOWS_DEPTH) && !defined(SHADOWS_SCREEN) && !defined(SHADOWS_CUBE)
        #if defined(LIGHTMAP_ON) && defined (LIGHTMAP_SHADOW_MIXING) && !defined (SHADOWS_SHADOWMASK)
            //In subtractive mode when there is no shadow we kill the light contribution as direct as been baked in the lightmap.
            return 0.0;
        #else
            return bakedShadowAttenuation;
        #endif
    #endif

    #if (SHADER_TARGET <= 20) || UNITY_STANDARD_SIMPLE
        //no fading nor blending on SM 2.0 because of instruction count limit.
        #if defined(SHADOWS_SHADOWMASK) || defined(LIGHTMAP_SHADOW_MIXING)
            return min(realtimeShadowAttenuation, bakedShadowAttenuation);
        #else
            return realtimeShadowAttenuation;
        #endif
    #endif

    #if defined(LIGHTMAP_SHADOW_MIXING)
        //Subtractive or shadowmask mode
        realtimeShadowAttenuation = saturate(realtimeShadowAttenuation + fade);
        return min(realtimeShadowAttenuation, bakedShadowAttenuation);
    #endif

    //In distance shadowmask or realtime shadow fadeout we lerp toward the baked shadows (bakedShadowAttenuation will be 1 if no baked shadows)
    return lerp(realtimeShadowAttenuation, bakedShadowAttenuation, fade);
}

总结

太恶心了,又是这么长的东西。不过unity对阴影的计算还是比较全面的,在我们进行角色与场景进行交互时还是很有用的,根据自己项目需求去选择定制的阴影处理还是很带感的,尤其是lightprob的应用。

你可能感兴趣的:(Unity-日常)