PA6 Shadows
Shader 与渲染算法 - 环境互动 - 阴影
Last updated
Shader 与渲染算法 - 环境互动 - 阴影
Last updated
Unity 采用一种最为通用的技术支持阴影,即将 shadow 制作并存储为阴影贴图,称作 shadow mapping。
具体来说,shadow map 结合了两张采样信息:
从光源视角生成阴影贴图(Shadow Map)
light - point:在光源视角渲染场景,记录光源到场景中最接近表面的深度信息,从而生成深度图作为阴影贴图;
Cascaded 技术:如果使用 directional light,可以在此阶段应用 Cascaded Shadow Mapping(级联阴影映射),基于摄像机视锥体的多个距离层级生成独立的深度图,以优化阴影质量;
从摄像机视角渲染并进行阴影判定
camera - point:在摄像机视角渲染时,将场景中每个像素的深度信息与阴影贴图中的深度信息进行比较:当前像素深度 > 阴影贴图中的深度,说明该像素被其他物体遮挡,处于阴影中;否则,该像素直接受到光照;
光照强度调整:根据深度比较的结果,调整像素的光照强度(实现为 attenuation
值的调整)。常见做法是将光源颜色乘以阴影因子:1 表示完全受光照,0 表示完全在阴影中,介于 0 和 1 之间的值用于软阴影的过渡区域。
由于阴影贴图的有限分辨率和纹理采样的离散性,阴影边缘可能会出现锯齿状的伪影,特别是在阴影贴图分辨率较低或物体靠近摄像机时。
Shadow Acne 指这样一种情况:由于深度比较的有限精度(片段的深度可能比存储的深度稍长),可能会在受光照的表面上出现不正确的自阴影,形成类似“粉刺”的斑点。也被称作 “self shadowing area”。
就会变成:
MSAA(多重采样抗锯齿)无法解决阴影映射中的锯齿问题。原因是阴影映射的锯齿源于纹理采样,而非几何边缘。
在这一步中进行光照的投影。具体来说,通过增加一个 ShadowCaster Pass,从光源的视角生成 shadow map。在这个 Pass 中,Shader 将场景中的顶点和片元转换到光源的坐标空间,记录从光源出发到场景中各个物体表面的距离。
增加一个新的 pass,将 light mode 设置为 ShadowCaster;
vertex shader:将物体顶点转换到裁剪屏幕空间,同时完成 depth bias 的矫正:
使用 UnityApplyLinearShadowBias
函数以支持 depth bias;
使用 UnityClipSpaceShadowCasterPos
函数以支持 normal bias;
fragment shader:直接返回 0。GPU 会乖巧的记录深度值。
前一章节通过 Casting Shadows 生成了 shadow map,现在需要借助采样坐标,对 shadow map 进行采样,最终渲染输出阴影。
pass:base pass & additional pass。
首先考虑主光源所产生的阴影。在 Main 文件的 base pass 中增加包含 SHADOWS_SCREEN
关键字的 shader 变体;
将 additional pass 中的 multiple compile 语句修改为 #pragma multi_compile_fwdadd_fullshadows
,从而实现对多种光源的支持;
vertex shader:基于物体坐标对阴影坐标赋值,并进行裁剪空间到屏幕空间的调整;
CreateLight:Unity 将阴影理解为光的衰减作用。因此根据阴影贴图的采样结果(使用 tex2D
函数),调整 attenuation 的值。
使用 AutoLight 中预定义的三个有用的 macros:
struct Interpolators:使用 SHADOW_COORD。它定义 shadow 的插值器;
vertex shader:使用 TRANSFER_SHADOW。它给出 vertex shader 阶段的阴影坐标;
CreateLight:在 UNITY_LIGHT_ATTENUATION 中使用 SHADOW_ATTENUATION。即使用插值作为它的第二个参数。
为帮助这些 macros 识别取值,修改数据名
vertex:vertex position(instead of position)
pos:interpolator position(instead of position)
Spotlight shadow 的 depth pass 具有不同的作用。
directional light 和 spotlight 都使用 depth pass。但与之不同的是,因为 spotlight 的光束有其实际位置,而且本身具备透视,因此 directional light 使用 depth pass 支持屏幕空间的 cascades(级联投影 ),而 spotlight 则是在其光束范围内生成深度贴图,进行更简单的遮挡判断。
Spotlight shadow 的 filtering 的实现阶段在采样贴图中(而不是生成贴图时)。
directional shadow:生成贴图阶段包含 filtering。制作 directional shadow 仅需对 screen-space shadow map 采样即可。Unity 在创建阴影贴图时就会实现 shadow filtering(用于柔化阴影边缘,实现软阴影)。
spotlight shadow:生成贴图阶段不包含 filtering。由于 spotlight 并不使用 screen-space shadow,因此制作柔和阴影就需要在 fragment shader 中实现 shadow filtering。
在场景中添加 point light。当通过 frame debugger 检查 shadow map 时,会发现 Unity 为每一个光源生成了四张 map。因为 point light 是在各个方向照射的,因此它的 shadow map 必须是 cube map 的形式。
cub map 通过摄影机的六个方向渲染场景而创建,每个方向创建 cube 的一个面。So shadows for point lights are expensive!
而且由于 Unity 不支持对 shadow cube map 的 filtering,所以 point light shadow 的边缘会更硬。
又贵,还糙。
因为没有足够的平台支持,所以 Unity 并不使用 cube map,因此在 Shadow 文件中无法输出深度值来支持 point light,而是输出距离(fragment's distance)。
因为使用了几乎完全不同的方法,所以在 Shadow 文件中为 point light 创建一个单独的函数。
pass:在 ShadowCaster 中加入 SHADOW_CUBE
关键字(用于 directional & spotlight shadows)。支持该功能可直接添加多编译指令 multi_compile_shadowcaster
;
shadow vertex program:根据片段与光源位置计算 i.lightVec
,输出距离(而非深度)值;
shadow fragment program:基于 i.lightVec
计算深度值 depth,将其作为 UnityEncodeCubeShadowDepth
的参数输出。
Anki
Reference
似乎 GAMES202 安排了阴影相关教程