题图:阳光照耀下的长柱廊,走道上投射出清晰、稳定的阴影

阴影是让 3D 场景显得真实可信的关键细节之一,却也最容易出现明显的渲染瑕疵。本次更新让太阳光阴影更加稳定、纯净且易于调校,并将同样的改进带到了聚光灯、点光源和面光源中。

简而言之:

本文内容

阴影为何重要

从场景中移除阴影,一切都会显得漂浮不定。物体失去与地面的联系,深度更难辨认,光照也显得平淡。好的阴影能告诉你物体的位置、大小以及光源的方向。

糟糕的阴影同样显眼。摄像机移动时边缘爬行、表面上出现条纹,或者阴影与投射它的物体分离,都会立刻打破真实感。这正是本次更新要解决的问题。

实时阴影是如何工作的?

真实的阴影源于光线被遮挡,但实时渲染器并不追踪光线。它使用一种更经济且非常有效的技巧,称为阴影贴图:

  1. 从光源视角观察。 我们从光源的视角渲染场景,但只存储每个像素到最近表面的距离。结果就是阴影贴图。
  2. 从摄像机视角观察。 对于屏幕上的每个像素,我们计算出它在光源视角下的位置,并将其距离与阴影贴图记录的值进行比较。
  3. 做出判断。 如果记录中有更靠近光源的物体,则该像素处于阴影中。否则,它被照亮。

示意图:阴影贴图的三个步骤

问题在于,阴影贴图是一张固定分辨率的纹理。可以把它想象成从光源处拍摄的一张低分辨率场景照片。几乎每一种阴影技术,都是在想方设法充分利用这有限的分辨率。

光线追踪和路径追踪阴影呢?

阴影贴图是一种近似方法,因此人们自然会问:为什么不直接进行光线追踪呢?在支持硬件光线追踪的 GPU 上,我们可以做到这一点:从每个可见点向光源发射一条射线,如果中途有物体遮挡,该点就处于阴影中。由于不存在纹理分辨率不足的问题,因此不会出现块状伪影、阴影失真(shadow acne)或阴影脱离(peter-panning)现象,软阴影也能精准呈现。路径追踪则更进一步,它会追踪大量在场景中反弹的随机光路,使阴影、反射和间接光照都由同一过程计算得出。Evergine 的路径追踪器示例展示了这种效果。

示意图:阴影贴图与光线追踪阴影的对比

代价是性能。清晰的图像需要每帧每个像素发射多条光线,通常还需配合降噪器。场景需要一个加速结构,且该结构必须随物体移动而重建。此外,光线向各个方向散射,相比将三角形光栅化到纹理中,这对GPU内存的压力要大得多。即使是高端桌面GPU也会谨慎分配光线预算,而大多数手机、平板和低端设备根本没有硬件光线追踪功能。

这就是阴影贴图仍是实时阴影主力军的原因:它们成本低、性能可预测,且几乎能在所有设备上运行。在硬件允许的情况下,光线追踪是绝佳选择,但阴影贴图确保了场景在其他所有地方都能运行,这也是我们持续投入改进它的原因。

太阳难题与级联

方向光代表太阳,而太阳照亮万物。单个阴影贴图必须覆盖整个可见世界,因此一张拉伸覆盖数百米的纹理,会在玩家眼前产生块状阴影,而那里恰恰是最需要细节的地方。

经典的解决方案是级联阴影贴图。我们根据与摄像机的距离,将视野分割成多个切片:离你近的一个小切片,其后一个更大的切片,依此类推。每个切片拥有自己的阴影贴图,因此近处的阴影清晰,远处的则更粗糙。远处的物体在屏幕上很小,所以你通常不会注意到。

图示:相机视锥体被分割成四个级联,每个级联渲染到相同分辨率的阴影贴图中

Evergine此前已使用级联技术。本次新版本旨在让它们表现更佳。

旧方法的不足之处

有三个问题反复出现:

新特性

不再闪烁的级联

现在,阴影空间仅取决于光源的方向,而非摄像机的朝向。每个级联还会对齐到阴影贴图的纹素网格,其大小仅以约2%的小步长变化。因此,移动或旋转摄像机不再改变哪个纹素覆盖世界的哪个部分,闪烁现象消失了。

