如何做一个卡通水面效果
本文参考自教程,加上自己的一点心得体会。
首先,根据水面和场景的深度差异划分为浅水和深水,浅水和深水各取两个极值,然后根据深度进行插值。所谓的浅水,就是水面下的物体与水面的距离较小,颜色较浅;所谓的深水,就是水面下的物体与水面的距离较大,颜色较深。
那么,如何获得场景深度呢?Unity引擎自带了**_CameraDepthTexture**,但这是基于屏幕坐标系的texture,如何在frag shader里去采样到正确的值呢?对于顶点
v
v
v而言,我们先通过UnityObjectToClipPos将其转换到齐次坐标系,此时
v
.
x
v.x
v.x和
v
.
y
v.y
v.y的取值范围均为
[
−
v
.
w
,
v
.
w
]
[-v.w,v.w]
[−v.w,v.w],然后根据视口变换:
s
c
r
e
e
n
.
x
=
(
v
.
x
∗
0.5
+
v
.
w
∗
0.5
)
∗
s
c
r
e
e
n
.
w
i
d
t
h
/
v
.
w
screen.x = (v.x * 0.5 + v.w * 0.5) * screen.width / v.w
screen.x=(v.x∗0.5+v.w∗0.5)∗screen.width/v.w
s c r e e n . y = ( v . y ∗ 0.5 + v . w ∗ 0.5 ) ∗ s c r e e n . h e i g h t / v . w screen.y = (v.y * 0.5 + v.w * 0.5) * screen.height / v.w screen.y=(v.y∗0.5+v.w∗0.5)∗screen.height/v.w
我们发现,实际的采样坐标即为 ( s c r e e n . x / s c r e e n . w i d t h , s c r e e n . y / s c r e e n . h e i g h t ) (screen.x / screen.width, screen.y / screen.height) (screen.x/screen.width,screen.y/screen.height)。Unity提供了两个内置函数帮助我们进行采样:ComputeScreenPos和tex2Dproj。ComputeScreenPos这个函数得到的结果为 ( ( v . x ∗ 0.5 + v . w ∗ 0.5 ) , ( v . y ∗ 0.5 + v . w ∗ 0.5 ) , v . z , v . w ) ((v.x * 0.5 + v.w * 0.5), (v.y * 0.5 + v.w * 0.5), v.z, v.w) ((v.x∗0.5+v.w∗0.5),(v.y∗0.5+v.w∗0.5),v.z,v.w),tex2Dproj函数会对传入的采样坐标进行透视除法再采样。
然而,采样出来的深度值是经过透视投影变换后的深度,已经不再是线性的,我们需要把它还原为原始的深度值再进行使用。这其实本质上是投影变换的逆变换。Unity也提供了一个内置函数:LinearEyeDepth。通过该函数得到原始深度,与水面的原始深度做差,利用这个差异值就可以对颜色进行线性插值了。有两点需要意识到:
- 场景深度texture里保存的深度信息是水面渲染之前的,也就是说是不包含水面的深度信息的,另外,在开启early-z的情况下,水面frag代码执行的条件就是水面的深度值要比场景深度值要小
- 水面的原始深度值就是ComputeScreenPos这个函数返回的 w w w。在进行透视除法之前,原始深度值一直保存在vertex的 w w w分量
综上,以上实现的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进行平滑。