HDRP Water & 云影

HDRP Water & 云影

【博物纳新】专栏是UWA旨在为开发者推荐新颖、易用、有趣的开源项目,帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果,并探索将其应用到自己项目的可行性。很多时候,我们并不知道自己想要什么,直到某一天我们遇到了它。

今天推荐的两个项目来自UWA开源库:
1)HDRP Water
2)云影

01 HDRP Water

ShaderGragh是继2018版之后出现的一种针对Shader的可视化编程工具,通过在ShaderGragh面板中编辑各种效果模块的连接方式,可以实现不同的效果。该工具将编辑Shader代码的操作简单化和可视化,也提高了Shader的编辑效率。

该项目是通过ShaderGragh制作的简单水面着色器。通过对该项目的学习,读者不仅可以对该工具有初步的认识,还可以对水面的实现原理有所了解。

一、原理概述

该项目从原理上可以划分为三部分:顶点波动、法线流动和边缘着色。

  1. 顶点波动:该部分主要计算通过3个参数各不相同的波叠加之后,平面上各个顶点的纵向位移规律。
  2. 法线流动:顶点的法线信息是通过法线贴图获取的,通过控制法线贴图的UV坐标偏移来实现水面的流动效果。
  3. 边缘着色:该部分主要通过深度图检测水面边缘,而后在边缘处加上了颜色渐变效果。

二、具体实现

1. 顶点波动
在渲染管线中,对顶点的编辑通常会放在顶点着色器(下图中的Vertex模块)中:

图中顶点着色器主要就是在计算Position,而从上半部分可知,可控输入包括3个波的波长、波速和振幅。

打开上图的3WaveGenerator,可以发现这三个作为输入的波的处理方式几乎相同,只不过每部分对应的参数稍作变换,目的是使得三种波之后的叠加避免谐振,从而让最终叠加后的波形显得更加自然。拿其中一个波来看,可以看到只有波的x和z值参与了计算(如下图),因为这里要先模拟水波的横向传播部分:

之后,通过乘上时间参数产生实时偏移,来模拟波在xz面上的传播,也就是最终水波的流动效果:

但是我们知道,波的横向传播并不是真的把波上的某个顶点随着波的传播方向“运送”过去,其作用的只是顶点在纵向的振动位置而已。所以这里计算顶点的x和z归根结底是为了算纵坐标,这里通过正弦函数求得:

并且三个波经过计算而叠加之后,作用的也只有模型上各点的纵坐标而已:

但我们要知道该纵坐标是包含了波的传播特性的,因为它是根据x、z坐标和时间算出来的。所以此时如果观看模型,它已经可以波动,且参与叠加的波越多、差别越大,波动就会越自然,不过由于法线信息尚未更新,因此远处的顶点是看不出起伏状态的(如下图)。

2. 法线流动
之所以称顶点为“波动”,法线为“流动”,是因为顶点只是在纵轴上运动,而法线贴图是在水平方向运动。而贴图的“流动”,其实就是对贴图采样点的定时偏移。其主要部分如下图:

值得一提的是该Shader同时输入了两张法线贴图(如下图)。其中第一张法线贴图描绘的比较陡峭、颠簸(下图左),旨在塑造波长较大的水波,而第二张贴图比较轻微、柔和(下图右),旨在塑造细小的波纹:

一般情况下,水面的法线贴图是需要有两张的,因为这样可以塑造出不同波长的水波互相干涉的场景。下图为单张法线贴图输入(左)和两张法线贴图输入(右)的效果图,可以看出来右边的水面描绘了更多细小的波纹。

3. 边缘着色
边缘需要通过深度来判断,下图中,上面的框是在拿当前点的深度,下面的框是在计算当前点对应的像素在深度图中的深度:

这里用两个深度的差将作为边缘色的分母进行除法(下图中Divide部分),那么可以知道只有深度差比较小的时候才能显现出边缘色,也符合现实中“接近边缘才会明显变色”的情况。最后通过Clamp把颜色限制在合理范围:

