Fixeds, Floats and a Block Damage Effect(定点值,浮点值和块状故障特效)

原作者链接

怎么做

我们需要做的第一件事就是找到某种可以将屏幕分成矩形块的方法,并且可以用缩放因子调整。你可以在着色器中用数学的方式调整UV,但是更简单的方法是用纹理去驱动,所以我们需要创建一张像下面这样的纹理:

通过将这张纹理映射到屏幕,我们为屏幕上每一个像素都分配一个Glitch值,这些值都在(0,1)区间,然后我们会向着色器传递另一个阈值与之比较,这个阈值也在(0,1)之间,并且我们可以调整它。当着色器执行时,对于任何被分配的Glitch值小于阈值的像素,我们会对它的UV坐标做增加或减少的偏移,这会使区域块中的像素采样周围的像素,产生我们希望的效果:

如果我们用上面的灰度图,我们的偏移UV总是斜的,指向同一方向,这并不是我们想要的。所以我使用R通道作为Glitch值,GB通道作为UV偏移的噪声值,我们最后的图像是这样的:

(我写了一个快速生成这种图象的工具,我不会解释如何编写的,你可以在
Github中找到它)

接着我们会增加UV偏移的随机性,因为采样图像不变,采样得到偏移值也是固定的,最终结果也就是固定的,然后我会做一写让人更信服的调整,并提出许多可以扩展的方法,还有一些优化技巧。

让我们开始吧!

在屏幕上添点东西

当我工作时我总是想一点一点的添加东西,这样我就能确定我的代码做的和我想得是否一样。所以以这种方式开始吧,先是能生成Glitch效果,弄乱整个屏幕。

通常我们制作一个后处理特效时我们需要做一些准备。这次不像以前,这个特效很小,所以并不需要额外配置相机,我们只需要用我们的材质贴到屏幕上。

[RequireComponent(typeof(Camera))]
public class GlitchFX: MonoBehaviour
{
public float glitchAmount = 0.0f;
public Texture2D blockTexture;

private Shader _glitchShader;
private Material _glitchMat;

void Start ()
{
    _glitchShader = Shader.Find("Hidden/GlitchFX/GlitchFX_Shift");
    _glitchMat = new Material(_glitchShader);
    _glitchMat.SetTexture("_GlitchMap", blockTexture);
}

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
    Graphics.Blit(source, destination, _glitchMat);
}

void Update ()
{
    glitchAmount = Mathf.Clamp(glitchAmount, 0.0f, 1.0f);
    _glitchMat.SetFloat("_GlitchAmount", glitchAmount);
}

后面我们会重新调整这段脚本,现在这些就足够了。接下来,我们需要配置我们的着色器。我假定你已经可以设置这些材质文件,现在转到片段着色器。如果你没跟上,你可以在Github中找到这个着色器代码

fixed4 frag (v2f i) : SV_Target
{
    fixed2 glitch = (tex2D(_GlitchMap, i.uv)).rg;           
    fixed4 col = tex2D(_MainTex, i.uv + glitch.rg);
    return col;
}

现在我们做好了!你运行Unity的话你会看到Glitch的效果!如果你看到像下面这样区域块边缘有白线的话,确保纹理的过滤模式为点过滤。

优化点1

我们刚才做的非常直接,现在有必要花点时间去考虑一个可以优化的地方。注意我只采样了2个通道。这比采样全部通道或用1个通道创建1个fixed2的结果稍微快点。

你可以像我一样做个测试,这个测试每帧执行101次特效的处理代码:

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
    RenderTexture t = RenderTexture.GetTemporary(source.width, source.height);
    for (int i = 0; i < 50; ++i)
    {
        Graphics.Blit(source, t, _glitchMat);
        Graphics.Blit(t, source, _glitchMat);
    }
    Graphics.Blit(source, destination, _glitchMat);
    RenderTexture.ReleaseTemporary(t);
}

