https://catlikecoding.com/unity/tutorials/custom-srp/shadow-masks/
1 烘培阴影
使用光照贴图的优势在于我们不受最大阴影距离的限制,烘培的阴影不会被剔除,但也不会变化。理论上,我们可以在最大阴影距离内使用实时阴影,范围外使用烘培阴影。
1.1 距离相关阴影遮罩
将Mixed Lighting
下的Lighting Mode
改为ShadowMask
。
Project Settings
下的Quality
的Shadows
可以进行阴影遮罩的配置,有Distance Shadowmask
和Shadowmask
。这里设置为distance的。
阴影遮罩贴图包含单个混合平行光的阴影衰减信息,即那些贡献全局光照的静态物体产生的阴影,数据存储在R通道。
1.2 检测阴影遮罩
为了使用阴影遮罩,我们必须检测其是否存在。我们使用关键字来控制其是否使用阴影遮罩:
static string[] shadowMaskKeywords =
{
"_SHADOW_MASK_DISTANCE"
};
添加一个布尔变量:
bool useShadowMask;
public void Setup (…)
{
…
useShadowMask = false;
}
在Render
的末尾开启或关闭关键字:
public void Render ()
{
…
buffer.BeginSample(bufferName);
SetKeywords(shadowMaskKeywords, useShadowMask ? 0 : -1);
buffer.EndSample(bufferName);
ExecuteBuffer();
}
为了知道是否需要阴影遮罩,我们需要判断是否有灯光使用它。在ReserveDirectionalShadows
中进行。
每个灯光包含烘培数据,存储在LightBakingOutput
结构体中,可通过Light.bakingOutput
属性获得。如果一个灯光的模式设置为混合,并且混合光照模式设置为阴影遮罩,那么就是要使用阴影遮罩:
public Vector3 ReserveDirectionalShadows (
Light light, int visibleLightIndex
)
{
if (…)
{
LightBakingOutput lightBaking = light.bakingOutput;
if (
lightBaking.lightmapBakeType == LightmapBakeType.Mixed &&
lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask
)
{
useShadowMask = true;
}
…
}
return Vector3.zero;
}
在shader中添加multi_compile
指令:
#pragma multi_compile _ _SHADOW_MASK_DISTANCE
1.3 阴影遮罩数据
shader中,我们需要知道阴影遮罩是否在使用中,如果在使用的话,那么烘培的阴影数据是什么。添加一个ShadowMask
结构体,包含距离和阴影属性:
struct ShadowMask
{
bool distance;
float4 shadows;
};
struct ShadowData
{
int cascadeIndex;
float cascadeBlend;
float strength;
ShadowMask shadowMask;
};
在GetShadowData
中初始化:
ShadowData GetShadowData (Surface surfaceWS)
{
ShadowData data;
data.shadowMask.distance = false;
data.shadowMask.shadows = 1.0;
…
}
阴影遮罩本质上还是烘培光照数据的一部分,因此我们需要在GI中进行:
struct GI
{
float3 diffuse;
ShadowMask shadowMask;
};
…
GI GetGI (float2 lightMapUV, Surface surfaceWS)
{
GI gi;
gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);
gi.shadowMask.distance = false;
gi.shadowMask.shadows = 1.0;
return gi;
}
我们可以通过unity_ShadowMask
来获取阴影遮罩纹理:
TEXTURE2D(unity_ShadowMask);
SAMPLER(samplerunity_ShadowMask);
然后添加一个采样方法SampleBakedShadow
:
float4 SampleBakedShadows (float2 lightMapUV)
{
#if defined(LIGHTMAP_ON)
return SAMPLE_TEXTURE2D(
unity_ShadowMask, samplerunity_ShadowMask, lightMapUV
);
#else
return 1.0;
#endif
}
修改GetGI
,在距离相关阴影遮罩模式后,使用上述方法采样阴影遮罩纹理:
GI GetGI (float2 lightMapUV, Surface surfaceWS)
{
GI gi;
gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);
gi.shadowMask.distance = false;
gi.shadowMask.shadows = 1.0;
#if defined(_SHADOW_MASK_DISTANCE)
gi.shadowMask.distance = true;
gi.shadowMask.shadows = SampleBakedShadows(lightMapUV);
#endif
return gi;
}
在GetLighting
中应用阴影遮罩:
float3 GetLighting (Surface surfaceWS, BRDF brdf, GI gi)
{
ShadowData shadowData = GetShadowData(surfaceWS);
shadowData.shadowMask = gi.shadowMask;
…
}
我们还需要让Unity将相关数据送往GPU,配置逐物体数据的属性:
perObjectData =
PerObjectData.Lightmaps | PerObjectData.ShadowMask |
PerObjectData.LightProbe |
PerObjectData.LightProbeProxyVolume
1.4 遮蔽探针
动态物体使用光照探针,而不是光照贴图,也就没有阴影遮罩数据,不过Unity也将阴影遮罩数据烘培到了光照探针中,即遮蔽探针。通过将unity_ProbesOcclusion
添加到UnityPerDraw
缓冲,我们可以访问该数据。
在SampleBakedShadows
中,我们可以简单的返回该数据:
float4 SampleBakedShadows (float2 lightMapUV)
{
#if defined(LIGHTMAP_ON)
…
#else
return unity_ProbesOcclusion;
#endif
}
同样,设置对应的逐物体数据属性:
perObjectData =
PerObjectData.Lightmaps | PerObjectData.ShadowMask |
PerObjectData.LightProbe | PerObjectData.OcclusionProbe |
PerObjectData.LightProbeProxyVolume
不过这么做会破坏GPU实例化,只有在定义SHADOWS_SHADOWMASK
时Unity才会自动的实例化遮蔽数据,因此在Common.hlsl
中添加:
#if defined(_SHADOW_MASK_DISTANCE)
#define SHADOWS_SHADOWMASK
#endif
1.5 LPPV
阴影遮罩也可以使用光照探针代理体,首先开启标志:
perObjectData =
PerObjectData.Lightmaps | PerObjectData.ShadowMask |
PerObjectData.LightProbe | PerObjectData.OcclusionProbe |
PerObjectData.LightProbeProxyVolume |
PerObjectData.OcclusionProbeProxyVolume
获得LPPV光照数据也很简单,使用SampleProbeOcclusion
方法:
float4 SampleBakedShadows (float2 lightMapUV, Surface surfaceWS)
{
#if defined(LIGHTMAP_ON)
…
#else
if (unity_ProbeVolumeParams.x)
{
return SampleProbeOcclusion(
TEXTURE3D_ARGS(unity_ProbeVolumeSH, samplerunity_ProbeVolumeSH),
surfaceWS.position, unity_ProbeVolumeWorldToObject,
unity_ProbeVolumeParams.y, unity_ProbeVolumeParams.z,
unity_ProbeVolumeMin.xyz, unity_ProbeVolumeSizeInv.xyz
);
}
else
{
return unity_ProbesOcclusion;
}
#endif
}
2 混合阴影
在最大阴影距离外的范围我们使用阴影遮罩。
2.1 在需要时使用烘培阴影
我们将实时的获取级联阴影的代码分离出来构建GetCascadedShadow
方法,在GetDirectionalShadowAttenuation
中调用:
float GetCascadedShadow (
DirectionalShadowData directional, ShadowData global, Surface surfaceWS
)
{
float3 normalBias = surfaceWS.normal *
(directional.normalBias * _CascadeData[global.cascadeIndex].y);
float3 positionSTS = mul(
_DirectionalShadowMatrices[directional.tileIndex],
float4(surfaceWS.position + normalBias, 1.0)
).xyz;
float shadow = FilterDirectionalShadow(positionSTS);
if (global.cascadeBlend < 1.0)
{
normalBias = surfaceWS.normal *
(directional.normalBias * _CascadeData[global.cascadeIndex + 1].y);
positionSTS = mul(
_DirectionalShadowMatrices[directional.tileIndex + 1],
float4(surfaceWS.position + normalBias, 1.0)
).xyz;
shadow = lerp(
FilterDirectionalShadow(positionSTS), shadow, global.cascadeBlend
);
}
return shadow;
}
float GetDirectionalShadowAttenuation (
DirectionalShadowData directional, ShadowData global, Surface surfaceWS
)
{
#if !defined(_RECEIVE_SHADOWS)
return 1.0;
#endif
float shadow;
if (directional.strength <= 0.0)
{
shadow = 1.0;
}
else
{
shadow = GetCascadedShadow(directional, global, surfaceWS);
shadow = lerp(1.0, shadow, directional.strength);
}
return shadow;
}
然后添加GetBakedShadow
方法:
float GetBakedShadow (ShadowMask mask)
{
float shadow = 1.0;
if (mask.distance)
{
shadow = mask.shadows.r;
}
return shadow;
}
接着,创建MixBakedAndRealTimeShadows
方法,混合实时阴影和烘培阴影:
float MixBakedAndRealtimeShadows (
ShadowData global, float shadow, float strength
)
{
float baked = GetBakedShadow(global.shadowMask);
if (global.shadowMask.distance)
{
shadow = baked;
}
return lerp(1.0, shadow, strength);
}
2.2 过渡到烘培阴影
为了基于深度从实时阴影过渡到烘培阴影,我们需要基于全局阴影强度在两者间插值,之后还需要应用灯光的阴影强度,也就不能直接在GetDirectionalShadowData
中混合强度了。
在开启距离控制的阴影遮罩模式后,我们基于全局强度在烘培阴影和实时阴影间插值,然后应用灯光阴影强度:
float MixBakedAndRealtimeShadows (
ShadowData global, float shadow, float strength
)
{
float baked = GetBakedShadow(global.shadowMask);
if (global.shadowMask.distance)
{
shadow = lerp(baked, shadow, global.strength);
return lerp(1.0, shadow, strength);
}
return lerp(1.0, shadow, strength * global.strength);
}
2.3 只有烘培阴影
如果只想使用烘培阴影而不使用实时阴影的话,我们设置一个GetBakedShadow
变体:
float GetBakedShadow (ShadowMask mask, float strength)
{
if (mask.distance)
{
return lerp(1.0, GetBakedShadow(mask), strength);
}
return 1.0;
}
然后,在GetDirectionalShadowAttenuation
中,检查灯光的阴影强度和全局强度的乘积是否小于或等于0,是的话,就表明只有烘培阴影:
if (directional.strength * global.strength <= 0.0)
{
shadow = GetBakedShadow(global.shadowMask, directional.strength);
}
除此之外,我们还需要修改ReserveDirectionalShadows
,让其不会在没有实时阴影投射物时就跳过该灯光,之后检测是否有实时阴影投射物:
if (
shadowedDirLightCount < maxShadowedDirLightCount &&
light.shadows != LightShadows.None && light.shadowStrength > 0f //&&
//cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b)
)
{
LightBakingOutput lightBaking = light.bakingOutput;
if (
lightBaking.lightmapBakeType == LightmapBakeType.Mixed &&
lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask
)
{
useShadowMask = true;
}
if (!cullingResults.GetShadowCasterBounds(
visibleLightIndex, out Bounds b
))
{
return new Vector3(-light.shadowStrength, 0f, 0f);
}
…
}
设为负就不会在阴影强度大于0时采样阴影贴图。然后在GetDirectionalShadowAttenuation
中调用GetBakedShadow
:
shadow = GetBakedShadow(global.shadowMask, abs(directional.strength));
2.4 总是使用阴影遮罩
还有一种阴影模式是Shadowmask
,该模式下Unity会为灯光省略哪些静态阴影投射物。阴影遮罩到处可以使用,我们可以让所有的静态物体使用,速度快,不过阴影质量可能不佳。
添加_SHADOW_MASK_ALWAYS
关键字,检查QualitySettings.shadowmaskMode
属性来看看使用哪个关键字:
static string[] shadowMaskKeywords =
{
"_SHADOW_MASK_ALWAYS",
"_SHADOW_MASK_DISTANCE"
};
…
public void Render ()
{
…
buffer.BeginSample(bufferName);
SetKeywords(shadowMaskKeywords, useShadowMask ?
QualitySettings.shadowmaskMode == ShadowmaskMode.Shadowmask ? 0 : 1 :
-1
);
buffer.EndSample(bufferName);
ExecuteBuffer();
}
添加对应的multi_compile
指令:
#pragma multi_compile _ _SHADOW_MASK_ALWAYS _SHADOW_MASK_DISTANCE
在Common.hlsl
中:
#if defined(_SHADOW_MASK_ALWAYS) || defined(_SHADOW_MASK_DISTANCE)
#define SHADOWS_SHADOWMASK
#endif
为ShadowMask
结构体添加新的属性:
struct ShadowMask
{
bool always;
bool distance;
float4 shadows;
};
…
ShadowData GetShadowData (Surface surfaceWS)
{
ShadowData data;
data.shadowMask.always = false;
…
}
在GetGI
中,若开启对应的关键字,获取对应的阴影数据:
GI GetGI (float2 lightMapUV, Surface surfaceWS)
{
GI gi;
gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);
gi.shadowMask.always = false;
gi.shadowMask.distance = false;
gi.shadowMask.shadows = 1.0;
#if defined(_SHADOW_MASK_ALWAYS)
gi.shadowMask.always = true;
gi.shadowMask.shadows = SampleBakedShadows(lightMapUV, surfaceWS);
#elif defined(_SHADOW_MASK_DISTANCE)
gi.shadowMask.distance = true;
gi.shadowMask.shadows = SampleBakedShadows(lightMapUV, surfaceWS);
#endif
return gi;
}
两个GetBakedShadow
都添加模式检查:
float GetBakedShadow (ShadowMask mask)
{
float shadow = 1.0;
if (mask.always || mask.distance)
{
shadow = mask.shadows.r;
}
return shadow;
}
float GetBakedShadow (ShadowMask mask, float strength)
{
if (mask.always || mask.distance)
{
return lerp(1.0, GetBakedShadow(mask), strength);
}
return 1.0;
}
最后,在MixedBakedAndReatimeShadows
中,对于总是开启阴影遮招的选项,使用不同的混合方式。首先,实时阴影通过全局强度调节,基于深度渐变,然后在烘培阴影和实时阴影间选择最小值:
float MixBakedAndRealtimeShadows (
ShadowData global, float shadow, float strength
)
{
float baked = GetBakedShadow(global.shadowMask);
if (global.shadowMask.always)
{
shadow = lerp(1.0, shadow, global.strength);
shadow = min(baked, shadow);
return lerp(1.0, shadow, strength);
}
if (global.shadowMask.distance)
{
shadow = lerp(baked, shadow, global.strength);
return lerp(1.0, shadow, strength);
}
return lerp(1.0, shadow, strength * global.strength);
}
3 多个灯光
因为阴影遮罩贴图有4个通道,而我们又至多支持4个混合平行光,那么这四个通道就可以利用起来。
3.1 阴影遮罩通道
为了应用所有的通道,我们需要将灯光的通道索引送往GPU。
在ReserveDirectionalShadows
中,通过LightBakingOutput.occlusionMaskChannel
来获得通道索引。并且该方法返回Vector4,第四个组件存储通道索引:
public Vector4 ReserveDirectionalShadows (
Light light, int visibleLightIndex
)
{
if (
shadowedDirLightCount < maxShadowedDirLightCount &&
light.shadows != LightShadows.None && light.shadowStrength > 0f
)
{
float maskChannel = -1;
LightBakingOutput lightBaking = light.bakingOutput;
if (
lightBaking.lightmapBakeType == LightmapBakeType.Mixed &&
lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask
)
{
useShadowMask = true;
maskChannel = lightBaking.occlusionMaskChannel;
}
if (!cullingResults.GetShadowCasterBounds(
visibleLightIndex, out Bounds b
))
{
return new Vector4(-light.shadowStrength, 0f, 0f, maskChannel);
}
shadowedDirectionalLights[shadowedDirLightCount] =
new ShadowedDirectionalLight {
visibleLightIndex = visibleLightIndex,
slopeScaleBias = light.shadowBias,
nearPlaneOffset = light.shadowNearPlane
};
return new Vector4(
light.shadowStrength,
settings.directional.cascadeCount * shadowedDirLightCount++,
light.shadowNormalBias, maskChannel
);
}
return new Vector4(0f, 0f, 0f, -1f);
}
3.2 选择合适的通道
在DirectionalShadowData
中添加通道索引属性:
struct DirectionalShadowData
{
float strength;
int tileIndex;
float normalBias;
int shadowMaskChannel;
};
在GI的GetDirectionalShadowData
中获取通道索引:
DirectionalShadowData GetDirectionalShadowData (
int lightIndex, ShadowData shadowData
)
{
…
data.shadowMaskChannel = _DirectionalLightShadowData[lightIndex].w;
return data;
}
对两个版本的GetBakedShadow
添加通道:
float GetBakedShadow (ShadowMask mask, int channel)
{
float shadow = 1.0;
if (mask.always || mask.distance)
{
if (channel >= 0)
{
shadow = mask.shadows[channel];
}
}
return shadow;
}
float GetBakedShadow (ShadowMask mask, int channel, float strength)
{
if (mask.always || mask.distance)
{
return lerp(1.0, GetBakedShadow(mask, channel), strength);
}
return 1.0;
}
在MixBakedAndRealtimeShadows
中添加:
float MixBakedAndRealtimeShadows (
ShadowData global, float shadow, int shadowMaskChannel, float strength
)
{
float baked = GetBakedShadow(global.shadowMask, shadowMaskChannel);
…
}
最后,在GetDirectionalShadowAttenuation
中使用:
float GetDirectionalShadowAttenuation (
DirectionalShadowData directional, ShadowData global, Surface surfaceWS
)
{
#if !defined(_RECEIVE_SHADOWS)
return 1.0;
#endif
float shadow;
if (directional.strength * global.strength <= 0.0)
{
shadow = GetBakedShadow(
global.shadowMask, directional.shadowMaskChannel,
abs(directional.strength)
);
}
else
{
shadow = GetCascadedShadow(directional, global, surfaceWS);
shadow = MixBakedAndRealtimeShadows(
global, shadow, directional.shadowMaskChannel, directional.strength
);
}
return shadow;
}