Unity Shader中的阴影渲染 理论与实践

前言:

表面遮挡住光源路径就会产生阴影,由理想点光源产生的阴影边缘是比较锐利的,而现实世界中的阴影边缘确是模糊的,该模糊部分成为半影。半影的出现,是由于现实世界的点光源不是空间意义上的一个点,它会覆盖一定的面积,就会产生以不同角度掠过物体边缘的光线。

在阴影的渲染技术中,通常都会把物体分为三个类别:投射阴影的物体,接收阴影的物体以及完全被阴影渲染忽略的物体。同时光源也可以被设置成产生阴影的和不产生阴影的,这些选择提供了重要的优化,它能够帮助我们在生成场景中的阴影时,限制所需处理的光源和物体的组合数量。

以上内容摘自于《游戏引擎架构》,而以下主内容来自于《Unity Shader入门精要》以及《游戏引擎架构》。

一、Unity阴影渲染技术之ShadowMap

shadowmap技术:在阴影渲染中较为常用的一种技术,它首先把摄像机的位置放置在与光源重合的地方,也就是该深度图的计算是基于光源的视角的(点光源使用透视投影来得到阴影贴图,平行光使用正射投影来得到),那么场景中该光源的阴影区域就是那些摄像机看不到的地方。所以这就把问题转化为了我们要去判断一个片元是否为相机可见,如果可见就不渲染阴影,否则就渲染阴影。而片元的可见性通常都使用深度缓冲来解决,所以接下来的我们主要关注深度缓冲了。

shadowmap里存储的到底是什么,

在前向渲染中,如果场景中最重要的平行光开启了阴影,unity就会为该光源计算它的阴影映射纹理,也就是shadowmap。shadowmap本质上就是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面的深度值。因此,每个开启了阴影的光源都会有一张shadowmap;

shadowmap是如何生成的,

在unity中我们要使用一个专门更新光源的阴影映射纹理的pass(如果一个物体不投射阴影,则不需要这个Pass),这个pass就是LightMode标签被设置为ShadowCaster(阴影投射的pass),这个pass与其它pass不同的是,它的渲染目标不是帧缓存,而是阴影映射纹理。当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的shader中找到LightMode为ShadowCaster的Pass,如果没有,它就会在Fallback制定的shader中继续寻找,如果仍然没找到,该物体就无法向其它物体投射阴影。当找到了一个LightMode为ShadowCaster的pass后,就会使用该pass来更新当前光源的阴影投射纹理。一个开启阴影的光源就需要产生一张shadowmap,它是通过计算所有开启了应用投射的物体的相应的pass来得到的。我们需要先计算好一个光源的shadowmap,也就是执行完所有的LightMode的ShadowCaster的pass后才能去进行接收阴影的计算。

我们又是如何使用shadowmap的纹理来实现阴影的呢?

当得到一张shadowmap时,我么为了判断一个片元是否可见,需要得到两个深度值,要知道之前的shadowmap是把摄像机移动到光源位置得到的,所以shadowmap的深度值是在光源空间下的,故而我们需要在正常渲染的pass中把顶点位置变换到光源空间中。然后,使用xy分量对阴影投射纹理进行采样,得到阴影纹理下的深度值,如果该深度值小于该顶点的深度值(z分量),说明该顶点被遮挡,也就是处于阴影的后面。这是传统的阴影采样技术,而在unity5中则采用了一种新的技术,屏幕空间的阴影映射技术。并不是所有的平台unity都会使用这种技术,因为这种技术需要硬件的支持,而有些移动平台不支持这种特性。

屏幕空间的阴影映射技术:

unity首先会通过调用LightMode为shadowcaster的pass来得到两张纹理,一张是可投射阴影的光源的阴影映射纹理(基于光源的位置得到的深度纹理),一张是摄像机的深度纹理(基于摄像机的位置得到的深度纹理),然后再根据这两张纹理得到屏幕空间的阴影图,原理是:摄像机的深度图记录了所有可见面片的深度,如果摄像机的深度图中记录的表面深度大于阴影映射纹理中的深度值,那么就说明这个表面虽然是可见的,但是却处于该光源的阴影中,这样阴影图就包含了屏幕空间中所有阴影的区域。如果我们想要一个物体接收来自其它物体的阴影,只需要再shader中对阴影图进行采样,而由于阴影图是在屏幕空间下的,因此我们首先要把表面坐标从模型空间变换到屏幕空间中,然后使用该坐标对阴影图进行采样即可。

