首页 > 渲染TA实战:移动端风格化SSR水体开发分享
头像
搜狐畅游
发布于 2023-07-03 11:27
+ 关注

渲染TA实战:移动端风格化SSR水体开发分享

哈喽,这里是畅游引擎部的技美Joon。近年来,随着移动设备性能不断提升,市场和业界对于游戏画面表现的期待越来越高,很多以前难以在移动端实际使用的渲染效果也逐渐进入项目的考虑范围。而实时反射,就是其中一个重要特性。

如果您对于渲染稍有了解,应该会知道,实时渲染领域苦反射久矣。为什么这玩意难呢?那是因为反射意味着光线至少需要进行一次弹射之后(实际情况那必然不止一次),再进入观察者眼中。也就是想要实现实时反射效果,就免不了光线追踪。而真实感的光线追踪,在pc上跑都费劲,何况在移动端呢。所以,就算是现在的移动设备性能不错,想要其能够高性能地跑光追,也得是在种种约束条件之下,对质量进行一定的妥协,才能做到。

那么,用咱们现在移动端桎梏重重下的光追,在保证性能的前提下,做哪种材质的反射效果能达到表现要求?答案其中之一就是水体反射,特别是波光粼粼的那种水体效果。咱们先来看一下真实世界中的水体反射:

笔者摄于古水北镇,慢门拍摄

可以观察到,真实世界中,这种带着轻微波浪的水反射成像已经基本上看不清实际的反射物,而只有轻微的轮廓。事实上,自然世界中真正如镜般的水面几乎是不存在的。网上看到那种镜面倒影的水面照片,也得是在拍摄时利用诸多手段,譬如偏振镜滤掉水的反光、慢门拍摄以及后期修图,才能达到镜子般的效果。而不需要清晰的反射成像,使得渲染水面实时反射的代价大大减少。

笔者摄于剑桥

从第二张图可以观察到,水体反射还有一个特点就是,对于远处的水面,我们的观察角度非常接近掠射角,从而导致水面细小的起伏都能对反射造成很大的干扰,同时也会使得视觉上感知到水面的高光更加密集。在这两点同时作用下,越远的倒影会越容易被高光遮盖,同时也会更加的模糊扭曲。这一点在实现水体反射的时候也可以考虑进去。

固然,真实感的水体渲染非常复杂,并不是靠笔者几张照片胡乱分析一下就能概括的。但是,越接近真实,越接近物理正确,那性能就愈加难以接受。咱们要在游戏里跑,首先得要让玩家流畅地运行,首先追求一个“有”,条件允许之后才能去追求“优”。所以,既然移动端上难以做到真实,那咱们起码还可以追求艺术性,也就是风格化。可能我们无法模拟真实水体作为不均匀介质而导致的颜色细微变化,但起码我们可以用深度差做阈值给水体颜色营造层次感;可能我们对于物理正确的波浪起伏效果无从下手,但起码我们可以用法线图UV动画的方式来让表面“会动”“有浪”。听起来确实是有点寒碜,但人们想要通过各种手段去表达真实世界的意愿一直是无比强烈的。而在大体比较接近真实世界的渲染效果中,加入一点自己的创意和思考,很难说这不是人们自己对于大自然的进一步理解和交融。

接下来,分析一下实现咱们的目标,风格化的水体实时反射,所能使用的技术。很凑巧,上个月(2022年11月)发布的骁龙8gen2和天玑9200,都相继实现了“移动硬光追”。笔者目前还没在这些设备上测试过光追性能,只是听业界朋友说,由于手机功耗限制,光追的性能非常受限。而软光追,我认为短时间内也不可能有实际应用,或许目前最大的盼头就是看UE5什么时候能在手机上跑Lumen。既然真正的场景光追跑不了,那咱们只能把希望寄托在光栅化就能实现的屏幕空间反射(screen space reflection,SSR)上了。(PS:其实也考虑过平面反射,但是与项目沟通之后考虑到大场景开发迭代中管理反射物会比较麻烦,故没有使用。不过这也是一个水面反射常用的实现手段,特别对于小场景来说很好用。)

