b7a15daae4d083f876b6ca329781fd94.png

做了这么久可是我们的管线依然没有全局光照,这是一个很尴尬的事情,因此,我们决定给管线加上这个功能。

那么我们首先要考虑的是技术的选型与管线的适配,之前文章中已经讲过,我们采用了Cluster Based Lighting,配合Shadowcache,使得性能在光影数量较多的情况下还能保证比较好的效果,Shadowcache可以使得静态的灯光在一帧内烘焙好一个深度,并在接下来几帧后更新,除非动态物体经过,否则是不需要更新的,,这也使得动态光影的性能限制被大大放宽,是一种成本较低收益较高的空间换时间的实现。

在我们面前摆着Unity提供的Subtractive, Shadowmask, Bake Indirect这三种,第一种通过将Irradiance和Light Color同时烘焙到光照贴图里,实现纯静态的光照,第二种将阴影的Mask和间接光烘焙到贴图中,只有直接光实时运算,第三种只会烘焙Indirect光照,也就是一张普通的自发光贴图。首先我们毙掉第二种,因为我们使用的CBDR如果费力的组织Mask bit,不仅十分麻烦,而且这还需要延迟渲染专门提供一个GBuffer,在PC或主机上,带宽往往是一个很容易产生短板的地方,这样突然多4个通道的操作也是很亏的。Subtractive直接强行烘死所有的灯光,这当然没问题,配合Directional Irradiance效果也足够好,然而对于动态物体的支持却很不怎么样,刚刚已经说过,我们的实时灯光计算效率本来就很高,完全没必要用静态灯光,所以最终选择了只烘焙间接光,同时配合实时光影的运算。

首先打开正常的Unity内置管线和Standard Shader并开始烘焙(为了省时间就暂时用Enlighten凑活一下):

451e884811d01a6dcdfcccdd367946b1.png

烘焙完毕后我们就可以选择存储一波光照贴图的数据,在之前的文章中我们短暂的介绍了一下Virtual Texture及其流式加载的方法:

MaxwellGeng:基于Unity3D的大地形研究(2):资源序列化与材质加载​zhuanlan.zhihu.com
7715e8970716140a9bfbbf3294159b50.png

Virtual Texture在这里的用法实际上可以理解成是使用一张Texarray或Atlas实现一个Object Pool,或者说Texture Pool,用到的贴图塞进去,用不到的贴图将其标记为删除,这样下次存入时直接去覆盖上一张贴图。使用这样的方法有诸多好处,最大的好处就是贴图的读取过程十分自由,在开放世界的开发时,每块地皮常需要数张贴图的纹理同时显示并混合,如果使用古典的Unity Terrain或者UE Landscape那种一层一层Pass叠上去的办法,绘制的时间复杂度是O(n),即使有Instance之类的也最多是个O(logN),而使用Virtual Texture可以把实时的,每帧都要做的合并过程转移到贴图空间一次性完成,使其变为O(1)复杂度。这样的方法用途甚广,无论是实时生成Detail Map,还是地形流程化生成,都非常适合,我们这里的光照贴图也不例外。

与之前的实现一样,我们在每个场景中专门储存了所有用到的GUID,GUID的唯一性使得每张贴图不会被重复加载,同时可以直接使用GUID进行二进制文件的储存和读取,存储方法如下:

2c01a782327a72b8692a8f4653e1dd10.png

每个存储位置存储着GUID和贴图的大小,因为Unity的Atlas设置的是最大值,所以有可能会有不同大小的贴图,这一点确实非常尴尬,只能强行把2048的贴图扩到4096的VT中,但是这一点其实完全可以避免!因为GPU driven pipeline对于模型如何分配十分不敏感,反正都是要统一合并的也不会产生多余DC,因此只要与美术达成共识,让美术在制作模型时尽可能拆的散一些,尽量少出现巨型的Mesh,这样在烘焙时就不会出现超出模型的三角形覆盖面积超出Atlas总面积的问题了,也就是说我们可以把Atlas的大小调整为2048,使所有光照贴图均使用2048分辨率的光照贴图,在缩小分辨率后只需要把VT的容量扩大几倍即可,就避免了VT浪费空间的问题。

合并完成后关掉内置管线,启动SRP,运行并加载场景,因为场景是异步线程中加载的,必须手动触发,所以在Editor Mode并看不到效果:

