今天要学习的是《底特律变人》在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、开灯等)
![](https://img.haomeiwen.com/i19200103/ce3974a351114bed.jpg)
![](https://img.haomeiwen.com/i19200103/5efae83c3b9fdb21.jpg)
内容分为三部分:
- PBR的设计(这里先跳过)
- 直接光照的设计
- 间接光照的设计
![](https://img.haomeiwen.com/i19200103/9ba81d572b82ac89.jpg)
![](https://img.haomeiwen.com/i19200103/475c5757ff58020d.jpg)
![](https://img.haomeiwen.com/i19200103/86e2dd5616a2c837.jpg)
![](https://img.haomeiwen.com/i19200103/696d318f26a7eace.jpg)
![](https://img.haomeiwen.com/i19200103/72a615728ceb5321.jpg)
![](https://img.haomeiwen.com/i19200103/139180908723d08a.jpg)
![](https://img.haomeiwen.com/i19200103/52a455578fc2f940.jpg)
![](https://img.haomeiwen.com/i19200103/55060d379bf61e2b.jpg)
![](https://img.haomeiwen.com/i19200103/d53dbcf9b47e9cfc.jpg)
![](https://img.haomeiwen.com/i19200103/05325becdcb4feb9.jpg)
![](https://img.haomeiwen.com/i19200103/29b3acf1f95a357c.jpg)
![](https://img.haomeiwen.com/i19200103/01b008af71d4e2ef.jpg)
![](https://img.haomeiwen.com/i19200103/92e6b79013a9e467.jpg)
![](https://img.haomeiwen.com/i19200103/85c51c1436e93449.jpg)
![](https://img.haomeiwen.com/i19200103/df87947e4773ad0a.jpg)
![](https://img.haomeiwen.com/i19200103/cea3ef5a8ee9bd26.jpg)
![](https://img.haomeiwen.com/i19200103/7a4e70cf6036ebc1.jpg)
![](https://img.haomeiwen.com/i19200103/481a608c7b2d1ed2.jpg)
![](https://img.haomeiwen.com/i19200103/a4ac46a4c73fb4e9.jpg)
![](https://img.haomeiwen.com/i19200103/afcd7d512fe84b8a.jpg)
![](https://img.haomeiwen.com/i19200103/fd6c325679e0c64a.jpg)
![](https://img.haomeiwen.com/i19200103/0564f708a42f5097.jpg)
![](https://img.haomeiwen.com/i19200103/92fce82d772817e4.jpg)
![](https://img.haomeiwen.com/i19200103/5f2bbd99f79ba345.jpg)
![](https://img.haomeiwen.com/i19200103/9e71039b9721e7d2.jpg)
![](https://img.haomeiwen.com/i19200103/c5f12669863e8b07.jpg)
所有的光源都是punctual的(有位置的),按照光源的介绍
所谓的punctual lights指具有位置(location)的光,区别于平行光。但是punctual lights没有形状、尺寸,和真实世界的光源不同。术语"punctual"和守时没有关系,而是来自拉语punctus,意思是"point",指从单个位置发光的光源
平行光应该被剔除这个行列,底特律的平行光跟我们日常理解的平行光是不一样的:
将平行光设计为有位置的,覆盖某个volume box的,且光强会随着距离而衰减的
![](https://img.haomeiwen.com/i19200103/8a78ba4e3e2c8171.jpg)
直接光照:
- 衰减是默认按照距离平方来计算的
- 美术同学可以手调参数,通过这种方式伪造一个大尺寸的光源
- 通过调整衰减半径可以控制性能,虽然会导致能量守恒规律的破坏,这里给出了一个参考的方案(应该是有一些技巧在里面)
- punctual光源在高光计算的时候,得到的光强会过高
![](https://img.haomeiwen.com/i19200103/971d504c769909dd.jpg)
面光源看起来可以提供一些帮助,但实际上因为性能与开发周期的问题,并没有用上。
最后采取的策略是调整材质的粗糙度来解决上述问题(高光问题?)
![](https://img.haomeiwen.com/i19200103/aad340516210a33c.jpg)
为光源设计了一个自定义近平面的能力,可以实现一些特殊的需要。
通过给每盏灯标注其影响的Scene Zone来优化消耗。
![](https://img.haomeiwen.com/i19200103/6c423546a8a4187c.jpg)
为了优化一些特写镜头的表现,这里设计一套close-up lighting的方案,大致意思就是针对镜头的移动曲线,对场景的布光做特殊的设计与处理,使得效果跟电影的效果相匹配。
主要的成本就是人力投入高。
![](https://img.haomeiwen.com/i19200103/8c0caf637a0ceab2.jpg)
close-up lighting是一套单独设置,单独生效的布光逻辑,在启用的时候,会替换正常普通的光照排布,其阴影也是单独设计的,同时还会对GI产生贡献。
![](https://img.haomeiwen.com/i19200103/d3b9b2adcc1626a8.jpg)
![](https://img.haomeiwen.com/i19200103/eeba6d05f2fa9bd6.jpg)
效果差异还是挺明显的,主要作用是优化角色光照。
就是不知道这类的场景多不多。
![](https://img.haomeiwen.com/i19200103/1ae9233489f16fe6.jpg)
阴影采用的是PCF,PCSS性能消耗有点高(寄存器压力)最后只用在了某些局部场景。
![](https://img.haomeiwen.com/i19200103/e2303993781dbb53.jpg)
阴影贴图采用了Atlas方案,类似VT,目的是根据需要调整Shadowmap的Size,同时避免频繁的分配与释放。
这里会根据到相机的距离来决定每个光源的阴影贴图尺寸
![](https://img.haomeiwen.com/i19200103/1b9223294d0ba6ec.jpg)
Shadowmap也做了cache,只会在场景物件有更新的(动态)的时候需要刷新,对于点光来说,可以逐个面来决定要不要更新,而不需要针对六个面做同时更新。
此外,通过对近平面处的阴影做一定的调整来提升其精度(不知道具体是咋做的?)
![](https://img.haomeiwen.com/i19200103/077ee3ad6f124dca.jpg)
方向光用的CSM:
- PCF+TAA
- 基于抖动等方式实现等级之间的过渡
- 最多四级,每级最多1440分辨率,精度是16bits,大部分场景只需要两到三级
- CSM的划分是程序化实现的(不知道是什么算法)
![](https://img.haomeiwen.com/i19200103/7cefb917bad1db53.jpg)
为了优化阴影的消耗,这里会在一定距离下停止阴影的更新,转成静态阴影:
- 用了一张2048的贴图,每个静态阴影只占用64,可以达到最多1024盏光源有静态阴影
- 方向光的静态阴影贴图是8192,覆盖整个地图
![](https://img.haomeiwen.com/i19200103/9a65343974e5e58a.jpg)
近景处的阴影还需要一些细节处理:
- 添加contact & self shadow
- 额外增加两张shadowmap,每张尺寸为1536
- 由美术同学控制那些物件需要参与到这两张贴图的绘制中来
![](https://img.haomeiwen.com/i19200103/a98263c01dac34b1.jpg)
参与close-up shadow的物件选取逻辑:
- 10m半径内所有被标注为需要参与绘制的物件
- 蒙皮的模型?
(光源视角下)这类shadow的远近平面需要匹配上对应阴影的覆盖范围
![](https://img.haomeiwen.com/i19200103/9a81df74f62df6b0.jpg)
处于frustum之外的投影物则直接投影到近平面上(远平面的说明超出承影范围了,不用考虑)
![](https://img.haomeiwen.com/i19200103/5fd9e2245d61bea1.jpg)
![](https://img.haomeiwen.com/i19200103/ccf2f8114dcb8395.jpg)
角色阴影的品质看起来确实有很大的提升。
![](https://img.haomeiwen.com/i19200103/a522ba0e4d359bd7.jpg)
这里是整体的阴影贴图的内存预算,总计消耗约276M(作为对比,UE的VSM默认有两张16k*4k用来存放vsm page的大RT,总共512MB)
![](https://img.haomeiwen.com/i19200103/3dc7759a14042485.jpg)
![](https://img.haomeiwen.com/i19200103/74aa4658dbb9a345.jpg)
整体性能:
- 每帧平均需要更新15~20个shadowmap,这里没有做数量限制,所以极端情况可能会飚得非常高
- 前面展示的视频中,每帧花费1.5~3.5ms(看起来是可以接受的,当然,这里是PS主机)
- Close-up Shadow的花费则跟具体的场景、视角有关,可快可慢,当场景中有树木这种需要做大量alpha test的物件的时候,消耗会高一些(为啥?)
![](https://img.haomeiwen.com/i19200103/e50e4f8e5df5665d.jpg)
体积光的渲染,采用的是Unified volumetric lighting方案(给了参考文献):
- 针对光照的覆盖深度做了适配
- 使用checkboard rendering方案
- 加了TAA降噪
- 可以受到直接光或者间接光probe的影响
- 在烘焙GI的时候,需要考虑fog的影响(没理解错吧?这个具体咋做),以实现对多次散射的模拟
![](https://img.haomeiwen.com/i19200103/0b7080a54798986d.jpg)
当某个物件比较细,尺寸小于两个采样点的间距,就有可能导致漏光
![](https://img.haomeiwen.com/i19200103/8dd66ec8131dfe1f.jpg)
这里给了解决方案:
- 每个tile存储场景的最大最小深度
- 在进行逐射线采样的时候需要判断当前点是否是已经被遮挡了
- 在采样的时候添加一个深度的偏移(否则就会出现上图中间小图这种边缘模糊的效果)
![](https://img.haomeiwen.com/i19200103/e53d8295564651e3.jpg)
![](https://img.haomeiwen.com/i19200103/feea515249371897.jpg)
路灯都是体积光实现的,17盏灯,大概1.8ms
![](https://img.haomeiwen.com/i19200103/eaa3920ec93c6ae7.jpg)
![](https://img.haomeiwen.com/i19200103/335012bb7aa548ee.jpg)
![](https://img.haomeiwen.com/i19200103/56f614e8cb21bb83.jpg)
之前研发Beyond:Two Souls的时候采用的是上图描述的方案,静态物件跟动态物件做了分别处理,但是研发底特律的时候希望用同一套方案兼顾所有的物件。
![](https://img.haomeiwen.com/i19200103/224d75abaf5c0434.jpg)
基于Probe的方案有如下细节:
- 适合用于IBL(那就不只是烘焙Diffuse,而是要烘焙高光cubemap了)
- 会在离线烘焙GGX NDF(需要通过重要性采样规避光斑),GGX是高光BRDF计算中的一项,取决于场景的粗糙度以及法线跟微表面法线的夹角,不知道这里突然提到这一点的目的是啥?
- 美术同学最好能够控制,不知道这里说的控制是指摆放位置吗?
![](https://img.haomeiwen.com/i19200103/8df96f74ca9b669f.jpg)
Diffuse Probe Grid的问题:
- 漏光,暂时没有完美解决方案,有众多的人对此做了研究,这里给了两个有意思的参考方案
- 插值的不规律:虽然效果影响不明显,但是在底特律这边会存在问题(啥问题?)
![](https://img.haomeiwen.com/i19200103/b34171ac2dc5be00.jpg)
常见的解决漏光的方案是拒绝掉被遮挡的probe,但是这种做法会导致上图所示的瑕疵。
最终底特律采用了其他的方案
![](https://img.haomeiwen.com/i19200103/e8ae30a41711361c.jpg)
这里将probe数据用稀疏八叉树来存储:
- 用一个volume来指定覆盖范围
- 采用自适应的算法来控制probe的布局与密度
- 会给美术同学一个工具来控制probe的布局:
- 密度zone(控制密度?)
- 在物件或者墙体周围,基于程序的体素化结果来调整probe的分布
![](https://img.haomeiwen.com/i19200103/394b645b86fda253.jpg)
八叉树上每个顶点对应于一个probe,也就是空间中的任意点,都可以找到与之匹配的8个probe。
漏光问题的解决是通过对probe的位置做一个(虚假的)偏移来实现。
![](https://img.haomeiwen.com/i19200103/71fbb5744046fdeb.jpg)
这里也设计了一个probe attractor,用于将probe朝着这个volume聚拢,这里有两个作用:
- 提升probe的有效密度
- 将probe沿着墙壁布局,可以更好的应对漏光问题
![](https://img.haomeiwen.com/i19200103/a8e07bf842b697da.jpg)
这里还设计了一个probe repulsor(推开、拒绝),用于隔绝掉内部(或外部)的无效probe
![](https://img.haomeiwen.com/i19200103/4663b54b61a1f85f.jpg)
再来看看偏移算法:
- 可以解决绝大部分的漏光问题
- 这个偏移是在离线烘焙的时候实现的
- 会考虑原始的grid位置
上面的小图貌似没有很好的说明具体的偏移方案,只是描述了说当(绿色)probe距离墙面有一段距离的时候,可能会选择墙外的红色probe,这时候就会出现漏光;而左侧的红色probe,由于是紧贴墙面的,所以就不会有问题(所以是在运行时自动选择最近的8个probe来做插值吗?)
![](https://img.haomeiwen.com/i19200103/d222f42bd9b70a18.jpg)
如果某个地方存在漏光(如何检测到?),就在这个地方对grid做细化分拆,并通过将probe靠近surface来规避。
当然,也有其他的规避方式,这是因为漏光比漏暗要更为明显。
![](https://img.haomeiwen.com/i19200103/8b594ad03409f203.jpg)
这里给了一个例子来介绍通过修改颜色来对比效果,但是好像不是很能看出来。
![](https://img.haomeiwen.com/i19200103/19f9bdd00b5cfb66.jpg)
当高密度跟低密度probe并存的时候,就会出现有的box的顶点并不能很好的匹配相邻边的密度,这时候会需要通过插值来补齐probe,从而保障下面的二叉树子节点都是满的。
![](https://img.haomeiwen.com/i19200103/b829707c13f1efa5.jpg)
如上所述,我们的稀疏八叉树可以大概用类似的二叉树结构来类比
![](https://img.haomeiwen.com/i19200103/ea7de53a77895c4f.jpg)
我们存储的话,只需要存储上面的叶子节点(对应于8个corner的probe数据)的数据,每个节点存储8个probe,但是这种做法由于相邻probe的共用,会有大量的冗余。
![](https://img.haomeiwen.com/i19200103/c712c7dc68976041.jpg)
这里尝试以3x3x3为一个单位来存储,冗余会少一点。
这里的疑问是,为啥不单个probe的存储呢?这是因为,我们需要拿到各个probe的相邻关系,直接存储单个probe的话,可能就不方便拿到其相邻probe的数据,而最终的计算则是需要拿到8个相邻probe做插值来得到结果的。
有没有其他的方式来优化或者表达相邻关系呢?这里有一个前提条件,那就是GPU没有指针的概念,相邻关系必须要通过数组的索引来访问。
![](https://img.haomeiwen.com/i19200103/94a93d442a00f8d0.jpg)
Diffuse只需要低频数据,因此经常会使用SH来存储,这里使用的是2阶的SH,即每个probe只需要占用四个系数,此外,这里也提了一个Geomerics重建算法,不知道其作用是否是提升2阶SH表达的精度。
最终针对每个颜色通道,都需要一张RGBA 16F的贴图(这里的RGBA对应于四个系数),而总共场景需要24055个probe,按照前面的存储方法,大概需要105x105个节点,每个节点存储9个probe,因此总计是3MB的内存消耗。
![](https://img.haomeiwen.com/i19200103/642afe82664c4394.jpg)
在使用的时候,由于不需要剔除,因此直接使用硬件的线性混合算法即可。
![](https://img.haomeiwen.com/i19200103/23c806d6ed2cd331.jpg)
而从世界坐标到采样节点对应数据的索引的转换,则是通过一个hash算法来实现,只需要O(1)的复杂度。
![](https://img.haomeiwen.com/i19200103/b6680a19b378ec9b.jpg)
基于这种做法,还有如下的好处:
- 可以很方便的实现GI效果的切换,切换可以用于灯的开闭等开关的支持
- 可以实现渐变的切换,如TOD,窗帘的拉开等
![](https://img.haomeiwen.com/i19200103/a1a30d8371d2e6d8.jpg)
这里给了一个演示效果
![](https://img.haomeiwen.com/i19200103/f9e9a105d86a1c69.jpg)
在切换的时候,需要注意一点,那就是需要避免Scene Zone之间数据的硬切,比如从室内到室外,解决方案是:
- 在切换的SceneZone中设计一段走廊
- 在走廊中的动态物件需要同时采样两套GI数据
- 基于到不同SceneZone的距离来加权
![](https://img.haomeiwen.com/i19200103/75e2736e1bf06b81.jpg)
![](https://img.haomeiwen.com/i19200103/f7c60f14a528f61a.jpg)
这里给了一个演示视频
![](https://img.haomeiwen.com/i19200103/d69b187c5f193be6.jpg)
大多数的静态物件都是出于同一个SZ中的,对于门窗等同时处于室内外的物件,则需要做特殊处理,同时采样两套(其实是不是可以根据相机位置,决定采样哪一套?)
![](https://img.haomeiwen.com/i19200103/b1ce4baa70fba8e0.jpg)
需要手动标注这类物件
![](https://img.haomeiwen.com/i19200103/abb27bb432982bfb.jpg)
![](https://img.haomeiwen.com/i19200103/5b38513cff6e2b57.jpg)
这里是结论
![](https://img.haomeiwen.com/i19200103/fa2b5d439a47840c.jpg)
网友评论