游戏后处理6大常用模糊算法
- 模糊算法介绍
- BoxBlur均值模糊
- 多次迭代
- 降低采样进行采样-优化
- 9次采样BoxBlur
- 高斯模糊
- 双重模糊技术(Dual Blur)
- Kawase模糊(Kawase Blur)
- 径向模糊(Radial Blur)
- 方向模糊(Directional Blur)
模糊算法介绍
模糊是游戏后处理特效中特别常用的一种,常用于表示速度,故障,环境视角,毛玻璃等。
BoxBlur均值模糊
对当前像素周围4个角的像素进行采样,最后求均值。通过多次迭代叠加模糊效果可以获得更好的效果。
- 片段着色器代码
sampler2D _MainTex;
// Unity内置变量获取贴图的像素, x = 1/width, y = 1/height, z = width w=height
float4 _MainTex_TexelSize;
float _BoxBlurOffset;
fixed4 frag(v2f_img i) : SV_Target
{
float4 uv = _MainTex_TexelSize.xyxy * half4(1, 1, -1, -1) * _BoxBlurOffset;
float4 col2 = tex2D(_MainTex, i.uv + uv.xy) // 1 1
+ tex2D(_MainTex, i.uv + uv.xw) // 1 -1
+ tex2D(_MainTex, i.uv + uv.zy) // -1 1
+ tex2D(_MainTex, i.uv + uv.zw); // -1 -1
col2 *= 0.25f;
return fixed4(col2.rgb, 1);
}
多次迭代
可以在后处理中循环中进行多次模糊来提升模糊效果。使用滚动数组代替两个变量的拷贝,减少一半的拷贝次数。
[ExecuteInEditMode()] // 脚本在编辑模式下也运行
public class BoxBlurImageEffect : MonoBehaviour
{
public Material material;
public int count = 1;
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (count <= 0) // 如果不模糊,避免多余计算
{
Graphics.Blit(src, dest);
return;
}
var tempTextureSrc = RenderTexture.GetTemporary(src.width, src.height); // 申请临时贴图(创建缓冲区)
var tempTextureDest = RenderTexture.GetTemporary(src.width, src.height); // 申请临时贴图(创建缓冲区)
RenderTexture[] tempTexArray = { tempTextureSrc, tempTextureDest }; // 滚动数组
int flag = 0;
Graphics.Blit(src, tempTextureSrc);
for (int i = 0; i < count; i++) // 多次迭代进行模糊
{
// 使用material中的shader处理src(帧缓冲区的像素)。并将结果给dest
Graphics.Blit(tempTexArray[flag], tempTexArray[1 - flag], material);
flag = 1 - flag;
// Graphics.Blit(tempTextureSrc, tempTextureDest, material);
// Graphics.Blit(tempTextureDest, tempTextureSrc);
}
Graphics.Blit(tempTexArray[flag], dest);
//Graphics.Blit(tempTextureSrc, dest);
RenderTexture.ReleaseTemporary(tempTextureSrc); // 不释放会造成内存泄漏
RenderTexture.ReleaseTemporary(tempTextureDest);
Debug.Log("exec Render Image");
}
}
降低采样进行采样-优化
降低每次进行模糊采样的像素
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (count <= 0) // 如果不模糊,避免多余计算
{
Graphics.Blit(src, dest);
return;
}
int width = src.width / 2;
int height = src.height / 2;
var tempTextureSrc = RenderTexture.GetTemporary(width, height); // 申请像素大小相同的临时贴图(创建缓冲区)
var tempTextureDest = RenderTexture.GetTemporary(width, height);
RenderTexture[] tempTexArray = { tempTextureSrc, tempTextureDest }; // 滚动数组
int flag = 0;
Graphics.Blit(src, tempTextureSrc);
for (int i = 0; i < count; i++) // 多次迭代进行模糊
{
// 使用material中的shader处理src(帧缓冲区的像素)。并将结果给dest
Graphics.Blit(tempTexArray[flag], tempTexArray[1 - flag], material, 1);
flag = 1 - flag;
}
Graphics.Blit(tempTexArray[flag], dest);
RenderTexture.ReleaseTemporary(tempTextureSrc); // 不释放会造成内存泄漏
RenderTexture.ReleaseTemporary(tempTextureDest);
}
9次采样BoxBlur
上面只对像素周围4个角进行了采样,我们可以通过对周围9个像素(包括当前像素)进行采样,然后再平均。
fixed4 box_blur_9tap_frag(v2f_img i) : SV_Target
{
float4 uv = _MainTex_TexelSize.xyxy * half4(1, 1, -1, -1) * _BoxBlurOffset;
float4 col2 = tex2D(_MainTex, i.uv + uv.xy) // 1 1
+ tex2D(_MainTex, i.uv + uv.xw) // 1 -1
+ tex2D(_MainTex, i.uv + uv.zy) // -1 1
+ tex2D(_MainTex, i.uv + uv.zw); // -1 -1
// 计算中心, 和4个方向
float2 uv2 = _MainTex_TexelSize.xy * _BoxBlurOffset;
col2 += tex2D(_MainTex, i.uv + uv2.xy * half2(1, 0));
col2 += tex2D(_MainTex, i.uv + uv2.xy * half2(0, 1));
col2 += tex2D(_MainTex, i.uv + uv2.xy * half2(-1, 0));
col2 += tex2D(_MainTex, i.uv + uv2.xy * half2(0, -1));
col2 += tex2D(_MainTex, i.uv);
col2 *= 0.111111; // 1/9
return fixed4(col2.rgb, 1);
}
高斯模糊
采样次数,5x5,采样次数为25次。这样的操作实在太耗费性能了。Unity Shader入门精要中介绍了一种优化的方法。我们可以将一次模糊拆分成两次,每次采样5个像素。这样采样次数从25次降到了10次,并且两次模糊效果完全相同。这种优化技巧也可以使用到其他的卷积核中。
实现方式和BoxBlur大同小异,不再给出代码。
双重模糊技术(Dual Blur)
双重模糊是一种模糊技巧,并不是模糊算法:
先将图像进行降采样模糊,然后升采样模糊。能够以很少的迭代次数达到非常好的模糊效果。模糊时可以使用任意的模糊算法。
缺点:在模糊半径参数较小时会出现一些奇怪的效果。
是游戏中一种性价比非常高的模糊算法。常用于光晕,Bloom效果的模糊计算。
仅仅迭代2两次的效果,并且无条纹状瑕疵。
[ExecuteInEditMode()] // 脚本在编辑模式下也运行
public class DualBlur : MonoBehaviour
{
public Material material;
[Range(0, 10)]
public int iteration = 1;
public float boxBlurRadius = 5.0f;
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (iteration <= 0) // 如果不模糊,避免多余计算
{
Graphics.Blit(src, dest);
return;
}
int width = src.width;
int height = src.height;
material.SetVector("_BoxBlurOffsetVec", new Vector4(boxBlurRadius / width, boxBlurRadius / height, 0, 0));
// 提前分配好降采样使用的数组, 方便进行重用,降采样时分配,升采样可以复用完成效果
RenderTexture[] dualIterationTempTexArray = new RenderTexture[iteration + 1];
dualIterationTempTexArray[0] = RenderTexture.GetTemporary(width, height);
var tempTextureSrc = dualIterationTempTexArray[0];
RenderTexture tempTextureDest = null; // 申请像素大小相同的临时贴图(创建缓冲区)
Graphics.Blit(src, tempTextureSrc);
int flag = 0;
// 降采样模糊 - 逐渐缩小
for (int i = 0; i < iteration; i++) // 多次迭代进行模糊
{
flag++;
width >>= 1;
height >>= 1;
tempTextureDest = RenderTexture.GetTemporary(width, height);
dualIterationTempTexArray[flag] = tempTextureDest;
Graphics.Blit(tempTextureSrc, tempTextureDest, material, 1);
tempTextureSrc = dualIterationTempTexArray[flag]; // src = dest
}
tempTextureSrc = dualIterationTempTexArray[flag];
// 升采样模糊 - 逐渐放大
for (int i = 0; i < iteration; i++) // 多次迭代进行模糊
{
flag--;
tempTextureDest = dualIterationTempTexArray[flag];
Graphics.Blit(tempTextureSrc, tempTextureDest, material, 1);
tempTextureSrc = dualIterationTempTexArray[flag]; // src = dest
}
Graphics.Blit(tempTextureSrc, dest);
for (int i = 0; i <= iteration; i++) // 释放
RenderTexture.ReleaseTemporary(dualIterationTempTexArray[i]);
}
Kawase模糊(Kawase Blur)
多用于光晕,Bloom模糊。是一种非常好的模糊算法,多用于手游。实践数据表明,在相似的模糊表现下,Kawase Blur比经过优化的高斯模糊的性能约快1.5倍到3倍。
Kawase Blur的思路是对距离当前像素越来越远的地方对四个角进行采样,且在两个大小相等的纹理之间进行乒乓式的blit。
具体思路是在runtime层(CPU),基于当前迭代次数,对每次模糊的半径进行设置,而Shader层实现一个4 tap的Kawase Filter即可:
片元shander
half4 KawaseBlur(TEXTURE2D_ARGS(tex, samplerTex), float2 uv, float2 texelSize, half pixelOffset)
{
half4 o = 0;
o += SAMPLE_TEXTURE2D(tex, samplerTex, uv + float2(pixelOffset +0.5, pixelOffset +0.5) * texelSize);
o += SAMPLE_TEXTURE2D(tex, samplerTex, uv + float2(-pixelOffset -0.5, pixelOffset +0.5) * texelSize);
o += SAMPLE_TEXTURE2D(tex, samplerTex, uv + float2(-pixelOffset -0.5, -pixelOffset -0.5) * texelSize);
o += SAMPLE_TEXTURE2D(tex, samplerTex, uv + float2(pixelOffset +0.5, -pixelOffset -0.5) * texelSize);
return o * 0.25;
}
同样,对模糊半径(Blur Radius)参数的调节,可以控制Dual Kawase Blur模糊的程度:
径向模糊(Radial Blur)
径向模糊(Radial Blur)可以给画面带来很好的速度感,是各类游戏中后处理的常客,也常用于Sun Shaft等后处理特效中作为光线投射的模拟。
径向模糊的原理比较直接,首先选取一个径向轴心(Radial Center),然后将每一个采样点的uv基于此径向轴心进行偏移(offset),并进行一定次数的迭代采样,最终将采样得到的RGB值累加,并除以迭代次数。
后处理代码
//径向模糊
[ExecuteInEditMode()]
public class RadialBlur : MonoBehaviour
{
public Material material;
public Vector2 radialCenter; // 径向模糊中心 [0, 1], uv坐标系中心
[Range(0, 30)]
public int iteration = 1; // 超过30会出现画面变暗(留个坑,不知道为啥)
[Range(0, 30)]
public float blurRadius = 1.0f;
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (iteration <= 0)
{
Graphics.Blit(src, dest, material);
return;
}
material.SetInt("_Iteration", iteration);
var blurRadiusVec = new Vector4(blurRadius/src.width, blurRadius/src.height);
material.SetVector("_BlurRadius", blurRadiusVec);
material.SetVector("_RadialCenter", new Vector4(radialCenter.x, radialCenter.y, 0, 0));
Graphics.Blit(src, dest, material);
}
}
片元shader
float4 _RadialCenter;
float4 _BlurRadius;
int _Iteration;
fixed4 frag (v2f_img i) : SV_Target
{
const float2 offsetVec = (_RadialCenter.xy - i.uv) * _BlurRadius;
half4 color = 0;
// [unroll(30)] --- 留个坑 好像和GPU循环优化有关
for (int j = 0; j < _Iteration; j++)
{
color += tex2D(_MainTex, i.uv + offsetVec);
i.uv.xy += offsetVec;
}
return color / _Iteration;
}
方向模糊(Directional Blur)
方向模糊(Directional Blur)可以看做是径向模糊(Radial Blur)的一个变体。其主要思路是传入一个角度,然后在runtime层计算出对应的矢量方向:
然后,在Shader层,将每一个采样点的uv基于此方向进行正负两次偏移(offset),接着进行一定次数的迭代采样,最终将采样得到的RGB值累加,并除以迭代次数,得到最终的输出。
后处理代码
[ExecuteInEditMode()]
public class DirectionBlur : MonoBehaviour
{
public Material material;
public float directionAngle; // 模糊方向
[Range(0, 3)]
public float blurRadius;
[Range(0, 20)]
public int iteration; // 迭代次数
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (iteration <= 0)
{
Graphics.Blit(src, dest);
return;
}
var sinVal = Mathf.Sin(directionAngle) * blurRadius * 0.05f / iteration;
var cosVal = Mathf.Cos(directionAngle) * blurRadius * 0.05f / iteration;
material.SetVector("_Direction", new Vector2(sinVal, cosVal));
material.SetInt("_Iteration", iteration);
Graphics.Blit(src, dest, material);
}
}
片元shader
int _Iteration;
float4 _Direction;
fixed4 frag (v2f_img i) : SV_Target
{
half4 color = 0;
for (int k = -_Iteration; k < _Iteration; k++)
{
color += tex2D(_MainTex, i.uv - _Direction.xy * k);
}
return color / (_Iteration * 2);
}