对于这个概念不熟悉的读者,建议先在网上了解一下相关知识。在这里推荐一下闫令琪老师的GAMES202,看完之后可以对现代实时光追技术有一个宏观的了解。要在工程上实现一个能用的SSR水体,那自然不止要写SSR和水体渲染就行。为了更好地平衡性能和表现,咱们还需要进行很多其他处理,以及与现有的管线进行适配,和已有效果进行正确的叠加混合,有可能的话进行rt的复用。分析表现需求,将其转化为渲染逻辑的思考过程往往妙趣横生,但是真的开始实现的时候,不免枯燥乏味,令人抓狂。从此往后,笔者会注重于整个实现的思路介绍,一些工程上的实现细节以及一些趟出来的坑,希望能对想要实现类似效果的开发者们有所帮助。

本次使用的是Unity 2021.3.5,URP 12.1.7版本,延迟管线,基于URP的RenderFeature接口实现。先放个视频看看最终的效果(场景素材来自网络):

视频见链接:https://zhuanlan.zhihu.com/p/595238274

性能方面,在移动端(米10为基准)复杂场景打包测试中,此效果的开关对于帧率基本没有影响,具体的毫秒数还待打包测试。并且由于目前还在造轮子阶段,为了方便后续移植,一些可以集成到管线中的代码写在了外部,项目实际使用时可以进行进一步优化。

总的实现流程如下图,接下来开始逐步介绍:

  1. 定时渲染天空Cubemap:渲这玩意呢主要是为了适配TOD(time of day,即动态的天空大气)系统。众所周知,所谓屏幕空间,就是咱们只能拿到屏幕上的信息。楼太高了超出屏幕上沿了,那底下的反射也会被截断了。以往为了弥补这种瑕疵,大家一般是采样一张cubemap填补超出屏幕的部分,或者直接就让反射逐渐消失完事。而带着TOD的时候,这张cubemap就不能是离线烘好的,而是得实时渲染。在Unity中,实时渲染cubemap大概有两种做法:新建一个CullingMask只有天空盒的相机,调用RenderToCubemap函数,优先主相机定时渲染一张rt;以及在RenderFeature里用V矩阵模拟六个方向的相机渲到一张rt上。第二个方法其实就是自己在RenderFeature里实现一遍RenderToCubemap函数,更底层一点,最重要是移植性更好,组织结构更清晰。性能上笔者没有测试过,但猜测两种实现应该都差不多。这里用的是第二种实现,上代码:

Execute中的代码

计算六向V矩阵,主要的坑在api不同的时候rt对应的面不同。

需要手动反z。

坑基本都是些api适配啥的。这里一个还能优化的点是目前裁剪裁了六次,属于是不知道怎么让管线不进行裁剪的下策。而我试过用fov为179度的矩阵去裁一次,得到的结果也不理想,猜测是fov太大了远裁变形了。如果哪位大佬知道怎么关闭裁剪还请麻烦赐教,感谢感谢。

2. 渲染半分辨率深度:

由于咱们对于反射质量要求不高,所以降一半分辨率目前在效果上看没啥问题。而为了能走earlyZ,SSR渲染的时候绑的深度rt也得是半分辨率的。这一步要注意的是得判断下reversed_z,来确定计算深度保守值时该取最大值还是最小值。

3. 基于模型SSR计算:

终于来到了重点部分。随着屏幕空间的各种技术发展,SSR的计算方法也在不断地迭代优化,总的来说就是更少的循环次数,更好的效果。但如前文所说,性能和质量本身就是不可兼得的,所以咱们得做出取舍。

目前的SSR做法要点是:视空间步进,不做重要性采样,采样直接采颜色而不是BRDF参数,无论有没有通过深度求交都进行采样。可以说是和传统SSR的做法大不相同了。用到的优化方法只有常用的jitter,即给逐片元射线的步长加上一个随机数,使得低步进次数造成的横条状变成分布较为均匀的点(在视觉上会觉得图像更为连续),从而达到降低步进次数的目的。之前也试过屏幕空间步进的方法,但是就算使用了jitter也会采样出一道一道的明显横线,猜测是屏幕空间步进算法本身导致的。不过现在视空间设置最大步进8次就能达到效果,性能也不错,效果比屏幕空间的好,就没折腾了。至于Hiz的优化,涉及到整体管线的设计,同时也需要其他相关功能的优化和复用,属于是得给整个管线捋一遍的工作量,暂时没有进行。

