简单来说,答案是 ShadowCaster Pass 先执行 ,用来生成场景的"深度地图";然后 ForwardBase 等主 Pass 才会去使用这张地图,来计算和绘制阴影效果。
为了帮你更清楚地理解,我们可以把整个过程拆解为两个阶段:
📋 阴影渲染核心流程
| 阶段 | 执行Pass | 关键任务 | 产出的数据 |
|---|---|---|---|
| 第一阶段: 生成阴影映射纹理 | ShadowCaster |
从光源视角渲染场景,只将深度信息写入一张特殊的"深度图"中 | 阴影映射纹理 (ShadowMap) |
| 第二阶段: 采样阴影映射纹理 | 主 Pass (如 ForwardBase/ ForwardLit) |
在主相机视角渲染物体时,通过采样第一步生成的 ShadowMap,判断当前片元是否处于阴影中 | 带阴影效果的最终画面 |
👣 步骤解析
第一阶段:投射阴影------生成"影子模板"
这个阶段的核心工作,就是为场景创建一个精确的高度地图。ShadowCaster Pass 在其中扮演关键角色:
-
阴影映射纹理 (ShadowMap) :此阶段的核心是一张从光源 正交/透视视角渲染的深度纹理。也叫阴影贴图,它不记录颜色,而是记录场景中每个表面到光源的最近距离或深度信息,记录了"哪里照得到光"。
-
ShadowCasterPass :每个想要投射 阴影的物体,其 Shader 都必须包含一个LightMode标签为ShadowCaster的特殊 Pass。若 Shader 本身未定义,Unity 会自动尝试使用 Fallback 中的替代实现。 -
渲染调用 :引擎会自动调用场景中所有物体的
ShadowCasterPass 来渲染并生成阴影映射纹理。
第二阶段:接收阴影------应用"影子模板"
在这个阶段,主摄像机视角下的物体开始渲染,并为最终画面添加阴影。
-
比较深度值判断阴影 :场景在主摄像机视角下正常渲染时,Shader 会利用第一阶段生成的 ShadowMap 进行阴影计算。具体来说,对于正在渲染的物体表面上的某点,将其世界坐标转换到光源空间,得到该点"距光源的深度"并与 ShadowMap 中记录的"该方向最近深度"进行比对:若当前深度值大于 ShadowMap 中的值,说明该点被前面物体遮挡了光线,从而判定其在阴影中。
-
混合光照结果:采样后得到的阴影值(通常为0到1)会和场景原有的光照、颜色等进行计算,最终输出到屏幕上,形成我们看到的明暗和阴影效果。
🛠️ 代码实现概要
简单来说,阴影的投射与接收依赖在 Shader 中插入特定的代码片段来实现:
-
投射阴影 :需要包含一个
ShadowCasterPass。 -
接收阴影 :需要在主渲染 Pass(例如
ForwardBase)中,利用 Unity 内置的阴影"三剑客"来完成相应功能。-
SHADOW_COORDS(...):在顶点着色器的输出结构体中定义。 -
TRANSFER_SHADOW(...):在顶点着色器中计算阴影纹理坐标。 -
SHADOW_ATTENUATION(...):在片元着色器采样获取实际阴影值。
-
💡 一些常见疑问
-
前向渲染路径如何处理阴影?
对于场景中最重要的平行光 ,Unity 会为其生成完整的阴影映射纹理。而点光源或聚光灯的阴影在移动端性能消耗较大,可能需要将渲染路径设为延迟渲染才能开启。
-
屏幕空间的阴影映射与普通阴影有何不同?
这是 Unity 在支持 MRT 的平台上常用的一种优化技术。它的不同之处在于,会利用两张深度图(摄像机深度图和光源 ShadowMap)生成一张屏幕空间的阴影纹理。然后,Shader 通过采样这张纹理来获得阴影,效率更高。
-
物体是否需要同时支持投射和接收?
这两个功能在 MeshRenderer 组件里是分开勾选的,可以自由搭配组合。
💎 总结
通过上面的拆解,应该可以清晰地看到,"阴影投射"指生成数据,"阴影接收"指消费数据,这自然构成"先投射,后接收"的逻辑。
如果还想深入理解 ShadowCaster Pass 的具体写法,或者 SHADOW_ATTENUATION 等内置宏的更多细节,我们可以随时一起进一步探讨。