前言
这是一辆没有任何阴影的狂野西部摩托车。
如果我们使用了阴影贴图,就可以增加一些明显好看的细节,这是非常重要的一步。
如果我们只启用屏幕空间阴影会怎样?我们会得到一些不错的小比例细节。
把shadowmap和屏幕空间阴影同时打开。
再看一下细节比较
2
行业比较
在进行阴影映射时丢失一些细节是一个典型的问题,尤其是对于旨在覆盖场景的大部分的那些光源(如定向光源)。正如我们所看到的,屏幕空间阴影可以帮上大忙 但在进一步详细探讨它们之前,让我们看看大多数游戏是如何处理小规模阴影质量的:
允许玩家自己控制阴影质量。这可能会比较费,但是在游戏中也是很常见的。
在关键时刻(如角色特写镜头)使用超高分辨率阴影光源。此方案避免了屏幕空间技术的典型缺陷,但需要逐场景手动调整灯光参数,工作量巨大。
采用屏幕空间阴影增强方案:部分案例中,阴影还会结合法线数据和其他渲染通道信息,进一步缓解屏幕空间技术的局限性。
Remedy Entertainment(代表作《控制》《心灵杀手2》)的屏幕空间阴影实现效果可作为优秀范例。其技术通过多通道数据融合,在保持实时性能的同时,显著提升了小尺度阴影的视觉精度。
3
算法
那具体如何实现?基本思路是:从像素点出发,朝着光源方向移动。我们分步移动,在每一步中,将光线的深度与摄像机感知的深度进行比较。如果光线的深度大于(即离摄像机更远)摄像机的深度值,我们就认为该像素处于阴影中。
正如我们所见,仅凭屏幕空间信息我们无法可靠判断像素是否处于阴影中。但我们不必担心这个问题,因为回想之前看到的对比图就会发现,我们只需要对阴影映射进行补充而非替代。因此真正的问题是:这种折中方案是否足以提供有意义的信息?答案是在短距离内效果尚可,但长距离精度会下降。所以明智的做法是将这种效果控制在小范围内。这也解释了为什么有些人将屏幕空间阴影称为接触阴影——因为只有当像素与其遮挡物非常接近(近乎接触)时,阴影才能稳定的显现。
以下是带必要注释的完整HLSL示例:
// Settingsstatic const uint g_sss_max_steps = 16; // Max ray steps, affects quality and performance.static const float g_sss_ray_max_distance = 0.05f; // Max shadow length, longer shadows are less accurate.static const float g_sss_thickness = 0.02f; // Depth testing thickness.static const float g_sss_step_length = g_sss_ray_max_distance / (float)g_sss_max_steps;float ScreenSpaceShadows(Surface surface, Light light){ // Compute ray position and direction (in view-space) float3 ray_pos = mul(float4(surface.position, 1.0f), g_view).xyz; float3 ray_dir = mul(float4(-light.direction, 0.0f), g_view).xyz; // Compute ray step float3 ray_step = ray_dir * g_sss_step_length; // Ray march towards the light float occlusion = 0.0; float2 ray_uv = 0.0f; for (uint i = 0; i 0.0f) && (depth_delta
4
一个取巧的改良方案
和许多效果一样,我们可以直接设置高步数来获得更好看的效果,承受性能损耗然后收工。毕竟,谁有时间通过偏移光线起始位置来将带状伪影转化为噪点,再进行模糊处理,最后才能使用呢?完全理解这种心态。不过需要提到的是,如果项目中已实现TAA(时间抗锯齿),我们可以这样做:
选择噪声函数并为其添加时序因子。我的版本改编自Jorge Jimenez的交错梯度噪声函数(interleaved gradient noise function),因其与TAA配合效果极佳。
float interleaved_gradient_noise(float2 position_screen){ position_screen += g_frame * any(g_taa_jitter_offset); // temporal factor float3 magic = float3(0.06711056f, 0.00583715f, 52.9829189f); return frac(magic.z * frac(dot(position_screen, magic.xy)));}
接着,在原着色器中开始光线步进前,我们对起始位置施加偏移。这实际上实现了随时间累积提高有效步数的效果。
// Offset starting position with temporal interleaved gradient noisefloat offset = interleaved_gradient_noise(g_resolution * surface.uv) * 2.0f - 1.0f;ray_pos += ray_step * offset;
虽然只用8个采样速度很快,但这不会产生条带伪影吗?
5
结语
在我们结束之前,让我们欣赏这台赋予了材质的摩托车
6
更优的屏幕空间阴影方案