300851d1c2679a80a51bd5b7eff6e6d8.png

依旧因为偷懒原因,太阳光的运算并没有采用流明亮度,而是和内置管线一样直接怼的Lighting Scale,这也使得太阳亮度不需要额外调整即可直接进入我们自己的管线运行,除了有了Color Grading后解决了过曝问题并使画面整体变灰,渲染上依旧给人一种晦暗感,这是为什么呢?其实我们换一个角度并把雾的浓度调高三倍来看就不难发现问题所在:

7ecd352ea9026573ad0947e3b0efa9b2.png

雾浓度提高以后被灯光照到的红圈的地方已经亮的夸张了,而被绿圈圈出的暗处则越来越黑,有了几分恐怖片的感觉,这是因为光照贴图实际只能解决模型的光照问题,却无论如何也解决不了3D空间的渲染,如体素渲染或动态物体,而之前在看 @MoonChildInSky 大神的文章时,看到他制作的场景Demo非常巧妙的利用了空间的辐照度进行体素雾渲染:

d25ddc62e69c7ca5363af9013a5791bb.png

抛开作者美术造诣不谈,单说这个间接光对雾效的影响也着实令画面效果增色不少,因此我也决定在管线中加入Irradiance Volume实现对体素雾的着色。光照的辐射度计算关键来自于计算球谐,也就是Sphere Harmonics,Wikipedia上关于Sphere Harmonics的解释是这样的:

eedcd018958e39afa45c82645ea261aa.png

至于该公式的推导过程,这是物理光学的研究,并不在我们的研究范围内,我们只需要拿过来用就可以了。根据公式,某个点的权重是由该点到球心(或球心到该点)的向量来决定的,在光栅化中将表现为点的法线,决定的公式如下:

f53efdd9e4c3f94923d709d2c753820c.png

如果将其可视化:

fcb6aa014343de599771f98d4394648b.png

那么我们如何实现这个公式呢。首先我们在上下左右前后6个面分别获取6张同等大小的透视投影,合成得到一张Cubemap,由于GPU Driven Pipeline高效的剔除和绘制,使的这个过程非常迅速,即使绘制几百万个采样点,也仅仅需要寥寥数秒,而且这个过程是可以在离线状态下完成的,除非有必要的实时GI需求。因此在这里我们将直接把Cubemap烘焙出来。

烘焙后就需要套入公式,先来看公式:

0bbcd265c21b8dfcc437f2ab408ea521.png

在光栅化中累计积分着实是一个令人头大的问题,如何积分,如何高效累计都是摆在面前的赤裸裸的大问题。而这里我的解决方案是直接使用Compute Shader的原子操作进行累计。首先,在宏定义中套公式,根据法线计算权重:

865ca4248034d0ad544dcfa2f95e780d.png

把当前像素的色彩累加到一个固定的Buffer中:

e6b55b942eb688d9d6f17ab1516278d2.png

累计完毕后将整数 / 256获得浮点数,并把这串Buffer写入到体素贴图里,我们总共采用了9个常量作为Coefficient,也就是27个浮点数,所以这里使用了7个ARGBHalf格式的体素贴图存储,并依靠硬件自带的Quadlinear完成线性插值。那么为什么要使用贴图而不是直接把结果存到StructuredBuffer中并手动降维+采样插值呢?其实直接插值9个数的消耗,并采样4 * 9个数,消耗是远高于使用贴图的,因为硬件提供的QuadLinear有许多硬件层的优化,包括Pipelining, Caching等,效率要远高于我们直接在Shader中,而且也绝对是更节省带宽访问的消耗的。

获取到这7张贴图,我们就可以在目标Shader(如动态物体的Shader,Froxel采样Shader等)中通过色彩和权重完成积分,获取最终色彩:

e18f985a8fe4b8f08417b3bbb94bbe33.png

来看一下最终的效果,从图中可以看到,即使在光线没有照到的地方,雾也不再是一片死黑,已经有了一定的亮度:

1ef280f88975aa1f5b64df6e225337b0.png

进行到了这一步,可以说在全局间接光照的渲染上已经达到了一个入门的水准,已经有了高效的光照贴图加载,支持动态物体和雾效的Irradiance Volume,在下一章中我们将会开始给管线加入反射球的支持,使全局光更加完整。

Logo

昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链

更多推荐