找回密码
 立即注册
查看: 605|回复: 0

Unity后处理实现The World时停效果

[复制链接]
发表于 2020-11-23 19:07 | 显示全部楼层 |阅读模式
因为是用的后处理方法实现的,和图形就没什么太大关系了,更多的是图像处理。先上效果:
猜猜卡比吞了啥?
参考了下面三张gif,对比一下原版,还是逊色了些,  可能是因为动画的场景在运动?我的扭曲效果也差了不少。
第五部中没有了扭曲效果,换成了轻微的抖动。
分析了一下,需要的效果有
    反色HSV空间下的hue变化圆形边界的扭曲与运动两个类似rim light的冲击波各向异性的斑纹(最后一张gif比较明显)时停范围内的扭曲时停结束后转为灰度图径向模糊
看上去很多,但每一个都不复杂,下面我们一一展开,Step by Step~
反色

顾名思义,颜色取反。Unity默认的ImageEffectShader就是这个效果。
  1. fixed4 frag(v2f i) : SV_Target
  2. {
  3.     fixed4 col = tex2D(_MainTex, i.uv);
  4.     return 1 - col;
  5. }
复制代码
HSV空间下的hue变化

仔细观察,发现这个特效不单单是取了反,还有一个色相的变化。
可以看到在4帧内,背景色从红色(0°)变成了紫色(-20°)
为了达成这个效果,我们首先要把颜色转到HSV空间,对H分量,也就是色阶进行一些时间相关的变化,然后再把颜色转回RGB空间。
我在网上找到了一个比较trick的算法,没有任何的条件判断语句,十分的GPU友好
  1. float3 RGB2HSV(float3 c)
  2. {
  3.     float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
  4.     float4 p = lerp(float4(c.bg, K.wz), float4(c.gb, K.xy), step(c.b, c.g));
  5.     float4 q = lerp(float4(p.xyw, c.r), float4(c.r, p.yzx), step(p.x, c.r));
  6.     float d = q.x - min(q.w, q.y);
  7.     float e = 1.0e-10;
  8.     return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
  9. }
  10. float3 HSV2RGB(float3 c)
  11. {
  12.     float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
  13.     float3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www);
  14.     return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y);
  15. }
复制代码
实现原理参照这个博客,具体我没有细看(我超虚的)。。
注意,这个算法得到的HSV分量是位于0~1之间的。H分量并不是传统的0~360的范围。
我们让H分量在原色阶右边1/5色环的范围内按时间的正弦函数周期变化。
  1. float3 hsvColor = RGB2HSV(col.rgb);
  2. hsvColor.x += lerp(0,0.2,sin( UNITY_TWO_PI * frac(_Time.y *0.5)));
  3. hsvColor.x = frac(hsvColor.x);
  4. reversedColor.rgb = 1 - HSV2RGB(hsvColor.rgb);
  5. return reversedColor;
复制代码
圆形边界的扭曲与运动