如果没有上面那样测试,我的iPhone6很难看出性能的影响,尽管这样做了,也只有2毫秒的差异。如果你不知道这项技术,你的项目可能不会有什么损失,但是尽可能去优化它,特别是我们想在移动设备上保持60帧时。

到目前为止涉及了纹理采样,现在把注意放到变量的精度上来,我会解释为什么用fixed2精度存储而不是float2。当发生类型转换时,会执行更多的指令。有的函数需要float类型的值作为参数,所以传递fixed类型的值就会产生类型转换。

我们需要看一下Unity编译器生成的上面着色器的GLSL代码:

uniform sampler2D _MainTex;
uniform sampler2D _GlitchMap;
varying highp vec2 xlv_TEXCOORD0;
void main ()
{
  lowp vec4 tmpvar_1;
  tmpvar_1 = texture2D (_GlitchMap, xlv_TEXCOORD0);
  highp vec2 P_2;
  P_2 = (xlv_TEXCOORD0 + tmpvar_1.xy);
  lowp vec4 tmpvar_3;
  tmpvar_3 = texture2D (_MainTex, P_2);
  gl_FragData[0] = tmpvar_3;
}

注意上面Unity默认编译器生成的GLSL代码中,sampler2D是低精度的(fixed)。GLSL的lowp、mediump、highp类型分别对应CG中fixed、half、float类型。这意味着如果我们用float2类型代替fixed2类型存储采样的结果,tex2D的返回值就会进行类型转换,fixed上升到float。你可以改成float2来看看发生了什么:

uniform sampler2D _MainTex;
uniform sampler2D _GlitchMap;
varying highp vec2 xlv_TEXCOORD0;
void main ()
{
  highp vec2 glitch_1;
  lowp vec2 tmpvar_2;
  tmpvar_2 = texture2D (_GlitchMap, xlv_TEXCOORD0).xy;
  glitch_1 = tmpvar_2;
  highp vec2 P_3;
  P_3 = (xlv_TEXCOORD0 + glitch_1);
  lowp vec4 tmpvar_4;
  tmpvar_4 = texture2D (_MainTex, P_3);
  gl_FragData[0] = tmpvar_4;
}

上面的代码可以看出有无意义的转换(glitch_1 = tmpvar_2),它所带来的性能影响是确实存在的。我建议你自己使用之前的方法(每帧运行100多次)亲自做一些测试。如果你做了,你会发现虽然单个类型转换的代价比不多,但是经过整个项目,这个代价就会增加许多。

因为我们没有用half或者float的sampler2D,所以用fixed类型是正好的。如果你需要采样这样的纹理,你可以在sampler2D加上对应精度的后缀:

sampler2D_float _GlitchMap;
sampler2D_half _GlitchMap;

好了,刚才分析了许多,让我们开始动手。

完成Glitch特效

到目前为止我们的后处理着色器只影响屏幕的某一时刻,但这不是我们想要的效果,我们想在不同的时间点对不同的区块应用Glith效果。

我现在要使用R通道了,用它为屏幕上每个区块的像素分配一个Glitch值,虽然这会产生一个预定义的效果,但这会比我们刚才的更令人信服。一旦完成了这种效果,我们就可以添加随机了。

我们还需要传递一个float的阈值到着色器中,让它与每个区块分配的Glitch值相比,如果分配的值小于等于我们的控制变量就会应用偏移效果,大于的话就是正常的效果。这样的话,如果我们传递1,所有地方都会生效,因为没有值可以大于1。

如果所有的GPU能很好的处理分支结构,我们会这样写:

fixed4 frag(v2f i) : SV_Target
{
    fixed2 glitch = (tex2D(_GlitchMap, i.uv)).rg;

    float2 uvShift = glitch.rg;
    if (glitch.r >= _GlitchAmount)
    {
        uvShift *= 0.0;
    }

    fixed4 col = tex2D(_MainTex, frac(i.uv + uvShift));
    return col;
}

