URP 渲染实战 / 第 6 章 多光源与阴影 / 案例 6.3
案例 6.3接收主光源阴影入门
目标:让 Lambert Shader 接收主光源的实时阴影,支持单张阴影图、级联、屏幕空间阴影和三档软阴影,并支持材质级的“不接收阴影”开关。
关键字
Assets/Cookbook/Ch06/ShadowReceive.shader(节选:keywords)
// 主光阴影:无 / 单张阴影图 / 级联 / 屏幕空间(由 URP Asset 与 Renderer 设置决定,运行时自动开关) #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN // 软阴影:_SHADOWS_SOFT 在运行时按每盏灯的质量分支;LOW/MEDIUM/HIGH 是给“必须静态分支”的平台准备的 #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH // 材质级开关:不接收阴影 #pragma shader_feature_local _RECEIVE_SHADOWS_OFF
这三行与 URP 14 Lit.shader 的写法一致:
_MAIN_LIGHT_SHADOWS:只有一个级联;_MAIN_LIGHT_SHADOWS_CASCADE:多级联;_MAIN_LIGHT_SHADOWS_SCREEN:Renderer 上加了 Screen Space Shadows 特性,阴影已经在一个屏幕空间 Pass 里算好,这里只需按屏幕 UV 采样(第 23 章)。三者都不开时,表示没有主光阴影。- 软阴影的四个关键字中,
_SHADOWS_SOFT在运行时读取每盏灯的质量设置做动态分支;_SOFT_LOW/MEDIUM/HIGH是给不擅长动态分支的平台准备的静态版本,URP 根据平台和设置自动选择。 _RECEIVE_SHADOWS_OFF是材质关键字:Shadows.hlsl开头检查它,定义了它就不会定义MAIN_LIGHT_CALCULATE_SHADOWS,阴影函数直接返回 1。
片元代码
Assets/Cookbook/Ch06/ShadowReceive.shader(节选:frag)
half4 frag(Varyings input) : SV_Target { half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * _BaseColor.rgb; half3 normalWS = normalize(input.normalWS); // 世界坐标 -> 阴影图坐标(xy = 阴影图 UV,z = 在光源空间里的深度)。 // 开了级联时,这里会先按距离选出级联号,再用对应的矩阵变换 float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS); // 采样阴影图并与 z 比较:1 = 受光,0 = 在阴影里(软阴影时是 0~1 的过渡) half shadow = MainLightRealtimeShadow(shadowCoord); // 超出“阴影距离”的地方,阴影要平滑淡出,而不是突然消失 shadow = lerp(shadow, 1.0, GetMainLightShadowFade(input.positionWS)); Light mainLight = GetMainLight(); half3 radiance = mainLight.color * mainLight.distanceAttenuation * shadow; half3 color = albedo * radiance * saturate(dot(normalWS, mainLight.direction)); color += albedo * SampleSH(normalWS) * _AmbientStrength; return half4(color, 1); }
void main() { vec3 albedo = texture(_BaseMap, uv).rgb * _BaseColor.rgb; vec3 N = normalize(normalWS); // 世界坐标 -> 阴影图坐标(xy = 阴影图 UV,z = 在光源空间里的深度)。 // 开了级联时,这里会先按距离选出级联号,再用对应的矩阵变换 vec4 shadowCoord = TransformWorldToShadowCoord(positionWS); // 采样阴影图并与 z 比较:1 = 受光,0 = 在阴影里(软阴影时是 0~1 的过渡)。 // URP 的 MainLightRealtimeShadow 在 _RECEIVE_SHADOWS_OFF 时直接返回 1(宏 MAIN_LIGHT_CALCULATE_SHADOWS 未定义) #if defined(_RECEIVE_SHADOWS_OFF) float shadow = 1.0; #else float shadow = MainLightRealtimeShadow(shadowCoord); #endif // 超出“阴影距离”的地方,阴影要平滑淡出,而不是突然消失 shadow = mix(shadow, 1.0, GetMainLightShadowFade(positionWS)); Light mainLight = GetMainLight(); vec3 radiance = mainLight.color * mainLight.distanceAttenuation * shadow; vec3 color = albedo * radiance * saturate(dot(N, mainLight.direction)); color += albedo * SampleSH(N) * _AmbientStrength; fragColor = vec4(color, 1.0); }
- TransformWorldToShadowCoord:开了级联时先调用
ComputeCascadeIndex,再用对应的_MainLightWorldToShadow[i]变换;开了屏幕空间阴影时则返回屏幕坐标。 - MainLightRealtimeShadow:按关键字选择采样方式(屏幕空间纹理 / 硬阴影 / 某档 PCF),乘上灯光的 Shadow Strength,并对超出阴影图深度范围的坐标返回 1。
- GetMainLightShadowFade:返回 0~1 的淡出系数,用它把阴影插值回 1。URP 自己的
MainLightShadow()函数把实时阴影、烘焙阴影(Shadowmask)和淡出组合在一起,接入光照贴图时应该用它,这里为了看清每一步拆开来写。
深入:阴影坐标在哪个阶段算
Shadows.hlsl 在只有一个级联(或屏幕空间阴影)时会定义 REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR,提示你可以在顶点着色器里算好阴影坐标再插值——单个矩阵是线性变换,逐顶点算再插值与逐像素算结果相同,还省了一次矩阵乘法。多级联时必须逐像素计算,因为同一个三角形可能横跨两个级联。案例 6.9 的 ClassicLit 两种情况都处理了。
