本文参考自教程,加上自己的一点心得体会。
首先,根据水面和场景的深度差异划分为浅水和深水,浅水和深水各取两个极值,然后根据深度进行插值。所谓的浅水,就是水面下的物体与水面的距离较小,颜色较浅;所谓的深水,就是水面下的物体与水面的距离较大,颜色较深。
那么,如何获得场景深度呢?Unity引擎自带了_CameraDepthTexture,但这是基于屏幕坐标系的texture,如何在frag shader里去采样到正确的值呢?对于顶点而言,我们先通过UnityObjectToClipPos将其转换到齐次坐标系,此时和的取值范围均为,然后根据视口变换:
我们发现,实际的采样坐标即为。Unity提供了两个内置函数帮助我们进行采样:ComputeScreenPos和tex2Dproj。ComputeScreenPos这个函数得到的结果为,tex2Dproj函数会对传入的采样坐标进行透视除法再采样。
然而,采样出来的深度值是经过透视投影变换后的深度,已经不再是线性的,我们需要把它还原为原始的深度值再进行使用。这其实本质上是投影变换的逆变换。Unity也提供了一个内置函数:LinearEyeDepth。通过该函数得到原始深度,与水面的原始深度做差,利用这个差异值就可以对颜色进行线性插值了。有两点需要意识到:
- 场景深度texture里保存的深度信息是水面渲染之前的,也就是说是不包含水面的深度信息的,另外,在开启early-z的情况下,水面frag代码执行的条件就是水面的深度值要比场景深度值要小
- 水面的原始深度值就是ComputeScreenPos这个函数返回的。在进行透视除法之前,原始深度值一直保存在vertex的分量
综上,以上实现的shader代码如下:
float existDepth01 = tex2Dproj(_CameraDepthTexture, i.screenPos).r;
float existDepth = LinearEyeDepth(existDepth01);
float difference = existDepth - i.screenPos.w;
float difference01 = saturate(difference / _MaxDistanceCutOff);
float4 waterColor = lerp(_ShallowWaterColor, _DeepWaterColor, difference01);
效果如图。
第二步,我们需要为水面加上一些扰动信息,来实现波纹效果。使用一张噪声贴图,直接采样出颜色与现有颜色叠加:
可以发现,由于颜色直接叠加,把大部分区域的颜色刷得太亮了,而我们想要的,其实是亮的地方颜色保持不变,暗的地方刷成波纹的颜色,因此加一个cutoff进行过滤:
接下来,我们希望靠近岸边的边缘部分也要有波纹。靠近岸边的水面和场景的深度差异较小,可以利用这一点降低对噪声的cutoff值,让边缘部分的噪声也可以add到水面上:
另外,可以再加一张扰动贴图,采样两个通道对噪声贴图的uv进行偏移,来实现流动的效果。
现在,大体的感觉已经有了。细心观察可以发现,水面与物体接触的地方,波纹应该更明显一点,也就是与物体接触的地方需要额外降低对噪声的cutoff值。这时,需要借助场景中的法线数据来处理。水面与物体接触的地方法线差异比较大,其他地方法线差异比较小。容易想到用两个法向量的点积来衡量向量的差异程度。
由于Unity自带的法线纹理精确有限,这里考虑自己渲染出一张场景的法线纹理图。额外新建一个camera,通过设置它的depth,使用SetReplacementShader方法,替换掉场景中其他所有shader,让其都用写法线的方式渲染,得到RT。这个方法可以替换掉所有对于Tag相同的shader,让它们用指定的shader渲染,对于Tag不同的,则跳过渲染,这里可以参照Unity官方文档。
接下来,我们可以通过修改shader的tag和blend模式,来让水面变得透明。同时,我们还希望能够调整波纹的颜色。为了让波纹自身的颜色能与水面本身的颜色融合而不是相加,我们采用alpha-blend的模式,让波纹自身的颜色blend到水面上,这时,之前用到的cutoff得到的结果可以用在设置波纹的alpha上。
最后,对波纹边缘处,可以用smoothstep对cutoff进行平滑。
总结一下,实现一个简单的卡通水面效果,分为如下几个步骤:
- 根据水面与场景的深度差异,划分出浅水区和深水区;
- 使用噪声贴图blend到水面上,达到水波纹的效果,注意在水面边缘,深度差异小的地方水波纹的效果大;在水面中央有物体的地方,法线差异大,水波纹效果也大;
- 通过调整blend的模式,让水波纹可以设置自身的颜色blend到水面上;
- 水波纹效果边缘可以用smoothstep进行平滑。