至于为什么无论有没有通过求交都采样呢,是因为想要采样到实时的天空。而天空渲染在远裁,真通过射线打到远裁那射线步长和最大循环不得大到姥姥家,性能上不可能允许……而不判断求交会产生的问题,就是远处的物体反射会被拉长,如图:

产生这样的原因,是射线太短了。比如离相机近的水面理应采样树林上面的天空,但是射线根本走不了这么远。知道原因之后,要弥补就好办了,咱们让离相机近的像素发出的射线步长更长就好了:

_PixelStrideZCutoff为可调的参数(rayOriginVS.z本身为负数),这么做就可以大致解决远视距拉伸的问题。

在视频中可以看到,这里做了一个反射效果随着视角变化的渐变过渡。关于这个渐变过渡遮罩生成,核心的判断条件是,像素离屏幕下左右三个边的距离、射线打到的位置实际深度、以及射线打到的位置离屏幕上沿的距离。同时,还使用了相机俯仰旋转和相机离水面高度作为输入。在映射的时候,主要是smoothstep和pow来控制效果变化,最后得到的渐变值保存在SSR绑定rt的a通道中。

最后要提一下的是关于Unity各个空间转换的坑。一开始解决这玩意以及api适配花了大量的时间精力去debug。一个比较坑的是在片元里通过屏幕坐标和裁剪的z重建视空间坐标:

重点在要对zbuffer的深度也进行映射,而Unity自带的相关api都没有封装这一句……唯一一个实现在动态模糊的shader里,偶然之间发现:

实在是很神秘。虽说视空间的实现可以通过顶点插值来得到vs坐标,但是屏幕空间步进就不得不进行这个转换了。4. 抖动采样:

这一步,是为了弥补屏幕空间的信息不足。由于水进行扰动之后,采样的坐标会移动,而如果咱们渲SSR的时候中间有个东西挡住了场景,得到的结果中间就会有一个黑色区域。如果不处理,就会如下图:

弥补的方法,就是判断当前像素是黑色的话,就往左右移动uv采样。移动的距离和次数与水体法线扰动程度有关。得到的结果如图:

这是对于中间黑两头有颜色的处理方法。如果只进行这样的判断,一些情况下会出现不需要的颜色:

对于中间有颜色两头黑的情况,就得比较取巧的判断了。目前做法是左右采样采到颜色之后,往反方向继续采样,如果采样到了颜色再对当前像素进行赋值。这样做依然不能解决所有抖动采样出现的问题,但起码能解决一部分。

5. 水体渲染:

这个部分的做法,就是各家有各家的美术要求,风格化嘛,解决方案没有什么标准。这里的实现由于不需要表现波涛汹涌的水面,所以没有加入顶点偏移。大概总结一下一些实现的效果:深浅水颜色,岸边泡沫,使用Worley噪声的焦散,Blinn-Phone高光,两张法线图叠加扰动,菲涅尔混合反射折射。其中前三个需设置深度阈值。以及关于前面提到的距离越远扰动越剧烈,可以把法线扰动偏移和深度建立函数关系,至于多远想要扰动多剧烈设置一个参数调节就好了。

值得提一下的是反射部分。如前文所说,咱们现在有一张每隔一段时间刷新一次的天空盒cubemap,以及通过SSR得到的实时反射rt。这两张rt是通过之前计算的ssr渐变遮罩值来混合的,他们共同组成咱们水面的反射颜色。具体的效果就是,反射物超出屏幕的部分,会替换成定时刷新的天空,配合TOD系统和雾效,整体观感是比较自然的。

其他部分的具体实现,网络上有大量的文章任君挑选,代码基本都是现成的,工作量主要是在对美术表现和渲染语言之间的翻译上。按我这边实现的过程来看,全都是一些渐变过渡效果,勤用smoothstep、pow和lerp,设置一大堆可以调节的阈值,完活。

分享到这里就结束了。这一套东西一路做下来,能达到现在的效果和性能,离不开几位引擎大佬的帮助,我也是受益良多,在这里对他们表达最诚挚的谢意!同时,也深感渲染领域还有太多太多的东西值得去学习。道阻且长,行则将至,与君共勉!

本文权当抛砖引玉,如果上文有任何错误的地方,或者有更好的做法,还请不吝赐教,感谢您的阅读!

欢迎加入我们!

感兴趣的同学可以投递简历至:CYouEngine@cyou-inc.com

全部评论

(0) 回帖
加载中...
话题 回帖