Lumen是UE5的GI系统,和传统意义上的实时GI只包含间接漫反射的贡献不同,它同时包含了间接漫反射和间接高光,提供了一套全新的完整间接光照。Lumen同时支持基于硬件的RTX和基于软件Trace两套算法,本文的入手点是Lumen GI使用基于软件Trace的间接漫反射部分的流程、算法和数据结构分析,从宏观上理解Lumen基本原理和运行机制。
Lumen的核心包括以下几个部分:
Lumen中有两种主要的Tracing求交加速结构:3D空间的Distance Field和HZB,其中Distance Field又分为Mesh Distance Field(以下简称MDF)和Global Distance Field(以下简称GDF)两种。MDF/GDF用于加速3D空间中的模型(MeshCard/Voxel)求交,Hierarchy Z Buffer(以下简称HZB)则用于屏幕空间中的SSGI的RayCast求交。
在没有BVH的前提下,RayTracing只能使用固定步⻓沿光线方向均匀步进且每次步进后对场景进行求交,为了穿透薄片和小模型不出错,则需要步进⻓度无限小,而为了运行效率则又希望步进⻓度越⻓越好,两者要求完全相反。均匀步进的示意如下图:
在有了MDF/GDF之后,这一情况可以得到有效的改观,因为MDF/GDF存储的是当前位置离模型表面最近的位置,所以有了MDF/GDF之后,RayMarch的步进⻓度绝大多数情况下可以大幅提高而又不出现穿透“模型的”错误。如下图所示,从相机前面发出一条光线,蓝色圆即为当前点所存储的Distance Field(离物体的最近距离),因为在这个距离内(圆)不可能有其它物体,所以其每次步进的⻓度可以直接为Distance Field的距离:
屏幕空间中的RayMatch和3D空间的RayTracing有完全类似的步进问题,屏幕空间缺乏完整的3D场景信息,所以其RayMatch的命中判断通过比较当前像素的深度Z和光线方程在当前位置的Z值的大小来近似——当前像素光线的深度大于等于ZBuffer中的深度(以下讨论基于一般ZBuffer,Reversed Z则规则相反),则认为已经相交,相反则认为没被遮挡,可以继续步进。HZB的基本思想类似于BVH,它先为当前的ZBuffer生成Mipmap链,其中高一级的Mip Z值取它上一线Mip中4个像素中的最小值,然后在执行RayMatch的时候从其中 某一级Mip开始。如果当前Mip中没有相交,则提高Mip层级继续求交,否则降低Mip在更精细的粒度上求交。它的意义是先在较大像素Block中去尝试RayMatching是否和当前Block有交点,如果在较小粒度的像素Block中去求交,且一直有交点,则一直下降到所需要的最高精度这一级去查询光线是否和当前像素相交:相反则会尝试更大Block中是否有交点,从而快速跳过大片不需要逐像素Step的区域。在两级HZB上的RayMatch示意如下图所示:
LumenScene即为Lumen在计算过程中所使用的场景,它是真实场景的一个不完整简化版。对于基于Trace的算法来说,选择一个简化的场景表达意味着对Trace提速,同时也会带来Trace精度的损失。MeshCard是LumenScene的基本组件,从场景结构的意义上来说,它是LumenScene的基本元件,对LumenScene来说,场景中不存在模型——MeshCard就是它们的替代品(Proxy)。
在实时GI中比较常⻅的模型简化是体素(Voxel)和面元(Surfel),Lumen中的MeshCard是一个⻓宽高不等的且只有一个坐标轴朝向Orientation ∈ [-x,+x,-y,+y,-z,+z]的六面体。从形状上来说,它是方块形的积木:
LumenScene就是靠这样的积木一块块拼成的,只不过它不需要手工去搭建,而是自动生成。MeshCard在Lumen中的主要功用是提供光照采样的位置和方向,用于大幅度减少它真正同时也是缓存光照信息的基本构件,MeshCard的6个Orientation即为Ambient Cube1的6个基底函数,即使在模型中的同一个位置,可能存在多达6个MeshCard,每个MeshCard正好表达不同方向。这一表达方式可以对DiffuseLighting有较高的保真度,被广泛应用于Probe Based的各类实时和烘焙GI解决方案中,同时它在还原数据时指令量也较SH精简。
Lumen选择的是使用当前帧屏幕像素所对应的世界位置来放置Probe,并为之命名为ScreenSpace Probe。
Probe的Radiance有两种表达方式,一种是八面体映射(OctahedralMap)[6]存储,默认大小为8*8,Probe的Radiance另一种表达方式是球谐函数,这将在Probe计算完成之后生成。八面体映射使用一张2D的RenderTarget来做为Atlas Texture存储所有的Probe,同时它又没有椭圆映射存在的边界接缝难以处理的问题。八面体映射从2D纹理到球面和半球的映射过程如下图所示,更详细的算法描述⻅参考6。
在有了Meshcard Atlas/Voxel之后,Lumen再次选择加入ScreenSpaceProbe来做为中间光照存储有许多好处,如:
Lumen的完整流程分为离线数据生成、运行时数据更新与光照计算两个大模块,两个模块的主要工作如下所示:
离线阶段
离线生成的数据有MeshCard、MDF、GDF三份数据,在静态模型导入或修改之后自动异步生成,并不需要执行Build步骤。之所以会同时存在MDF和GDF两部分数据,是因为MDF的精度比GDF更高。在运行时,MeshCard将同时作为场景组件和光照缓存结构使用。
运行时阶段
Lumen在运行时有四个主要工作来完成最终的GI计算:
[1] 更新LumenScene
[2] 注入直接和间接Diffuse光照到光照缓存中(包括MeshCards和基于3D Clipmap的Voxel等)
[3] 基于当前屏幕空间自动化放置Probe及Trace得到Probe的光照信息,对Probe的光照信息进行二次编码及生成Irradniance Cahe
[4] 使用Probe的光照信息、二次编码的光照信息及Irradiance Cache信息计算最终Indirect DIffuse和Indirect Reflection,并混合History光照信息做为最终的GI输出
下面针对这几个阶段的工作要点进行介绍和分析。
1. 基于Primitive的数据更新
LumenScene中的数据分为两部分,一部分来直接来源于FScene的静态模型(包括MeshCards,两种DF数据),另一部分则来源于使用MeshCards和DF等数据生成用于渲染的光照缓存数据(包括各种编码后的Atlas纹理)。故LumenScene的更新的驱动也有两部分:一是场景中的数据变化,二是每帧需要更新特定的光照缓存数据用于后续的光照计算。这些数据更新是在多线程执行的。
LumenScen的的Primitive数据更新操作包括:
LumenScene中把场景划分为远景和近景两部分,故除Primitive外,场景中的相机移动也会触发LumenScene更新:
除此之外,针对Nanite模型:
关于Meshcards和AtlasTexture的说明
2. 捕获MeshCards的材质属性(MaterialAttributes)
从MeshCard的存储结构可以看到,它没有保存⾃⼰的材质属性和深度信息,⼤概是为了节省磁盘空间和保证其精度的灵活可控性,这些在离线烘焙所缺失的数据需要在运⾏时补⾜⽣成存入对应的AtlasTexture中以备后⽤。接下来步骤,就是为MeshCards准备好⽤于计算GI的材质和其它⼏何形体的相关数据。这些数据属性主要有:
在步骤1中已经处理好Mesh渲染列表,这⼉需要先组织先组织成Instance,尽量减少DrawCall调⽤以节省性能。因为Nanite的Cull-Draw流程和传统的静态模型是两套完全不同的流程,所以这里的流程也需要执⾏两遍。还需要注意的是因为MeshCards⽤于计算GI,所以不管它是不是在视野范围内都应该提交渲染,因此Cull流程在此不⽣效。
在完成材质属性捕获操作后,还会执⾏以下操作:
在完成MeshCards数据更新及材质属性捕获之后,下⼀步就是需要把当前场景中的光照信息注入到光照缓存中去。Lumen使⽤了三种主要的数据结构来缓存场景中的光照信息,它们分别是MeshCards和其对应的AtlasTexture(最⾼精度)、Voxel 3D Clipmap[2](中精度)和GI Volume(⽤于体渲染)。这三种类型的数据同时可以覆盖场景中的静态物体、动态物体和体渲染的物体,可形成完成的GI照明来源。
注入光照的整体流程如下图所示:
1. VoxelLighting
Lumen中的VoxelLighting和VXGI类似,使⽤是基于3D Clipmap的⽅式以节省存储空间。但Lumen的VoxelLighting中的每个3D纹素和VXGI中的Voxel并不相同——Lumen的每个3D纹理表⽰的是Ambient Cube[1]某⼀个⽅向上的光照投射参数,实际上它所需要的3D纹素数量是其Size的6倍。这⼉可以看到VoxelLighting选择了和MeshCards⼏乎完全相同的结构(六⾯体)和基函数(AmbientCube)。关于Clipmap的详细介绍可以在参考[2]和[5]中找到。
下图展⽰了⼤⼩为4096的3D纹理的全mip与64 * 64的3D Clipmap两者所需加载的纹素数量的对比:
VoxelLighting默认使⽤4级3d Clipmap,存储的是以相机世界位置为中⼼点周围200米范围(可配置)的间接光照信息。所有MipLevels和Directions均直接平铺在⼀张3D纹理中:
2. 为MeshCards注入光照
从上⾯的流程图可知,MeshCards的光照注入分为直接光和间接光两部分,且直接光和间接光也都只计算Diffuse贡献⽽不计算&Specular贡献。
第⼀步是先计算MeshCards的间接光,间接光的计算可以按光照源数据来源,Trace⽅式两个维度进⾏拆分,Lumen共⽀持4种不同的间接光计算模式,如下表所⽰:
默认的,Lumen使⽤VoxelLighting做为间接光的来源并使⽤分块Trace复⽤来计算MeshCard的间接照明。此外,间接光的光源和采样⽅式还有两个值得注意的点:
关于间接光的Voxel Trace部分的简要说明:
第⼆步是对间接光的结果再次进⾏Bilinear Filter并过滤掉<0的异常值。
第三步是计算直接光照的贡献,Lumen⽀持的直接光类型包括: PointLight 、SpotLight 、RectLight和DirectionalLight。除DirectionalLight是逐盏灯计算外外,其它三种类型的光照都是分批执⾏的——因为它们光照范围有限,可以只把这⼀批次内影响的MeshCards找出来,每批次的直接光渲染只对在他们影响范围内的MeshCards⽣效。
直接光计算的另⼀个问题是需要考虑当⾯MeshCard对灯光的可⻅性问题,Lumen⽀持使⽤ShadowMap或RTX光追来确定灯光到当前MeshCard的遮挡比例(ShadowFactor)。
直接光计算的最后⼀个要点是:只计算Diffuse贡献项。
光照注入的最后两步是:采样Albedo和Emissive到MeshCards对应的DiffuseAtlas和EmissiveAtlas,为MeshCard的最终光照(Indirect+Direct)⽣成Mipmaps,⽣成Mipmaps的时候和⼀般的Mipmap⽣成⽆⼆,使⽤双线性采样过滤⽣成⾼⼀级的纹素。
3. 更新VoxelLighting Mips,Voxel光照注入
从上⾯VoexlLighting介绍⼀节可以看到它是⼀个以相机世界位置为中⼼点的Voxel Clipmap,这样在相机发⽣移动时,Clipmap也同样需要进⾏更新。为减少单次需要更新的Voxel数量,有如下优化。
(1)保证每帧最多更新⼀级Mips,Voxel Clipmap四级Mip更新的的顺序如下如⽰:
a. 第 0,2,4,6,8,... 帧允许更新第0级mip
b. 第 1 ,5,9,13,17,... 帧允许更新第1级mip
c. 第 3,11,19,27, ... 帧允许更新第2级mip
d. 第 7,15,23,31,... 帧允许更新第3级mip
(2)在相机未发⽣剧烈移动时,理论上只需要更新移动⽅向上影响部分的Voxel,如下图所示:
除了相机更新,场景中的Primitive增删修改也会影响到它周围的Voxel,所以这个策略实际上在Lumen的实作中采⽤的是更⼀般的PrimitiveUpdateBounds去和Clipmap Tile Bound求交,来确定真正需要更新的Voxel数量,更新的也不是Voxel,⽽是接下来会介绍到的VisibilityBuffer。
VoxelLighting光照解算过程使⽤MeshDistanceField来加速求交:Lumen选择的是先使⽤当前Voxel Clipmap的Boundbox去剔除掉不在此范围内的所有Objects及其对应的MeshDistanceField,再使⽤这些通过剔除的MeshDistanceField包围盒来计算它们⾃⼰所覆盖了哪些Voxel并把⾃⼰的索引写入所有覆盖Voxel的Trace参数中。在接下来的Voxel Trace Pass⾥,每个Voxel仅需处理上⼀步所填入的MeshDistanceFiled即可。Voxel Trace Pass的输出数据是包含HitDistance和HitObjectIndex组合的VisibilityBuffer。VisibilityData结构如下所示:
uint32_t NormalizedHitDistance : 8 ; //相交距离
uint32_t HitObjectIndex : 24 ; //物体ID
最后的Voxel Shading Pass则从压缩过的VisibilityBuffer中获取到最佳的三张MeshCard来用对Voxel解算光照,这儿计算光照的权重系数不只使用AmbientCube的系数,同时考虑到物体的透明度和MeshCard的可⻅性(类似于VSM,使用切比雪夫不等式估算)。
4. ⽣成和计算GIVolume
此处的GIVolume即为传统Irradiance Volume,其默认覆盖离相机距离80米的世界(z轴)。实现要点如下:
根据⼀般的RTGI⽅案,在有了MeshCards及对应的LightingAtlas+MaterialAtlas,⼜有了VoxelLighting和GI Volume的信息,已有⾜够信息解算正常游戏中的GI了。比如我们可以这样去算:
光源:把场景分为近景和远景,近景使⽤VoxelLighting,远景使⽤DistantMeshCard(相当于⼀个巨⼤的AmbientCube)。
光照计算:使⽤PixelWorldPosition和PixelWorldNormal获取最近且⽅向匹配的3个Voxel来解算当前GI。
效率:可以使⽤半屏或更⼩分辨率的GI RenderTarget。
效果:使⽤spatial和temporal Filter平滑光照,使⽤⼀些粗糙的⼿段处理漏光(比如Normal offset,限制墙体厚度,使⽤stencil标记室内外,使⽤SDF推采样点到物体外等等)。
也可以把VoxelLighting换成GI Volume,仅仅在光照计算获取Volume及计算权重时有所不同:
光源:把场景分为近景和远景,近景使⽤GI Volume,远景使⽤DistantMeshCard(相当于⼀个巨⼤的AmbientCube)。
光照计算:使⽤PixelWorldPosition和PixelWorldNormal获取最近的数个GI Volume
效率:可以使⽤半屏或更⼩分辨率的GI RenderTarget。
效果:使⽤使⽤spatial和temporal Filter平滑光照,使⽤⼀些粗糙的⼿段处理漏光(比如Normal offset,限制墙体厚度,使⽤stencil标记室内外,使⽤SDF推采样点到物体外等等)。
如果效果想要好一点,我们也可以Per Pixel根据PDF Importance Sample生成采样方向去Trace MeshCard,注意到MeshCards的缓存数据中包含深度相关信息,这就可以直接照搬VoxelLighting的 VisibilityWeigth的计算方式来估算可⻅性,得到类似于GGDI的遮挡效果。
Lumen同时使用了SSGI,Detail MeshCard Trace,VoxelLighting Trace,Distant Meshcard Trace四种方式去 求解最终光照,其中各种Trace的作用距离和优先性排列如下:
很容易看到在室外场景中,主要是靠VoxelLighting(含DistantMeshCard)照明,如下图所示:
有了Probe之后,Lumen的Indirect Diffuse解算流程调整如下:
sample周围4个probe {sample_probe(uv) , uv|uv + [(0,0),(1,0),(0,1),(1,1)]}
计算bilinear weight
获取这些probe和当前position的深度差和夹⾓, 计算depth weight & corner weight 叠加
biliner weight成finalWeight
如果所有 finalWeight < 0 ,说明周围没有有效的Probe可以采样,则放置⼀个新的probe
使⽤ConeTrace采样MeshCard和VoxelLighting及SSGI⽣成probe的radiance、对probe做sptial filter和修复边界上的采样点,转⼀份probe到基于SH的数据但并不清理掉原始的probe数据。这样,probe实际上存在两份radiance数据——octahedral map + sh双料存储。
Upsample Probe到屏幕⼤⼩,temporal blend Indirect Diffuse。Upsample有两个主要分⽀:
如果不使用RTX reflection,接下来还会进行indirect specular的解算,最后把indirect diffuse和上一帧的indirect diffuse进行混合,做为最终indirect diffuse输出。这样就完成整个Lumen GI的计算工作。可以看到的是Lumen在整个流程中没有像RTX一样,依赖很重的时频域Filter来压像素间的方差。
Lumen的无限反弹是如何实现的?
MeshCard采样的VoxelLighting是上一帧的数据,这样MeshCards反弹数据会从第二帧开始累积,每帧都会多一次反弹。
官方文档中的SurfaceCache在哪?
MeshCard Lighting + Voxel Lighting大概等同于官方文档中的SurfaceCache。
World Space Probe Cache
Cache方式、作用范围、更新策略类似于Voxel Lighting(3D Clipmap),只是其存储的数据为Radiance/Irradiance且Trace方式和数据格式等同于Screen Space Probe。
参考
1 . Jason Mitchel , Gary McTaggart and Chris Green , Shading in Valve’s Source Engine ,Advanced Real-Time Rendering in 3D Graphics and Games Course – SIGGRAPH 2006
2 . Cyril Crassin1,Fabrice Neyret1,Miguel Sainz,Simon Green4 Elmar Eisemann , Interactive Indirect Illumination Using Voxel Cone Tracing
3 . William Donnelly, Andrew Lauritzen, Variance Shadow Maps
4 . Zander Majercik Jean-Philippe Guertin,Derek Nowrouzezahrai,Morgan McGuire, Dynamic Diffuse Global Illumination with Ray-Traced Irradiance Fields
5 . Christopher C. Tanner, Christopher J. Migdal, and Michael T. Jones, The Clipmap: A Virtual Mipmap
6 . Thomas Engelhardt, Carsten Dachsbacher, Octahedron Environment Maps
这是侑虎科技第991篇文章,感谢作者Jiff供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)
作者主页:https://www.zhihu.com/people/jeff-wong-92,再次感谢Jiff的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)