UnityShader-阴影

渲染阴影的过程

一 渲染屏幕空间的深度贴图 (DepthTexture)

在正向渲染里,unity会先用ShadowCaster这个Pass渲染一遍场景,得到一张深度贴图 

 

(PS:延迟渲染,深度贴图和albedo specular之类在Deferred Pass里统一计算一并放在Gbuffer里,不会专门单独渲染)

二 渲染光源方向的深度贴图(ShadowMap)

然后unity会从光源的照射方向,以一定的距离再渲染另一份深度贴图,每个有阴影的光源都会渲染一份 这里是直射光源的shadowmap,其余光源的阴影(因为不是平行光)计算略有出入。

 上图是两个直射光延各自光线方向渲染的深度图,这里每一份ShadowMap都包含了4张相同角度,不同距离的四张深度图,这是因为我们在QualitySeting里将Shadow Cascades设为了Four Cascades,你还可以设置成Two Cascades或者No Cascades 。这里每张都要单独渲染一次,所以Four Cascades就得渲染四次。

我们可以看到,这里的深度贴图是正交视图的(因为直射光是平行光),但是我们渲染场景用的一般是透视视图,近大远小,同样大小的一块地方,近处占屏幕的比例更多,所以近处阴影所需的分辨率应该要比远处的要高。但是如果只用单张的shadowmap,远近物体的阴影享有相同的分辨率,就很浪费,效果也不好。(

左: No Cascades 右:Four Cascades)

          

直射光是没有位置的,所以渲染深度图相机的具体位置(离物体的远近距离)Unity会根据上面的Cascade spilt以及其他一些数据来确定。 

通过将Scene视图的着色凡是改为Shadow Cascades,就可以看到Cascade spilt具体的分布情况

左:Stable Fit  右:CloseFIt

Stable Fit是按离相机距离来划分,CloseFIt则是直接按屏幕控件的深度贴图划分,最后生成的shdowmap也不一样,CloseFIt可以有效的利用阴影贴图,同等条件下阴影质量更高,但是会出现 shadow edge swimming的问题,就是你移动视角时,阴影的锯齿也会移动,阴影分辨率越低越明显 

三 计算屏幕空间的阴影  CollectShadow=f(depthtexture,shadowmap)

采样屏幕空间每个像素点的深度贴图,就可以通过矩阵变换得到每个点在世界的坐标,然后再转化到渲染shadowmap的对应的裁减空间,然后再NDC之后,此时该点的z值就和用该点xy采样shadowmap的值是同一个距离概念的表示单位了,然后比较两个值,如果shdowmap里的值比z值小的话,就说明光线在到达该点前就被其他物体遮挡了,所以该点对于该光源是阴影的区域(这里指深度贴图近处0远处1的情况下,因为很多资料里都是近黑远白的深度贴图,但是我用framedebug看到的深度贴图都是近白远黑,也就是近处1远处0,这样的话运算时可能要z=1-z才行)

好在这一步Unity自己会帮我们处理

两个直射光的屏幕空间阴影贴图

   ----------------------   

上图可以看到这张贴图会被渲染到 Screenspace ShadowMap,而完成这一系列计算的的Pass则来自一个Unity内置的shader

下面是这个Subshader用到的一些属性

Unity官网可以下载内建shader 所以我找了一下 

Hidden/Internal-ScreenSpaceShadows里有很多很多的SubShader,每个subshader一个pass,这里只截取部分,第一个subshader是处理硬阴影的 ,所以主要看frag_hard这个片元函数,如果是软阴影会多次采样叠加。