但不幸的是我们并不能保证所有设备都能很好的运行,所以我用达成同样结果的数学式子来替换条件表达式:

fixed4 frag(v2f i) : SV_Target
{
    fixed2 glitch = (tex2D(_GlitchMap, i.uv)).rg;

    float2 uvShift = glitch.rg * ceil(_GlitchAmount - glitch.r);

    fixed4 col = tex2D(_MainTex, i.uv + uvShift));
    return col;
}

我们做的是让两个值相减并对结果做向上取整,我们可以这样做的原因是这两个值的范围是一样的,都是(0到1)。然而有个特殊情况:如果Glitch值是1的话,这个式子可能会得到-1,这会使我们不想Glitch的地方也扭曲,这显然不对,所以我用取最大值的操作来解决这个问题:

float2 uvShift = glitch.rg * ceil(max(-0.99,_GlitchAmount - glitch.r));

在实际项目中,你可以对Glith纹理预先处理一下,让它没有等于1的区域,你就能移除多余的指令从而提升性能。

你运行时可能会看到奇怪的颜色,我这就多了点棕色:

这是因为当我们向UV坐标添加偏移时,最终结果可能超过(0,1),我们就会采样超出屏幕边界的图像。帧缓存设置为clamp模式,就是如果超出边界就会选择边界的颜色。我们可以用frac()函数让我们循环采样。

fixed4 col = tex2D(_MainTex, frac(i.uv + uvShift));

所有都做好后,在(0,1)之间调整_GlithAmount的值就会产生以下效果:

优化点2

让我们考虑一下下面的一行代码:

    float2 uvShift = glitch.rg * ceil(max(-0.99,_GlitchAmount - glitch.r));

首先,最好不使用除了float类型外的类型来存储UV坐标,其他类型并没有足够的精度去采样纹理,所以99%的时候你都应该用flaot。因为我们并不关心是否非常精确,因此,选择什么类型的问题由性能绝定。

但是这并不意味着我们要降低精度,因为_GlithAmount是float的,tex2D()函数也期待传递来float精度的值,所以无论怎样都需要转换到float精度,所以,在这里我们遵循标准的规则”使用最高精度进行UV运算“。

尽管我们使用了在这篇文章中使用了大量fixed精度的变量,但这在新的硬件上是没有意义的,大多数GPU都支持half精度,并且会忽略fixed标识符,所有的运算都用half和float进行。这取决于你要发布的目标设备,然而通常也是安全的。如果你的IOS设备支持Metal,用half替代fixed也是安全的。在PC上就更普遍了。

好了,该回到正题了。

随机化Glitch

我们的特效表现得不错,但它还不是真的”glitchy“,对吧?如果我们什么都不做,这个特效就是静态的,只在屏幕上的固定地方扭曲。同样,就算通过_GlitchAmount变化,我们的特效也总是遵从预定义的模式,Glitch的地方总是从一样的顺序开始。是时候增加一些随机了。

我们需要一个随机值而不是R通道固定的Glitch值。并且同一区块的Glitch值也是不同的,还要能够控制随机值改变的速度。

幸运的是,这只需要复制/粘贴生成随机数的一行代码就够了:

float rand(float2 co)
{
    return frac(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453);
}

我们用R通道作为co的第一个成员,然后传递一个从C#脚本传递的常数作为第二个成员。说明这行代码已经超出了这篇文章的范围,你有时间的话可以Googe一下。

这次我们使用float精度的原因是我们想要随机数尽可能的不同。half或fixed在(0,1)范围值的数量相对较少。如果你用half替代float,结果可能会有很大不同。

我们的着色器现在是这样的:

sampler2D _MainTex;
sampler2D _GlitchMap;

float _GlitchAmount;
float _GlitchRandom;

float rand(float2 co)
{
    return frac(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453);
}

fixed4 frag(v2f i) : SV_Target
{
    fixed2 glitch = (tex2D(_GlitchMap, i.uv)).rg;

    float r = (rand(float2(glitch.r, _GlitchRandom)));
    float gFlag = max(0.0, ceil(_GlitchAmount - r));

    float2 uvShift = glitch.rg * gFlag;

    fixed4 col = tex2D(_MainTex, frac(i.uv + uvShift));
    return col;
}

我们还需要在C#脚本的Update函数中中添加一行代码:

void Update ()
{
    glitchAmount = Mathf.Clamp(glitchAmount, 0.0f, 1.0f);
    _glitchMat.SetFloat("_GlitchRandom", Random.Range(-1.0f, 1.0f));

    _glitchMat.SetFloat("_GlitchAmount", glitchAmount);
}

如果你设置_GlitchAmount的值为0.2,运行后你会看到这样的效果:

这个结果很好,但是颤抖的太快了。我把_GlitchRandom 放到另一个函数里,这样我就可以用Invoke方法控制特效执行的频率:

void Start ()
{
    _glitchShader = Shader.Find("Hidden/GlitchFX/GlitchFX_Shift");
    _glitchMat = new Material(_glitchShader);
    _glitchMat.SetTexture("_GlitchMap", blockTexture);

    Invoke("UpdateRandom", 0.25f);
}

void UpdateRandom()
{
    _glitchMat.SetFloat("_GlitchAmount", glithAmount);
    _glitchMat.SetFloat("_GlitchRandom", Random.Range(-1.0f, 1.0f));
    Invoke("UpdateRandom", Random.Range(0.01f, 0.15f));
}

对这段代码的一点改变产生的效果差异很大:

增加采样方向

我们还有最后两个问题需要解决:

  • 当_GlitchAmount设置为1时,屏幕上仍然是静态的
  • 我们采样出来的方向是一样的

这两个问题很好解决。对于第一个问题,我们需要做的是给UV偏移量乘一个随机值。用这种方式,每个区块都会有不同的坐标,还会减少相同的地方。

float2 uvShift = glitch.rg * gFlag * r;

对于第二个问题,我们最终会使用我之前展示的带颜色的纹理作为噪声图像,刚才我们是使用RG通道来产生UV偏移,现在我们使用了颜色图像,就可以使用GB通道了:

fixed4 frag(v2f i) : SV_Target
{
    fixed3 glitch = (tex2D(_GlitchMap, i.uv)).rgb;

    float r = (rand(float2(glitch.r, _GlitchRandom)));
    float gFlag = max(0.0, ceil(_GlitchAmount-r));

    float2 uvShift = glitch.gb * gFlag;

    fixed4 col = tex2D(_MainTex, frac(i.uv + uvShift));
    return col;
}

这很好,但GB通道都是正值,我们仍然只能得到两个方向的向量。我们可以用过将(0,1)映射到(-1,1)来修复它。做一次乘法和减法就行了:

float2 uvShift = (glitch.gb * 2.0 - 1.0) * gFlag;

如果你现在运行场景,你就会得到这篇文章开头的效果。

结尾

像往常一样,我今天谈论的所有东西都会放在Github,你可以轻松获得他们。

最后我们讨论一下性能和可以扩展这个特效的方法。

从性能出发,这个特效很轻量级。即使我们使用全部屏幕的像素都来读取纹理,我的iPone6只花了0.2ms(1s60帧,1帧16ms)。你需要注意的是不论应用整个屏幕(_GlitchAmount>0)还是一点也不应用(_GlitchAmount=0),他们带来性能损耗是一样的。所以在实际项目中,设置_GlitchAmount为0时用C#去关闭这个特效是很值得的。

最后,有许多方法去扩展这个特效。你可以偏移被Glitch区块的色调,调整他们的颜色,增加色差,或者用噪声纹理增加奇怪的效果。如果你想要一些灵感,可以看一下这。Glitch效果很好玩,因为你不是使事物看起来“正确”,这是很多人喜欢他的原因。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值