unity 外部摄像头特效_Unity后处理实现The World时停效果

fc0607a6b171a49e677aee325e64ff10.png

因为是用的后处理方法实现的,和图形就没什么太大关系了,更多的是图像处理。先上效果:

bbc3b0456a76026a44551181595ff513.gif猜猜卡比吞了啥?

7c4534dfb88e762a188d4395dc564427.gif参考了下面三张gif,对比一下原版,还是逊色了些, 可能是因为动画的场景在运动?我的扭曲效果也差了不少。

77deff56fffefec7193552727de9672e.gif9741c789afcd62a0bf7dc145c84fcee3.gif第五部中没有了扭曲效果,换成了轻微的抖动。

081b57dcd89cb61efdbddc6617c280d1.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

HSV空间下的hue变化

仔细观察,发现这个特效不单单是取了反,还有一个色相的变化。

a4352aeed2a7dce426ec3492df30d53d.png
可以看到在4帧内,背景色从红色(0°)变成了紫色(-20°)

为了达成这个效果,我们首先要把颜色转到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;

775f43dd19002efbdfc901de87a5c5cb.gif圆形边界的扭曲与运动

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

但是这个圆它有些"太圆了",我们想要的是一个坑坑洼洼的圆。我的思路是,根据圆心角角度的不同,在原有半径上再叠加一个函数,并保证该函数的连续。首先我们拿正弦函数试试。

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;

eb7192845e2fa63de6160c956e7d5c38.gif

在求圆心角的时候, 要注意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))                    );

311694136d0966b957c26fa62e3b5657.gif

是不是有那么点意思啦!

两个类似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
power=1.5
f0fef20372658ccf9d38c07e20585303.png
power=15

各向异性的斑纹

我们从第五部中的时停中截出一帧,可以看到以白金之星的手指为中点发散出去的扭曲的斑纹。

ea5f5d113fb08bcf279941e17b27bbb5.png
食堂泼辣酱——咋瓦鲁多!

于是我们首先想到的就是……光盘!

5a62fa9f4b4b70adcaebfc66d318c78c.png
批发光碟啦

额不好意思,放错图了。

6c61404e6995b315e1641fc43680209e.png

其实各项异性指的是从不同的角度去看光碟,得到的颜色不同,而并不是指的光碟上不同圆心角的颜色不同,我这里就借这个词用一下,可别被我误导了哈!

为了实现这种效果,我们需要一个函数或者一张噪声图,然后将不同的圆心角映射到不同的颜色值上面去。这里我选择使用贴图。

92abed67c2e75b2308baf2e3332e179a.png
这张贴图有一个妙的地方,之后会提到

其实我们只有一个变量,就是角度,那对于图片来说,我们也只需要一个维度就行了,也就是从图片中取一条线来用。

fixed4 wave = tex2D(_NoiseTex, half_theta*2) * waveIntensity;
return wave;

我们直接输出,看看效果:

67da77340b5f68e6c165a1115e279b82.png

嗯,是我们想要的效果。现在我们还需要加上扭曲的斑纹,以及让它随着时间动起来。

思路是扭曲,从刚刚的噪声图中取值,并让该值随着时间规律变化,然后适用这个值对这个大光碟中的点的uv进行偏移。

float4 noise = tex2D(_NoiseTex, i.uv + _Time.xy * twistSpeed);
fixed4 wave = tex2D(_NoiseTex, half_theta*2 + noise.xy * waveShape) * waveIntensity;

b3c0daa763bdcfab14aabc83ff2e4d03.gif

最后我们把wave的值加到内圈的颜色上去就行了。

0bcc56a232e029228745af7c9f83eb0b.gif

时停范围内的扭曲

我们已经简单的叠加上了斑纹,然而内部并没有正真的扭曲,所以我们再加上一个扭曲的效果。而扭曲其实我们刚才已经实现过一遍了(uv偏移),将扭曲斑纹的方法故技重施。

fixed4 twistedColor = tex2D(_MainTex, i.uv + noise.xy * twistIntensity);
...
-float3 hsvColor = RGB2HSV(col.rgb);
+float3 hsvColor = RGB2HSV(twistedColor.rgb);

f33a8c7abaab7d04729634c290b0d7e6.gif

大功告成!

现在刚刚那张噪声图的优越性就体现出来了,因为它是一张边界连续的噪声图,如果不是的话,你会看到很明显的十字扫描线。这里就不贴这种情况的效果图啦。

时停结束后转为灰度图

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

8b34069d-1b32-eb11-8da9-e4434bdf6706.svg

我们使用一个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

径向模糊

这是一种沿方向的模糊,很容易营造出速度感。

这个的实现我参照的这篇Blog blog.csdn.net/xoyojank/

0402fcd92d5e94abb8fcfb1c1644154d.png

因为径向模糊是一个比较独立的效果,这里我们新起一个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

在时停的过程中,径向模糊的强度发生脉冲一样的变化,很有冲击感。

再见,谢谢所有的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让时停的发动者不为灰色。

这里也希望各路大神对我的方法提出评判指正,这是我第一次完整的去做一个效果,所以肯定有不少问题与错误…

最后回顾一下我们的效果:

172e0c57e0ad1a3907e0583a91ad838a.gif下次见!

7de4645ff9bf3b04b167c54f28b89614.png

往期精选

Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着

Shader学习应该如何切入?

写出一手烂代码的19条准则


声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。

作者:陈泽日天

来源:https://zhuanlan.zhihu.com/p/144948248


More:【微信公众号】 u3dnotes


efc8b5497d39a06702463cc7ccd4f698.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值