下图分别是添加了边缘着色(左)与不添加(右)的效果,可见添加该功能会让场景变得更丰富、真实。

02 云影

在以自上而下视角为主的游戏(策略或建设类)中,云影是比较常见的功能。通过控制其形状、移动等各种参数,可以逼真地反映出天气的变化,这对游戏场景的视觉效果及气氛烘托都起到了比较重要的作用。

本篇的Unity Cloud Shadows就是一种动态、高效的云影工具,不仅可以很方便地调节云影的多种参数,也不会对性能造成很大压力。

一、原理概述

该工具的实现主要依靠的就是光源的Cookie功能。官网解释:“如果创建包含Alpha通道的纹理并将其分配给光源的Cookie变量,则会从光源投射剪影。剪影的Alpha遮罩会调制光源亮度,从而在表面上产生亮点和暗点。这是场景增加复杂性或氛围的好方法。”

该功能的位置如下:

二、具体实现

当运用光源的Cookie时,即使场景中没有物体,Frame Debugger中也会出现下图中下的Draw Dynamic,而Draw Dynamic就是在画云影,也就是下图中的噪声图:


图1:Frame Debugger

然后,我们来看一下CloudShadows.cs。其实最核心的就是RenderCloudShadows这个函数,从它的Blit函数中我们可以看到,在每一个云层所属的for循环中,有且只有一个Pass会被执行:


图2:RenderCloudShadows函数(from CloudShadows.cs)

那具体是怎样被渲染的呢,我们看这个材质球的Shader,也就是CloudShadows.shader,几种Pass工整排列(如下图),分别对应上图红框中不同的混合模式,当然这个混合模式是可以从外部直接选择的。


图3:不同混合模式对应的Pass(from CloudShadows.shader)

几个Pass主要是从片元着色器作了区分,下图以FragSubtract类型的片元着色器举例。从图中可以看到,我们只在乎a通道的结果,这是由于Cookie功能只会识别目标Texture的a通道。


图4:FragSubtract函数(from CloudShadows.shader)

这一点也可以在Frame Debugger中得到体现,只有点击下图中的a通道时,Game窗口才会显现出图像,而其他通道不会让Game窗口显出内容:


图5:Frame Debugger中Draw Dynamic的a通道

该工具提供了许多种云影的预制体,不过每种预制体的差别其实就是下图右侧的三种模式的调节方法不同而已。我们可以看到调节方式主要有三种:混合设置(叠加、差值等)、云层设置(速度、方向等)和贴图设置(噪声图选择等)。


图6:该工具的预制体

该工具的用法其实并不局限于噪声图,依照Cookie的特性,任何有透明通道的Texture都可以被附在该工具上,然后进行叠加、移动等,用法比较灵活。效果图如下:


图7:整体效果图

三、Tips

看图2的309行,其实全篇一直都有两个RT:m_RenderTexture1和m_RenderTexture2,并且312行还要交换一下两个RT的引用,为何呢?

其实只是在配合Blit函数,它需要有两个RT作为输入,所以我们不得不多创建一个作为“替补”,这也就是为何在下图中,每次都只需要重置m_RenderTexture1了,毕竟我们只需要操作一个,因为只需要一个RT传给Cookie。


图8:每次只重置m_RenderTexture1(from CloudShadows.cs)

四、性能分析

通过UWA GOT Online在低端机型荣耀畅玩9A(4GB RAM)上进行了性能测试后,得到了下图的FPS曲线,其中中间凹陷处是开启了云影功能的时刻。


图9:FPS曲线图

可以看出云影开启之后,FPS均值保持在原来水平,而当我们看具体堆栈,代码层面增加了插件中的CloudShadows.Update,不过耗时很低,大约0.7ms:


图10:CPU耗时曲线图

该插件是一个轻便又简单的插件,值得一试。