【GDC2018】The lighting technology
今天要学习的是《底特律变人》在GDC2018上分享的他们的渲染技术,按照管理做一个总结:
- 所有的光都是有衰减有位置的(包括方向光),场景会被分割成一个个的SceneZone,每盏灯会标注需要影响哪些SceneZone来优化性能
- 针对一些特写镜头,设计了Close-up的light/shadow
- 阴影用的PCF,同时采用了Shadowmap Atlas,可以根据距离选择分辨率,以自适应的方式调控阴影的效果
- 阴影还做了cache,点光可以只更新单一的face,方向光的阴影是CSM方案,通过通过TAA来优化锯齿表现,最多四级(大部分情况下只需要两到三级),每一级是1440分辨率,depth是16bits的格式,通过自适应的算法来调整Shadow的voluem
- 距离稍远的光源会转成静态阴影,只需要64分辨率即可,最多可以支持1024盏静态阴影灯
- 针对近景处,采用了单独的shadowmap实现contact/self shadow,覆盖附近10m范围内的部分物件(动态物件,标注为需要绘制的物件)
7.总体阴影内存消耗为276M,每帧只需要更新1520个shadowmap,每帧花费为1.53.5ms - 通过存储min/max的tile depth,针对体积光的漏光问题做了处理
- 间接光采用的是纯粹的probe方案,通过virtual offset来解决其漏光问题,同时数据结构是基于稀疏八叉树来存储的,总体内存消耗比较低。
- 采用的GI方案可以通过烘焙多套数据的方式实现GI的动态变化(TOD、开灯等)
内容分为三部分:
- PBR的设计(这里先跳过)
- 直接光照的设计
- 间接光照的设计
所有的光源都是punctual的(有位置的),按照光源的介绍
所谓的punctual lights指具有位置(location)的光,区别于平行光。但是punctual lights没有形状、尺寸,和真实世界的光源不同。术语"punctual"和守时没有关系,而是来自拉语punctus,意思是"point",指从单个位置发光的光源
平行光应该被剔除这个行列,底特律的平行光跟我们日常理解的平行光是不一样的:
Page 29.将平行光设计为有位置的,覆盖某个volume box的,且光强会随着距离而衰减的
直接光照:
- 衰减是默认按照距离平方来计算的
- 美术同学可以手调参数,通过这种方式伪造一个大尺寸的光源
- 通过调整衰减半径可以控制性能,虽然会导致能量守恒规律的破坏,这里给出了一个参考的方案(应该是有一些技巧在里面)
- punctual光源在高光计算的时候,得到的光强会过高
面光源看起来可以提供一些帮助,但实际上因为性能与开发周期的问题,并没有用上。
最后采取的策略是调整材质的粗糙度来解决上述问题(高光问题?)
Page 31.为光源设计了一个自定义近平面的能力,可以实现一些特殊的需要。
通过给每盏灯标注其影响的Scene Zone来优化消耗。
Page 32.为了优化一些特写镜头的表现,这里设计一套close-up lighting的方案,大致意思就是针对镜头的移动曲线,对场景的布光做特殊的设计与处理,使得效果跟电影的效果相匹配。
主要的成本就是人力投入高。
Page 33.close-up lighting是一套单独设置,单独生效的布光逻辑,在启用的时候,会替换正常普通的光照排布,其阴影也是单独设计的,同时还会对GI产生贡献。
Page 34. Page 35.效果差异还是挺明显的,主要作用是优化角色光照。
就是不知道这类的场景多不多。
Page 36.阴影采用的是PCF,PCSS性能消耗有点高(寄存器压力)最后只用在了某些局部场景。
Page 37.阴影贴图采用了Atlas方案,类似VT,目的是根据需要调整Shadowmap的Size,同时避免频繁的分配与释放。
这里会根据到相机的距离来决定每个光源的阴影贴图尺寸
Page 38.Shadowmap也做了cache,只会在场景物件有更新的(动态)的时候需要刷新,对于点光来说,可以逐个面来决定要不要更新,而不需要针对六个面做同时更新。
此外,通过对近平面处的阴影做一定的调整来提升其精度(不知道具体是咋做的?)
Page 39.方向光用的CSM:
- PCF+TAA
- 基于抖动等方式实现等级之间的过渡
- 最多四级,每级最多1440分辨率,精度是16bits,大部分场景只需要两到三级
- CSM的划分是程序化实现的(不知道是什么算法)
为了优化阴影的消耗,这里会在一定距离下停止阴影的更新,转成静态阴影:
- 用了一张2048的贴图,每个静态阴影只占用64,可以达到最多1024盏光源有静态阴影
- 方向光的静态阴影贴图是8192,覆盖整个地图
近景处的阴影还需要一些细节处理:
- 添加contact & self shadow
- 额外增加两张shadowmap,每张尺寸为1536
- 由美术同学控制那些物件需要参与到这两张贴图的绘制中来
参与close-up shadow的物件选取逻辑:
- 10m半径内所有被标注为需要参与绘制的物件
- 蒙皮的模型?
(光源视角下)这类shadow的远近平面需要匹配上对应阴影的覆盖范围
Page 43.处于frustum之外的投影物则直接投影到近平面上(远平面的说明超出承影范围了,不用考虑)
Page 45. Page 46.角色阴影的品质看起来确实有很大的提升。
Page 47.这里是整体的阴影贴图的内存预算,总计消耗约276M(作为对比,UE的VSM默认有两张16k*4k用来存放vsm page的大RT,总共512MB)
Page 48. Page 49.整体性能:
- 每帧平均需要更新15~20个shadowmap,这里没有做数量限制,所以极端情况可能会飚得非常高
- 前面展示的视频中,每帧花费1.5~3.5ms(看起来是可以接受的,当然,这里是PS主机)
- Close-up Shadow的花费则跟具体的场景、视角有关,可快可慢,当场景中有树木这种需要做大量alpha test的物件的时候,消耗会高一些(为啥?)
体积光的渲染,采用的是Unified volumetric lighting方案(给了参考文献):
- 针对光照的覆盖深度做了适配
- 使用checkboard rendering方案
- 加了TAA降噪
- 可以受到直接光或者间接光probe的影响
- 在烘焙GI的时候,需要考虑fog的影响(没理解错吧?这个具体咋做),以实现对多次散射的模拟
当某个物件比较细,尺寸小于两个采样点的间距,就有可能导致漏光
Page 52.这里给了解决方案:
- 每个tile存储场景的最大最小深度
- 在进行逐射线采样的时候需要判断当前点是否是已经被遮挡了
- 在采样的时候添加一个深度的偏移(否则就会出现上图中间小图这种边缘模糊的效果)
路灯都是体积光实现的,17盏灯,大概1.8ms
Page 55. Page 56. Page 57.之前研发Beyond:Two Souls的时候采用的是上图描述的方案,静态物件跟动态物件做了分别处理,但是研发底特律的时候希望用同一套方案兼顾所有的物件。
Page 58.基于Probe的方案有如下细节:
- 适合用于IBL(那就不只是烘焙Diffuse,而是要烘焙高光cubemap了)
- 会在离线烘焙GGX NDF(需要通过重要性采样规避光斑),GGX是高光BRDF计算中的一项,取决于场景的粗糙度以及法线跟微表面法线的夹角,不知道这里突然提到这一点的目的是啥?
- 美术同学最好能够控制,不知道这里说的控制是指摆放位置吗?
Diffuse Probe Grid的问题:
- 漏光,暂时没有完美解决方案,有众多的人对此做了研究,这里给了两个有意思的参考方案
- 插值的不规律:虽然效果影响不明显,但是在底特律这边会存在问题(啥问题?)
常见的解决漏光的方案是拒绝掉被遮挡的probe,但是这种做法会导致上图所示的瑕疵。
最终底特律采用了其他的方案
Page 61.这里将probe数据用稀疏八叉树来存储:
- 用一个volume来指定覆盖范围
- 采用自适应的算法来控制probe的布局与密度
- 会给美术同学一个工具来控制probe的布局:
- 密度zone(控制密度?)
- 在物件或者墙体周围,基于程序的体素化结果来调整probe的分布
八叉树上每个顶点对应于一个probe,也就是空间中的任意点,都可以找到与之匹配的8个probe。
漏光问题的解决是通过对probe的位置做一个(虚假的)偏移来实现。
Page 63.这里也设计了一个probe attractor,用于将probe朝着这个volume聚拢,这里有两个作用:
- 提升probe的有效密度
- 将probe沿着墙壁布局,可以更好的应对漏光问题
这里还设计了一个probe repulsor(推开、拒绝),用于隔绝掉内部(或外部)的无效probe
Page 65.再来看看偏移算法:
- 可以解决绝大部分的漏光问题
- 这个偏移是在离线烘焙的时候实现的
- 会考虑原始的grid位置
上面的小图貌似没有很好的说明具体的偏移方案,只是描述了说当(绿色)probe距离墙面有一段距离的时候,可能会选择墙外的红色probe,这时候就会出现漏光;而左侧的红色probe,由于是紧贴墙面的,所以就不会有问题(所以是在运行时自动选择最近的8个probe来做插值吗?)
Page 66.如果某个地方存在漏光(如何检测到?),就在这个地方对grid做细化分拆,并通过将probe靠近surface来规避。
当然,也有其他的规避方式,这是因为漏光比漏暗要更为明显。
Page 67.这里给了一个例子来介绍通过修改颜色来对比效果,但是好像不是很能看出来。
Page 68.当高密度跟低密度probe并存的时候,就会出现有的box的顶点并不能很好的匹配相邻边的密度,这时候会需要通过插值来补齐probe,从而保障下面的二叉树子节点都是满的。
Page 69.如上所述,我们的稀疏八叉树可以大概用类似的二叉树结构来类比
Page 70.我们存储的话,只需要存储上面的叶子节点(对应于8个corner的probe数据)的数据,每个节点存储8个probe,但是这种做法由于相邻probe的共用,会有大量的冗余。
Page 71.这里尝试以3x3x3为一个单位来存储,冗余会少一点。
这里的疑问是,为啥不单个probe的存储呢?这是因为,我们需要拿到各个probe的相邻关系,直接存储单个probe的话,可能就不方便拿到其相邻probe的数据,而最终的计算则是需要拿到8个相邻probe做插值来得到结果的。
有没有其他的方式来优化或者表达相邻关系呢?这里有一个前提条件,那就是GPU没有指针的概念,相邻关系必须要通过数组的索引来访问。
Page 72.Diffuse只需要低频数据,因此经常会使用SH来存储,这里使用的是2阶的SH,即每个probe只需要占用四个系数,此外,这里也提了一个Geomerics重建算法,不知道其作用是否是提升2阶SH表达的精度。
最终针对每个颜色通道,都需要一张RGBA 16F的贴图(这里的RGBA对应于四个系数),而总共场景需要24055个probe,按照前面的存储方法,大概需要105x105个节点,每个节点存储9个probe,因此总计是3MB的内存消耗。
Page 73.在使用的时候,由于不需要剔除,因此直接使用硬件的线性混合算法即可。
Page 74.而从世界坐标到采样节点对应数据的索引的转换,则是通过一个hash算法来实现,只需要O(1)的复杂度。
Page 75.基于这种做法,还有如下的好处:
- 可以很方便的实现GI效果的切换,切换可以用于灯的开闭等开关的支持
- 可以实现渐变的切换,如TOD,窗帘的拉开等
这里给了一个演示效果
Page 77.在切换的时候,需要注意一点,那就是需要避免Scene Zone之间数据的硬切,比如从室内到室外,解决方案是:
- 在切换的SceneZone中设计一段走廊
- 在走廊中的动态物件需要同时采样两套GI数据
- 基于到不同SceneZone的距离来加权
这里给了一个演示视频
Page 80.大多数的静态物件都是出于同一个SZ中的,对于门窗等同时处于室内外的物件,则需要做特殊处理,同时采样两套(其实是不是可以根据相机位置,决定采样哪一套?)
Page 81.需要手动标注这类物件
Page 83. Page 84.这里是结论
Page 87.