fixed4 frag_hard (v2f i) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); // required for sampling the correct slice of the shadow map render texture array

    float3 vpos = computeCameraSpacePosFromDepth(i);//得到裁减空间坐标

    float4 wpos = mul (unity_CameraToWorld, float4(vpos,1));//转为世界空间坐标


    fixed4 cascadeWeights = GET_CASCADE_WEIGHTS (wpos, vpos.z);//如果有cascades的话得到权重,决定采样哪一张
    float4 shadowCoord = GET_SHADOW_COORDINATES(wpos, cascadeWeights);//得到该点对应在shadowmap中的用于采样的坐标

    //1 tap hard shadow
    //UNITY_SAMPLE_SHADOW我找半天在HLSLSupport.cginc里找到了一点定义,但有各种各样的分支,所以我懵逼了
    //总之就是采样ShadowMap吧,应该是里面顺便-z了吧- -|||,毕竟他要了xyzw,后面点光源阴影也有用到,这里的shadowmap是指光源方向的深度图,不是最后的阴影图
    fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord);
    //note阴影值最后是乘的,所以0才是阴影
    shadow = lerp(_LightShadowData.r, 1.0, shadow);

    fixed4 res = shadow;
    return res;
}

GET_SHADOW_COORDINATES宏如下

#if defined (SHADOWS_SINGLE_CASCADE)
    #define GET_SHADOW_COORDINATES(wpos,cascadeWeights) getShadowCoord_SingleCascade(wpos)
#else
    #define GET_SHADOW_COORDINATES(wpos,cascadeWeights) getShadowCoord(wpos,cascadeWeights)
#endif

....
inline float4 getShadowCoord_SingleCascade( float4 wpos )
{
    return float4( mul (unity_WorldToShadow[0], wpos).xyz, 0);//这里就是无cascade的情况,直接将该点从世界控件转到这个Shadow空间(?应该就是我前面说的光源的裁减空间吧)
}

inline float4 getShadowCoord( float4 wpos, fixed4 cascadeWeights )
{//这就是多cascade的情况了
    float3 sc0 = mul (unity_WorldToShadow[0], wpos).xyz;
    float3 sc1 = mul (unity_WorldToShadow[1], wpos).xyz;
    float3 sc2 = mul (unity_WorldToShadow[2], wpos).xyz;
    float3 sc3 = mul (unity_WorldToShadow[3], wpos).xyz;
    float4 shadowMapCoordinate = float4(sc0 * cascadeWeights[0] + sc1 * cascadeWeights[1] + sc2 * cascadeWeights[2] + sc3 * cascadeWeights[3], 1);
#if defined(UNITY_REVERSED_Z)
    float  noCascadeWeights = 1 - dot(cascadeWeights, float4(1, 1, 1, 1));
    shadowMapCoordinate.z += noCascadeWeights;
#endif
    return shadowMapCoordinate;
}

_LightShadowData.x - 1 - shadow strength
_LightShadowData.y - Appears to be unused
_LightShadowData.z - 1.0 / shadow far distance
_LightShadowData.w - shadow near distance 

至此,阴影贴图渲染完成,物体可以从中得到相应光源阴影信息啦! 


投射阴影

前面无论是屏幕空间的深度贴图还是shadowmap,都是通过渲染物体的ShdowCaster 这个Pass得到的,所以我们要实现这个pass,才能让物体投射阴影(shader里没写这个pass也能投射阴影,可能是因为fallback的shader有实现shdowcaster的pass)

