因为是用的后处理方法实现的,和图形就没什么太大关系了,更多的是图像处理。先上效果:
猜猜卡比吞了啥?
参考了下面三张gif,对比一下原版,还是逊色了些, 可能是因为动画的场景在运动?我的扭曲效果也差了不少。
第五部中没有了扭曲效果,换成了轻微的抖动。
反色
HSV空间下的hue变化
圆形边界的扭曲与运动
两个类似rim light的冲击波
各向异性的斑纹(最后一张gif比较明显)
时停范围内的扭曲
时停结束后转为灰度图
径向模糊
看上去很多,但每一个都不复杂,下面我们一一展开,Step by Step~
反色
顾名思义,颜色取反。Unity默认的ImageEffectShader就是这个效果。
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return 1 - col;
}
![f9762dc1d2a584efa6247914ae537297.png](https://i-blog.csdnimg.cn/blog_migrate/e18260e094debb7480e6fe34a7b9c8e6.jpeg)
HSV空间下的hue变化
仔细观察,发现这个特效不单单是取了反,还有一个色相的变化。
![a4352aeed2a7dce426ec3492df30d53d.png](https://i-blog.csdnimg.cn/blog_migrate/ef4379b6c7f95b9dedd3978e358bf7ef.jpeg)
为了达成这个效果,我们首先要把颜色转到HSV空间,对H分量,也就是色阶进行一些时间相关的变化,然后再把颜色转回RGB空间。
我在网上找到了一个比较trick的算法,没有任何的条件判断语句,十分的GPU友好
float3 RGB2HSV(float3 c)
{
float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
float4 p = lerp(float4(c.bg, K.wz), float4(c.gb, K.xy), step(c.b, c.g));
float4 q = lerp(float4(p.xyw, c.r), float4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
float3 HSV2RGB(float3 c)
{
float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
float3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y);
}
实现原理参照这个博客,具体我没有细看(我超虚的)。。
注意,这个算法得到的HSV分量是位于0~1之间的。H分量并不是传统的0~360的范围。
我们让H分量在原色阶右边1/5色环的范围内按时间的正弦函数周期变化。
float3 hsvColor = RGB2HSV(col.rgb);
hsvColor.x += lerp(0,0.2,sin( UNITY_TWO_PI * frac(_Time.y *0.5)));
hsvColor.x = frac(hsvColor.x);
reversedColor.rgb = 1 - HSV2RGB(hsvColor.rgb);
return reversedColor;
圆形边界的扭曲与运动
The World的效果类似一个半径不断扩大的球体,球体内部发生反色,而球体外部不变。放在后处理中就是一个圆。另外我们想要的是一个正圆,所以我们求出屏幕长宽比后进行修正。
float aspect = _ScreenParams.x / _ScreenParams.y;
float x = (i.uv.x - 0.5f - _CenterX) * aspect;//would be ellipse if not multiply the aspectfloat y = i.uv.y - 0.5f - _CenterY;
float d = x * x + y * y;
...
if ( d < _Radius * _Radius ) {
return reversedColor;
![2e5dc9ab393b1c60e39d6e7237cdbd2e.png](https://i-blog.csdnimg.cn/blog_migrate/19ebbf874297d8e9fd16dc7bfb4836c5.jpeg)
但是这个圆它有些"太圆了",我们想要的是一个坑坑洼洼的圆。我的思路是,根据圆心角角度的不同,在原有半径上再叠加一个函数,并保证该函数的连续。首先我们拿正弦函数试试。
float sin_theta = saturate(y / sqrt(d));//d = r^2 小心NAN!!!float half_theta = asin(sin_theta) * (step(0,x)-0.5);
float deformFactor = (1 + 0.02 * sin(half_theta * 24) * lerp(0, 0.5, sin(UNITY_TWO_PI * _Time.y * 0.5)));
_Radius *= deformFactor;
在求圆心角的时候, 要注意arcsin函数只够管一四象限的点。对于x小于零的点,我们要做一些计算,保证能得到正确的角度。
嗯,现在边缘不再那么圆了,但是还是很规整,于是自然想到多个正弦波叠加,能够保证它的周期连续性。我这里用了三个,数值是自己乱填的。
float deformFactor = (1 + 0.1 * sin(half_theta * 24) * lerp(0, 0.5, sin(UNITY_TWO_PI * _Time.y * 0.5)) + 0.25 * x * sin(1 + half_theta * 6.5) * lerp(0.25, 0.75, sin(UNITY_TWO_PI * _Time.y * 0.2)) + 0.1 * x * x * sin(2 + half_theta * 9.5) * lerp(0.25, 0.75, sin(UNITY_TWO_PI * _Time.y * 0.1)) );
是不是有那么点意思啦!
两个类似rim light的冲击波
回想一下在三维空间中我们是怎么给物体加上泛光描边的:计算视线与法相的夹角,判断该像素是否在边缘,然后根据夹角大小叠加颜色。
而回到我们的二维情况,事情就变的简单多了:指定一个半径,然后进行颜色插值。半径上的光效最强,圆心的颜色为零。
half power = 1.5;
if (d < _ImpactRadius * _ImpactRadius) {
float t = saturate(d / (_ImpactRadius * _ImpactRadius));//小心NAN!!! fixed4 rim = lerp(0, _ImpactColor, pow(t, power));
finalColor += rim * rim.a;
}
power决定了到底按照离圆心距离的几次方进行插值,表现在图像上就是光环的粗细或者说强度。
![74109f6de366f35b48b57acbd3280ec2.png](https://i-blog.csdnimg.cn/blog_migrate/d941fbd659b4377c9e4755e67ae6a1d1.jpeg)
![f0fef20372658ccf9d38c07e20585303.png](https://i-blog.csdnimg.cn/blog_migrate/dbd3436765316e944ed508d6738b8721.jpeg)
各向异性的斑纹
我们从第五部中的时停中截出一帧,可以看到以白金之星的手指为中点发散出去的扭曲的斑纹。
![ea5f5d113fb08bcf279941e17b27bbb5.png](https://i-blog.csdnimg.cn/blog_migrate/bee929cf7fcc3ca1dad09e1b867c547c.jpeg)
于是我们首先想到的就是……光盘!
![5a62fa9f4b4b70adcaebfc66d318c78c.png](https://i-blog.csdnimg.cn/blog_migrate/5208b1dc99f51040e0e38cd85351d194.jpeg)
额不好意思,放错图了。
![6c61404e6995b315e1641fc43680209e.png](https://i-blog.csdnimg.cn/blog_migrate/d5d7fb54c8bbb73705992a3389a0d519.jpeg)
其实各项异性指的是从不同的角度去看光碟,得到的颜色不同,而并不是指的光碟上不同圆心角的颜色不同,我这里就借这个词用一下,可别被我误导了哈!
为了实现这种效果,我们需要一个函数或者一张噪声图,然后将不同的圆心角映射到不同的颜色值上面去。这里我选择使用贴图。
![92abed67c2e75b2308baf2e3332e179a.png](https://i-blog.csdnimg.cn/blog_migrate/e22a7f955f644dbbbebffacefe5f4851.jpeg)
其实我们只有一个变量,就是角度,那对于图片来说,我们也只需要一个维度就行了,也就是从图片中取一条线来用。
fixed4 wave = tex2D(_NoiseTex, half_theta*2) * waveIntensity;
return wave;
我们直接输出,看看效果:
![67da77340b5f68e6c165a1115e279b82.png](https://i-blog.csdnimg.cn/blog_migrate/9ecbaa49a6f4acdb24862c777cdddd4f.jpeg)
嗯,是我们想要的效果。现在我们还需要加上扭曲的斑纹,以及让它随着时间动起来。
思路是扭曲,从刚刚的噪声图中取值,并让该值随着时间规律变化,然后适用这个值对这个大光碟中的点的uv进行偏移。
float4 noise = tex2D(_NoiseTex, i.uv + _Time.xy * twistSpeed);
fixed4 wave = tex2D(_NoiseTex, half_theta*2 + noise.xy * waveShape) * waveIntensity;
最后我们把wave的值加到内圈的颜色上去就行了。
时停范围内的扭曲
我们已经简单的叠加上了斑纹,然而内部并没有正真的扭曲,所以我们再加上一个扭曲的效果。而扭曲其实我们刚才已经实现过一遍了(uv偏移),将扭曲斑纹的方法故技重施。
fixed4 twistedColor = tex2D(_MainTex, i.uv + noise.xy * twistIntensity);
...
-float3 hsvColor = RGB2HSV(col.rgb);
+float3 hsvColor = RGB2HSV(twistedColor.rgb);
大功告成!
现在刚刚那张噪声图的优越性就体现出来了,因为它是一张边界连续的噪声图,如果不是的话,你会看到很明显的十字扫描线。这里就不贴这种情况的效果图啦。
时停结束后转为灰度图
转化为灰度图也是很方便的事情,使用一个著名的经验公式:
我们使用一个float变量控制灰度的开关,在发波时为0,收缩时为1。然后我们可以把上述的式子简写成dot的形式。
if(d < _Radius * _Radius){
...
}
else{
if(isGray){
fixed3 grayFactor = { 0.299,0.587,0.114 };
fixed grayColor = dot(grayFactor, col);
...
}
}
![a8940b39ed28545ec545a43a504778c3.png](https://i-blog.csdnimg.cn/blog_migrate/77a1e2b3aa14b5a5a53f22bbe2ed015d.jpeg)
径向模糊
这是一种沿方向的模糊,很容易营造出速度感。
这个的实现我参照的这篇Blog blog.csdn.net/xoyojank/
![0402fcd92d5e94abb8fcfb1c1644154d.png](https://i-blog.csdnimg.cn/blog_migrate/73ac38c098c5af5fb2a08deda49cb511.jpeg)
因为径向模糊是一个比较独立的效果,这里我们新起一个Shader来完成这个效果,然后在OnRenderImage方法中Graphics.Blit两次 , 先完成刚刚一系列操作,再对结果进行径向模糊。
void Awake (){
mySource = new RenderTexture(origin.width / 2, origin.height / 2, 0);
RenderTexture.active = mySource;
Graphics.Blit(origin, mySource);
}
void OnRenderImage(RenderTexture source, RenderTexture destination)
{
...//check material and set properties RenderTexture rt = RenderTexture.GetTemporary(source.width, source.height);
Graphics.Blit(mySource, rt, colorMaterial);
Graphics.Blit(rt, destination, blurMaterial);
RenderTexture.ReleaseTemporary(rt);
}
具体的算法很简单。在模糊中心到该像素的方向上选取n个点,并将其求平均。
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
fixed4 sum = col;
float2 dir = 0.5 - i.uv + float2(_CenterX,_CenterY);
float dist = length(dir);
dir /= dist;
float samples[10] =
{
-0.08,
-0.05,
-0.03,
-0.02,
-0.01,
0.01,
0.02,
0.03,
0.05,
0.08
};
for (int it = 0; it < 10; it++) {
sum += tex2D(_MainTex, i.uv + dir * samples[it] * _SampleDist);
}
sum /= 11;
float t = saturate(dist * _SampleStrength);
float4 blur = lerp(col, sum, t);
return blur;
}
![8b2325334e2fb13c3d16d8832090353b.png](https://i-blog.csdnimg.cn/blog_migrate/00bf6f17bd4aa8acab913d681b18da17.jpeg)
在时停的过程中,径向模糊的强度发生脉冲一样的变化,很有冲击感。
再见,谢谢所有的if
至此我们就粗略的完成了一个时停效果的制作啦。最后提一下如何优化我们的代码,将我们的老朋友if都改成step。
为什么说if不好呢?因为GPU是并行计算的,保证一样的执行顺序是有利于GPU的。而当你使用if时,同一段代码的运行顺序与时间就会随着情况的不同而发生变化,这是非常不适合并行计算的。
让我们从代码中最先出现if的地方开始
if ( d < _Radius * _Radius ) {
final = reversedColor;
}
else{
final = col;
}
代码很好理解。判断点是否在圆内,如果在里面,则反色。要把这个if干掉,首先我们来介绍一下我们的主人公:step
step这个函数其实很简单,看一下就明白啦
t1 = step(x , y);
t2 = x<y?1:0;
//t1等价于t2
t3 = step(0,0.5);
//t3 == 1;t4 = step(1,0.5);
//t4 == 0;
简单来说,就是比较x是不是比y大,那我们要如何用这个函数来干掉if呢?
float rr = _Radius * _Radius;
half insideCircle = step(d, rr);
finalColor = lerp(col, reversedColor, insideCircle);
嗯,对,就这样就完成了,大家可以验算一下,不一定要这样写,只要你能保证得到的结果与ifelse一致即可,十分的简单粗暴。下面我们来看一下if嵌套的情况:
if ( d < _Radius * _Radius ) {
final = reversedColor;
}
else{
if(isGray)
final = grayColor;
}
可以看到,这里套了两层,那怎么来表示同时满足呢?利用逻辑&的性质,final = grayColor在两个step一个为真,一个为假的情况下执行,也就是step* (1- step)为真,于是上面的代码可以写成这样。
float rr = _Radius * _Radius;
half isGray = step(0.5, _Gray);
half insideCircle = step(d, rr);
finalColor = lerp(col, reversedColor, insideCircle);
fixed3 grayFactor = { 0.299,0.587,0.114 };
fixed grayColor = dot(grayFactor, col);
finalColor = lerp(finalColor, grayColor, isGray * (1-insideCircle));//step1 * (1-step2)
大致就是这样,使用逻辑运算与多项式的组合达到目标效果。
好啦,就到这里了,详细工程文件与代码点击这里下载。之后可能会加一个mask让时停的发动者不为灰色。
这里也希望各路大神对我的方法提出评判指正,这是我第一次完整的去做一个效果,所以肯定有不少问题与错误…
最后回顾一下我们的效果:
下次见!
![7de4645ff9bf3b04b167c54f28b89614.png](https://i-blog.csdnimg.cn/blog_migrate/b8b18d968d2b277dda8408908ad7d397.jpeg)
往期精选
Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着
Shader学习应该如何切入?
写出一手烂代码的19条准则
声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。
作者:陈泽日天
来源:https://zhuanlan.zhihu.com/p/144948248
More:【微信公众号】 u3dnotes