原文链接
版权声明:本文为CSDN博主「zengjunjie59」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
正文
关于阴影深度的基础知识
传统的 ShadowMap
ShadowMap 说起来十分简单,把 摄像机 和 光源 的位置重叠,那么场景中该光源的阴影区域就是那些摄像机看不到的地方,主要应用在 前向渲染 路径中。
具体实现分以下几个步骤:
- 如果有 平行光 开启了阴影,Unity 就会为该光源计算它的 ShadowMap(只会计算一个平行光),这张 ShadowMap 其实就是 深度图,记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。
- Unity中实现了一个额外的 Pass 来专门更新光源的 ShadowMap,这个 Pass 就是 LightMode 标签被设置为
ShadowCaster的 Pass。 - 然后再 正常渲染 的 Pass 中把顶点位置变换到 光源空间 下,以得到它在光源空间中的三维位置信息。然后根据坐标信息对 ShadowMap 采样,得到该点在 ShadowMap 中的深度信息。比较后,判断该点是否应该在阴影中。
注意:利用
Receive Shadows的开关可以控制物体是否显示阴影,但是不影响渲染。而Cast Shadows的开关则控制该物体是否会加入到 Shadowmap 的渲染。
屏幕空间的阴影
延迟渲染 中的光照计算绝大部分都是在屏幕空间里进行的,同样也包括阴影。
这种屏幕空间的阴影实现主要有这么几个步骤:
- 首先得到从当前摄像机处观察到的深度纹理。在 延迟渲染 里这张深度图本来就有,如果是 前向渲染 的话就需要把场景整个渲染一遍,把深度渲染到深度图中。
- 然后再从光源出发得到 从该光源处观察到的深度纹理,也被称为这个光源的 ShadowMap。
- 然后在 屏幕空间 做一次 阴影收集 计算(
Shadows Collector),这次计算会得到一张屏幕空间阴影纹理,也就是说这张图里面需要有阴影的部分已经显示在图上了。
这个过程概括来说就是把每一个像素根据它在 摄像机 深度纹理中的深度值得到 世界空间 坐标,再把它的坐标从世界空间转换到 光源空间 中,和 光源 的 ShadowMap 里面的深度值对比,如果大于 ShadowMap 中的深度距离,那么就说明 光源无法照到,在阴影内。
- 最后,在正常渲染物体为它计算阴影的时候,只需要按照当前处理的 fragment 在 屏幕空间 中的位置对
步骤 3得到的屏幕空间阴影图采样就可以了。
二者的主要异同
两者都会渲染 光源空间 的深度图,但 前者 的采样发生在 光源空间,片元坐标转换到 光源空间 对 ShadowMap 采样,而 后者 会做一次阴影收集计算(Shadow Collecctor)得到 屏幕空间 阴影纹理,片元在 屏幕空间 对阴影纹理采样。
Unity3D 中开启不同阴影的情况
截止到 Unity 5.4,当项目工程的目标平台是 Mobile 的时候,就 不会 使用屏幕空间的 Shadows Map 技术,即使用原始的 Shadows Map 方法。在代码里,Unity 会定义内置宏UNITY_NO_SCREENSPACE_SHADOWS 来控制。而当项目工程的目标平台是支持屏幕空间阴影的话,例如 PC, Mac & Linux Standalone 平台时,会开启 屏幕空间的阴影映射技术。
我们可以通过帧调试器(Frame Debugger)来分辨当前是否使用了屏幕空间的阴影映射技术:
让物体能够接受阴影的三剑客
三个内置宏(SHADOW_COORDS, TRANSFER_SHADOW, SHADOW_ATTENUATION) 就是计算阴影的三个主要部分。可以在 AutoLight.cginc 中找到它们的声明。
作用:
让物体能够接收阴影,原理是采样 LightMode = ShadowCaster Pass 里渲染出来的阴影深度图,然后与光照融合,阴影越强烈,贴图像素数值越靠近 0 。
需要在 Pass 中包含新的内置文件
#include "AutoLight.cginc"
SHADOW_COORDS
SHADOW_COORDS 声明 了一个名为 _ShadowCoord 的阴影纹理坐标变量。
它的 作用 是 声明一个用于对阴影纹理采样的 uv 坐标。一般用于片元着色器的输入结构体中,而且这个宏的参数 需要 是下一个可用插值寄存器的索引值,本例中是 2。
例如:
// 片元着色器的输入结构体
struct Interpolators{
flaot4 uv : TEXCOORD0;
float3 normal : TEXCOORD1;
float4 pos : SV_POSITION; //裁剪坐标,变量名要写死为pos,配合TRANSFER_SHADOW
SHADOW_COORDS(2) // 相当于float4 _ShadowCoord : TEXCOORD2
}
源码:
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
TRANSFER_SHADOW
TRANSFER_SHADOW 会 得到 一个用于提取阴影贴图的 uv 坐标,即 它的作用 是得到一个用于采样阴影贴图的坐标
TRANSFER_SHADOW 会根据平台的不同而有所 差异:
- 如果当前平台可以使用屏幕空间的阴影映射技术(SCREENSPACE_SHADOWS),则会调用内置的
ComputeScreenPos函数计算屏幕空间的 uv 坐标,存储在_ShadowCoord,后续直接用屏幕 uv 坐标采样屏幕阴影贴图; - 如果不支持则会使用传统的阴影映射技术,
TRANSFER_SHADOW会把顶点坐标从 模型空间 转换到 光源空间 后存储到_ShadowCoord中,后续根据坐标信息对 ShadowMap 采样。
例如:
// 顶点着色器, 参数命名要写死 v,v 里面的顶点坐标要写死 vertex,配合 TRANSFER_SHADOW
Interpolators MyVertexProgram(appdata v){
Interpolators o;
o.pos = UnityObjectToClipPos(v.vertex); // 裁剪坐标,变量名要写死为 pos,配合 TRANSFER_SHADOW
TRANSFER_SHADOW(o);
return o;
}
源码:
如下红框部分
SHADOW_ATTENUATION
SHADOW_ATTENUATION 负责 使用 _ShadowCoord 对对应的纹理进行采样,得到阴影信息。
用法1:
在片元着色器中,把片元函数输入结构体传给 SHADOW_ATTENUATION,直接得到衰减
UnityLight light;
// 片元着色器中使用
float attenuation = SHADOW_ATTENUATION(i)
light.color = _LightColor0.rgb * attenuation;
用法2:
在片元着色器中,把片元函数输入结构体传给 UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos) 的第二个参数,然后间接调用 SHADOW_ATTENUATION
UnityLight light;
// 片元着色器中使用
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
light.color = _LightColor0.rgb * attenuation;
源码:
如下红框部分
最后
以上是以 直射光 作为例子讲解的,不同灯光类型的宏会略有不同,具体请看 Unity Buildin 的源码