小结:一个物体接收来自其它物体的阴影以及它向其它物体投射阴影是完全不相干的两个过程。

如果一个物体要接收来自其他物体的阴影,就需要再shader中相应物体的阴影投射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。

如果我们想要一个物体向其它物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其它物体在对阴影映射纹理采样时可以得到该物体的相关信息。

二、不透明物体的阴影

首先要为光源打开阴影投射,并在物体的mesh renderer组建中选择cast shadows以及receive shadow。

第一步,阴影投射纹理的计算,这一步通常会使用unity内置的shader中的pass,在Fallback "Specular"和Fallback "Diffuse"中都可以找到这样一个pass,当然也可以自己写一个,代码可以参考官方的标准源代码。

Pass{
			Name "ShadowCaster"
			Tags{"LightMode"="ShadowCaster"}

			CGPROGRAM
				#pragma vertex vertShadow
				#pragma fragment fragShadow
				#pragma multi_compile_shadowcaster

				#include "UnityCG.cginc"
				
				struct v2fShadow {
					V2F_SHADOW_CASTER;
				};

				v2fShadow vertShadow(a2v v) {
					v2fShadow o;
					TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
					return o;
				}

				fixed4 fragShadow(v2fShadow i) : SV_Target{
					SHADOW_CASTER_FRAGMENT(i);
				}
			ENDCG
		}

第二步,则是要写接收投影的物体的shader,也就是对阴影投射纹理进行采样。

因为默认情况下,base pass是支持投影的,二add pass是不支持投影的,所以我们现在base pass里做一个接收投影的计算。我们要包含一个新的头文件#include "AutoLight.cginc",之后计算阴影所用的宏都是在这个文件中。

首先要向结构体v2f中添加一个内置宏:SHADOW_COORDS()。如下所示:

struct v2f {
	float4 pos : SV_POSITION;
	float3 worldNormal : TEXCOORD0;
	float3 worldPos : TEXCOORD1;
	float2 uv : TEXCOORD2;
	SHADOW_COORDS(3)
};

这个宏的作用,就是声明一个用于对阴影纹理采样的坐标,也就是我们要在顶点着色器内计算阴影纹理采样坐标,需要注意的是,这个宏有一个参数,就是下一个可用的插值寄存器的索引值,这里之前已经占用3个了(TEXCOORD0,TEXCOORD1,TEXCOORD2),所以下面就是3(注意索引从0开始),注意使用该宏时结束不要用分号。

之后就是在顶点着色器内计算阴影的纹理坐标,也就是对顶点坐标进行空间的转换,我们只需要在vert函数里使用一个宏叫做TRANSFER_SHADOW。如下所示。

v2f vert(a2v v) {
	v2f o;
	o.pos = UnityObjectToClipPos(v.vertex);
	o.worldNormal = UnityObjectToWorldNormal(v.normal);
	o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
	o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
	TRANSFER_SHADOW(o);
	return o;
}

最后就是我们在片元着色器中计算阴影值,使用的宏是SHADOW_ATTENUATION,计算出阴影值之后与漫反射以及高光反射值相乘即可。

fixed shadow = SHADOW_ATTENUATION(i);
return fixed4(ambient + (diff0+diff1)*shadow, 1.0);

三、统一管理光照衰减和阴影

光照衰减和阴影对物体最终的渲染效果的影响是相同的,我们都是把光照衰减因子和阴影值及光照结果相乘得到最终的渲染效果。unity提供了UNITY_LIGHT_ATTENUATION宏可以用来同时计算两个信息。这个函数需要的三个参数,其中第一个参数并没有提前声明,是因为这个宏会帮我们声明。

UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
return fixed4(ambient+(diff0+diff1)*atten,1.0);

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值