Pass{
    Tags{"LightMode"="ShadowCaster"
    CGPROGRAM
    #include "UnityCG.cginc"
    //...
    #pragma vertex shadowVert
    #pragma fragment shadowFrag
    ENDCG
}

片元函数不用输出什么值,因为我们只需要深度值,GPU会为我们记录

struct a2vShadow{
    float4 vertex:POSITION;
};
float4 shadowVert(appdata_base v):SV_POSITION{
    return UnityObjectToClipPos(v.vertex);
}
fixed4 shadowFrag(float4 pos:SV_POSITION):SV_Target{
    return 0;
}

 打开FrameDebuger就可以看到Unity调用了自己写的Pass,而且也阴影也可以正常的显示

我们只写了一个subshader,所以#0就是我们的这个shader,如果显示#1或#2,就说明它可能调用了fallback的shader中的subshader了

我们可以继续完善一下这个阴影,在每个投射阴影的光源面板可以看到这两个值,拖动一下滑条就可以发现,Bias值越大,阴影就会向光源方向(也就是shadowmap的z轴方向)偏移,Normal Bias则是会沿法线内缩。

这两个值都是为了解决不正常的自阴影 Shadow Acne。因为我们我们阴影采样贴图得到了,贴图是一个个像素,所有如果阴影分辨率很低,阴影就会由很大的像素块组成,这就容易出现这个问题,本来应该在物体背后的阴影,跑到了物体前面去,非常的诡异 

(这图其实还好,阴影没投到自己身上,总之了解怎么个情况就行了- -||)

 

所以我们至少要把阴影缩到物体底下 而不是前面,这就是两个bias的作用了,通过缩小或者便宜阴影,避免阴影跑到了物体的迎光一侧

 

Unity已经为我们准备好对应的两个方法了。 

struct a2vShadow{
    float4 vertex:POSITION;
    float4 normal:NORMAL;
};
float4 shadowVert(appdata_base v):SV_POSITION{
    //normal bias
    float4 position = UnityClipSpaceShadowCasterPos(v.vertex.xyz, v.normal);
    //bias
    return UnityApplyLinearShadowBias(position);
}

两个函数的具体内容

https://www.zhihu.com/question/270585735/answer/355147675 这里有一点讲解

float4 UnityApplyLinearShadowBias (float4 clipPos) {
    //让物体顶点增加z轴偏移 /(雾)应该是靠近摄像头,所以后面限制才乘的nearclip ?但+不应该远离吗?/
    clipPos.z += saturate(unity_LightShadowBias.x / clipPos.w);
    float clamped = max(clipPos.z, clipPos.w * UNITY_NEAR_CLIP_VALUE);
    //unity_LightShadowBias.y 在点灯和聚光灯下是0,平行光是1,也就是平行光时会加限制,避免超过边界(视锥体?)
    clipPos.z = lerp(clipPos.z, clamped, unity_LightShadowBias.y);
    return clipPos;
}
float4 UnityClipSpaceShadowCasterPos (float3 vertex, float3 normal) {
	float4 clipPos;
    
    // Important to match MVP transform precision exactly while rendering
    // into the depth texture, so branch on normal bias being zero.
    if (unity_LightShadowBias.z != 0.0) {
        //转世界 位置、法线、光照
		float3 wPos = mul(unity_ObjectToWorld, float4(vertex,1)).xyz;
		float3 wNormal = UnityObjectToWorldNormal(normal);
		float3 wLight = normalize(UnityWorldSpaceLightDir(wPos));

	// 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.
        //单位向量,光线和法线的夹角cos->sin值
		float shadowCos = dot(wNormal, wLight);
		float shadowSine = sqrt(1 - shadowCos * shadowCos);
        //sin值和用户定义的normalbias值结合成一个缩放值
		float normalBias = unity_LightShadowBias.z * shadowSine;
        //延法线方向收缩
		wPos -= wNormal * normalBias;
        //转回裁减空间
		clipPos = mul(UNITY_MATRIX_VP, float4(wPos, 1));
    }
    else {
        clipPos = UnityObjectToClipPos(vertex);
    }
	return clipPos;
}

投射阴影完成后就是让物体接受阴影,也就是读取我们的那张屏幕空间阴影贴图。 


接收阴影 

当主方向光投射阴影的时候,Unity将寻找启用SHADOWS_SCREEN关键字的着色器的变体。因此,我们必须为我们的base pass创建两个变体,一个启用了SHADOWS_SCREEN关键字,一个没有启用SHADOWS_SCREEN关键字。

#pragma multi_compile _ SHADOWS_SCREEN

首先添加一个插值器存储屏幕空间坐标用于读取阴影贴图 可以用宏代替 

struct v2f{
        //...
	    #ifdef SHADOWS_SCREEN
	    float4 shadowCoord:TEXCOORD6;
	    #endif

        //OR
        SHADOW_COORDS(6)
    };

然后再顶点函数为 shadowCoord赋值屏幕空间坐标

//...

#ifdef SHADOWS_SCREEN
//1、剪裁空间转屏幕空间,用于采样阴影纹理,未进行齐次除法(/w),因为插值会破坏齐次除法,所以必须在片元函数里进行
//2、_ProjectionParams.x 处理api或平台差异,比如dx的屏幕空间坐标原点在左上而别的在左下,所以要倒转y轴
o.shadowCoord.xy=(float2(o.pos.x,o.pos.y*_ProjectionParams.x)+o.pos.ww)*0.5f;//o.pos.w;
o.shadowCoord.zw=o.pos.zw;

//OR
o.shadowCoord=ComputeScreenPos(o.pos);

#endif


//OR 3、用宏就不用再手动判断SHADOWS_SCREEN了
TRANSFER_SHADOW(o);

//...

最后在片元函数里读取阴影贴图


#ifdef SHADOWS_SCREEN
float attenuation=tex2D(_ShadowMapTexture,i.shadowCoord.xy/i.shadowCoord.w);

//OR 
//1、如果是用宏实现阴影,那么最好SHADOW_COORDS TRANSFER_SHADOW SHADOW_ATTENUATION一起用,因为他们之间有一些命名约定,像阴影坐标在SHADOW_COORDS里命名为_ShadowCoord,另外两个都假设他叫这个名字,此外输入参数i的剪裁空间坐标默认叫Pos,你如果设为别的名字就报错了
float attenuation=SHADOW_ATTENUATION(i);

#else
UNITY_LIGHT_ATTENUATION(attenuation, 0, i.worldPos);
#endif
--------------------------------------------------
//2、事实上,UNITY_LIGHT_ATTENUATION已经包含了SHADOW_ATTENUATION,在第二个参数导入信息即可
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);

 多重阴影

 前面的计算了BasePass中直射光的阴影,如果想要addpass则需要修改一个多重编译指令,代替之前的#pragma multi_compile_fwdadd

#pragma multi_compile_fwdadd_fullshadows

 会添加四个关键字 SHADOW_CUBE、 SHADOW_DEPTH 、SHADOW_SCREEN 、SHADOW_SOFT

前面几个阴影的宏包括了这些情况的阴影的处理

 其中,直射光计算方式一样,但是聚光灯和点光源就有点不一样了,

首先是阴影贴图的不同,他们都没有collectShadow这一步骤来计算屏幕空间阴影贴图,毕竟他们都是范围光,用这个太浪费了

对于聚光灯而言,它的shadowmap是一张光源角度的深度图

而点光源则需要渲染六张深度图,构成立方体贴图,因为点光源是向四面八方所有方向发射的

   


我也不知道为什么我不做下面的修改,Unity仍然支持点光源的立体深度贴图,可能2017版本Unity(我看的文章作者用的是Unity 5.4.0f3.)已经支持深度立方体贴图了吧,不管怎样还是放着标记一下吧。

点光源深度贴图是立方体贴图,所以shadowcaster这个Pass要有一个针对立方体阴影贴图的变种

#pragma multi_compile shadowcaster

它提供了两个关键字

SHADOWS_DEPTH:用于平行光好和聚光灯的阴影

SHADOW_CUBE:用于点光源的阴影

#if defined(SHADOWS_CUBE)
		    struct v2fShadow {
		    float4 position : SV_POSITION;
		    float3 lightVec : TEXCOORD0;
	        };
	        v2fShadow shadowVert (a2vShadow v) {
		        v2fShadow o;
		        o.position = UnityObjectToClipPos(v.vertex);
		        o.lightVec =
			    mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz;
		        return o;
	        }
	        float4 shadowFrag (v2fShadow i) : SV_TARGET {
		        float depth = length(i.lightVec) + unity_LightShadowBias.x;
		        depth *= _LightPositionRange.w;
		        return 1; //UnityEncodeCubeShadowDepth(depth);
	        }
//...

 


原文:https://catlikecoding.com/unity/tutorials/rendering/part-7/

翻译:https://www.jianshu.com/p/2e277dca316a

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值