一.简介
最近在打黑魂,被虐得死去活来,每次都是想砸电脑的节奏,然而还是忍不住想玩,唉。下面步入正题,黑魂的传送技能之前会播放一个帅帅的屏幕特效-屏幕水波,从屏幕中间向四周扩散开来,很有赶脚。下面附上一张截图:
正好最近在学习shader,决定研究一下这种效果怎样实现。
二.原理介绍
水波纹的原理其实比较简单,与其他后处理效果一样,vertex shader千篇一律,所有操作都有fragment shader完成。我们仔细观察一下上图中的效果,其实这种波纹并不是叠加上去的,而是波纹位置的图像被向外拉伸了,而这种拉伸的强度是按照类似sin函数的波形进行拉伸的。
下面我们通过一个场景进行试验,一步一步实现屏幕水波纹的效果,下图是场景原始效果:
1.基本拉伸效果
我们先进行一版简单的拉伸效果实验。
fixed4 frag(v2f_img i) : SV_Target
{
//计算uv到中间点的向量(向外扩,反过来就是向里缩)
float2 dv = float2(0.5, 0.5) - i.uv;
//归一化
float2 dv1 = normalize(dv);
//计算每个像素uv的偏移值
float2 offset = dv1 * _distanceFactor;
//像素采样时偏移offset
float2 uv = offset + i.uv;
return tex2D(_MainTex, uv);
}
效果如下图所示,有一种恐怖的赶脚:
如果我们修改一个地方,将shader中的dv1反向,就会得到一种收缩的效果,如果以后做这种类似的效果,可以考虑用这个实现哈:
2.波纹式拉伸
第一步实现了整体拉伸,但是这并不是我们想要的波纹效果,我们如果给一个线性的输入,通过什么样的函数能够得到类似波纹效果的输出呢?
对了,就是传说中的三角函数,我们通过一个sin值,可以把线性的输入变化成波形的输出,这样就可以模拟了水波纹的效果。知道了用什么函数,函数的输入和输出分别是什么,就是偶们下一步要考虑的问题了。我们上一步中是通过像素采样时uv坐标增加一个偏移值来达到拉伸的效果,我们就可以让这个偏移值作为这个三角函数的输出,这样,有的地方拉伸的少,有的地方拉伸的多,这样就形成了不同的拉伸效果,也就形成了一个波纹的感觉。那么,输入就很明显了,输入就是距离中心位置的绝对距离。上代码:
fixed4 frag(v2f_img i) : SV_Target
{
//计算uv到中间点的向量(向外扩,反过来就是向里缩)
float2 dv = float2(0.5, 0.5) - i.uv;
//计算像素点距中点的距离
float dis = sqrt(dv.x * dv.x + dv.y * dv.y);
//用sin函数计算出波形的偏移值factor
//dis在这里都是小于1的,所以我们需要乘以一个比较大的数,比如60,这样就有多个波峰波谷
//sin函数是(-1,1)的值域,我们希望偏移值很小,所以这里我们缩小100倍,据说乘法比较快,so...
float sinFactor = sin(dis * _distanceFactor) * _totalFactor * 0.01;
//归一化
float2 dv1 = normalize(dv);
//计算每个像素uv的偏移值
float2 offset = dv1 * sinFactor;
//像素采样时偏移offset
float2 uv = offset + i.uv;
return tex2D(_MainTex, uv);
}
结果如下:
3.让波纹动起来
有了波纹效果,我们下一步的操作是让波纹动起来,说道动起来,最简单的办法就是随着时间调整某个参数。这个值我们可以从外界传递进来,也可以使用shader中内置的_Time变量。附上一张Unity官方的变量说明,一些常用的shader中特殊内置变量:
我们直接在sin函数的输入中增加一个Time变量,这样,波纹就不是固定不变的效果,而是逐渐向外扩展的效果了。
fixed4 frag(v2f_img i) : SV_Target
{
//计算uv到中间点的向量(向外扩,反过来就是向里缩)
float2 dv = float2(0.5, 0.5) - i.uv;
//计算像素点距中点的距离
float dis = sqrt(dv.x * dv.x + dv.y * dv.y);
//用sin函数计算出波形的偏移值factor
//dis在这里都是小于1的,所以我们需要乘以一个比较大的数,比如60,这样就有多个波峰波谷
//sin函数是(-1,1)的值域,我们希望偏移值很小,所以这里我们缩小100倍,据说乘法比较快,so...
float sinFactor = sin(dis * _distanceFactor + _Time.y * _timeFactor) * _totalFactor * 0.01;
//归一化
float2 dv1 = normalize(dv);
//计算每个像素uv的偏移值
float2 offset = dv1 * sinFactor;
//像素采样时偏移offset
float2 uv = offset + i.uv;
return tex2D(_MainTex, uv);
}
好了,这次来一张gif,(折腾了半天,终于用Fraps+迅雷看看播放器做了个GIF图片):
4.怎么把波形变成圆形
首先,我们注意到这里波纹是全屏幕的,会按照屏幕的分辨率进行变化,波纹不是真正的圆形,而是一个椭圆,作为一个强迫症,真是很蛋疼,所以,要处理一下这个问题,其实也很简单,我们在计算distance的时候,按照屏幕的长宽比将dis进行一下缩放就可以了,也就是在我们计算dis之前增加下面一步操作:
//按照屏幕长宽比进行缩放
dv = dv * float2(_ScreenParams.x / _ScreenParams.y, 1);
这下,我们的波纹就成了一个真正的圆形,强迫症终于不难受了....
5.让波形从中间从小到大扩散出去
我们已经可以模拟波形效果了,剩下的问题是,我们想像黑魂那种,让波形从画面的中间从小到大,扩散出去,只播放一次,而不是像我们现在这种无限循环的鬼畜。不过这个操作我们直接在shader里面就不好实现了,需要用外面的脚本来进行配合,一种思路是,从外界传递进来一个值,这个值随着时间逐渐增大,只有在距离这个值小于波纹宽度的部分才进行波纹操作,这样就可以模拟波纹从中间产生,然后一点一点向外界扩散的效果了。该部分代码直接在最终的代码中给出,此处只贴上一张原理图:
三.代码实现
上面已经详述了原理以及各种问题,这一部分不多说,直接上代码。
shader部分:
Shader "Custom/WaterWave Effect"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
}
CGINCLUDE
#include "UnityCG.cginc"
uniform sampler2D _MainTex;
uniform float _distanceFactor;
uniform float _timeFactor;
uniform float _totalFactor;
uniform float _waveWidth;
uniform float _curWaveDis;
fixed4 frag(v2f_img i) : SV_Target
{
//计算uv到中间点的向量(向外扩,反过来就是向里缩)
float2 dv = float2(0.5, 0.5) - i.uv;
//按照屏幕长宽比进行缩放
dv = dv * float2(_ScreenParams.x / _ScreenParams.y, 1);
//计算像素点距中点的距离
float dis = sqrt(dv.x * dv.x + dv.y * dv.y);
//用sin函数计算出波形的偏移值factor
//dis在这里都是小于1的,所以我们需要乘以一个比较大的数,比如60,这样就有多个波峰波谷
//sin函数是(-1,1)的值域,我们希望偏移值很小,所以这里我们缩小100倍,据说乘法比较快,so...
float sinFactor = sin(dis * _distanceFactor + _Time.y * _timeFactor) * _totalFactor * 0.01;
//距离当前波纹运动点的距离,如果小于waveWidth才予以保留,否则已经出了波纹范围,factor通过clamp设置为0
float discardFactor = clamp(_waveWidth - abs(_curWaveDis - dis), 0, 1);
//归一化
float2 dv1 = normalize(dv);
//计算每个像素uv的偏移值
float2 offset = dv1 * sinFactor * discardFactor;
//像素采样时偏移offset
float2 uv = offset + i.uv;
return tex2D(_MainTex, uv);
}
ENDCG
SubShader
{
Pass
{
ZTest Always
Cull Off
ZWrite Off
Fog { Mode off }
CGPROGRAM
#pragma vertex vert_img
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
}
Fallback off
}
c#部分:
using UnityEngine;
public class WaterWaveEffect : PostEffectBase {
//距离系数
public float distanceFactor = 60.0f;
//时间系数
public float timeFactor = -30.0f;
//sin函数结果系数
public float totalFactor = 1.0f;
//波纹宽度
public float waveWidth = 0.3f;
//波纹扩散的速度
public float waveSpeed = 0.3f;
private float waveStartTime;
void OnRenderImage (RenderTexture source, RenderTexture destination)
{
//计算波纹移动的距离,根据enable到目前的时间*速度求解
float curWaveDistance = (Time.time - waveStartTime) * waveSpeed;
//设置一系列参数
_Material.SetFloat("_distanceFactor", distanceFactor);
_Material.SetFloat("_timeFactor", timeFactor);
_Material.SetFloat("_totalFactor", totalFactor);
_Material.SetFloat("_waveWidth", waveWidth);
_Material.SetFloat("_curWaveDis", curWaveDistance);
Graphics.Blit (source, destination, _Material);
}
void OnEnable()
{
//设置startTime
waveStartTime = Time.time;
}
}
注:PsotEffectBase为后处理基类,在 之前的文章中有完整实现,此处不予贴出。
四.效果展示
最终结果如下面的gif所示: