Unity Shader实现时空扭曲效果
发表于2017-05-27
扭曲效果是游戏里面经常有的一个效果,本篇文章介绍的是在Unity开发中利用Shader实现时空扭曲效果,缺少这方面经验的开发人员可以学习下。











简介
最近刚刚通关《耻辱2》,在有一关的时候,竟然送给我了一个能够穿越时空的“神器”,有了这货,就可以一下子传送到过去或者回到现在。在使用这个道具的时候,会有一个屏幕扭曲的穿越的效果,感觉效果不错。

在穿越了无数回之后,我终于下决心准备在Unity里面实现一个简化的版本(肯定还原不到这种大作的水平,就当是练习啦,2333)。
屏幕收缩的效果
观察上面的效果图,最明显的一块就是屏幕有一个收缩的效果,这也是这种屏幕扭曲里面最明显也最容易实现的一部分。首先,扭曲效果就是uv偏移,超哪偏移,这个我们可以自己输入一个点给shader,默认就是屏幕中心点。我们让每个采样的点都朝着我们定义的中心点的方向偏移一段距离,就可以实现类似的屏幕收缩的效果。
c#脚本如下:
/******************************************************************** FileName: PassthoughEffect.cs Description: "传说中的穿越"效果 Created: 2017/05/07 by :puppet_master *********************************************************************/ using UnityEngine; public class PassthoughEffect : PostEffectBase { //扭曲强度 [Range(0, 0.15f)] public float distortFactor = 1.0f; //扭曲中心(0-1)屏幕空间,默认为中心点 public Vector2 distortCenter = new Vector2(0.5f, 0.5f); void OnRenderImage(RenderTexture source, RenderTexture destination) { if (_Material) { _Material.SetFloat("_DistortFactor", distortFactor); _Material.SetVector("_DistortCenter", distortCenter); Graphics.Blit(source, destination, _Material); } else { Graphics.Blit(source, destination); } } }
shader代码如下:
//屏幕收缩效果 //by:puppet_master Shader "ApcShader/PaththoughEffect" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } CGINCLUDE uniform sampler2D _MainTex; uniform float _DistortFactor; //扭曲强度 uniform float4 _DistortCenter; //扭曲中心点xy值(0-1)屏幕空间 #include "UnityCG.cginc" fixed4 frag(v2f_img i) : SV_Target { //计算偏移的方向 float2 dir = i.uv - _DistortCenter.xy; //最终偏移的值:方向 * (1-长度),越靠外偏移越小 float2 offset = _DistortFactor * normalize(dir) * (1 - length(dir)); //计算采样uv值:正常uv值+从中间向边缘逐渐增加的采样距离 float2 uv = i.uv + offset; return tex2D(_MainTex, uv); } ENDCG SubShader { Pass { ZTest Always Cull Off ZWrite Off Fog { Mode off } //调用CG函数 CGPROGRAM //使效率更高的编译宏 #pragma fragmentoption ARB_precision_hint_fastest //vert_img是在UnityCG.cginc中定义好的,当后处理vert阶段计算常规,可以直接使用自带的vert_img #pragma vertex vert_img #pragma fragment frag ENDCG } } }
找个测试场景,未收缩前的效果如下:

调整扭曲系数后,屏幕朝中心点收缩的效果如下:

屏幕扭曲效果与收缩效果结合
有了屏幕收缩效果,最基本的功能完成了。但是我们观察上面的动态图,还能发现,在马上要“穿越”到另一个场景的时候,屏幕会出现比较强烈的扰动效果,也正好是在这个状态下,进行的场景切换。其实这个状态很重要,切场景就相当于准备演员神马的,还是需要放块幕布之类的遮一下,不然就都现场直播了。我们需要做的就是让屏幕尽可能地扭曲,看不粗来到底发生了什么就好了。
关于扭曲效果,在上一篇文章中已经介绍过原理并且实现过一次,不过这里的扭曲要和上面的屏幕收缩相结合。两者都是uv偏移的原理,这里我选择把两者进行相减操作,首先计算正常屏幕收缩的uv偏移值,这个偏移值一般比较大。然后再计算根据一张噪声图,得到一个小的uv偏移值,作为扰动的uv偏移,两者结合就能够得到最终扭曲+扰动的效果。
c#脚本如下,增加了一个噪声图槽位以及扰动控制系数:
/******************************************************************** FileName: PassthoughEffect.cs Description: "传说中的穿越"效果 Created: 2017/05/07 by :puppet_master *********************************************************************/ using UnityEngine; public class PassthoughEffect : PostEffectBase { //收缩强度 [Range(0, 0.15f)] public float distortFactor = 1.0f; //扭曲中心(0-1)屏幕空间,默认为中心点 public Vector2 distortCenter = new Vector2(0.5f, 0.5f); //噪声图 public Texture NoiseTexture = null; //屏幕扰动强度 [Range(0, 2.0f)] public float distortStrength = 1.0f; void OnRenderImage(RenderTexture source, RenderTexture destination) { if (_Material) { _Material.SetTexture("_NoiseTex", NoiseTexture); _Material.SetFloat("_DistortFactor", distortFactor); _Material.SetVector("_DistortCenter", distortCenter); _Material.SetFloat("_DistortStrength", distortStrength); Graphics.Blit(source, destination, _Material); } else { Graphics.Blit(source, destination); } } }
shader代码如下:
//屏幕收缩效果 //by:puppet_master Shader "ApcShader/PaththoughEffect" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _NoiseTex("Noise", 2D) = "black"{} } CGINCLUDE uniform sampler2D _MainTex; uniform sampler2D _NoiseTex; uniform float _DistortFactor; //扭曲强度 uniform float4 _DistortCenter; //扭曲中心点xy值(0-1)屏幕空间 uniform float _DistortStrength; #include "UnityCG.cginc" fixed4 frag(v2f_img i) : SV_Target { //计算偏移的方向 float2 dir = i.uv - _DistortCenter.xy; //最终偏移的值:方向 * (1-长度),越靠外偏移越小 float2 scaleOffset = _DistortFactor * normalize(dir) * (1 - length(dir)); //采样Noise贴图 fixed4 noise = tex2D(_NoiseTex, i.uv); //noise的权重 = 参数 * 距离,越靠近外边的部分,扰动越严重 float2 noiseOffset = noise.xy * _DistortStrength * dir; //计算最终offset = 两种扭曲offset的差(取和也行,总之效果好是第一位的) float2 offset = scaleOffset - noiseOffset; //计算采样uv值:正常uv值+从中间向边缘逐渐增加的采样距离 float2 uv = i.uv + offset; return tex2D(_MainTex, uv); } ENDCG SubShader { Pass { ZTest Always Cull Off ZWrite Off Fog { Mode off } //调用CG函数 CGPROGRAM //使效率更高的编译宏 #pragma fragmentoption ARB_precision_hint_fastest //vert_img是在UnityCG.cginc中定义好的,当后处理vert阶段计算常规,可以直接使用自带的vert_img #pragma vertex vert_img #pragma fragment frag ENDCG } } }
还是上面的测试场景,这次,我们用一个噪声图,比如下图所示的这种:

然后设置一下扰动的权重,就可以看到,场景除了朝中心收缩了,还带有了扰动的效果:

动态的收缩和扭曲效果
我们实现了收缩和扭曲的效果。和上面的动图比较的话,除了挫了点,就是不能动了。下面研究下怎么让这个动起来。首先,这种类型的控制,基本就不能再在shader里面做了,我们下面要做的,就是动态改变我们在静态时设置的几个参数。在update或者开一个协程,让收缩系数从0逐渐增加到最大,然后迅速降低为0;在快要结束时,突然增大扰动系数,并执行切换镜头等的操作。
先来看收缩效果,这里呢,我们为了方便调整,直接给一个曲线控制。曲线控制可以得到一些特别好玩的效果,而且可以免去我们写很多麻烦的控制代码,还可以直接把效果参数开放出来,让策划和美术同学根据需要来调整,最主要的是可以顺便偷下懒,2333。还是上面的shader,我们仅仅修改了C#脚本,用一个协程控制采样曲线:
/******************************************************************** FileName: PassthoughEffect.cs Description: "传说中的穿越"效果 Created: 2017/05/10 by :puppet_master *********************************************************************/ using UnityEngine; using System.Collections; public class PassthoughEffect : PostEffectBase { //收缩强度 [Range(0, 0.15f)] public float distortFactor = 1.0f; //扭曲中心(0-1)屏幕空间,默认为中心点 public Vector2 distortCenter = new Vector2(0.5f, 0.5f); //噪声图 public Texture NoiseTexture = null; //屏幕扰动强度 [Range(0, 2.0f)] public float distortStrength = 1.0f; //屏幕收缩总时间 public float passThoughTime = 4.0f; //当前时间 private float currentTime = 0.0f; //曲线控制权重 public float curveFactor = 0.2f; //屏幕收缩效果曲线控制 public AnimationCurve curve; void OnRenderImage(RenderTexture source, RenderTexture destination) { if (_Material) { _Material.SetTexture("_NoiseTex", NoiseTexture); _Material.SetFloat("_DistortFactor", distortFactor); _Material.SetVector("_DistortCenter", distortCenter); _Material.SetFloat("_DistortStrength", distortStrength); Graphics.Blit(source, destination, _Material); } else { Graphics.Blit(source, destination); } } //ContexMenu,可以直接在Component上右键调用该函数,比较好用的小技巧哈 [ContextMenu("Play")] public void StartPassThoughEffect() { currentTime = 0.0f; StartCoroutine(UpdatePassthoughEffect()); } private IEnumerator UpdatePassthoughEffect() { while(currentTime < passThoughTime) { currentTime += Time.deltaTime; //根据时间占比在曲线(0,1)区间采样,再乘以权重作为收缩系数 distortFactor = curve.Evaluate(currentTime / passThoughTime) * curveFactor; yield return null; //结束时强行设置为0 distortFactor = 0.0f; } } }
然后呢,我们就可以来调整曲线来得到我们希望的效果啦。《耻辱》中的扭曲效果是一个缓慢加大,然后再突然恢复的一个过程,我们用曲线调整一个类似的效果,如下:

得到的效果如下面的动态图所示,基本还原了这种收缩的效果:

下面就是在收缩到最大的时候,给一个比较大的扰动系数,达到转场的目的。尴尬,好不容易调好的曲线,把变量改了个名字,曲线丢掉了……只好重新调一遍。最终版本如下:
/******************************************************************** FileName: PassthoughEffect.cs Description: "传说中的穿越"效果 Created: 2017/05/10 by :puppet_master *********************************************************************/ using UnityEngine; using System.Collections; public class PassthoughEffect : PostEffectBase { //收缩强度 [Range(0, 0.15f)] public float distortFactor = 1.0f; //扭曲中心(0-1)屏幕空间,默认为中心点 public Vector2 distortCenter = new Vector2(0.5f, 0.5f); //噪声图 public Texture NoiseTexture = null; //屏幕扰动强度 [Range(0, 2.0f)] public float distortStrength = 1.0f; //屏幕收缩总时间 public float passThoughTime = 4.0f; //当前时间 private float currentTime = 0.0f; //曲线控制权重 public float scaleCurveFactor = 0.2f; //屏幕收缩效果曲线控制 public AnimationCurve scaleCurve; //扰动曲线系数 public float distortCurveFactor = 1.0f; //屏幕扰动效果曲线控制 public AnimationCurve distortCurve; void OnRenderImage(RenderTexture source, RenderTexture destination) { if (_Material) { _Material.SetTexture("_NoiseTex", NoiseTexture); _Material.SetFloat("_DistortFactor", distortFactor); _Material.SetVector("_DistortCenter", distortCenter); _Material.SetFloat("_DistortStrength", distortStrength); Graphics.Blit(source, destination, _Material); } else { Graphics.Blit(source, destination); } } //ContexMenu,可以直接在Component上右键调用该函数,比较好用的小技巧哈 [ContextMenu("Play")] public void StartPassThoughEffect() { currentTime = 0.0f; StartCoroutine(UpdatePassthoughEffect()); } private IEnumerator UpdatePassthoughEffect() { while(currentTime < passThoughTime) { currentTime += Time.deltaTime; float t = currentTime / passThoughTime; //根据时间占比在曲线(0,1)区间采样,再乘以权重作为收缩系数 distortFactor = scaleCurve.Evaluate(t) * scaleCurveFactor; distortStrength = distortCurve.Evaluate(t) * distortCurveFactor; yield return null; //结束时强行设置为0 distortFactor = 0.0f; distortStrength = 0.0f; } } }
各项参数以及扰动曲线设置如下图所示,噪声图换了一张其他的:

最终效果如下面动图所示:

漩涡扭曲效果
漩涡扭曲效果,本来打算尝试一下卡卡西的“神威”技能效果,不过效果不是很好,所以只实现了一版漩涡扭曲的效果,以后有时间再慢慢研究。说道漩涡类型的东东,第一个想到的应该就是sin,cos函数,这俩跟漩涡非常搭边。然后我们分析一下漩涡的效果,我们先需要在fragment shader中对应每个像素点先平移到中心点(也可以是我们自己定义的中心),然后让这个像素点绕着当前中心点的轴旋转一定角度,最后再将这个像素点平移回去,就能够达到了整个图像绕着固定点旋转的操作。而为了让漩涡效果看起来更加自然,我们需要按照离中心点的距离作为权重,缩放旋转的角度值,离中心点越远的像素点,旋转值越小,而离中心点越近的位置,旋转得就越剧烈,这是一个反比的关系,所以我们在给旋转值的时候,除以一个距离中心点的距离即可。下面附上漩涡扭曲效果的shader以及C#脚本。
Shader部分如下,将扭曲部分改为漩涡形,附加一个噪声扰动的效果:
//漩涡扭曲效果 //by:puppet_master Shader "ApcShader/RotationDistortEffect" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _NoiseTex("Noise", 2D) = "black"{} } CGINCLUDE uniform sampler2D _MainTex; uniform sampler2D _NoiseTex; uniform float _DistortFactor; //扭曲强度 uniform float4 _DistortCenter; //扭曲中心点xy值(0-1)屏幕空间 uniform float _DistortStrength; #include "UnityCG.cginc" fixed4 frag(v2f_img i) : SV_Target { //平移坐标点到中心点,同时也是当前像素点到中心的方向 fixed2 dir = i.uv - _DistortCenter.xy; //计算旋转的角度:对于像素点来说,距离中心越远,旋转越少,所以除以距离。相当于用DistortFactor作为旋转的角度值Distort/180 * π,π/180 = 0.1745 float rot = _DistortFactor * 0.1745 / (length(dir) + 0.001);//+0.001防止除零 //计算sin值与cos值,构建旋转矩阵 fixed sinval, cosval; sincos(rot, sinval, cosval); float2x2 rotmatrix = float2x2(cosval, -sinval, sinval, cosval); //旋转 dir = mul(dir, rotmatrix); //再平移回原位置 dir += _DistortCenter.xy; //采样noise图 fixed4 noise = tex2D(_NoiseTex, i.uv); //noise的权重 = 参数 * 距离,越靠近外边的部分,扰动越严重 float2 noiseOffset = noise.xy * _DistortStrength * dir; //用偏移过的uv+扰动采样MainTex return tex2D(_MainTex, dir + noiseOffset); } ENDCG SubShader { Pass { ZTest Always Cull Off ZWrite Off Fog { Mode off } //调用CG函数 CGPROGRAM //使效率更高的编译宏 #pragma fragmentoption ARB_precision_hint_fastest //vert_img是在UnityCG.cginc中定义好的,当后处理vert阶段计算常规,可以直接使用自带的vert_img #pragma vertex vert_img #pragma fragment frag ENDCG } } }
C#脚本部分,仍然使用了两条曲线进行控制,一条控制旋转值,一条控制扰动值:
/******************************************************************** FileName: PassthoughEffect.cs Description: 漩涡扭曲效果 Created: 2017/05/10 by :puppet_master *********************************************************************/ using UnityEngine; using System.Collections; public class RotationDistortEffect : PostEffectBase { //收缩强度 [Range(0, 20.0f)] public float distortFactor = 1.0f; //扭曲中心(0-1)屏幕空间,默认为中心点 public Vector2 distortCenter = new Vector2(0.5f, 0.5f); //噪声图 public Texture NoiseTexture = null; //屏幕扰动强度 [Range(0, 2.0f)] public float distortStrength = 1.0f; //屏幕扭曲时间 public float passThoughTime = 3.0f; //当前时间 private float currentTime = 0.0f; //曲线控制权重 public float rotationCurveFactor = 10.0f; //屏幕全传效果曲线控制 public AnimationCurve rotationCurve; //扰动曲线系数 public float distortCurveFactor = 0.1f; //屏幕扰动效果曲线控制 public AnimationCurve distortCurve; void OnRenderImage(RenderTexture source, RenderTexture destination) { if (_Material) { _Material.SetTexture("_NoiseTex", NoiseTexture); _Material.SetFloat("_DistortFactor", distortFactor); _Material.SetVector("_DistortCenter", distortCenter); _Material.SetFloat("_DistortStrength", distortStrength); Graphics.Blit(source, destination, _Material); } else { Graphics.Blit(source, destination); } } //ContexMenu,可以直接在Component上右键调用该函数,比较好用的小技巧哈 [ContextMenu("Play")] public void StartPassThoughEffect() { currentTime = 0.0f; StartCoroutine(UpdatePassthoughEffect()); } private IEnumerator UpdatePassthoughEffect() { while (currentTime < passThoughTime) { currentTime += Time.deltaTime; float t = currentTime / passThoughTime; //根据时间占比在曲线(0,1)区间采样,再乘以权重作为收缩系数 distortFactor = rotationCurve.Evaluate(t) * rotationCurveFactor; distortStrength = distortCurve.Evaluate(t) * distortCurveFactor; yield return null; //结束时强行设置为0 distortFactor = 0.0f; distortStrength = 0.0f; } } }
配置两条曲线以及参数:

漩涡扭曲效果动态图如下:
