unity 特效_Unity实现吸收冲击波的能量罩特效!

0e3e4b8413ed42da6cbda9657593e32c.png

本文来源微信号:unitymanual

版权归原作者所有

一个能量罩(力场)特效,能够在受到攻击的部位产生扩散的波纹。(视频有音效,建议选择超清)

能吸收冲击波的能量罩特效

(慢速版,可观察波纹扩散)

能吸收冲击波的能量罩特效(慢速)

能量罩最基本的构成就是发亮的轮廓,先用菲涅尔点亮它的边缘,然后它与其他物体相交的部位也要变亮。这些是所有能量罩特效的共有特征,我会在第1节快速讲完。但要让它看起来像一个真正在发光的物体,还要用到bloom,以及一些微妙的shader技巧,这些都会在第2节详细介绍。怎样让球知道它的哪里被击中了,以及怎样产生有折射的波纹,会在第3节讲到。

1. 点亮轮廓

和所有的能量罩特效一样,先要赋予它一个发亮的轮廓,方法也没什么新鲜的:仍然是菲涅尔+与其他物体的交接。菲涅尔在vertex shader计算,交接则在fragment shader计算。采样深度纹理,然后把它和当前fragment的深度比较,就可以得到交接程度。值得注意的是我用了pow()函数来让轮廓上亮的部位变窄,而不是让亮的部分在球上蔓延开来。(ForceField.shader)

// vertex shader
// ...
o.fresnel = 1.0 - saturate(dot(viewDir, v.normal));
// ...
// fragment shader
// ...
fixed depth = 1.0 - saturate(LinearEyeDepth(tex2D(_CameraDepthTexture, screenUV).r) - i.screenPos.z);
// roi indicates where is rim or intersection
fixed roi = max(depth, i.fresnel);
fixed alpha = pow(roi * col.g, _Power) + _BasicOpacity;

值得注意的是,有些部位既不在边缘上,有没有和其他物体交接,但是这些地方也不是完全透明的。所以我加了一个“基本不透明度”,营造有厚度的感觉,就像在真玻璃上看到的那样。

以下是有厚度和没厚度的比较:

d7c82e0377852a486d89c5590629ac68.png

2.让它发光

接下来,事情会变得微妙起来。

想要让东西看起来在发光,我们常常会想到HDR,但是为了性能的考虑,要尽量避免使用HDR。取而代之的是,我让最亮的部位变白,来让它看起来过饱和。在现实生活中,特别特别亮的物体会超过相机能承受的范围,然后三个颜色通道都会达到上限,也就是说它变白了。白+蓝的搭配也赋予了场景的颜色多样性和动态感。(ForceField.shader)

// make the edges look like saturating to white, which is to fake a high emittance intensity
fixed3 srcCol = lerp(_Color.rgb, fixed3(1.0, 1.0, 1.0), roi);

这里的"roi"用于指示那里属于轮廓/交接处和哪里不属于。最亮的就是轮廓和交接处。

以下是有白色饱和和没白色饱和的比较:

393676e1fcf99a6d6f9586c1c6c89230.png

想让它发光,最有力的武器还是使用bloom。但是在加上真正的后处理bloom之前,我们可以先在shader里用一些trick来让它有一点点辉光(起码在球上有一点)。

透明度(alpha通道)是由两个因素来决定的:轮廓 / 交接,以及六边形纹理。只有同时在轮廓 / 交接处和六边形以内的部位才能是不透明的。

轮廓和交接处非常亮,我想在这里fake出一点bloom,而bloom则会填充六边形之间的空隙。所以在轮廓和交接处,我通过减小透明度的方式来往缝隙里填充了一点颜色。(ForceField.shader)

fixed alphaDest0 = alpha + roi * 0.3;// + roi * 0.3 to make a fake bloom on the edges

以下是缝隙里有bloom和缝隙里没有bloom的比较:

28ca362725f24bcb8dabd101144367c2.png

现在可以加上真正的后处理bloom了。这个bloom是我自己做的,一个专门针对移动平台优化过的bloom:

Marcus Xie:手游快速Bloom: 比Unity自带bloom快60%zhuanlan.zhihu.comc53d1c389466268fd4f89bd1800b99cb.png

3.扩散的波纹

我觉得一个能量罩应该能对攻击起反应,但是仍然完好无损。所以我就有了波纹扩散的这个想法,波纹扩散看起来就像是冲击波被吸收了。

