
前言
提到屏幕空间的反射,熟悉 3d 渲染的同学应该第一时间会想到 ssr,需要深度图、法线、raymarching。
相对来说,2d 的反射就简单多了,不需要复杂的算法,我们就能实现反射效果。
比如光滑的大理石地面、水面等。
本文就以水面为题,做一个动态的2d屏幕的反射效果。

原理
2d的反射,不需要深度相关信息,其实就是对反射物体的基于自己y方向的一个镜像,可是我们也不能简单的实例化一个自己,然后scale的y给-1。这种方式虽然也能实现镜像,但是会让场景的渲染开销成倍增加,并且达不到我们想要的细节效果。
但我们真正想要的,是这个镜像被渲染到接收反射物的物体上去,比如水面,而且不想除了水面的其他物体接受反射,比如陆地,石头等非水面区域。
所以我们需要利用后处理,把想要反射的物体单独绘制成一张图,然后对这张图进行处理,最后输出到水面的材质里,作为水面渲染的一部分。
反射物体渲染
第一步,创建一个 cocos 自带的 3d 的 mesh片 quad,作为反射物的子物体,scale 要和反射物的 width 和 height 一致,如果你想反射效果拉长或缩短,那就调整它的 height。
第二步,给它一个自定义的 layer,我这里叫 reflect,然后我们自定义个材质,赋予它反射的魔法。

我们要对它做 y 方向的镜像,其实就是在模型空间对它的顶点的 y 值取反,然后再转到世界空间,shader 的顶点着色器代码如下:

如上所示,vec3(1, -1, 1) * a_position;
就是对模型空间y值的取反。现在它就已经倒过来了,转到世界空间后,我们要给他个坐标偏移,因为锚点在图的中心,所以倒过来也是基于这个锚点翻转的,所以我们要让它向下偏移,偏移到脚对脚,一般来说,这个 offset 设置为图片的 height 就行。
片段着色器没啥好说的,就是把反射物的图给它,采样就行了。
这里要提一个 mesh 的好处,就是勾选use instancing,可以让我们的相同材质相同 mesh 的物体通过 gpuinstancing 合批。

反射相机
做完上一步,我们就获得了物体的反射效果,原理中说了,我们要把这些反射效果绘制到一张图中,然后对这个图一顿操作,所以这个图应该只有反射的倒影,那就需要一个 camera 去专门做这个事情了。这也就是我上面为什么给我们的 quad 一个 layer--reflect。记住主相机的 visibility 不要加 reflect。这时候,我们的反射相机大概会渲染成这样。

反射图拿到了,我们要对它进行操作了。
自定义反射效果
我们要模拟好看的反射效果,就要考虑你现实中看到的反射是啥样的,答案是跟接受反射的物体相关,比如地砖等相对光滑的物体,你能看到相对清晰的倒影,而稍微粗糙点的表面,可能看到模糊并且淡淡的倒影,而对水流的倒影,你还会看到它随着水流来回抖动。那我们就先定义它的清晰度和模糊度。
到这一步,我们就要用到自定义后处理了,有关自定义后处理的知识,cocos 官方文档和社区开发者分享过很多,如果不了解的请自行查找,本文不做介绍。
我们到目前为止已经拿到了反射图,自定义清晰度其实很简单,就是调整图的 alpha 就行了。

_reflectpprt
是反射图,_intesity
就是清晰度。
模糊呢,可能很多人会想到高斯,那太复杂了,毕竟是反射的模糊,不要求那么完美,简单的对周围像素求个均值就行。shader 如下:

cc_nativesize
,它是系统内置的 vec4
变量,xy
是屏幕的宽高,zw
就是宽高的倒数,也就是屏幕 rt
的纹素,_blursize
是模糊度,越大越模糊,这里是对当前像素相邻四个角的像素取平均值。
shader 里我会拆分成 2 个 pass,就如同上面两个代码截图,第一个 pass 是没有模糊的,第二个 pass 是有模糊的。然后在后处理的代码里,我通过是否勾选模糊来判断走哪个 pass。

后处理编辑器如图:

intensity
就是清晰度,open blur
是判断是否开启模糊,会影响上面走哪个 pass
,blur size
就是模糊度。
到这一步,我们自定义了反射图的模糊和清晰度,如果是地砖等静态的模糊,我们做的工作就差不多完成了,只需要把反射图赋给地砖做渲染就行了,而如果做水的反射,那我们还得做动态扭曲。
把它交给水吧
反射 camera 经过自定义后处理,我们把它渲染到自定义的 rendertexture
,我起名叫 reflect-rt
,然后就把它交给水做最后的渲染吧。
话不多说,直接上代码,下面是水的shader。

主要看 use_reflect_map
这个宏定义的部分,这是水是否有反射效果的开关,而 use_reflect_noise
这个宏,是反射是否开启抖动的开关。
我先说说我们要干吗,我们要反射图抖动起来,那采样反射图时它的 uv
就得是抖动的,怎么让它的 uv
抖动呢,我们得用一张噪声图去影响这个 uv
,然后让噪声图动起来,然后就发生了连锁反应。
我们来看这行代码,vec2 noiseuv = v_uv_reflect * _reflectuvscale - cc_time.x * _reflectnoisespeed;
其中:
-
v_uv_reflect
是水的当前像素在屏幕上的uv -
_reflectuvscale
是控制这个uv的大小。 -
_reflectnoisespeed
是噪声的速度,所以这行代码是搞噪声图uv的,让噪声图的uv动起来。 -
_reflectnoisetex
噪声图,动态的uv采样得到动态的噪声,我们只关心noisecol的r值就行,它是从0到1的 -
_reflectnoisestrength
是抖动的程度,最后我们把这个噪声加到反射图的uv--reflectuv上去 -
_reflecttex
反射图
这样,我们就得到了反射图的抖动效果。
最后,我们用反射图的alpha值对水的颜色做一个混合,反射图的alpha大家还记得是什么吗,就是反射图的清晰度啊,这个值越小,反射就越淡。
我们看一下效果。

这是清晰度为 0.5,模糊度为 1.0 的效果,扭曲参数如下图:

看英文名字应该就能对号入座了。不多解释。
如果不要扭曲抖动,取消 use_reflect_noise
勾选即可,那些抖动相关的参数也不用设置了,比如下面的地板反射效果

到此为止就是全部的内容了,如果有不明白的欢迎留言探讨。
往期文章
-
2d光影系统-光源一:全局方向光 -
https://forum.cocos.org/t/topic/156363 -
2d光影系统-光源二:sprite光 -
https://forum.cocos.org/t/topic/156390 -
2d光影系统-阴影 -
https://forum.cocos.org/t/topic/156536 -
2d描边-内描边 -
https://forum.cocos.org/t/topic/156861 -
2d描边-外描边 -
https://forum.cocos.org/t/topic/156878
发表评论