在团结引擎 1.4.0 版本中,我们发布了重磅功能:虚拟阴影贴图(Virtual Shadow Maps,VSM),全面升级开发体验,为开发者提供更加逼真的光影效果。
虚拟阴影贴图介绍
虚拟阴影贴图(Virtual Shadow Maps,VSM)是一种 GPU 驱动的实时阴影渲染技术,它能够为高面数模型与大型场景提供超高分辨率的阴影。随着虚拟几何体(Virtual Geometry,VG)的推出,常规的阴影渲染技术,如级联阴影贴图(CSM),已经不能满足高面数模型对于阴影质量的要求,而虚拟阴影贴图则能够通过其超高的分辨率(16k x 16k)很好地对这一类模型和场景进行适配。
但是在复杂光源场景下,显存很难容纳下如此高分辨率的阴影贴图,因此,与虚拟纹理(Virtual Texture)类似,虚拟阴影贴图采用了分块管理和按需加载的方法来优化内存占用和渲染性能。即使在一种非理想复杂场景中,需要渲染的阴影也只是摄像机当前所能观察到的,虚拟阴影贴图便利用了这一点来优化内存分配。
虚拟阴影贴图会预分配一张固定大小的物理纹理(16384 x 4096)来存储阴影深度,并将其划分为 128 x 128 的物理页(Page)。针对投射阴影的光源,虚拟阴影贴图会为其提供若干虚拟的 16k x 16k 的超高分辨率纹理,并将这些虚拟纹理分为同样 128 x 128 的虚拟页。从深度缓冲还原出像素的世界空间位置,再将像素投射到光源的视口下,系统可以获取到当前帧所需要的页。虚拟阴影贴图只会渲染这些需要的虚拟页,并将阴影深度写入物理页。
局部光
局部光通过 16k x 16k 的虚拟阴影贴图以及它的 Mipmap 机制,保证阴影渲染的质量与效率。目前,我们支持了聚光源与点光源两种局部光。
○聚光源:使用单张 16k x 16k 的阴影贴图进行阴影渲染。
○点光源:通过六张 16k x 16k 的阴影贴图组成立方体贴图(Cube Map)。
平行光
相较于局部光,平行光通常要覆盖场景中更大的范围,因而对于阴影贴图的分辨率要求更高。为了满足这一要求,平行光会使用到多张 16k x 16k 的裁剪图(Clipmap)来进行阴影的渲染。每张裁剪图对应一个阴影渲染的层级,裁剪图的覆盖范围以摄像机为圆心,2^(层级 + 1)厘米为半径。因此层级每提升一级,裁剪图的覆盖半径提升一倍,而分辨率不变。默认的裁剪图层级(Clipmap Level)为 6 - 22 级,最高可覆盖半径为 84 千米的面积,相较于传统的阴影渲染技术取得了极大幅度的提升。
用户可通过 Project Settings > Quality > HDRP 中 Virtual Shadow Map 模块的 Clipmap Min Level 和 Clipmap Max Level 选项,灵活调整裁剪图层级的范围。
缩小覆盖的层级数量能够以阴影渲染质量为代价换取虚拟阴影贴图的整体开销降低,用户可根据具体的使用场景进行选择。如果当前工程对于近处阴影的质量要求不高,则可以提升裁剪图的最低有效层级;如果当前工程所用的场景范围不大,则可以降低裁剪图的最高有效层级。
效果和性能对比
相比于传统的阴影渲染技术,虚拟阴影贴图依靠其超高的分辨率,合理的页分配与管理,能够在大型场景或高精度模型的阴影渲染中取得更好的阴影效果,同时保证运行效率。
效果对比
下面的两组图片对比了 Tuanjie Editor 中级联阴影贴图(CSM)和虚拟阴影贴图(VSM)的效果差异。对于级联阴影贴图,我们将 Resolution 选项设置为 High,并将 Angular Diameter Scale for Softness 设置为 0 来渲染硬阴影。在 Volume 中,级联阴影贴图的 Max Distance 被设置为 400。
可以看到的是得益于分辨率的提升,VSM 能够取得更好的阴影质量。
除了阴影质量的提升外,虚拟阴影贴图还能在保证阴影质量的基础上覆盖更大的范围。在如上图所示的阴影渲染中,整条街的长度远远超过了级联阴影贴图(CSM)400 米的 Max Distance,因而蓝框所示区域就缺少了阴影的渲染。同时,在红框所示区域中,CSM 所渲染的阴影出现了锯齿,而 VSM 仍能够呈现高精度的阴影。虽然将 Max Distance 提升到 1200 米后,级联阴影能够覆盖整个区域(如下图),但是显而易见的是,街道上的硬阴影十分模糊,渲染质量大幅下降,远远不及 VSM 所取得的效果。
*CSM (Max Distance = 1200)
性能对比
我们将在两个场景中对比 VSM 和 CSM 的性能差异,第一个场景为 3200m * 1000m,包含大约40万个 Game Object 的大型场景,而第二个场景则为包含大量复杂高精度模型的小型场景。
在这两个场景当中,我们将分别比较在开启 VSM;同时开启 CSM 和 VG;开启 CSM 的同时关闭 VG 的情况下,所表现出来的阴影渲染性能差异。因为目前 VSM 只支持渲染硬阴影,所以在测试时 CSM 也同样只渲染了硬阴影。
大型场景
1、开启 VSM 后
-
GPU 时间:总计 2.33ms。
-
CPU 时间:初始化消耗0.327ms,执行消耗0.094ms,总计0.421ms。
2、同时开启 CSM 和 VG 后(CSM 的 Max Distance 为 2000 米)
-
GPU 时间:总计 22.5ms。
-
CPU 时间:初始化消耗0.243ms,执行消耗2.737ms(RenderShadowMaps 消耗2.73ms,剩余过程消耗0.007ms),总计2.98ms。
3、同时开启 CSM 和 VG 后(CSM 的 Max Distance 为 400 米)
-
GPU 时间:总计 18.16ms。
-
CPU 时间:初始化消耗0.288ms,执行消耗0.446ms(RenderShadowMaps 消耗0.44ms,剩余过程消耗0.006ms),总计0.734ms。
4、开启 CSM 的同时关闭 VG(CSM 的 Max Distance 为 2000 米)
-
GPU 时间:总计 69.17ms。
-
CPU 时间:初始化消耗0.055ms,执行消耗28.05ms,总计28.105ms。
5、开启 CSM 的同时关闭 VG(CSM 的 Max Distance 为 400 米)
-
GPU 时间:总计 18.46ms。
-
CPU 时间:初始化消耗 0.04ms,执行消耗 5.23ms,总计 5.27ms。
6、性能总结与分析
我们为 CSM 选择了两个 Max Distance,2000米 与 400米。当 Max Distance 为 2000 米时,CSM 大致能覆盖到摄像机视野中最远的距离,但是此时 CSM 的性能大幅下降,因此我们也测试了在常见的 Max Distance 为 400 米的情况下,CSM 的性能表现,但是从图中可以看出,此距离下视野中出现了明显的阴影缺失。
从表中可以看出,VSM 的性能是最为优越的,且显著优于其他几种方案,耗时约为表现第二的 CSM+VG(400m) 方案的一半。同时,在采用 CSM 的方案中,我们也能看到开启 VG 后能够极大提升 CSM 的性能表现,因此我们推荐大家同时开启 VSM 和 VG 以取得最好的效果。
高精度模型场景
场景大小 70m*90m,包含了数百个面数为 300 万的高模,整个场景面数在 10 亿数量级,可以用于验证当画面近处包含大量高模时,VSM 的效果和性能表现。
1、开启 VSM 后
-
GPU 时间:共计 2.44MS
-
CPU 时间:共计 0.633MS
2、同时开启 CSM 和 VG 后(CSM 的 Max Distance 为 400 米)
-
GPU 时间:共计 4.95MS
-
CPU 时间:共计 0.465MS
3、开启 CSM 的同时关闭 VG(CSM 的 Max Distance 为 400 米)
-
GPU 时间:共计 382.55MS
-
CPU 时间:共计 0.448MS
4、性能总结与分析
从渲染图中可以看出,VSM 渲染出的硬阴影细节更为丰富,例如能够渲染出雕像手指投射出的阴影,而 CSM 则无法做到这一点。而从性能上看,在渲染小场景中的高精度模型阴影时,虽然 VSM 的 CPU 耗时略微高于 CSM(我们还将持续优化这部分开销),但是 GPU 耗时大大降低了。
如何开启虚拟阴影贴图
打开工程的 Project Settings,在 Quality > HDRP 中找到 Virtual Shadow Map 模块,通过开关 “Globally Enabled” 来开启或关闭 VSM。
开启后,虚拟阴影贴图将替代工程中原先所有的阴影贴图方案,如平行光所使用的级联阴影贴图。您几乎不需要对现有的工程做任何调整,便能够一键将场景中的所有阴影渲染方案替换为虚拟阴影贴图。
目前若要开启虚拟阴影贴图,则需要同步开启 Project Settings 中虚拟几何体(Virtual Geometry)的全局开关。但是,虚拟阴影贴图并非只能作用于 VG 物体。即使 Renderer 没有开启虚拟几何体,虚拟阴影贴图仍能够为这些 Renderers 生成高质量阴影。
*VSM全局开关
*VG全局开关
技术细节
虚拟阴影贴图的阴影渲染主要涉及到如下图所示的四个流程:
-
从 GBuffer 提取当前帧所需要的虚拟页号
-
为这些虚拟页分配实际的物理页
-
光栅化得到当前帧的深度值
-
采样深度图渲染阴影
标记当前帧所需页
因为虚拟阴影贴图超高的分辨率,实际显存容纳不下这么大的纹理。因此 16k x 16k 的阴影贴图为虚拟纹理,且分割为了基本单位是 128 x 128 个纹素(texel)的虚拟页。根据当前帧需要哪些物体在光源视角下的深度值,我们标记这些深度值对应的虚拟页为当前帧所需的页,并在之后的流程中建立起虚拟页和物理页的映射关系,从而真正给这些虚拟页分配物理显存。
那么应该如何去确定当前帧需要哪些物体的深度信息呢?获取深度信息的最终目的是渲染屏幕中对应像素的阴影,因此,所需的物体即为出现在像素对应的虚拟页中的物体。我们可以从 GBuffer 中通过像素找到对应虚拟页所需的相应信息,如顶点位置和法线。VSM 在 GBuffer 的 Pass 之后插入了一个 Pass VSMClearAndExtractPagesFromGBuffer,用于从 GBuffer 中提取信息。这个 Pass 会遍历 GBuffer 的每一个纹素,从中获取顶点的位置信息并映射到以光源为视角的屏幕空间中,找到对应的虚拟页,这些虚拟页便是当前帧所需的页。
管理页分配
现在我们知道了如何确定当前帧所需的页,从原理可以看出每一帧所需的页都有所不同,所以每一帧我们都需要重新建立起所需的虚拟页和物理页之间的映射关系,并保证当前帧所需的页都能被映射到物理页,否则我们便丢失了一些渲染阴影要求的深度信息。
光栅化生成阴影贴图
因为 16k x 16k 的阴影贴图为虚拟纹理,所以在光栅化时我们并不会将当前纹素的深度值作为 Fragment Shader 的输出,而是写入到对应的物理页中。在 Vertex Shader 中,将当前顶点进行坐标变换,变换为以光源为视角的裁剪空间坐标。在 Fragment Shader 中,根据当前顶点的坐标确定所属的虚拟页,再向映射的物理页的对应位置写入深度值。
采样生成阴影
类似于光栅化的过程,我们将屏幕中当前像素点对应的顶点位置进行坐标变换,切换到以光源为视角,从而确定当前像素对应的虚拟页。通过页的映射关系,获取到物理页中存储的深度值,并与当前像素在光源视角下的深度值做比较,若当前像素更靠近光源,则 Shadow Factor 为 1,否则为 0。Shadow Factor 作为采样的结果参与 LightLoop 相关的计算,从而得到阴影。
性能优化
上述过程包含了虚拟阴影贴图渲染阴影的基本流程,整个过程涉及大量的计算以及内存读写,为了对性能进行优化,我们使用了如下的几个加速算法。
GPUCulling
类似于虚拟几何体,我们将虚拟阴影贴图的剔除(Culling)流程放到了 GPU 端实现。除了常规的以光源为视角的视锥体剔除(Frustum Culling)和遮挡剔除(Occlusion Culling)之外(对于开启了虚拟几何体的物体,还有额外的 LOD Culling),剔除流程中还判断了当前物体所在的虚拟页是否被标记,若未被标记,则当前物体被剔除。这么做是因为被标记的页是当前帧渲染阴影所需页,即使我们写入了未被标记页的深度值,在之后的阴影深度采样流程中也不会用到这些页,这部分算力就被浪费了。
深度缓存
从上述流程可以看出虚拟阴影贴图每一帧需要更新的数据量非常庞大,如果我们每一帧都对这些数据进行全量更新的话,是一笔极大的开销,会显著降低帧率。因此我们需要尽可能地避免一些不必要的数据处理和更新。
在大多数情况下,场景中的物体保持了和上一帧相同的状态,因而它们的深度信息是不变的。对于这些物体,我们将它们的深度信息进行缓存保留,从而跳过上述流程中的一部分,直到这些物体出现了变化导致缓存失效。导致缓存失效的常见情况有:物体的移动,物体的出现与消失,摄像机的切换。在接下来的 Rendering Debugger 中 Cached Pages 可视化功能展示里,我们通过一个视频演示了深度缓存的保留与更新机制。
在引入缓存机制后,上述流程中的一部分细节随之发生变化,具体如下。
在未开启缓存之前,对于管理页分配,我们只需要简单地按顺序遍历所有需要的虚拟页和所有的物理页,并将它们一一建立起映射关系。但是在开启缓存后,并非所有的物理页都是可以被覆写的,因为其中一些页存储了上一帧有效的深度缓存信息,这些深度需要被这一帧复用。为了避免这些物理页被覆写,我们需要将这些物理页重新映射到当前帧对应的虚拟页。具体操作为在给所有虚拟页分配物理页之前,我们先判断系统中是否保留了当前虚拟页的有效深度缓存信息,如果有,那么我们根据上一帧的映射关系,找到对应的物理页,并与这一帧的虚拟页建立起映射关系,同时跳过这一帧的物理页分配。
在进行 Culling 时,若系统判断当前 cluster 或 instance 对应的虚拟页为缓存页,那么便会被剔除。这么做是因为我们已知这些物体的深度在这一帧并没有发生变化,因而我们也就不需要 Culling 之后的光栅化流程来更新这些物体的深度值,从而节省一部分计算量。
用户可通过 Project Settings 中 Virtual Shadow Map 模块的 Cache Enabled 控制缓存的开关。同时,用户可以通过 Directional Light Cache Disabled 开关单独控制平行光的缓存。当场景中存在昼夜切换时,平行光的照射角度不断变化,导致缓存频繁失效,在这种情况下,缓存带来的收益可能低于其计算成本,因此可以考虑关闭缓存提高效率。
Single Page VSM
我们通过对局部光的阴影精度进行调整来进一步优化虚拟阴影贴图的性能表现。对于距离摄像机较远的局部光,高精度的阴影贴图并不会带来显著的阴影效果提升,因此,我们针对这一部分局部光进行了优化,使得它们的阴影精度降低为128 x 128,即一个页的大小。
用户可通过 Project Settings 中 Virtual Shadow Map 模块的 Distant Light Mode 控制这一选项。当选项为 OFF 时,所有局部光都将使用高精度的阴影贴图;当选项为 ON 时,摄像机远处的局部光将使用低精度的阴影贴图;当选项为 ALWAYS 时,所有的局部光,无论远近,都将使用低精度的阴影贴图。
Rendering Debugger 相关可视化功能
开启虚拟阴影贴图后,用户可通过 GDRP 栏目下的 VirtualShadowMap Visualization 选项来控制虚拟阴影贴图相关的可视化。通过该功能,用户可以确认虚拟阴影贴图是否如自己预期般工作。在接下来我们将以平行光为例,展示各种不同的可视化效果。
ClipmapLevelOrMipLevel
渲染屏幕中每个位置的阴影精度层级,平行光对应 ClipmapLevel,局部光则对应 MipLevel。每一种不同的颜色都代表了一个阴影精度层级。在如下图所示的平行光阴影精度层级可视化中,根据与摄像机的距离,屏幕中的每个位置都被可视化了不同的精度层级,越靠近摄像机则阴影精度越高,层级越低。
*正常渲染
*ClipmapLevel
Request Pages
渲染屏幕中每个位置对应的虚拟页,每一种相邻的不同颜色块都代表了一张虚拟页。可以看到的是,距离摄像机越近,虚拟页越密集,每个虚拟页覆盖的区域越小,而密度的边界线则于上一栏中的 ClipmapLevel 对应。这是因为 ClipmapLevel 越低,则阴影精度越高,所以需要更多的虚拟页去覆盖相同大小的区域。图中的每一个不同颜色的方块都代表了一张 128 x 128 的虚拟页,在 ClipmapLevel 变换的边缘处虚拟页可能存在不完整的情况,这是因为两个层级间不同的虚拟页发生了切换。
*正常渲染
*Request Pages
Cached Pages
渲染屏幕中每个位置对应的虚拟页的缓存情况。若虚拟页为绿色,则代表当前页完全使用了缓存的深度信息;若虚拟页为蓝色,则代表当前页的静态物体部分使用了缓存的深度信息,而动态物体则没有使用缓存;若虚拟页为红色,则代表当前页内的所有物体都没有使用缓存信息。区分物体是否为静态物体则是依据 Inspector 窗口中 Mesh Renderer 组件的 Static Shadow Caster 选项是否开启,若开启则为静态物体,否则为动态物体。
视频展示了开启 Cached Pages 可视化之后的效果。在第一帧时系统中没有有效的缓存,因此所有物体在逐步由红转变为绿。当镜头被移动时,不同 Clipmap 层级的边界处出现了新的虚拟页,这一部分新页是没有缓存信息的,因此层级交界处出现了红色。随着镜头转换,画面中出现了一辆在移动的汽车,这辆汽车所覆盖的虚拟页被标记为了蓝色,这是因为汽车作为动态物体在移动,使得虚拟页的动态缓存失效了。随着镜头接近汽车,汽车所覆盖的虚拟页发生了变化,具体表现为从高 Clipmap 层级的虚拟页变换为低 Clipmap 层级的虚拟页,因而缓存失效的虚拟页也随之变化。
ShadowFactor
渲染屏幕中每个位置对应的阴影系数,因为目前虚拟阴影贴图只支持硬阴影,所以在目前 ShadowFactor 的可视化中只有 0 和 1 两种值存在。值为 0 代表当前位置存在阴影,值为 1 代表当前位置不存在阴影。
*正常渲染
*Shadow Factor
兼容性
目前虚拟阴影贴图处于试验性版本,仍在积极开发当中,因此使用上存在一定局限性。当前虚拟阴影贴图只能在团结引擎 1.4.0 版本的 HDRP 管线中使用,适配的材质包括 Lit.shader,LayeredLit.shader 以及 Shader Graph,支持的平台包括 Windows(DX11, DX12, Vulkan)和Linux(Vulkan)。
同时,目前虚拟阴影贴图只支持渲染硬阴影。在所有光源类型中,平行光(Directional),点光源(Point)和聚光灯(Spot)能够通过虚拟阴影贴图渲染阴影,而面光源(Area)的适配工作仍在进行中。