第一,想要让球知道它身上哪里被击中了,我们需要在OnCollisionEnter()函数里把碰撞点位置传给shader,这个函数是在球被击中的时候被调用的。(ForceField.cs)

第二,扩散的波纹其实就是一种折射,就像水面。要做折射当然不能简单直接地用blending。我们需要得到下面已经被渲染好的像素。这里我用GrabPass来访问渲染纹理。

GrabPass的底层原理是:GPU把一整个帧从back-buffer(默认framebuffer)拷贝到一个渲染纹理里,然后可以在shader中采样这个渲染纹理,shader的计算结果则输出回到back-buffer。这个过程看起来人畜无害,除了拷贝这一步。拷贝一整帧对性能伤害很大,尤其是对移动平台来说。所以如果一定要grab pass,最好是只grab一次。为了确保只拷贝一次,像下面这么写,然后再其他所有地方用_GrabTexture这同一个名字。

GrabPass{ "_GrabTexture" }

采样渲染纹理时,通过采样位置的偏移我们可以扭曲球背后的场景。接下来我们要确定在哪里扭曲、以及扭曲的程度。

在shader中,smoothstep()函数画出来是这样的:

c78aeb2162ee6db0f75020bc12ff364f.png

如果倒转它,然后把两个放在一起,就有了:

353d2a4b470f43acf786e7bd2f25eee3.png

这就是个脉冲。

通过控制mix和max,可以控制脉冲的位置和宽度。我们让当前fragment和碰撞点的距离为t,然后再碰撞点的周围就形成了一圈脉冲,就像月球陨石坑(环形山)。

既然要做波纹,那么就让它波动起来。简单地把脉冲乘以一个Sine函数就可以了。

这个脉冲还是静止不动的,要让它从中心传播开,我们可以随着时间增加smoothstep()的min和max。这里的这个“时间”,是指懂碰撞开始的时间,但是只有CPU才知道是什么时候撞上去的。所以我们从CPU(使用coroutine)把时间传过去:(ForceField.cs)

IEnumerator OneShotOfWave()
{
float i = 0f;
float rate = 1f / waveTime;
while (i < 1f)
{
i += Time.deltaTime * rate;
mat.SetFloat("_WaveScale", 1.0f - i);
yield return 0;
}
mat.SetFloat("_WaveScale", 0.0f);
}

现在这个脉冲终于动起来了,但是如果要让冲击波渐渐被吸收,我需要在脉冲扩散开的同时,少扭曲一点。这里要再次利用_WaveScale,来调节脉冲的宽度和幅度。像这样脉冲就可以在扩散开的同时渐渐消逝。

以上思路全都在shader代码里呈现:

//Distortion
float distToCo = distance(i.worldPos, _CollisionPos.xyz); // distance from current fragment to collision point
// after the wave starts, _WaveScale goes from one to zero
// zeroToRadius controls the spreading-out of the wave, and I pow it to make the spreading speed faster at first and slower later
half zeroToRadius = pow(1.0 - _WaveScale, 1.0 / 2.2) * 0.8; // the constant multiplied here can be seen as radius
// multiply by _WaveScale to make the width of the wave wider at first and narrower later
half waveWidth = 0.2 * _WaveScale;
// use two smoothstep to create a single pulse along the direction to the collision point, that is, a prominent circle around the collision point
float pulseRange = smoothstep(zeroToRadius, zeroToRadius + waveWidth, distToCo) * (1.0 - smoothstep(zeroToRadius + waveWidth, zeroToRadius + 2.0 * waveWidth, distToCo));
// multiply by sin to make it more like a wave, rather than a single pulse,
// multiply by _WaveScale to make the distortion less intensive as the wave spreading out
float wave = pulseRange * sin(distToCo * 50.0) * 30.0 * _WaveScale;
fixed3 dstCol = tex2D(_GrabTexture, screenUV + _GrabTexture_TexelSize.xy * wave).rgb;

源码和示例工程:

debug提示:画面黑了就在Quality关掉抗锯齿,球发白就把building platform换成Android。

https://github.com/MarcusXie3D/ForceFieldShockWaveVFXgithub.com

参考:

1.关于grab pass的一个回答:

How to apply Image effect only on specific part of objects?gamedev.stackexchange.com

2. 油管上的一个能量罩特效:youtube/wbL2FCxlCe4

本文来源知乎专栏:Graphics Fanatic

END

3dbde04d8b6e6a31e7226d02a5d6f32f.png

智慧知行 放飞梦想

扫码关注 兆 泰 源

94226f558df095d70137c571b059fdcf.gif

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值