示意图:级联对齐纹素网格前后,阴影边缘在三帧中的表现

理解场景的偏移值

偏移(Bias)现在在世界空间中应用,并以**阴影贴图纹素(texel)**为单位进行度量。新增的两个属性 ShadowDepthBias 和 ShadowNormalBias 会根据每个级联的纹素大小以及阴影滤波器的柔和度自动缩放。现在,仅需一项设置即可从最近的级联到最远的级联都保持合理表现,从而消除掠射角处的阴影失真条纹(acne bands)以及远处的阴影悬浮现象。

图示:Bias 过小、Bias 平衡和 Bias 过大

聚光灯、点光源和面光源

基于像素的 Bias 模型不仅限于太阳光。聚光灯、点光源和面光源现在也使用它,因此 ShadowDepthBias 和 ShadowNormalBias 在所有光源类型上含义相同。在此过程中,我们修复了两个问题:

图示:使立方体面在点光源周围重叠的保护带

可调节的级联

方向光现在公开了质量与性能之间的权衡选项:

图示:稳定和紧密级联适配模式

**注意:**当一个光源被多个相机渲染时,更新间隔无效。

更好的调试体验

光源调试模式现在会显示每个级联的颜色(对于点光源,则显示每个立方体面的颜色),以及一个 8x8 阴影贴图像素的棋盘格,让您可以精确地看到屏幕上阴影的密度。聚光灯也拥有了自己的 DebugMode。

我们还修复了相关问题:级联分割距离现在使用正确的公式,近裁剪面为零的正交相机不再产生无效分割,并且使用多个投射阴影的方向光渲染多个相机时,不再导致光源相互覆盖彼此的阴影贴图。后端一致地处理深度钳制,因此级联前方的物体不再被裁剪。

查看效果对比

拖动滑块,在相同的场景和相机位置下对比旧版与新版阴影系统的效果。

改进前:使用旧版阴影系统时,从走道看到的柱廊 改进后:使用新版阴影系统时,从走道看到的柱廊
对比 1
改进前:使用旧版阴影系统的测试场景(视角 2) 改进后:使用新版阴影系统的测试场景(视角 2)
对比 2
改进前:使用旧版阴影系统时的围栏与柱基 改进后:使用新版阴影系统时的围栏与柱基
对比 3
改进前:使用旧版阴影系统的测试场景(视角 4) 改进后:使用新版阴影系统的测试场景(视角 4)
对比 4
改进前:使用旧版阴影系统的测试场景(视角 5) 改进后:使用新版阴影系统的测试场景(视角 5)
对比 5
改进前:使用旧版阴影系统时,远处的柱廊与方尖碑 改进后:使用新版阴影系统时,远处的柱廊与方尖碑
对比 6

入门指南

您可以在编辑器或代码中调整所有参数。以下数值是说明性的起点,请根据您的场景进行调整:

// Get the directional light of your sun entity
var sun = sunEntity.FindComponent<DirectionalLight>();

sun.IsShadowEnabled = true;
sun.ShadowDistance = 100;                   // How far from the camera shadows are rendered
sun.ShadowCascadeCount = 3;                 // Between 1 and 4

// Keep cascades stable, and fade shadows out smoothly
sun.CascadeFitMode = DirectionalLight.CascadeFitModes.Stable;
sun.ShadowFadeFraction = 0.1f;
sun.CascadeBlendFraction = 0.1f;

// Bias is measured in shadow map texels
sun.ShadowDepthBias = 1;
sun.ShadowNormalBias = 1;

这将启用一个带有三个稳定级联的投射阴影太阳光,级联之间平滑过渡,并在阴影最大距离处柔和淡出。如果看到阴影失真(shadow acne),请稍微调高偏移(bias)值;如果阴影看起来与投射物分离,请调低它们。

从先前版本升级

一些更改会影响现有项目:

立即试用并分享你的想法

准备好让您的场景拥有更稳定、更干净的阴影了吗?更新 Evergine,打开一个带有方向光的场景,并尝试新选项。

我们很想听听您的想法。

José Antonio Collado Araque
作者
José Antonio Collado Araque
图形工程师 - 研究员