一、效果图
如何在画面中产生一个涟漪的效果?
仔细看上面的效果视频,不难发现扩散的涟漪就是高中物理学过的波,事实上游戏中也通常使用正弦函数/正弦波来逼近真实世界中的涟漪的效果。正弦函数/正弦波是最基础的波形,如果想要更加复杂的表现效果可以通过修改波的公式或者修改计算的坐标空间。(我们非常机智的使用了自定义的曲线来定义了波形)
我们自定义的波形
有了波形,然后呢?
有了波形并不意味着就能产生涟漪的效果,画面中的折射、反射、扭曲效果还需要我们实现。但如果仔细观察效果并提炼规律,其实也不难得到涟漪效果的原理:
对于涟漪(水波)上的某一点,我们很轻松的就能根据上面的波形曲线得到它的振幅。此处的振幅就对应着这一处对附近空间的扭曲,可能文字说起来有点难以想象,可以看下面的图示:
看图中的涟漪效果,之所以人眼看起来像涟漪,是因为在涟漪处空间发生了轻微的扭曲(珊瑚、鲸鱼的身体),而“空间扭曲”,也就是贴图偏移。
OK,核心思想我们已经阐述完毕,所以我们现在可以自信的说出:
涟漪 ≈ 波形 ≈ 振幅 ≈ 画面的扭曲 ≈ 贴图偏移
回过头来看代码片段:
首先是将预设的波形传给shader:
//伪代码 [rippleeffect.cs]
//初始化波形贴图(也就是把waveform曲线初始化到gradTexture上面)
//初始化之后gradTexture的color.a即为波形曲线上对应的值
gradTexture = new Texture2D;
for (var i = 0; i < gradTexture.width; i++)
{
var x = 1.0f / gradTexture.width * i;
var a = waveform.Evaluate(x);
gradTexture.SetPixel(i, 0, new Color(a, a, a, a));
}
然后是如何得到振幅:
//伪代码 [RippleEffect.shader]
float wave(float2 position, float2 origin, float time) //当前点位置, 出发点位置, 时间
{
float d = length(position - origin);//计算当前点到出发点的距离
float t = time - d * _Params1.z;//计算已扩散时间
return (tex2D(_GradTex, float2(t, 0)).a - 0.5f) * 2;//在波形曲线上得到振幅
}
最后是根据振幅算出贴图偏移:
//伪代码 [RippleEffect.shader]
//_MainTex是原来没有涟漪效果的贴图
half4 frag(v2f_img i) : SV_Target
{
const float2 dx = float2(0.01f, 0);//delta x
const float2 dy = float2(0, 0.01f);// delta y
float2 p = i.uv * _Params1.xy;//根据相机比例变换当前点的UV坐标
float w = allwave(p);//振幅,用振幅来对当前点做UV上面的偏移,即可产生涟漪效果
float2 dw = float2(allwave(p + dx) - w, allwave(p + dy) - w);//xy上振幅
float2 duv = dw * _Params2.xy * 0.2f * _Params2.z; //UV上的振幅
half4 c = tex2D(_MainTex, i.uv + duv);//在原图上做偏移,到这一步涟漪效果已经出来了
float fr = pow(length(dw) * 3 * _Params2.w, 3);
return lerp(c, _Reflection, fr);//lerp来实现灰色的反射效果,优化表现
}
最核心的代码就是上面这一块了,在这个过程中将原贴图的某些区域进行了一些像素的偏移,使得这些区域看起来就像高低起伏的水面一样,产生了涟漪的效果。