Fixeds, Floats and a Block Damage Effect(定点值,浮点值和块状故障特效)
2017-04-21 22:59
267 查看
原作者链接
通过将这张纹理映射到屏幕,我们为屏幕上每一个像素都分配一个Glitch值,这些值都在(0,1)区间,然后我们会向着色器传递另一个阈值与之比较,这个阈值也在(0,1)之间,并且我们可以调整它。当着色器执行时,对于任何被分配的Glitch值小于阈值的像素,我们会对它的UV坐标做增加或减少的偏移,这会使区域块中的像素采样周围的像素,产生我们希望的效果:
如果我们用上面的灰度图,我们的偏移UV总是斜的,指向同一方向,这并不是我们想要的。所以我使用R通道作为Glitch值,GB通道作为UV偏移的噪声值,我们最后的图像是这样的:
(我写了一个快速生成这种图象的工具,我不会解释如何编写的,你可以在
Github中找到它)
接着我们会增加UV偏移的随机性,因为采样图像不变,采样得到偏移值也是固定的,最终结果也就是固定的,然后我会做一写让人更信服的调整,并提出许多可以扩展的方法,还有一些优化技巧。
让我们开始吧!
通常我们制作一个后处理特效时我们需要做一些准备。这次不像以前,这个特效很小,所以并不需要额外配置相机,我们只需要用我们的材质贴到屏幕上。
后面我们会重新调整这段脚本,现在这些就足够了。接下来,我们需要配置我们的着色器。我假定你已经可以设置这些材质文件,现在转到片段着色器。如果你没跟上,你可以在Github中找到这个着色器代码
现在我们做好了!你运行Unity的话你会看到Glitch的效果!如果你看到像下面这样区域块边缘有白线的话,确保纹理的过滤模式为点过滤。
你可以像我一样做个测试,这个测试每帧执行101次特效的处理代码:
如果没有上面那样测试,我的iPhone6很难看出性能的影响,尽管这样做了,也只有2毫秒的差异。如果你不知道这项技术,你的项目可能不会有什么损失,但是尽可能去优化它,特别是我们想在移动设备上保持60帧时。
到目前为止涉及了纹理采样,现在把注意放到变量的精度上来,我会解释为什么用fixed2精度存储而不是float2。当发生类型转换时,会执行更多的指令。有的函数需要float类型的值作为参数,所以传递fixed类型的值就会产生类型转换。
我们需要看一下Unity编译器生成的上面着色器的GLSL代码:
注意上面Unity默认编译器生成的GLSL代码中,sampler2D是低精度的(fixed)。GLSL的lowp、mediump、highp类型分别对应CG中fixed、half、float类型。这意味着如果我们用float2类型代替fixed2类型存储采样的结果,tex2D的返回值就会进行类型转换,fixed上升到float。你可以改成float2来看看发生了什么:
上面的代码可以看出有无意义的转换(glitch_1 = tmpvar_2),它所带来的性能影响是确实存在的。我建议你自己使用之前的方法(每帧运行100多次)亲自做一些测试。如果你做了,你会发现虽然单个类型转换的代价比不多,但是经过整个项目,这个代价就会增加许多。
因为我们没有用half或者float的sampler2D,所以用fixed类型是正好的。如果你需要采样这样的纹理,你可以在sampler2D加上对应精度的后缀:
好了,刚才分析了许多,让我们开始动手。
我现在要使用R通道了,用它为屏幕上每个区块的像素分配一个Glitch值,虽然这会产生一个预定义的效果,但这会比我们刚才的更令人信服。一旦完成了这种效果,我们就可以添加随机了。
我们还需要传递一个float的阈值到着色器中,让它与每个区块分配的Glitch值相比,如果分配的值小于等于我们的控制变量就会应用偏移效果,大于的话就是正常的效果。这样的话,如果我们传递1,所有地方都会生效,因为没有值可以大于1。
如果所有的GPU能很好的处理分支结构,我们会这样写:
但不幸的是我们并不能保证所有设备都能很好的运行,所以我用达成同样结果的数学式子来替换条件表达式:
我们做的是让两个值相减并对结果做向上取整,我们可以这样做的原因是这两个值的范围是一样的,都是(0到1)。然而有个特殊情况:如果Glitch值是1的话,这个式子可能会得到-1,这会使我们不想Glitch的地方也扭曲,这显然不对,所以我用取最大值的操作来解决这个问题:
在实际项目中,你可以对Glith纹理预先处理一下,让它没有等于1的区域,你就能移除多余的指令从而提升性能。
你运行时可能会看到奇怪的颜色,我这就多了点棕色:
这是因为当我们向UV坐标添加偏移时,最终结果可能超过(0,1),我们就会采样超出屏幕边界的图像。帧缓存设置为clamp模式,就是如果超出边界就会选择边界的颜色。我们可以用frac()函数让我们循环采样。
所有都做好后,在(0,1)之间调整_GlithAmount的值就会产生以下效果:
首先,最好不使用除了float类型外的类型来存储UV坐标,其他类型并没有足够的精度去采样纹理,所以99%的时候你都应该用flaot。因为我们并不关心是否非常精确,因此,选择什么类型的问题由性能绝定。
但是这并不意味着我们要降低精度,因为_GlithAmount是float的,tex2D()函数也期待传递来float精度的值,所以无论怎样都需要转换到float精度,所以,在这里我们遵循标准的规则”使用最高精度进行UV运算“。
尽管我们使用了在这篇文章中使用了大量fixed精度的变量,但这在新的硬件上是没有意义的,大多数GPU都支持half精度,并且会忽略fixed标识符,所有的运算都用half和float进行。这取决于你要发布的目标设备,然而通常也是安全的。如果你的IOS设备支持Metal,用half替代fixed也是安全的。在PC上就更普遍了。
好了,该回到正题了。
我们需要一个随机值而不是R通道固定的Glitch值。并且同一区块的Glitch值也是不同的,还要能够控制随机值改变的速度。
幸运的是,这只需要复制/粘贴生成随机数的一行代码就够了:
我们用R通道作为co的第一个成员,然后传递一个从C#脚本传递的常数作为第二个成员。说明这行代码已经超出了这篇文章的范围,你有时间的话可以Googe一下。
这次我们使用float精度的原因是我们想要随机数尽可能的不同。half或fixed在(0,1)范围值的数量相对较少。如果你用half替代float,结果可能会有很大不同。
我们的着色器现在是这样的:
我们还需要在C#脚本的Update函数中中添加一行代码:
如果你设置_GlitchAmount的值为0.2,运行后你会看到这样的效果:
这个结果很好,但是颤抖的太快了。我把_GlitchRandom 放到另一个函数里,这样我就可以用Invoke方法控制特效执行的频率:
对这段代码的一点改变产生的效果差异很大:
当_GlitchAmount设置为1时,屏幕上仍然是静态的
我们采样出来的方向是一样的
这两个问题很好解决。对于第一个问题,我们需要做的是给UV偏移量乘一个随机值。用这种方式,每个区块都会有不同的坐标,还会减少相同的地方。
对于第二个问题,我们最终会使用我之前展示的带颜色的纹理作为噪声图像,刚才我们是使用RG通道来产生UV偏移,现在我们使用了颜色图像,就可以使用GB通道了:
这很好,但GB通道都是正值,我们仍然只能得到两个方向的向量。我们可以用过将(0,1)映射到(-1,1)来修复它。做一次乘法和减法就行了:
如果你现在运行场景,你就会得到这篇文章开头的效果。
最后我们讨论一下性能和可以扩展这个特效的方法。
从性能出发,这个特效很轻量级。即使我们使用全部屏幕的像素都来读取纹理,我的iPone6只花了0.2ms(1s60帧,1帧16ms)。你需要注意的是不论应用整个屏幕(_GlitchAmount>0)还是一点也不应用(_GlitchAmount=0),他们带来性能损耗是一样的。所以在实际项目中,设置_GlitchAmount为0时用C#去关闭这个特效是很值得的。
最后,有许多方法去扩展这个特效。你可以偏移被Glitch区块的色调,调整他们的颜色,增加色差,或者用噪声纹理增加奇怪的效果。如果你想要一些灵感,可以看一下这。Glitch效果很好玩,因为你不是使事物看起来“正确”,这是很多人喜欢他的原因。
怎么做
我们需要做的第一件事就是找到某种可以将屏幕分成矩形块的方法,并且可以用缩放因子调整。你可以在着色器中用数学的方式调整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效果很好玩,因为你不是使事物看起来“正确”,这是很多人喜欢他的原因。
相关文章推荐
- Authorization and Profile Application Block 1.0研究总结
- Cause and Effect Diagram- Fishbone Diagram Analysis
- 快速解决"is marked as crashed and should be repaired"故障
- 定点dsp和浮点dsp的对比(转)
- How do I choose grid and block dimensions for CUDA kernels?
- 517A - 关于浮点小数与定点小数的一点自己想法
- Damage :after normal block:
- Azure Basic - Retrieve data from BlockBlob, Table and Queue
- 实现三个div同一行显示,IE6 and Ie7 :display:inline-block
- [MySQL]快速解决"is marked as crashed and should be repaired"故障[转]
- Qt 图形特效(Graphics Effect)介绍
- greecket - a python not block socket module base the greenlet and thread - Google Project Hosting
- The Complete Effect and HLSL Guide_7(转载)
- Unity3D图像后处理特效——Edge Detect Effect Normals
- Installing Oracle Block Browser and Editor tool (bbed)
- DSP的定点与浮点
- spark 2.1 metrics Source and BlockManagerSource
- WPF里的一些Effect特效
- make filesystem and erase mtdblock指令
- DSP中浮点转定点运算--定点数模拟浮点数运算及常见的策略