阅读本文前最好先看一下Unity custom shader中调用内置Lightmap和Light Probes
我们知道,unity中的Lighting Mode分为三种,baked indirect,subtractive和shadowmask三种。
baked indirect模式混合了实时直接光和烘焙间接光,并提供了实时阴影。由于光照比较真实,阴影精确度比较可信,所以非常适合中端硬件。
subtractive模式提供直接和间接光的烘焙,直接实时阴影只受一盏方向光的影响。由于它并不能提供非常真实的光照结果,所以风格化的渲染或者低端硬件比较适合这种模式。
shadowmask模式混合了实时直接光和烘焙间接光,它能烘焙物体的阴影并能自动混合实时阴影。这种模式看起来最为真实但也最消耗资源,不过可以在Quality Settings进行调整。高中端设备适合这种模式。
有了初步的印象,我们来看看具体使用时的不同之处
从生成的文件来看,shadowmask模式比subtractive模式多了一张贴图,这张贴图保存了静态物体的阴影烘焙信息。那么subtractive模式没有这张贴图是不是意味着没有阴影信息呢?其实不是的,subtractive模式中静态物体的阴影也烘焙了,并且保存在了光照贴图中,也就是说阴影信息其实是shadowmask模式额外多用了一张贴图来保存,并不是说subtractive模式阴影信息没了。
那么在shadowmask模式下有了这张阴影贴图后我们该如何去使用呢?看下面的代码
float directAtten = SHADOW_ATTENUATION(i);
float viewZ = dot(_WorldSpaceCameraPos - i.worldPos, UNITY_MATRIX_V[2].xyz);
float shadowFadeDistance = UnityComputeShadowFadeDistance(i.worldPos, viewZ);
float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
float bakedAtten = UnitySampleBakedOcclusion(i.uvLM,i.worldPos);
float atten = UnityMixRealtimeAndBakedShadows(directAtten, bakedAtten, shadowFade);
col.rgb *= atten;
col.rgb += lm;
首先我们需要UnitySampleBakedOcclusion
这个方法来获取烘焙阴影,它需要光照贴图的uv和世界坐标来进行采样,采样完毕所得到的阴影需要再扔进UnityMixRealtimeAndBakedShadows
这个方法,同时扔进实时阴影和一个距离来进行混合,使得烘焙和实时的阴影不至于泾渭分明。
现在我们来详细看看这两个方法在干什么,UnitySampleBakedOcclusion
的源码如下
fixed UnitySampleBakedOcclusion (float2 lightmapUV, float3 worldPos)
{
#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
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.
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.
atten = unity_SHC.w;
#endif
#if UNITY_LIGHT_PROBE_PROXY_VOLUME && !defined(LIGHTMAP_ON) && !UNITY_STANDARD_SIMPLE
fixed4 rawOcclusionMask = atten.xxxx;
if (unity_ProbeVolumeParams.x == 1.0)
rawOcclusionMask = LPPV_SampleProbeOcclusion(worldPos);
return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
#endif
return atten;
#endif
}
首先可以知道,我们要用UnitySampleBakedOcclusion
不得不声明一下SHADOWS_SHADOWMASK
,否则进入else逻辑就不读取阴影贴图了(这时的阴影就要去读取光照探针中的数据了,传入的世界坐标worldPos
这时就有用了)。。。 然后主要的代码就涉及到如果有光照贴图的话,去采样unity_ShadowMask
贴图。采样出来的rawOcclusionMask
记录的是该像素,被灯光影响的情况,比如采样的是(1,1,0,1)那么表示这个像素,被0号和1、3号灯照射到了,但是2号灯,不能照射到,这是原始的遮挡信息。 unity_OcclusionMaskSelector
就是说,unity会自动的进行筛选最强的灯(像是(0,1,0,0),不可能出现(1,1,0,0)这种有最强的两盏灯),这个unity_OcclusionMaskSelector就记录对应的灯的编号而已。dot
就是过滤出来真正需要的信息,比如说现在rawOcclusionMask
是(0.5,0.1,0,0.8),unity_OcclusionMaskSelector
是(0,1,0,0),那么过滤出来就是0.1,rawOcclusionMask
的g通道的值被保留下来了。
UnityMixRealtimeAndBakedShadows
的源码如下
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);
}
相对于上一个方法,这个理解起来比较简单,根据不同情况返回实时阴影或者烘焙阴影,或者通过距离(点到摄像头的距离)来进行烘焙阴影与实时阴影的混合。
现在来看这个方法里的fade
参数,通过代码可以看出我们需要调用UnityComputeShadowFadeDistance
和UnityComputeShadowFade
来计算得到最终的fade
,那么就来看看这两个方法在干什么吧。
UnityComputeShadowFadeDistance
源码如下
float UnityComputeShadowFadeDistance(float3 wpos, float z)
{
float sphereDist = distance(wpos, unity_ShadowFadeCenterAndType.xyz);
return lerp(z, sphereDist, unity_ShadowFadeCenterAndType.w);
}
这里unity_ShadowFadeCenterAndType
包含了阴影中心位置(xyz分量保存)和阴影的类型(w分量保存),由世界坐标wpos
计算得到与阴影中心位置的距离sphereDist
,按照阴影类型在视线距离z
和sphereDist
之间做lerp操作来得到阴影fade的距离。不过我谷歌了一下,这个阴影类型w
分量不是0就是1,也就是说最终结果返回的不是z
就是sphereDist
。
UnityComputeShadowFade
源码如下
half UnityComputeShadowFade(float fadeDist)
{
return saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
}
上面计算得到的阴影fade的距离还需要进一步的被矫正,所以需要把计算得到的距离扔进UnityComputeShadowFade
方法,而_LightShadowData
的z
分量包含着scale信息,w
分量包含着offset信息,计算一下后将结果限制在0-1之间,就是最终阴影fade的距离了。
最后,我们把UnityMixRealtimeAndBakedShadows
计算得出的阴影乘上输出的颜色,再加上(shadowmask模式下烘焙出来的光照贴图比较暗,加比较能还原真实结果。而subtractive模式下是乘上去比较能还原结果,个人觉得这里是加还是乘以美术导向为准)光照贴图就好了。
这里要注意,shadowmask有两种模式,distance shadowmask和shadowmask。Distance shadowmask模式使用时第一眼看上去和baked indirect模式一样,阴影都是实时计算的。然而,还是会有一张阴影贴图生成,这张贴图中的阴影是当超出场景设置中的阴影距离时使用的,而在这个距离内都会使用实时阴影,所以这个模式会是最为昂贵的模式。我们之前代码里已经用上了UnityMixRealtimeAndBakedShadows
这个方法,所以可以提供实时和烘焙阴影的过渡。
PS. 只有直接光的情况下UNITY_LIGHT_ATTENUATION
就是UNITY_SHADOW_ATTENUATION
,而UNITY_SHADOW_ATTENUATION
就是SHADOW_ATTENUATION
。
# define UNITY_SHADOW_ATTENUATION(a, worldPos) SHADOW_ATTENUATION(a)
#ifdef DIRECTIONAL
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) fixed destName = UNITY_SHADOW_ATTENUATION(input, worldPos);
#endif
最后,我们来明确下各个模式下对动静态物体的影响。
distance shadowmask模式
- 动态物体被混合光照到会接受到:
- 实时直接光
- 烘焙间接光,通过光照探针
- 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
- 在阴影距离内静态物体投出的实时阴影,阴影源自shadowmap技术
- 超出阴影距离后使用了光照探针的静态物体所产生的烘焙阴影
- 静态物体被混合光照到会接受到:
- 实时直接光
- 烘焙间接光,通过光照贴图
- 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
- 在阴影距离内静态物体投出的实时阴影,阴影源自shadowmap技术
- 超出阴影距离后静态物体所产生的烘焙阴影,通过shadowmask贴图实现
shadowmask模式
- 动态物体被混合光照到会接受到:
- 实时直接光
- 烘焙间接光,通过光照探针
- 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
- 超出阴影距离后使用了光照探针的静态物体所产生的烘焙阴影
- 静态物体被混合光照到会接受到:
- 实时直接光
- 烘焙间接光,通过光照贴图
- 在阴影距离内动态物体投出的实时阴影,阴影源自shadowmap技术
- 超出阴影距离后使用了阴影贴图的静态物体所产生的烘焙阴影
subtractive模式
- 动态物体被混合光照到会接受到:
- 实时直接光
- 烘焙间接光,通过光照探针
- 在阴影距离内动态物体由主光源投出的实时阴影,阴影源自shadowmap技术
- 使用了光照探针的静态物体所产生的阴影
- 静态物体被混合光照到会接受到:
- 烘焙直接光,通过光照贴图
- 烘焙间接光,通过光照贴图
- 静态物体产生的烘焙阴影,通过光照贴图
- 在阴影距离内动态物体由主光源投出的实时阴影,阴影源自shadowmap技术
baked indirect模式
- 动态物体被混合光照到会接受到:
- 实时直接光
- 烘焙间接光,通过光照探针
- 在阴影距离内动态物体投出的阴影,阴影源自shadowmap技术
- 在阴影距离内静态物体投出的实时阴影,阴影源自shadowmap技术
- 静态物体被混合光照到会接受到:
- 实时直接光
- 烘焙间接光,通过光照贴图
- 静态物体产生的实时阴影,阴影源自shadowmap技术
- 在阴影距离内动态物体产生的实时阴影,阴影源自shadowmap技术
参考
unity中UnityGlobalIllumination.cginc的分析
Mixed Lighting Lightmap & Shader In Unity | Unity中混合光照Lightmap研究
Rendering 17 Mixed Lighting
Unity 阴影淡入淡出效果中Shader常量 unity_ShadowFadeCenterAndType和_LightShadowData的问题