先上效果图:
分析思路
这次我们仅针对湖面的渲染进行分析,个人建议的思考方式是先宏观,再细节。
从直观的来看,有这几个主要效果:
- 水颜色的渐变(模拟水的深度渐变)
- 白色的波浪
没错,就这两个较为直观的效果,下面的任务不是分析细节,而是针对这两项效果思考解决方案
针对效果一:
既然是模拟水的深度,那我们就可以采用基于深度着色的方法来实现,最后对不同深度值进行边界控制,分配颜色就行了
针对效果二:
首先很容易想到的是噪声图,对灰度值进行边界处理,加上UV动画,基本上大功告成了
其实分析出这些就可以开始动手干活了,大多数的细节部分是在做的过程中发现并弥补的,shader的编写也是由简入繁的,我们没必要一开始就把效果全部想明白,这会阻碍我们的创意与想法。这里由于讲解需要,就把细节部分也一并说了
细节部分:
岸边的波纹更集中
漂浮物周围存在波纹
水的颜色需要透明(根据项目需求来定,有的项目不透明效果更好,比如海水)
波纹的扰乱程度多变
波纹颜色可调(看需求)
具体实现
-
基于深度着色
为了实现第一个效果,我们需要计算湖面和地面的距离,从而根据距离对湖面颜色进行渐变。我们可以通过两者到摄像机的深度差来模拟这一过程。很幸运的是,我们可以通过_CameraDepthTexture来获取地面到摄像机的深度值,这是一个shader的内置属性,可以被全局着色器访问。它通过在Camera上添加Camera Depth Texture Mode脚本来实现对屏幕空间的深度采样,输出一张全屏大小的Render Texture
我们获取到的深度图大致如下,深度接近零的被渲染为白色,接近一的被渲染为黑色。这里的深度并非是线性的,关于其中公式算法的内容可以自行百度,注意到这些,我们需要在代码中手动改为线性深度,这方便我们进行操作与计算。
注意水面的渲染应在地面渲染和物体渲染之后,这样我们才能得到正确的深度信息,可以通过设置Queue来实现
有了地面到摄像机的深度信息后,我们需要水面到摄像机的深度信息。因为前面是在屏幕空间获取的深度值,所以我们也从屏幕空间计算湖面的位置信息。ComputeScreenPos方法可以帮助我们实现这个功能,这个方法返回一个float4类型的位置信息,其中W分量存储的就是我们需要的水面到摄像机的深度信息
关于tex2Dproj与tex2D的区别,可以简单理解为前者用来处理具有透视关系的纹理采样。相较于后者,前者在纹理采样之前,会将UV坐标的xy值除以w(个人认为这是做透视除法),实现的效果是将UV坐标从正交投影变为透视投影
- 波纹噪声图
噪声图的使用这里就不多赘述了,大体就是采样一张噪声图,利用step函数进行离散化,再加上UV偏移就差不多了
直接讲如何实现波纹的不均匀分布:
这原理其实与上一步很像,我们可以再设置一个变量,同样是对前面深度设置的阈值,让这个阈值去影响控制波纹强度的那个阈值,代码如下:
至于波纹在UV动画后较为单调,完全看得出来是平移,我们可以采用干扰图的方法来扰乱波纹。下面这张干扰图只包含红色和绿色两个通道,我们通过采样这张干扰图可以在每个片元的UV坐标偏移过程中添加一个二维向量,这就达到了我们干扰的目的
在制作的过程中可以发现,在只设置了一个阈值的情况下,漂浮物周围的波纹很小,这是由于水面与物体底部的深度差是很小的,不足以产生理想的波纹。由于水面与漂浮物的接触处法线点积值具有明显特征(趋近于0),我们可以通过采样屏幕空间法线来结局这一问题。采样屏幕空间法线的方法与采样深度的方法类似,其实Camera Depth Texture Mode里面我们也可以找到DepthNormals选项,但此选项会降低我们采样深度的精度值(将深度缓冲与法线缓冲打包成一张纹理,分被占用一个通道),所以我们采用独立渲染的方式。
首先我们需要一个脚本挂载到主摄像机上,其实现的功能是程序运行时生成一个与主摄像机同位置与旋转的子摄像机,这个摄像机用于将屏幕空间的物体法线渲染到一张全局纹理上(本案例为_CameraNormalsTexture),渲染过程使用一个单独的着色器,这个着色器的功能只是计算屏幕空间法线并传递给片元着色器。代码见文末。
屏幕空间法线图如下:
透明效果开启混合,关闭深度写入即可
至于给波浪更换颜色,我们容易发现上面功能实现后,尽管我们可以给shader添加一个_FoamColor属性来控制,但颜色值会和水面颜色混合,并不能得到我们想要的效果。