The World的效果类似一个半径不断扩大的球体,球体内部发生反色,而球体外部不变。放在后处理中就是一个圆。另外我们想要的是一个正圆,所以我们求出屏幕长宽比后进行修正。
  1. float aspect = _ScreenParams.x / _ScreenParams.y;
  2. float x = (i.uv.x - 0.5f - _CenterX) * aspect;//would be ellipse if not multiply the aspect
  3. float y = i.uv.y - 0.5f - _CenterY;
  4. float d = x * x + y * y;
  5. ...
  6. if ( d < _Radius * _Radius )  {
  7.     return reversedColor;
复制代码
但是这个圆它有些"太圆了",我们想要的是一个坑坑洼洼的圆。我的思路是,根据圆心角角度的不同,在原有半径上再叠加一个函数,并保证该函数的连续。首先我们拿正弦函数试试。
  1. float sin_theta = saturate(y / sqrt(d));//d = r^2  小心NAN!!!
  2. float half_theta = asin(sin_theta) *  (step(0,x)-0.5);
  3. float deformFactor = (1 + 0.02 * sin(half_theta * 24) * lerp(0, 0.5, sin(UNITY_TWO_PI * _Time.y * 0.5)));
  4. _Radius *= deformFactor;
复制代码
在求圆心角的时候, 要注意arcsin函数只够管一四象限的点。对于x小于零的点,我们要做一些计算,保证能得到正确的角度。
嗯,现在边缘不再那么圆了,但是还是很规整,于是自然想到多个正弦波叠加,能够保证它的周期连续性。我这里用了三个,数值是自己乱填的。
  1. float deformFactor = (1 + 0.1 * sin(half_theta * 24) * lerp(0, 0.5, sin(UNITY_TWO_PI * _Time.y * 0.5))
  2.                     + 0.25 * x * sin(1 + half_theta * 6.5) * lerp(0.25, 0.75, sin(UNITY_TWO_PI * _Time.y * 0.2))
  3.                     + 0.1 * x * x * sin(2 + half_theta * 9.5) * lerp(0.25, 0.75, sin(UNITY_TWO_PI * _Time.y * 0.1))
  4.                     );
复制代码
是不是有那么点意思啦!
两个类似rim light的冲击波

回想一下在三维空间中我们是怎么给物体加上泛光描边的:计算视线与法相的夹角,判断该像素是否在边缘,然后根据夹角大小叠加颜色。
而回到我们的二维情况,事情就变的简单多了:指定一个半径,然后进行颜色插值。半径上的光效最强,圆心的颜色为零。
  1. half power = 1.5;
  2. if (d < _ImpactRadius * _ImpactRadius) {
  3.     float t = saturate(d / (_ImpactRadius * _ImpactRadius));//小心NAN!!!
  4.     fixed4 rim = lerp(0, _ImpactColor, pow(t, power));
  5.     finalColor += rim * rim.a;
  6. }
复制代码
power决定了到底按照离圆心距离的几次方进行插值,表现在图像上就是光环的粗细或者说强度。
power=1.5
power=15
各向异性的斑纹

我们从第五部中的时停中截出一帧,可以看到以白金之星的手指为中点发散出去的扭曲的斑纹。
食堂泼辣酱——咋瓦鲁多!
于是我们首先想到的就是……光盘!
批发光碟啦
额不好意思,放错图了。
其实各项异性指的是从不同的角度去看光碟,得到的颜色不同,而并不是指的光碟上不同圆心角的颜色不同,我这里就借这个词用一下,可别被我误导了哈!
为了实现这种效果,我们需要一个函数或者一张噪声图,然后将不同的圆心角映射到不同的颜色值上面去。这里我选择使用贴图。
这张贴图有一个妙的地方,之后会提到
其实我们只有一个变量,就是角度,那对于图片来说,我们也只需要一个维度就行了,也就是从图片中取一条线来用。
  1. fixed4 wave = tex2D(_NoiseTex, half_theta*2) * waveIntensity;
  2. return wave;
复制代码
我们直接输出,看看效果:
嗯,是我们想要的效果。现在我们还需要加上扭曲的斑纹,以及让它随着时间动起来。
思路是扭曲,从刚刚的噪声图中取值,并让该值随着时间规律变化,然后适用这个值对这个大光碟中的点的uv进行偏移。
  1. float4 noise = tex2D(_NoiseTex, i.uv + _Time.xy * twistSpeed);
  2. fixed4 wave = tex2D(_NoiseTex, half_theta*2 + noise.xy * waveShape) * waveIntensity;
复制代码
最后我们把wave的值加到内圈的颜色上去就行了。


时停范围内的扭曲

我们已经简单的叠加上了斑纹,然而内部并没有正真的扭曲,所以我们再加上一个扭曲的效果。而扭曲其实我们刚才已经实现过一遍了(uv偏移),将扭曲斑纹的方法故技重施。
  1. fixed4 twistedColor = tex2D(_MainTex, i.uv + noise.xy * twistIntensity);
  2. ...
  3. -float3 hsvColor = RGB2HSV(col.rgb);
  4. +float3 hsvColor = RGB2HSV(twistedColor.rgb);
复制代码
大功告成!
现在刚刚那张噪声图的优越性就体现出来了,因为它是一张边界连续的噪声图,如果不是的话,你会看到很明显的十字扫描线。这里就不贴这种情况的效果图啦。
时停结束后转为灰度图

转化为灰度图也是很方便的事情,使用一个著名的经验公式:


我们使用一个float变量控制灰度的开关,在发波时为0,收缩时为1。然后我们可以把上述的式子简写成dot的形式。
  1. if(d < _Radius * _Radius){
  2.     ...
  3. }
  4. else{
  5.     if(isGray){
  6.         fixed3 grayFactor = { 0.299,0.587,0.114 };
  7.         fixed grayColor = dot(grayFactor, col);
  8.         ...
  9.     }
  10. }
复制代码
径向模糊

这是一种沿方向的模糊,很容易营造出速度感。
这个的实现我参照的这篇Blog https://blog.csdn.net/xoyojank/article/details/5146297
因为径向模糊是一个比较独立的效果,这里我们新起一个Shader来完成这个效果,然后在OnRenderImage方法中Graphics.Blit两次 , 先完成刚刚一系列操作,再对结果进行径向模糊。
  1. void Awake (){
  2.     mySource = new RenderTexture(origin.width / 2, origin.height / 2, 0);
  3.     RenderTexture.active = mySource;
  4.     Graphics.Blit(origin, mySource);
  5. }
  6. void OnRenderImage(RenderTexture source, RenderTexture destination)
  7. {
  8.     ...//check material and set properties
  9.     RenderTexture rt = RenderTexture.GetTemporary(source.width, source.height);
  10.     Graphics.Blit(mySource, rt, colorMaterial);
  11.     Graphics.Blit(rt, destination, blurMaterial);
  12.     RenderTexture.ReleaseTemporary(rt);
  13. }
复制代码
具体的算法很简单。在模糊中心到该像素的方向上选取n个点,并将其求平均。
  1. fixed4 frag (v2f i) : SV_Target
  2. {
  3.     fixed4 col = tex2D(_MainTex, i.uv);
  4.     fixed4 sum = col;
  5.     float2 dir = 0.5 - i.uv + float2(_CenterX,_CenterY);
  6.     float dist = length(dir);
  7.     dir /= dist;
  8.     float samples[10] =
  9.     {
  10.         -0.08,
  11.         -0.05,
  12.         -0.03,
  13.         -0.02,
  14.         -0.01,
  15.         0.01,
  16.         0.02,
  17.         0.03,
  18.         0.05,
  19.         0.08
  20.     };
  21.     for (int it = 0; it < 10; it++) {
  22.         sum += tex2D(_MainTex, i.uv + dir * samples[it] * _SampleDist);
  23.     }
  24.     sum /= 11;
  25.     float t = saturate(dist * _SampleStrength);
  26.     float4 blur = lerp(col, sum, t);
  27.     return blur;
  28. }
复制代码
在时停的过程中,径向模糊的强度发生脉冲一样的变化,很有冲击感。
再见,谢谢所有的if

至此我们就粗略的完成了一个时停效果的制作啦。最后提一下如何优化我们的代码,将我们的老朋友if都改成step。
为什么说if不好呢?因为GPU是并行计算的,保证一样的执行顺序是有利于GPU的。而当你使用if时,同一段代码的运行顺序与时间就会随着情况的不同而发生变化,这是非常不适合并行计算的。
让我们从代码中最先出现if的地方开始
  1. if ( d < _Radius * _Radius )  {
  2.     final = reversedColor;
  3. }
  4. else{
  5.     final = col;
  6. }
复制代码
代码很好理解。判断点是否在圆内,如果在里面,则反色。要把这个if干掉,首先我们来介绍一下我们的主人公:step
step这个函数其实很简单,看一下就明白啦
  1. t1 = step(x , y);
  2. t2 = x<y?1:0;
  3. //t1等价于t2
  4. t3 = step(0,0.5);
  5. //t3 == 1;
  6. t4 = step(1,0.5);
  7. //t4 == 0;
复制代码
简单来说,就是比较x是不是比y大,那我们要如何用这个函数来干掉if呢?
  1. float rr = _Radius * _Radius;
  2. half insideCircle = step(d, rr);
  3. finalColor = lerp(col, reversedColor, insideCircle);
复制代码
嗯,对,就这样就完成了,大家可以验算一下,不一定要这样写,只要你能保证得到的结果与ifelse一致即可,十分的简单粗暴。下面我们来看一下if嵌套的情况:
  1. if ( d < _Radius * _Radius )  {
  2.     final = reversedColor;
  3. }
  4. else{
  5.     if(isGray)
  6.         final = grayColor;
  7. }
复制代码
可以看到,这里套了两层,那怎么来表示同时满足呢?利用逻辑&的性质,final = grayColor在两个step一个为真,一个为假的情况下执行,也就是step* (1- step)为真,于是上面的代码可以写成这样。
  1. float rr = _Radius * _Radius;
  2. half isGray = step(0.5, _Gray);
  3. half insideCircle = step(d, rr);
  4. finalColor = lerp(col, reversedColor, insideCircle);
  5. fixed3 grayFactor = { 0.299,0.587,0.114 };
  6. fixed grayColor = dot(grayFactor, col);
  7. finalColor = lerp(finalColor, grayColor, isGray * (1-insideCircle));//step1 * (1-step2)
复制代码
大致就是这样,使用逻辑运算与多项式的组合达到目标效果。


好啦,就到这里了,详细工程文件与代码点击这里下载。之后可能会加一个mask让时停的发动者不为灰色。
这里也希望各路大神对我的方法提出评判指正,这是我第一次完整的去做一个效果,所以肯定有不少问题与错误…
最后回顾一下我们的效果:
下次见!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-17 12:40 , Processed in 0.098926 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表